⚙️

server-preset+crilent-presetを使用したgraphql-codegenの設定ファイルを作成してみた

2024/02/01に公開

1.はじめに

Graphql Code Generatorには様々なプラグインが存在しますが、今回はserver-presetとclient-presetを使用します。(Presetは、基本的な開発に必要となるプラグインの組み合わせを提供してくれます。)
本記事では、設定した内容がどのように自動生成されたファイルに作用しているか見ていきます。

This plugin is meant to be used for low-level use cases or as building block for presets.
For building a GraphQL client application we recommend using the client-preset.
For building a GraphQL server schema we recommend using the server-preset.

ここでは、low-level と表記されていますが、server-presetの方ではスケーラビリティについて言及されています。
設定ファイルのgeneratesで細かく分岐させずとも、フォルダ構成で関連ごとにファイルを切り分けられる点は便利ですね。
https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset#conventions-to-support-schema-modules

2.完成サンプルコード

codegen.ts
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files';
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'src/schema/**/*.graphql',
  generates: {
    'src/schema': defineConfig({
      typesPluginsConfig: {
        contextType: '@/schema/context#Context', 
      },
    }),
    'src/gql/': {
      preset: 'client',
      presetConfig: {
        fragmentMasking: { unmaskFunctionName: 'getFragmentData' } 
      },
      documents: ['src/app/**/*.tsx'],
    },
  },
  hooks: {
    afterAllFileWrite: ['prettier --write', 'eslint --fix'],
  },
};
export default config;

2-1.フォルダ構成

一見多いように見えますが、ほとんどがGraphql Code Generatorによって自動生成されたフォルダやファイルで構成されています。

./src/
├── gql                         //client-presetによって自動生成(フォルダ配下も含めて)
│   ├── fragment-masking.ts 
│   ├── gql.ts              
│   └── graphql.ts          
│
├── schema
│   ├── base
│   │   └── schema.graphql
│   ├── context.ts
│   ├── resolvers.generated.ts  //server-presetによって自動生成
│   ├── typeDefs.generated.ts   //server-presetによって自動生成
│   ├── types.generated.ts      //server-presetによって自動生成
│   └── user
│       ├── resolvers           //server-presetによって自動生成(フォルダ配下も含めて)
│       │   ├── Query
│       │   │   └── user.ts     *ファイルは自動生成されるが、Usecaseな処理を追記する必要あり
│       │   └── User.ts         *ファイルは自動生成されるが、Usecaseな処理を追記する必要あり
│       ├── schema.graphql
│       └── schema.mappers.ts

3. server-preset関連

3-1. contextTypeについて

結論:contextTypeがない場合、自動生成されるQueryResolversの型引数ContextTypeがanyになります。
リゾルバーに共通して参照したい値がある場合などに有効です。

src/schema/user/resolvers/Query/user.ts(一応自動生成ファイル)
import type { QueryResolvers } from './../../../types.generated';

export const user: NonNullable<QueryResolvers['user']> = async (_parent, _arg, _ctx) => {
  // ここに任意の処理を追加。関数やファイル自体は自動で生成される
  // _ctxには、任意のcontextを設定可能。
  // 今回の場合、@/schema/context#Context がcontextのインターフェースとなる
  return null;
};
 src/schema/types.generated.ts(自動生成ファイル)
export type QueryResolvers<
  ContextType = Context, // 設定がない場合、ContextType = anyになります
  ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'],
> = {
  user?: Resolver<
    Maybe<ResolversTypes['User']>,
    ParentType,
    ContextType,
    RequireFields<QueryuserArgs, 'id'>
  >;
};

QueryResolvers型は自動生成される型になりますが、ContextTypeをカスタマイズする場合にはcodegen.tsファイルに参照先の型を追加する必要があります。

 codegen.ts
typesPluginsConfig: {
  contextType: '@/schema/context#Context', 
 },

@/schema/context#Contextの中身のコードは、AppRouterとApolloServerにも関連してくるため、別記事として取り上げます。

4. client-preset関連

4-1. fragmentMaskingについて

結論:fragmentMaskingがない場合、useFragmentが関数として自動生成されますが、Reactのhook関数ではないため、ESLintの設定によっては引っかかる可能性があります。この問題を回避するには以下の設定によって関数名を変換します。
この関数自体は、GraphQLクエリで取得したデータから、実体のデータを取り出す際に使用します。

 codegen.ts
presetConfig: {
  fragmentMasking: { unmaskFunctionName: 'getFragmentData' } // useFragment → getFragmentData
},

https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#the-usefragment-helper

4-2. documentsについて

結論:今回の場合、src/app/**/*.tsx内でGraphQLクエリを作成すると、自動生成されたsrc/gql/gql.tsの方にも新たにgraphql関数が追加されます。documentsで定義されたファイルの対象にあるGraphQLクエリであれば、gql.tsに追記されていきます。

 codegen.ts
typesPluginsConfig: {
  ...,
  documents: ['src/app/**/*.tsx'],
 },
src/app/hoge/sample.tsx
// Code Generatorの処理が走り終わるまではqueryはunknown型のままなので、エラーが出ます。
// 今回のように、hooksでlintや整形ツールを実行する場合、正しく定義できていても処理が終わるまでは、エラーが出る点に注意です。
const query = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
    }
  }
`);
 src/gql/gql.ts(自動生成ファイル)
export function graphql(source: string): unknown;
export function graphql(
  source: '\n  query GetUser($id: ID!) {\n    user(id: $id) {\n      id\n      name\n    }\n  }\n',
): (typeof documents)['\n  query GetUser($id: ID!) {\n    user(id: $id) {\n      id\n      name\n    }\n  }\n'];

5. Mapperファイル

prismaを使用している場合、@prisma/clientで定義されている型とGraphql Code Generatorで自動生成される型が異なる場合があります。そのため、@prisma/clientの型を自動生成に反映させる必要があります。
ファイル名.graphqlとファイル名.mappers.tsのファイル名は同じでないと自動生成に反映されませんでした。。。

 ├── hoge.graphql
 └── hoge.mappers.ts

自動生成がうまくいくと定義した型がtypes.generated.tsにimportされます。

src/schema/user/schema.mappers.ts
// 設定ファイル(codegen.ts)のmapperTypeSuffixは、型エイリアスによって再現可能です。
export type { User as UserMapper } from '@prisma/client';
src/schema/types.generated.ts(自動生成ファイル)
import type { UserMapper } from './user/schema.mappers';

おまけ

graphql-scalarsライブラリを入れた場合と、加えてcodegen.tsにscalarsを定義した場合の挙動について
現状:自動生成されたgraphql.tsとtypes.generated.tsの両ファイルにScalars定義が作成されるものの、codegenが失敗することはありませんでした。graphql-scalarライブラリのみでも今のところは動いていますが、エラーが出た場合は、追記しようと思います。

src/schema/types.generated.ts(自動生成ファイル)
export type Scalars = {
  ID: { input: string; output: string | number };
  String: { input: string; output: string };
  Boolean: { input: boolean; output: boolean };
  Int: { input: number; output: number };
  Float: { input: number; output: number };
  DateTime: { input: Date | string; output: Date | string }; //設定ファイルに定義しなくとも、any型ではなくなる
};

https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset#adding-custom-graphql-scalars

設定ファイルに定義した場合

 codegen.ts
'src/gql/': {
  preset: 'client',
  presetConfig: {
    fragmentMasking: { unmaskFunctionName: 'getFragmentData' } 
  },
  documents: ['src/app/**/*.tsx'],
+  config: {
+    scalars: { DateTime: 'string' },
+  },
},

以下のようにgraphql.tsファイルのScalars.DateTimeがstring型になります。(ない場合は、any型)

src/gql/graphql.ts(自動生成ファイル)
export type Scalars = {
  ID: { input: string; output: string };
  String: { input: string; output: string };
  Boolean: { input: boolean; output: boolean };
  Int: { input: number; output: number };
  Float: { input: number; output: number };
  DateTime: { input: string; output: string }; // any → string
};

Discussion