Setup Hasura with GraphQL Code Generator

What is GraphQL Code Generator

Graphql Code Generator is a library that helps in generating all the code from a reference schema. This schema could be a file, or a live GraphQL endpoint. It is a pluggable solution so you can use plugins provided by the library or write your own. The benefit is that your GraphQL schema is completely typed, meaning the library can derive and generate TypeScript types. Not only that, it can generate Apollo hooks, resolvers, code for other libraries beyond React, and even beyond JavaScript.

Another benefit is that it will guarantee your schema and queries/mutation are valid otherwise it won't compile. With Hasura and the Postgres database being typed we don't have to write any schema, it is automatically generated for us. We can flow the types from the database, through a GraphQL schema, all the way to TypeScript types and more. Further more we can verify per-role permissions that are defined in Hasura so your query/mutations (including return values) are typed and validated against the role.

Setup Hasura

Clone the repo provided. The hasura folder is setup already, and additionally there is a docker-compose.yaml file that outlines the hasura container and postgres. You can find a complete guide to getting started with Hasura here Setup a Local Hasura Development Environment

version: "3.6"
services:
  postgres:
    image: postgres:12
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgrespassword
  graphql-engine:
    image: hasura/graphql-engine:v2.0.7
    ports:
      - "8080:8080"
    depends_on:
      - "postgres"
    restart: always
    environment:
      HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      HASURA_GRAPHQL_ADMIN_SECRET: admin_secret
volumes:
  db_data:

Run docker compose up -d, and once that is running you're mostly all setup.

Setup Basic Config

GraphQl Code Generator operates off of a cli that you install into your project, and then takes a config. This config can be yaml, or JavaScript. I generally lean towards using the JavaScript because then you can add logic if necessary.

First off we'll need to install a few packages. We will need graphql and the codegen cli.

yarn add graphql @graphql-codegen/cli --dev

Then we will need our plugins for whatever our resulting code base will use. For us we'll use TypeScript, and additionally Apollo with React.

yarn add @graphql-codegen/typescript @graphql-codegen/typescript-react-apollo @graphql-codegen/typescript-graphql-request --dev

Table Setup for Data

To make this actually function we will need some tables with permissions.

users
- id
- username
posts
- id
- title
- body
- user_id

These tables and metadata are in stored in the code already so run the commands below to set it up.

hasura metadata apply
hasura migrations apply --database-name default
hasura metadata reload

Permissions

Lets take a look at the permissions we have setup. There are 2 roles admin and user. The admin role can do anything it wants, this will be what your backend systems operate as. Then there is the user role. This role is locked down to be able to load themselves as a user. Then additionally for posts we setup insert, select, update, and delete anything related where the user_id matches the x-hasura-user-id in their token.

The CodeGen

Graphql Code Generator can operate on a yaml file, json file, and additionally a js file. When we run the command to generate our stuff we just need to pass it the config file path, to generate our code we setup a script in our package.json.

"scripts": {
    "generate": "graphql-codegen --config codegen.js"
}

We execute the graphql-codegen and give it our codegen.js.

The codegen.js file just lives in the root of our app and is setup to generate different custom API libraries depending on the need. The frontend hypothetically would use Apollo, so the user role will output apollo hooks.

{
  "./src/user.tsx": {
    "schema": [
      {
        "http://localhost:8080/v1/graphql": {
          "headers": {
            "x-hasura-role": "user",
            "x-hasura-admin-secret": "admin_secret"
          }
        }
      }
    ],
    "documents": ["./src/user/**/*.graphql"],
    "plugins": [
      "typescript",
      "typescript-operations",
      "typescript-react-apollo"
    ],
    "config": {
      "preResolveTypes": true,
      "skipTypename": false,
      "withHooks": true,
      "withHOC": false,
      "withComponent": false,
      "enumsAsTypes": true,
      "constEnums": true,
      "reactApolloVersion": 3
    }
  }
}

Lets break this down a little bit. The structure starts with where the file this all will be output into which for this is at the path ./src/user.tsx. This can be any file you want pointing to anywhere.

Next we have the schema section. This can at a file, but instead we point it at our running hasura instance. We provide headers that are the admin-secret then because we want to load stuff for the user role we pass the x-hasura-role to user.

What this does is that the access to fields, permissions checks, etc will all be validated when the GraphQL queries are compiled. So if you accidentally fudge a permissions, forget to give access to certain fields. The Graphql Code Generator will verify that a specific role has access to what it is requesting.

Then we have documents, this is where all the queries and mutations are stored. We use a glob path and say compile all the graphql files in the src/user directory.

Next our plugins define how and what should be generated. Up until now is just saying "what and how to access the graphql schema" and now we want to run it through these plugins to generate the output.

["typescript", "typescript-operations", "typescript-react-apollo"]

In our this case we setup TypeScript so all our generated code will have types, and then also the end result will also be React Apollo hooks.

Finally the config portion is just bits passed into the various plugins. To understand all of the config variables you can check on the specific plugin page for each plugin.

For our admin we want to generate a client that works with Node.js. This is the exact same structure as above but just tweaked for admin which will generally run on a server. So rather than leveraging Apollo we'll use graphql-request. A library built by the team at Prisma.

{
  "./src/admin.ts": {
    "schema": [
      {
        "http://localhost:8080/v1/graphql": {
          "headers": {
            "x-hasura-role": "admin",
            "x-hasura-admin-secret": "admin_secret"
          }
        }
      }
    ],
    "documents": ["./src/admin/**/*.graphql"],
    "plugins": [
      "typescript",
      "typescript-operations",
      "typescript-graphql-request"
    ],
    "config": {
      "preResolveTypes": true,
      "skipTypename": false,
      "enumsAsTypes": true,
      "constEnums": true
    }
  }
}

All together our codegen.js file looks like this.

module.exports = {
  overwrite: true,
  generates: {
    "./src/user.tsx": {
      schema: [
        {
          "http://localhost:8080/v1/graphql": {
            headers: {
              "x-hasura-role": "user",
              "x-hasura-admin-secret": "admin_secret",
            },
          },
        },
      ],
      documents: ["./src/user/**/*.graphql"],
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
      config: {
        preResolveTypes: true,
        skipTypename: false,
        withHooks: true,
        withHOC: false,
        withComponent: false,
        enumsAsTypes: true,
        constEnums: true,
        reactApolloVersion: 3,
      },
    },
    "./src/admin.ts": {
      schema: [
        {
          "http://localhost:8080/v1/graphql": {
            headers: {
              "x-hasura-role": "admin",
              "x-hasura-admin-secret": "admin_secret",
            },
          },
        },
      ],
      documents: ["./src/admin/**/*.graphql"],
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-graphql-request",
      ],
      config: {
        preResolveTypes: true,
        skipTypename: false,
        enumsAsTypes: true,
        constEnums: true,
      },
    },
  },
};

Setting up GraphQL Queries and Mutations

In the src/user/user.graphql we would have something like the below. A way to load a user, maybe a way to load the posts, and or insert a post.

query LoadUser($where: users_bool_exp = {}) {
  users(where: $where) {
    id
    username
  }
}

query LoadPosts($where: posts_bool_exp!) {
  posts(where: $where) {
    id
    title
    body
  }
}

mutation InsertPost($post: posts_insert_input!) {
  insert_posts_one(object: $post) {
    id
    title
    body
    user_id
  }
}

One benefit of Hasura with queries and mutations like these is we can setup the permissions and default insert fields. So for the LoadUser we have setup permissions to only allow you to access yourself.

For posts, this is exactly the same. So when we query we just say "load posts" and it'll load all of our posts.

With InsertPost it will use the column preset for user_id and insert the x-hasura-user-id from your token. So Hasura takes care of this for security purposes and for the user doesn't have to pass back the user_id field.

Codegen Result

Running yarn generate should run through a series of things and give the all good that it's generated the outputs.

Every query, mutation, and everything will be generated. So while you type you won't have to wonder if a user can insert a title or content of a post. It will tell you what exactly they can insert.

For the Admin backend you would import the getSdk from the admin.ts file and then with graphql-request you would instantiate a new client with the admin secret set as the header.

Then pass that to the getSdk and get a fully typed client back read to use in your code.

import { getSdk } from "./src/admin";
import { GraphQLClient } from "graphql-request";

const gqpClient = new GraphQLClient(
  process.env.HASURA_GRAPHQL_ENDPOINT as string,
  {
    headers: {
      [`x-hasura-admin-secret`]: process.env.HASURA_GRAPHQL_ADMIN_SECRET,
    },
  }
);

export default getSdk(gqpClient);

In our case you'd be able to call the 2 methods.

client.LoadPosts({});
// or
client.LoadUsers({});

Ending / Drawbacks

The main drawback is that Hasura has to be running in order to get the permission checking that we want. But this will generally be built by developers while they are working. You can then commit the libraries built, or publish it as a package.

In the end generating Apollo hooks, a backend graphql-request library, or whatever your use case is will eliminate a whole class of bugs. Developers working will be able to move faster because all of the data access, querying, and more will be have TypeScript types generated for it.

Discord Logo

Code Daily Discord

Join our community and get help with React, React Native, and all web technologies. Even recommend tutorials, and content you want to see.