Skip to content

Traditional Codegen Workflow

cwstra edited this page Jul 23, 2024 · 10 revisions

Traditional Codegen Workflow

In this approach, a developer creates .graphql files. These files are recognized by the graphql-codegen cli, which then generates corresponding rescript output.

Installation

Prerequisite packages:

  • graphql
  • @graphql-codegen/cli
  • @graphql-codegen/near-operation-file-preset (this requirement might be swapped out for a custom preset in the future)

We provide two plugins particular to rescript:

  • @rescript-graphql-codegen/base-types: This generates GQL input objects and enums for the overall schema.
  • @rescript-graphql-codegen/operations: This generates query and mutation types for individual .graphql files.

To install all of these dependencies at once:

# npm
npm i graphql
npm i -D @graphql-codegen/cli @graphql-codegen/near-operation-file-preset @rescript-graphql-codegen/base-types @rescript-graphql-codegen/operations

# yarn
yarn add graphql
yarn add --dev @graphql-codegen/cli @graphql-codegen/near-operation-file-preset @rescript-graphql-codegen/base-types @rescript-graphql-codegen/operations

Example Configuration

codegen.ts

import type {CodegenConfig} from '@graphql-codegen/cli'

// Absolute name of your scalars module (described later)
const scalarModule = "GraphqlBase__Scalars";

const config: CodegenConfig = {
  // Path to GQL schema file
  schema: "src/Graphql/schema.graphql",
  generates: {
    // Entry to generate the base types file
    "src/GraphqlBase/GraphqlBase__Types.res": {
      plugins: ["@rescript-graphql-codegen/base-types"],
      config: {
        scalarModule

        // Turning this on will generate an array of all enum values (`allvalues`),
        // in the event you need to manipulate those in some way.
        //
        // includeEnumAllValuesArray: true

        // By default, nullable GraphQL types are translated to `null<'a>`,
        // and list GraphQL types to `array<'a>.
        // Setting the following properties allow swapping these types out.
        // The effective default values are shown commented out below:
        //
        // nullType: "null",
        // listType: "array",

        // Allow optional props in input types.
        // See the notes above `optionalVariables` and `optionalOutputs`
        // in the operations section for more details.
        //
        // optionalInputTypes: "wrapped"

        // Adding this will append its value to any enum modules.
        // See the similar options in the operations plugin for more details.
        //
        // appendToEnums: "..."
      }
    },
    // Entry for operations files
    "src/": {
      // Glob to all source files
      documents: "src/**/*.graphql",
      // Here we make use of a typescript preset that does _roughly_ what we want
      preset: "near-operation-file",
      presetConfig: {
        extension: ".res",

        // If baseTypesPath is anything other than '.', the preset will generate
        // typescript imports, which is bad news.
        baseTypesPath: ".",

        // Not required, but adding generated extensions doesn't really play nice
        // with rescript's module names; much easier to use folders as ignore targets.
        "folder": "__generated__",
      },
      config: {
        // Module name of the generated base types file, as defined above.
        baseTypesModule: "GraphqlBase__Types",
        scalarModule,

        // Without this, the preset will generate typescript imports;
        // still not what we want.
        globalNamespace: true,

        // See the settings with the same names in the base types section.
        //
        // nullType: "null",
        // listType: "array",

        // If needed, can also add this import to customize the definition of the gql tag.
        // Defaults to "GraphqlTag"
        //
        // gqlTagModule: "GQLTag"
        
        // Also if needed, this fully-qualified import is the function/constructor that
        // fragments will be wrapped in when inserted via the gql tag.
        // Defaults to "GraphqlTag\.Document"; if using the provided graphql-tag
        // package, no config should be needed.
        //
        // fragmentWrapper:  "GQLTag.document"

        // By default, nullable properties will just be wrapped with the `nullType`.
        // For example:
        // `nullableProp: null<int>`
        // If you would like to make these props optional, set this option.
        // - A value of `wrapped` will make the prop optional _in addition to_ `nullType`.
        //     `nullableProp?: null<int>`
        // - A value of `unwrapped` will make the prop optional _instead of_ `nullType`.
        //     `nullableProp?: int`
        //   Note: `nullType` will still appear in lists; e.g.
        //     `nullableProp: null<array<null<int>>>`
        //   will turn into
        //     `nullableProp?: array<null<int>>`
        // This can be configured separately for variables and output types.
        //
        // optionalVariables: "wrapped"
        // optionalOutputs: "unwrapped"

        // In practice, it's often useful to add additional code after generating types.
        // We currently provide a simplistic way to do that via strings.
        // This _should_ be enough for most use cases, thanks to rescript's global namespacing.
        // See the 'generating additional code' section for more details
        //
        // appendToFragments: "...",
        // appendToQueries: "...",
        // appendToMutations: "...",
        // appendToSubscriptions: "...",
      },
      plugins: [
        "@rescript-graphql-codegen/operations"
      ]
    }
  }
}

export default config

Scalars module

As seen in codegen.ts, the plugins expect a particular scalarsModule to be present in your application. This module should contain one submodule for each scalar defined in your server’s schema; each of these submodules must contain a type t, describing the runtime value of that scalar type.

This gives you some additional flexibility around using these types. For basic scalars, such as the built-ins, these modules can be very simple:

module Boolean = {
  type t = bool
}
module Float = {
  type t = float
}
module Id = {
  type t = string
}
module Int = {
  type t = int
}
module String = {
  type t = string
}

At the same time, we can customize the interface of more particular scalars. For example, if we have a Date type, encoded to a string, we can create an opaque type to ensure proper encoding and decoding:

module Date: {
  type t
  let toJsDate: t => Js.Date.t
  let fromJsDate: Js.Date.t => t
} = {
  type t = string
  let toJsDate = ...
  let fromJsDate = ...
}

The ever-helpful rescript compiler should point out any missing scalars as they appear in generated code.

gql tag

@rescript-graphql-codegen/operations uses the gql tag to generate document nodes. If your use case doesn’t require compatibility with an existing library, we provide a default type within @rescript-graphql-codegen/lib, and tag implementation in @rescript-graphql-codegen/graphql-tag. To install:

# npm
npm i @rescript-graphql-codegen/lib @rescript-graphql-codegen/graphql-tag

# yarn
yarn add @rescript-graphql-codegen/lib @rescript-graphql-codegen/graphql-tag

If needed, this can be replaced with a binding to a custom document type. Assuming `Document.t` is where that is located:

@unboxed
type input =
  | String(string)
  | Document(Document.t)

@module("graphql-tag") @taggedTemplate
external gql: (array<string>, array<input>) => Document.t = "gql"

Generating additional code

For operations, frequently we want to generate some additional runtime code alongside each definition. For example, if we had this binding to @apollo/client’s useQuery:

// Apollo.res
type queryConfig<'variables, 'result> = {
  variables?: 'variables,
  ...<snip>
}
type queryResult<'variables, 'result> = {
  data: option<'result>,
  ...<snip>
}
@module("@apollo/client")
external useQuery: (Document.t, config<'variables, 'result>) => queryResult<'variables, 'result> = "useQuery"

We can then automatically generate hooks for generated queries by using the appendToQueries configuration option:

// in codegen.ts
...
  appendToQueries: `
    // This code will be inserted at the end of a generated query;
    // as such, the generated types ~variables~ and ~t~, along with the matching ~document~, will be in scope.
    let use: 
      Apollo.config<variables, t> => Apollo.queryResult<variables, t> = Apollo.useQuery(document, ...)
  `
...

For a more elaborate example, see this repository, which uses custom bindings to @apollo/client.

Clone this wiki locally