Gql Tag Operations Preset

Package nameWeekly DownloadsVersionLicenseUpdated
@graphql-codegen/gql-tag-operations-presetDownloadsVersionLicenseOct 20th, 2022

Installation

yarn add -D @graphql-codegen/gql-tag-operations-preset

This preset generates typings for your inline gql function usages, without having to manually specify import statements for the documents. All you need to do is import your gql function and run codegen in watch mode.

Huge thanks to Maël Nison, who conceptualized the foundation for this preset over here.

import { gql } from './gql'
 
// TweetFragment is a fully typed document node
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)
 
const TweetsQueryWithFragment = gql(/* GraphQL */ `
  query TweetsWithFragmentQuery {
    Tweets {
      id
      ...TweetFragment
    }
  }
`)

Getting Started

In order to use this preset, you need to add the following configuration to your codegen.yml:

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'src/path/to/your/schema.graphql',
  documents: ['src/**/*.ts', '!src/gql/**/*'],
  generates: {
    './src/gql/': {
      preset: 'gql-tag-operations-preset',
      plugins: []
    }
  }
}
export default config

Now start your codegen in watch mode via yarn graphql-codegen --watch.

Create a new file within your src directory, e.g. ./src/index.ts and add a query for your schema:

import { gql } from './gql'
 
// TweetsQuery is a fully typed document node!
const TweetsQuery = gql(/* GraphQL */ `
  query TweetsQuery {
    Tweets {
      id
    }
  }
`)

Next we can simply add our GraphQL client of choice and use the typed document! Let's try urql!

import { gql } from './gql'
import { useQuery } from 'urql'
 
// TweetsQuery is a fully typed document node/
const TweetsQuery = gql(/* GraphQL */ `
  query TweetsQuery {
    Tweets {
      id
      body
    }
  }
`)
 
const Tweets = () => {
  const [result] = useQuery({ query: TweetsQuery })
  const { data, fetching, error } = result
 
  if (fetching) return <p>Loading…</p>
  if (error) return <p>Oh no… {error.message}</p>
 
  return (
    <ul>
      {/* data is fully typed 🎉 */}
      {data.Tweets.map(tweet => (
        <li key={tweet.id}>{tweet.body}</li>
      ))}
    </ul>
  )
}

If we want to use fragments, we can use some utilities for accessing the fragment types:

import { gql, DocumentType } from '../gql'
 
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)
 
const Tweet = (props: {
  /* tweet property has the correct type 🎉 */
  tweet: DocumentType<typeof TweetFragment>
}) => {
  return <li data-id={props.tweet.id}>{props.tweet.body}</li>
}
 
const TweetsQuery = gql(/* GraphQL */ `
  query TweetsQuery {
    Tweets {
      id
      ...TweetFragment
    }
  }
`)
 
const Tweets = () => {
  const [result] = useQuery({ query: TweetsQuery })
  const { data, fetching, error } = result
 
  if (fetching) return <p>Loading…</p>
  if (error) return <p>Oh no… {error.message}</p>
 
  return (
    <ul>
      {data.Tweets.map(tweet => (
        <Tweet key={tweet.id} tweet={tweet} />
      ))}
    </ul>
  )
}

You can find a full gql-tag-operations example of this in the GraphQL Code Generator GitHub repository.

Improving bundle-size with the gql-tag-operations babel plugin

Because the generated code output is a single gql function that looks similar to the following code, code-splitting and tree-shaking is not easily possible.

import * as graphql from './graphql'
 
const documents = {
  '\n  query Foo {\n    Tweets {\n      id\n    }\n  }\n': graphql.FooDocument,
  '\n  fragment Lel on Tweet {\n    id\n    body\n  }\n': graphql.LelFragmentDoc,
  '\n  query Bar {\n    Tweets {\n      ...Lel\n    }\n  }\n': graphql.BarDocument
}
 
export function gql(
  source: '\n  query Foo {\n    Tweets {\n      id\n    }\n  }\n'
): typeof documents['\n  query Foo {\n    Tweets {\n      id\n    }\n  }\n']
export function gql(
  source: '\n  fragment Lel on Tweet {\n    id\n    body\n  }\n'
): typeof documents['\n  fragment Lel on Tweet {\n    id\n    body\n  }\n']
export function gql(
  source: '\n  query Bar {\n    Tweets {\n      ...Lel\n    }\n  }\n'
): typeof documents['\n  query Bar {\n    Tweets {\n      ...Lel\n    }\n  }\n']
 
export function gql(source: string): unknown
export function gql(source: string) {
  // OH NO gql accesses the documents object, the bundler cannot make assumption over what is actually used at runtime :(
  return (documents as any)[source] ?? {}
}

However, the @graphql-codegen/gql-tag-operations-preset package ships with a babel plugin for rewriting the gql usages to actual imports.

This is the code that you write:

import { gql, DocumentType } from '../gql'
 
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)

This is the code after the babel transformation:

import { TweetFragmentDoc } from './graphql'
 
const TweetFragment = TweetFragmentDoc

This will result in much smaller bundles when you code-split parts of your code.

Applying the babel plugin is straight-forward:

.babelrc.mjs
import { babelPlugin } from '@graphql-codegen/gql-tag-operations-preset'
export default {
  plugins: [
    // your other plugins such as typescript syntax stripping etc.
 
    // make sure the artifactDirectory is the same as your generates path within the `codegen.yml`
    [babelPlugin, { artifactDirectory: './src/gql' }]
  ]
}

Afterwards, you won't have to worry about bundle size anymore!

Using module augmentation for an existing gql function from a library

Sometimes the library you are using, let's use urql for this example, already exposes a gql function and instead of generating a new one you would prefer to just generate types that extend the type-definitions of that gql function.

This can be easily achieved by running the gql-tag-operations plugin in module augmentation mode!

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'src/path/to/your/schema.graphql',
  documents: ['src/**/*.ts', '!src/gql/**/*'],
  generates: {
    'src/gql/': {
      preset: 'gql-tag-operations-preset',
      plugins: [],
      presetConfig: {
        augmentedModuleName: '@urql/core'
      }
    }
  }
}
export default config

When the augmentedModuleName option is configured instead of the ./src/gql/index.ts file a ./src/gql/index.d.ts file is generated.

/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
 
declare module '@urql/core' {
  export function gql(
    source: '\n  query Bar {\n    Tweets {\n      ...Lel\n    }\n  }\n'
  ): typeof import('./graphql').BarDocument
  export function gql(source: string): unknown
}

Now you can simply use your gql function imported from @url/core in your application code for having fully typed document nodes.

import { gql } from 'urql'
 
const FooQuery = gql(/* GraphQL */ `
  query Foo {
    Tweets {
      id
    }
  }
`)
💡

In case you are using fragments you MUST use the gql-tag-operations babel plugin as otherwise the gql calls that reference global fragments will cause runtime errors, as the gql operation cannot find the global fragment. Unfortunately, it is also NOT possible to embed fragments with template string interpolation, as it breaks TypeScript type-inference.

import { gql } from 'urql'
 
// This causes an error without the babel plugin
const FooQuery = gql(/* GraphQL */ `
  query Foo {
    Tweets {
      ...SomeTweetFragment
    }
  }
`)

You can find a full gql-tag-operations-urql example of this in the GraphQL Code Generator GitHub repository.

Fragment Masking

Fragment masking is a powerful tool that allows building scalable UI components, where each component only has access to the data dependencies described by its fragments. The fragments of those components are then composed on a query operation. This pattern is also known as data masking in the context of relay. gql-tag-operations-preset allows using fragment masking with any GraphQL client.

Fragment masking is enabled via the fragmentMasking config option.

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'src/path/to/your/schema.graphql',
  documents: ['src/**/*.ts', '!src/gql/**/*'],
  generates: {
    'src/gql/': {
      preset: 'gql-tag-operations-preset',
      plugins: [],
      presetConfig: {
        fragmentMasking: true
      }
    }
  }
}
export default config

After enabling fragment masking and re-generating your codegen artifacts you now have access to a useFragment function, which is utilized for unmasking dependencies.

import { gql, FragmentType, useFragment } from '../gql'
 
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)
 
const Tweet = (props: {
  /* tweet property has the correct type 🎉 */
  tweet: FragmentType<typeof TweetFragment>
}) => {
  // we unmask our fragment to the actual type
  const tweet = useFragment(TweetFragment, props.tweet)
  return <li data-id={tweet.id}>{tweet.body}</li>
}
 
const TweetsQuery = gql(/* GraphQL */ `
  query TweetsQuery {
    Tweets {
      id
      ...TweetFragment
    }
  }
`)
 
const Tweets = () => {
  const [result] = useQuery({ query: TweetsQuery })
  const { data, fetching, error } = result
 
  if (fetching) return <p>Loading…</p>
  if (error) return <p>Oh no… {error.message}</p>
 
  return (
    <ul>
      {data.Tweets.map(tweet => {
        // TypeScript error, as the fragment properties are masked.
        tweet.body
        return <Tweet key={tweet.id} tweet={tweet} />
      })}
    </ul>
  )
}
💡

The default useFragment implementation does only data masking on a TypeScript type level. If you log the object, you can all properties, including the masked ones, are available on the object. For true runtime data masking, the GraphQL client must implement it. Currently, there is no client that does this (aside from relay which should best be used with relay-compiler).

Fragment Masking with custom unmask name

By default, the data unmask function name is useFragment, which follows the React hook naming convention. For React users we recommend keeping that name. If you need to customize the name of the function you can provide the fragmentMasking.unmaskFunctionName option.

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'src/path/to/your/schema.graphql',
  documents: ['src/**/*.ts', '!src/gql/**/*'],
  generates: {
    'src/gql/': {
      preset: 'gql-tag-operations-preset',
      plugins: [],
      presetConfig: {
        fragmentMasking: {
          unmaskFunctionName: 'getFragment'
        }
      }
    }
  }
}
export default config

Module augmentation with fragment masking

Similar to the gql module augmentation configuration it is also possible to generate type definitions for the unmask function. The fragmentMasking.unmaskFunctionName will be used when doing so.

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'src/path/to/your/schema.graphql',
  documents: ['src/**/*.ts', '!src/gql/**/*'],
  generates: {
    'src/gql/': {
      preset: 'gql-tag-operations-preset',
      plugins: [],
      presetConfig: {
        fragmentMasking: {
          augmentedModuleName: '@something/fragment'
        }
      }
    }
  }
}
export default config

Config API Reference

augmentedModuleName

type: string

Instead of generating a gql function, this preset can also generate a d.ts that will enhance the gql function of your framework.

E.g. graphql-tag or @urql/core.

Usage Examples

codegen.ts
 import type { CodegenConfig } from '@graphql-codegen/cli';
 
 const config: CodegenConfig = {
   // ...
   generates: {
     'path/to/file.ts': {
       preset: 'gql-tag-operations',
       plugins: [],
       presetConfig: {
         augmentedModuleName: '@urql/core'
       },
     },
   },
 };
 export default config;
fragmentMasking

type: FragmentMaskingConfig | boolean

Fragment masking hides data from components and only allows accessing the data by using a unmasking function.

Usage Examples

codegen.ts
 import type { CodegenConfig } from '@graphql-codegen/cli';
 
 const config: CodegenConfig = {
   // ...
   generates: {
     'path/to/file.ts': {
       preset: 'gql-tag-operations',
       plugins: [],
       presetConfig: {
         augmentedModuleName: '@urql/core',
         fragmentMasking: {
           augmentedModuleName: '@urql/fragment',
         },
       },
     },
   },
 };
 export default config;
gqlTagName

type: string default: `"gql"

E.g. graphql or gql.`

Specify the name of the "graphql tag" function to use

Usage Examples

codegen.ts
 import type { CodegenConfig } from '@graphql-codegen/cli';
 
 const config: CodegenConfig = {
   // ...
   generates: {
     'path/to/file.ts': {
       preset: 'gql-tag-operations',
       plugins: [],
       presetConfig: {
         gqlTagName: 'graphql'
       },
     },
   },
 };
 export default config;
Last updated on October 13, 2022