loading...
Cover image for Writing another ORM for Hasura with a codegen

Writing another ORM for Hasura with a codegen

mrspartak profile image Spartak ・4 min read

Alt Text
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

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

Discussion

markdown guide