Writing another ORM for Hasura with a codegen
Spartak
Posted on May 28, 2020
Hello, world. My first post, also a first post in English.
A way of samurai from copy-paster to a library builder
There is nothing bad not to use any library for Graphql codegen. Graphql is simple by itself. Just import something like apollo
and you good to go.
But time goes, and you came to an idea that you write too often the same field's definition
query {
user {
id
name
}
}
mutation {
insert_user(objects: {}) {
returning {
id
name
}
}
}
for many queries and export them as Fragments to graphql
folder:
export const userFragment = `
fragment userFragment on user {
id
name
}
`
Then you create 20 tables and get annoyed to write tons of similar text every query/mutation/subscription, where the only table_name is changing, and you come with an idea to autogenerate functions for all these operations.
What we have
- ahrnee/graphql-codegen-hasura this creates everything from Fragments you need to write by yourself
- timeshift92/hasura-orm this is less popular repo and also provides another way to query Graphql with method chaining to generate code
- mrspartak/hasura-om and of course my stuff I was amused that there is so little tooling for this repeatable task.
Approach
First of all, I saw only the timeshift92/hasura-orm
library, because I searched for ORM only, and the first one was not caught on a search page. This library didn't fit my needs.
At that time, we had our code split up with exporting Fragments and "base" queries
export const GET_USER = gql`
${USER_PUBLIC_FRAGMENT}
query GET_USER($limit: Int, $offset: Int, $where: user_bool_exp, $order_by: [user_order_by!]) {
user(where: $where, limit: $limit, offset: $offset, order_by: $order_by) {
...userPublicFields
}
}
`;
As I mentioned above, this is just stupid copy-pasting stuff for all tables. Also, this a simple 1 table request. We came to a new task to make a transaction request between microservices. Yes, there is a way to solve this just by architecture, but this was a point, I got that we need a convenient library for that.
What library should do
First of all, this module is for backend, so it will have full access to Hasura (yes, this is bad also, but it is elementary to fix).
- Autogenerating code for queries/mutations/subscriptions
- Have request/ws apps preinstalled in the module
- Autogenerate base fragments
And that's it. The last task was easy to solve with Hasura's /query
endpoint, where you can just make a couple of SQL queries to Postgres and get all table names, table fields and also primary keys.
The result
I'm not happy with the result, because the library seemed easy on the first look, but then got ugly quickly. The reason is simple, and it is tough to maintain architecture for the tasks you don't know yet. One of the tasks was arguments for nested fields.
But lib is here and working! So take a look at it:
npm i hasura-om
const { Hasura } = require('hasura-om')
const orm = new Hasura({
graphqlUrl: 'your hasura endpoint',
adminSecret: 'your hasura admin secret'
})
//this method will get all info from DB and generate everything for you
await om.generateTablesFromAPI()
But of course, you can do everything manually
const { Hasura } = require('hasura-om')
const orm = new Hasura({
graphqlUrl: '',
adminSecret: ''
})
orm.createTable({ name: 'user' })
.createField({ name: 'id', 'isPrimary': true })
.createField({ name: 'name' })
.generateBaseFragments()
Assuming that we have generated everything we need, now we can query like a pro
let [err, users] = await orm.query({
user: {
where: { last_seen: { _gt: moment().modify(-5, 'minutes').format() } }
}
})
//users = [{ ...allUserTableFields }]
let isLiveCondition = {
last_seen: { _gt: moment().modify(-5, 'minutes').format() }
}
let [err, userinfo] = await orm.query({
user: {
select: {
where: isLiveCondition
},
aggregate: {
where: isLiveCondition,
count: {}
}
}
})
/*
users = {
select: [{ ...allUserTableFields }],
aggregate: {
count: 10
}
}
*/
Let's make a mutation in a transaction
var [err, result] = await orm.mutate({
user: {
insert: {
objects: { name: 'Peter', bank_id: 7, money: 100 },
fragment: 'pk'
},
},
bank: {
update: {
where: { id: { _eq: 7 } },
_inc: { money: -100 },
fields: ['id', 'money']
}
}
}, { getFirst: true })
/*
result = {
user: { id: 13 },
bank: { id: 7, money: 127900 }
}
*/
Or we can subscribe to new chat messages
let unsubscribe = orm.subscribe({
chat_message: {
where: { room_id: { _eq: 10 } },
limit: 1,
order_by: { ts: 'desc' }
}
}, ([err, message]) => {
console.log(message)
}, { getFirst: true })
And for all queries above all you have to do is install module, import and initiate. That's it. All tables/fields/primary keys are generated from a query API. 2 base fragments are also auto-generated: 'base' (all table/view fields), 'pk' (just primary keys). And all you have to do is create new Fragments you need:
orm.table('user')
.createFragment('with_logo_posts', [
orm.table('user').fragment('base'),
[
'logo',
[
orm.table('images').fragment('base'),
]
],
[
'posts',
[
'id',
'title',
'ts'
]
]
])
/*
this will create such fragment, and you can use it by name in any query
fragment with_logo_fragment_user on user {
...baseUserFields
logo {
...baseImagesFields
}
posts {
id
title
ts
}
}
*/
Drawbacks
This is time-consuming. Most of the time was spent on tests + docs + lint because initially it was combined like a Frankenstein from some parts. And currently, it needs a bit of cleaning and refactoring.
Object declaration is a bit messy but easier than write tons of text.
No typescript, sorry. Of course, it will suit this lib very well, but I'm still a noob in this field, so didn't want to spend MORE time on it too.
A Wish
Please, if you find typos or just stupid sounding text, feel free to write somewhere, so I can improve my elven speech. Also, you are welcome to issues https://github.com/mrspartak/hasura-om/issues
Also, if this is really helpful somehow, I can write more about building queries and ES6 tagged template
I used in some places in the lib
Posted on May 28, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.