code-generator plugins for hasura/apollo-gql/typescript development
graphql-codegen-hasura is a collection of code generator plugins for graphql-code-generator. These plugins are designed to automate coding tasks around the development of a strongly typed client for a Hasura backend. The majority of the code generated is strongly-typed wrappers for Apollo GraphQL, in addition to a number of convenience features, including:
Important Note: Despite the version number, this project is at "Beta" status, with an evolving codebase. However, any breaking changes will be released with new minor or major version bumps in order to minimize disruption to any consumers, and detailed in Release Notes
These plugins follow a Fragment First approach to Hasura client code generation:
You Define A Fragment Like So:
export const DogFragmentDoc = gql`
fragment Dog on dogs {
breed
id
}
`;
The graphql-codegen-hasura plugins can then generate a 'GQLHooks' and 'GQLHelper' object which you can then use to make CRUD calls to your API. All the generated methods are strongly typed counterparts for methods on the ApolloGraphQL client (with some augmentations).
// GQLHelper Object - Available Methods
// Query
GQLHelper.Fragments.Dog.queryById(/* params */) // Query By id
GQLHelper.Fragments.Dog.queryObjects(/* params */) // Query
// Subscribe
GQLHelper.Fragments.Dog.subscribeToById(/* params */) // Subscribe by id
GQLHelper.Fragments.Dog.subscribeToObjects(/* params */) // Subscribe
// Insert
GQLHelper.Fragments.Dog.insert(/* params */) // Insert single entity
GQLHelper.Fragments.Dog.insertWithOnConflict(/* params */) // Insert with strongly types onconflict options
GQLHelper.Fragments.Dog.insertObjects(/* params */) // Insert one or multiple entities
// Update
GQLHelper.Fragments.Dog.updateById(/* params */) // Update by id
GQLHelper.Fragments.Dog.updateObjects(/* params */) // Update one or multiple entities
// Delete
GQLHelper.Models.Dog.removeById(/* params */) // Delete entity by id
GQLHelper.Models.Dog.removeObjects(/* params */) // Delete one or multiple entities
// Watch
GQLHelper.Fragments.Dog.watchQueryById(/* params */) // Subscribe to observable by id
GQLHelper.Fragments.Dog.watchQueryObjects(/* params */) // Subscribe to observable
// Cache Read/Write
GQLHelper.Fragments.Dog.cacheReadFragmentById(/* params */) // Read fragment from cache
GQLHelper.Fragments.Dog.cacheReadQueryById(/* params */) // Read query from cache
GQLHelper.Fragments.Dog.cacheWriteFragmentById(/* params */) // Write fragment to cache
GQLHelper.Fragments.Dog.cacheWriteQueryById(/* params */) // Write query to cache
GQLHelper.Fragments.Dog.cacheWriteQueryObjects(/* params */) // Write query to cache
GQLHelper.Fragments.Dog.cacheWriteQueryInsert(/* params */) // Write 'InsertInput' to cache
// QueryById
GQLHelper.Fragments.Dog.queryById({ apolloClient, dogsId });
// Query Collection
GQLHelper.Fragments.Dog.queryByObjects({ apolloClient, options: { variables: { where: { breed: { _eq: 'scottie' } } } });
// Insert Mutation
GQLHelper.Fragments.Dog.insert({ apolloClient, dog: newDog);
// ... ETC. All the methods follow a the same pattern, and all allow the standard ApolloClient options objects to be populated and passed through.
// GQLHooks Object - Available Methods
// Query
GQLHooks.Fragments.Dog.useQueryById(/* params */) // Hook to query by id
GQLHooks.Fragments.Dog.useQueryByIdLazy(/* params */) // Hook to query by id - lazy execution
GQLHooks.Fragments.Dog.useQueryObjects(/* params */) // Hook to query one or multiple entities
GQLHooks.Fragments.Dog.useQueryObjectsLazy(/* params */) // Hook to query one or multiple entities - lazy execution
// Subscribe
GQLHooks.Fragments.Dog.useSubscribeToById(/* params */) // Hook to subscribe by id
GQLHooks.Fragments.Dog.useSubscribeToObjects(/* params */) // Hook to subscribe to one or multiple entities
// Insert
GQLHooks.Fragments.Dog.useInsert(/* params */) // Hook to insert single entity
GQLHooks.Fragments.Dog.useInsertWithOnConflict(/* params */) // Hook to insert with strongly types onconflict options
GQLHooks.Fragments.Dog.useInsertObjects(/* params */) // Hook to insert one or multiple entities
// Update
GQLHooks.Fragments.Dog.useUpdateById(/* params */) // Hook to update by id
GQLHooks.Fragments.Dog.useUpdateObjects(/* params */) // Hook to update one or multiple entities
// Delete
GQLHooks.Models.Dog.useRemoveById(/* params */) // Hook to delete entity by id
GQLHooks.Models.Dog.useRemoveObjects(/* params */) // Hook to delete one or multiple entities
// QueryById
{ dog, loading, errors } = await GQLHooks.Fragments.Dog.queryById({ apolloClient, dogsId });
// Query Collection
{ objects, loading, errors } = await GQLHooks.Fragments.Dog.useQueryByObjects({ apolloClient, options: { variables: { where: { breed: { _eq: 'scottie' } } } });
// Insert Mutation
const [addDog] = GQLHooks.Fragments.Dog.insert({ apolloClient, dog: newDog);
addDog({ type:'scottie' });
// ... ETC. All the methods follow a the same pattern, and all allow the standard ApolloClient options objects to be populated and passed through.
All client/cache WriteQuery
and WriteFragment
methods accept Hasura <TypeName>_Insert_Input
object graphs as well as the expected Fragment graphs.
Some usage notes:
fieldMap Parameter
This parameter is options. It allows typenames to be specified for child relationships (and automatically added during the conversion). It also allows for specific fields to be ignored. This can be automated in the future, but is non-trivial dev. So, for future version or community pull request.
Client Fields
Client (@client) fields can be added via in <TypeName>_Insert_Input
object, by prefixing them with a triple underscore (___).
This is converted to the fieldname (without prefix) for cache adds. Client fields are removed completely for api inserts (useInsert* and insert* methods).
This is kind of hacky, and there is probably a much better way to achieve this. #futureDev
<TypeName>_Insert_Input Types
All of the client* and cache* methods accept both Fragment
and <TypeName>_Insert_Input
graphs. However, when adding <TypeName>_Insert_Input
graphs to child properties of an object (for example - adding a new object to a child array field), the <TypeName>_Insert_Input
will need to be cast to the fragment type. However, it will still be converted appropriately at runtime.
Example Usage
See packages/graphql-codegen-hasura-core/src/utils.test.ts for example usage
Please note, this library currently only supports version 3 of the React Apollo client
To quickly view the type of code that gets generated, view the autogen code files in the demo project.
To quickly try out the code-generation:
1. lerna bootstrap
2. npm run build-and-generate
These plugins require and augment the existing fantastic GraphQL code generator plugins available from graphql-code-generator
This is a mono-repo project that contains multiple npm packages (under the packages folder), in addition to a demo project (under the demo folder).
The easiest way to quickly get going is to view the following files in the demo project as a starting point:
demo/graphql-codegen-gql.yaml
demo/graphql-codegen-typescript.yaml
It is important to note: The TypeScript Generation leverages the files created in the GQL generation step. As such, it is important to run the GQL generation step prior to the TypeScript generation step.
create-react-app my-project --typescript
npm install --save-dev @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
npm install graphql-codegen-hasura-core;
npm install --save-dev graphql-codegen-hasura-gql graphql-codegen-hasura-typescript graphql-codegen-hasura-react graphql-codegen-hasura-client-config graphql-codegen-hasura-shared
Create configuration YAML files (see below)
Run the codegen commands in a command console
Note: node v12 or greater is required.
graphql-codegen --config=graphql-codegen-gql.yaml;
graphql-codegen --config=graphql-codegen-typescript.yaml
If you would like to run the graphql-codegen-hasura packages locally from the demo project (in src/demo), you can run the following command:
npm run bootstrap
This will use Lerna to Link local packages together and install remaining package dependencies. See Lerna documentation for further details.
See graphql-code-generator documentation for configuration instructions. See below for the specific configuration flags that are available on these plugins
Generates CRUD gql mutations and queries for every Fragment defined in the targeted (code) documents.
See demo/src/autogen/hasura/gql.ts for generated output files.
Generates CRUD TypeScript helper methods for every Fragment defined in the targeted (code) documents. Provides wrapped client.query & client.mutate calls, in addition to adding some convenience features.
See demo/src/autogen/hasura/ts.ts for generated output files.
Generates CRUD React Hooks for every Fragment defined in the targeted (code) documents.
See demo/src/autogen/hasura/ts-react.ts for generated output files.
Generates TypeScript Type Policies and Resolver Types for tables related to GQL fragments found in the targeted documents (code).
See demo/src/autogen/hasura/ts-config.ts for generated output files.
A basic naming convention being followed:
*Model
is used for any entity or fieldset that contains only scalars, and not child entities are referenced or included*Graph
is used for any entity or fieldset that contains or references child entitiesThe demo project is simply:
/demo/src/autogen
folderHasura is a fantastic tool for the rapid development of a GraphQL backend. Apollo GraphQL is a suite of GraphQL tooling, including a great Client. graphql-code-generator is a very helpful code generation tool and library, to automate the generation of much of the boilerplate code required to use GraphQL. However, there still remained quite a bit of boiler plate code to be written.
The consistency and predictability of the Hasura GQL backend implementation, provides an opportunity to automate much of the remaining code for standard single-table mutations and queries, and multi-table helper code. These plugins are designed to provide this.
TBD
This code was initially developed for use in a single separate commercial project. It is being shared in case useful to others, and as a contribution to the development community and the great GraphQL tools that already exist. The original implementation did just enough to meet the goals and needs of the initial project.
Known Issues Include:
id
field. See (Refinements Needed)[##Refinements-and-Enhancements-Needed] section below for detailsThere are many refinements and enhancements that would be beneficial, and contributions to that end are encouraged. Notable examples include:
id
*id
. If anyone knows how to identify a primary key field from the GQL schema, let me know, or submit a pull request with the fix. The relevant section of code is here.This library is part of a collection of companion tools and libraries under the AbleStack umbrella. All of these libraries share the common goal:
Helping small teams and solo-developers build big ideas rapidly and affordably
To achieve these goals, the following principles are applied: