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.
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.
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
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
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.
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, }, }, }, };
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.
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({});
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.