Skip to content

A collection of custom graphql-code-generator plugins by ATMINA

License

Notifications You must be signed in to change notification settings

atmina/graphql-codegen-plugins

Repository files navigation

typescript-graphql-codegen

This repository contains the code for two GraphQL Codegen plugins that customize the generated code following suggested changes inside ATMINA Solutions GmbH.

All examples in this document are based on the example schema used for testing.

Installation

While it's not required to do so, installing and using both plugins is recommended since they have been built together to achieve a certain output. Using either of the plugins standalone might lead to type errors.

Using Yarn:

yarn add -D @atmina/only-enum-types @atmina/local-typescript-operations graphql @graphql-codegen/near-operation-file-preset @graphql-codegen/cli

Using npm:

npm install --save-dev @atmina/only-enum-types @atmina/local-typescript-operations graphql @graphql-codegen/near-operation-file-preset @graphql-codegen/cli

only-enum-types

This plugin is to be used like this (part of codegen.yml):

generates:
  src/__generated__/graphql.ts:
    plugins:
      - "@atmina/only-enums"

The plugin takes care of only generating enum types in the file generated outside of any module.

local-typescript-operations

Requirements

Your GraphQL server should have a mechanism to deal with the @exported directive. Two options are

  • Configuring the directive (recommended to allow copy-pasting of queries to tools like Insomnia)
  • An interceptor that removes the directive before handing it off to the GraphQL processor

If you are using the JS GraphQL Plugin for Jetbrains IDEs (and likely other plugins for other editors), you need to include a static graphql file defining the directive in your .graphqlconfig:

{
  "schemaPath": "schema.graphql",
  "includes": ["*"], // This is the relevant line; adapt the glob if you have to
  "extensions": {
    "endpoints": {
      "Default Introspection Endpoint": {
        "url": "http://localhost:5000",
        "introspect": true,
        "headers": {
          "user-agent": "JS GraphQL"
        }
      }
    }
  }
}
# export-directive.graphql
directive @export(exportName: String!) on FIELD

Usage

This plugin should be configured like this:

  src/:
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/graphql.ts
      extension: .generated.ts
    plugins:
      - add:
          content: "/* tslint:disable */"
      - "@atmina/local-typescript-operations"

@atmina/local-typescript-operations has been developed for, and not tested without the near-operation-file preset.

This plugin achieves the larger part of the concept with features including:

  • No use of Scalars[], instead the TS types are used immediately
  • No use of Pick<Type, 'field1', 'field2'>, instead the types are cleanly built as one
  • No use of Maybe<Type>, instead Type | null is used.
  • Local generation of the required input types
  • Enums are prefixed with Types. so they are imported from the file generated by a typescript plugin variant
  • Fragments are imported between files to make them available everywhere where they are needed (as they have previously all been generated in the same file)
  • Non-primitive fields (fields with types other than scalar or enum) may be annotated with a directive (@export(exportName: "Example")) to generate a type called Example specific to the selection performed on the annotated field.
    • Attempting to export a primitive field will throw an error during code generation

An example of using the export directive looks like this:

# example.graphql
query GetShelves {
    shelves @export(exportName: "Shelf") {
       id
       floor
    }
}
// example.generated.ts
/* tslint:disable */
import * as Types from './__generated__/types';

export type BasicQueryQueryVariables = Types.Exact<{ [key: string]: never; }>;

export type BasicQueryQuery = { __typename?: 'Query'; shelves: Array<Shelf>};
export type Shelf = { __typename?: 'Shelf', id: string, floor: number };

The directive works in fragments and inline fragments as well and generates multiple types for graphql interface types. The naming for those is <exportName>_<typename> with exportName being the exportName defined in the directive and typeName being the name of the type implementing the parent type the selection is performed on:

# example.graphql
query GetSomeFragen {
    shelves {
        items @export(exportName: "Borrowable") { # parent (interface) type is Borrowable, implementations are Book and VideoGame
            id
        }
    }
}
// example.generated.ts
/* tslint:disable */
import * as Types from './__generated__/types';

export type GetSomeFragenQueryVariables = Types.Exact<{ [key: string]: never; }>;

export type GetSomeFragenQuery = {
  __typename?: 'Query',
   shelves: Array<{ __typename?: 'Shelf'; items: Array<Borrowable>}>
};

export type Borrowable = Borrowable_Book | Borrowable_VideoGame;

export type Borrowable_Book = { __typename?: 'Book', id: string };
export type Borrowable_VideoGame = { __typename?: 'VideoGame', id: string };

This also works with additional properties from a specific implementing type that are exported:

# example.graphql
query AllFragen {
    shelves {
        items @export(exportName: "Borrowable") {
            id
            title
            ... on Book {
                author @export(exportName: "Author") {
                    id
                    name
                }
            }
           ... on VideoGame {
              publisher
           }
        }
    }
}
// example.generated.ts
/* tslint:disable */
import * as Types from './__generated__/types';

export type AllFragenQueryVariables = Types.Exact<{ [key: string]: never; }>;

export type AllFragenQuery = { __typename?: 'Query',
   shelves: Array<{ __typename?: 'Shelf'; items: Array<Borrowable>}>
};

export type Borrowable = Borrowable_Book | Borrowable_VideoGame;

export type Borrowable_Book = { __typename?: 'Book', id: string, title: string, author: Author};
export type Borrowable_VideoGame = { __typename?: 'VideoGame', publisher: string, id: string, title: string };

export type Author = { __typename?: 'Author', id: string, name: string };

Using the packages

Codegen config

To use the plugins in codegen, add them as plugin with their packagename:

schema: "schema.graphql"
documents: "src/**/*.graphql"
hooks:
  afterAllFileWrite:
    - prettier --write
generates:
  src/__generated__/graphql.ts:
    plugins:
      - "@atmina/only-enum-types"
  src/:
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/graphql.ts
      extension: .generated.ts
    plugins:
      - add:
          content: "/* tslint:disable */"
      - "@atmina/local-typescript-operations"

If you want to generate additional code like injectable Angular services, install and add plugins to the config as required.