かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

初めての GraphQL。Code Generator で型を生成するまでのメモ

GraphQL に入門していて、GraphQL の schema などから TypeScript の型定義を自動生成するのがナウでヤングだと聞いたので試してみたのメモ。

この記事の対象
  • とりあえず GraphQL Code Generator で GraphQL の Schema から型定義ファイルを生成してみたい
  • 調べると高度な情報が出てくるけど断片的で、まずどうやって導入するのかにで詰まった

GraphQL Code Generator

GraphQL のスキーマから TypeScript の方を生成できるライブラリ。他にもあるとおもうけど情報が多かったので GraphQL Code Generator を使用することにしました。

インストール

$ npm i -D @graphql-codegen/cli
# graphpl も必要なのでインストールする
$ npm i graphql

設定ファイルの生成

npm run graphql-codegen init コマンドを実行すると対話式で設定ファイル (codegen.yml) が作成できる
Missing script: "graphql-codegen" になる場合は npx を使えばOK

$ npm run graphql-codegen init

    Welcome to GraphQL Code Generator!
    Answer few questions and we will setup everything for you.

# どこで使うか・フレームワークを使うか
? What type of application are you building? (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◯ Backend - API or server
 ◯ Application built with Angular
❯◯ Application built with React
 ◯ Application built with Stencil
 ◯ Application built with other framework or vanilla JS
# スキーマの場所 (よく分かってない)
? Where is your schema?: (path or url) (http://localhost:4000) http://localhost:4000
# オペレーションとフラグメントの場所 (よく分かってない)
? Where are your operations and fragments?: (src/**/*.graphql) src/graphql/**/*.graphql
# 使用するプラグインの選択
? Pick plugins: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ TypeScript (required by other typescript plugins)
❯◉ TypeScript Operations (operations and fragments)
 ◯ Flow (required by other flow plugins)
 ◯ Flow Operations (operations and fragments)
 ◯ TypeScript React Apollo (typed components and HOCs)
❯◉ TypeScript GraphQL files modules (declarations for .graphql files)
 ◯ TypeScript GraphQL document nodes (embedded GraphQL document)
 ◯ Introspection Fragment Matcher (for Apollo Client)
 ◯ Urql Introspection (for Urql Client)
# 型定義ファイルの出力先
? Where to write the output: (src/generated/graphql.ts) src/generated/graphql.ts
# introspection ファイルを生成するか (よく分かってない)
? Do you want to generate an introspection file? (Y/n) Y
# 設定ファイル
? How to name the config file? (codegen.yml) codegen.yml
# codegen を実行する npm script のメソッド名
? What script in package.json should run the codegen? codegen
Fetching latest versions of selected plugins...

    Config file generated at codegen.yml
    
      $ npm install

    To install the plugins.

      $ npm run codegen

    To run GraphQL Code Generator.

# 必要なパッケージをインストール
$ npm install

生成された設定ファイル codegen.yml

overwrite: true
# スキーマの場所
schema: "http://localhost:4000"
# オペレーションとフラグメントの場所
documents: "src/graphql/**/*.graphql"
generates:
  # 型定義ファイルの出力先
  src/generated/graphql.ts:
    # 使用するプラグインの
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-graphql-files-modules"
  ./graphql.schema.json:
    # introspection ファイルを生成するためのプラグイン
    plugins:
      - "introspection"

CLI で理解できていなかった箇所の設定

Configuration options | GraphQL Code Generator

schema

schema (required) - A URL to your GraphQL endpoint, a local path to .graphql file, a glob pattern to your GraphQL schema files, or a JavaScript file that exports the schema to generate code from. This can also be an array that specifies multiple schemas to generate code from.

schema field
The schema field should point to your GraphQLSchema - there are multiple ways you can specify it and load your GraphQLSchema.
cf. schema field

実際に使うGraphQL サーバーのエンドポイントのURLか、GraphQLサーバー側のスキーマを定義したファイルを指定すれば良さそう

documents

documents - Array of paths or glob patterns for files which export GraphQL documents using a gql tag or a plain string; for example: ./src/**/*.graphql. You can also provide these options with a string instead of an array if you're dealing with a single document.

documents field
The documents field should point to your GraphQL documents: query, mutation, subscription, and fragment.
documents is only required if you use plugins that generate code for the client-side.
cf. documents field

クライアントサイドから GraphQL で呼び出す際の Query が書かれているファイルの場所を指定すれば良さそう

introspection

Introspection: 内観

GraphQL Playground は schema の詳細を調べる機能を提供します。Introspection という GraphQL の基本的な技術で、これを用いることによって graph の schema に関する詳細情報を全て表示することができます。
cf. 1. Schema を構築する - Apollo Basics - Apollo GraphQL Docs

Reserved Names
Types and fields required by the GraphQL introspection system that are used in the same context as user-defined types and fields are prefixed with "__" two underscores. This in order to avoid naming collisions with user-defined GraphQL types.
Otherwise, any Name within a GraphQL type system must not start with two underscores "__".
cf. Introspection | GraphQL

__ から始まる名前で定義された GraphQL chema の構成などがどうなっているのかを見ることができるようにするための機能っぽい


Schema を作成する

Schema は実際の GraphQL server かサーバーのスキーマファイルを指定します

  1. Express で簡易な GraphQL Server を作成して試してみる
  2. GraphQL のスキーマファイルを作成して試してみる

codegen.ymldocuments の指定は削除しておく

1. Express で簡易な GraphQL Server を作成する

/api ディレクトリを作成して Express (TypeScript) で簡易な GraphQL サーバーを作成し Code Generator を試してみます

構成

/api
  |- server.ts # Express server
  |- schema.ts # GraphQL の Schema を定義
  |- teconfig.json

必要なパッケージのインストール

$ npm i express graphql express-graphql
$ npm i -D typescript @types/express @types/node ts-node-dev

teconfig.json (npx tsc --init したものに少し手を入れました)

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

package.json にサーバーを開始する npm script を追加

{
  "scripts": {
+   "server": "ts-node-dev server.ts"
  }
}

GraphQL のスキーマを定義

簡単な TODO リストを返すスキーマを定義しました

schema.ts

import { buildSchema } from 'graphql';

export const schema = buildSchema(`
  type Query {
    hello: String
    todos: [Todo]
    todo(id: String): Todo
  }
  type Todo {
    id: String!
    title: String!
  }
`);

export type TodoType = {
  id: string;
  title: string;
};

Express GraphQL Server

作成したスキーマが問題なく動作しているか確認できるように GraphiQL を起動できるようにします

server.ts

import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { schema, TodoType } from './schema';

// ダミーデータ
const Todos: TodoType[] = [
  {
    id: '1',
    title: 'todo1',
  },
  {
    id: '2',
    title: 'todo2',
  },
  {
    id: '3',
    title: 'todo3',
  },
];

// GraphQL の Resolver
const root = {
  hello: () => {
    return 'Hello world!';
  },
  todo: ({ id }: { id: string }) => {
    const todo = [...Todos].find((todo) => todo.id === id);
    console.log(todo, id);

    return todo;
  },
  todos: () => {
    return [...Todos];
  },
};

const app = express();
app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  })
);
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');
動作確認
$ npm run server
Running a GraphQL API server at http://localhost:4000/graphql

http://localhost:4000/graphql にアクセスして GraphiQL の画面が表示され Document に定義したスキーマが表示されていればOK

GraphiQL

GraphQL サーバーを使って GraphQL Code Generator を実行する

設定ファイルの schema に作成した GraphQL サーバーを指定

codegen.yml

overwrite: true
- schema: "http://localhost:4000"
+ schema: "http://localhost:4000/graphql"
generates:
  src/generated/graphql.d.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-graphql-files-modules"
  ./graphql.schema.json:
    plugins:
      - "introspection"

GraphQL Code Generator で型ファイルの生成

※ 予め GraphQL サーバーを起動しておく

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml

  ✔ Parse configuration
  ❯ Generate outputs
    ❯ Generate src/generated/graphql.d.ts
  ✔ Parse configuration
  ✔ Generate outputs

./graphql.schema.json./src/generated/graphql.d.ts が生成されていればOK!

graphql.d.ts にスキーマの方が TypeScript の型として出力されている

./src/generated/graphql.d.ts

export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Query = {
  __typename?: 'Query';
  hello?: Maybe<Scalars['String']>;
  todo?: Maybe<Todo>;
  todos?: Maybe<Array<Maybe<Todo>>>;
};


export type QueryTodoArgs = {
  id?: InputMaybe<Scalars['String']>;
};

export type Todo = {
  __typename?: 'Todo';
  id: Scalars['String'];
  title: Scalars['String'];
};

2. GraphQL のスキーマファイルを作成する

local server を用意せず schema の定義ファイルから型生成を行う
構成

/
 |-/src
 |- codegen.yml
 |- package.json
 |- schema.graphql # GraphQL サーバーのスキーマ

GraphQL サーバーのスキーマを定義

Express サーバーのものと同じ簡単な TODO リストを返すスキーマを定義する

./schma.graphql (サーバーがない場合はどこに置くのが良いのだろう🤔)

type Query {
  hello: String
}
type Todo {
  id: String!
  title: String!
}
type Query {
  todos: [Todo]
}
type Query {
  todo(id: String): Todo
}

GraphQL Code Generator の schema にスキーマファイルのパスを指定する

codegen.yml

overwrite: true
- schema: 'http://localhost:4000/graphql'
+ schema: './schema.graphql'
generates:
  src/generated/graphql.d.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-files-modules'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

GraphQL Code Generator で型ファイルの生成

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml

  ✔ Parse configuration
  ✔ Generate outputs

./graphql.schema.json./src/generated/graphql.d.ts が生成されていればOK!


Document の指定

document はクライアントから呼ぶ query なので暫定的に /src/graphql ディレクトリ内に .graphql ファイルで作成する
構成

/
 |- /src
 |    |- /graphql
 |        |- getTodo.graphql
 |        |- hello.graphql
 |- codegen.yml
 |- package.json
 |- schema.graphql

schema.graphql

type Query {
  hello: String
}
type Todo {
  id: String!
  title: String!
}
type Query {
  todos: [Todo]
}
type Query {
  todo(id: String): Todo
}

graphql.document (クライアントから実行する query ) の作成

/src/graphql/getTodo.graphql

query GetTodos {
  todos {
    id
    title
  }
}

query GetTodo($id: String) {
  todo(id: $id) {
    id
    title
  }
}

/src/graphql/hello.graphql

query SeyHello {
  hello
}

GraphQL Code Generator の document を指定

documents にクエリファイルのパスを指定する

codegen.ymll

overwrite: true
schema: './schema.graphql'
+ documents: './src/graphql/**/*.graphql'
generates:
  src/generated/graphql.d.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-files-modules'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

GraphQL Code Generator で型ファイルの生成

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml

  ✔ Parse configuration
  ✔ Generate outputs

./graphql.schema.json./src/generated/graphql.d.ts が生成されることを確認。
./src/generated/graphql.d.ts 内に クライアントから呼び出す想定のクエリ (GraphQL.document) の型が含まれていればOK

./src/generated/graphql.d.ts

// … 略
export type GetTodosQueryVariables = Exact<{ [key: string]: never }>;

export type GetTodosQuery = {
  __typename?: 'Query';
  todos?: Array<{
    __typename?: 'Todo';
    id: string;
    title: string;
  } | null> | null;
};

export type GetTodoQueryVariables = Exact<{
  id?: InputMaybe<Scalars['String']>;
}>;

export type GetTodoQuery = {
  __typename?: 'Query';
  todo?: { __typename?: 'Todo'; id: string; title: string } | null;
};

export type SeyHelloQueryVariables = Exact<{ [key: string]: never }>;

export type SeyHelloQuery = { __typename?: 'Query'; hello?: string | null };

declare module '*/getTodo.graphql' {
  import { DocumentNode } from 'graphql';
  const defaultDocument: DocumentNode;
  export const GetTodos: DocumentNode;
  export const GetTodo: DocumentNode;

  export default defaultDocument;
}

declare module '*/hello.graphql' {
  import { DocumentNode } from 'graphql';
  const defaultDocument: DocumentNode;
  export const SeyHello: DocumentNode;

  export default defaultDocument;
}
終えてみて

GraphQL Code Generator で TypeScript の型生成すると幸せになれる。と聞いていましたがイマイチやり方が判らず放置していたので今回時間を作って着手できてよかったです。
やはりまずは公式ドキュメントをよく読んで、プレーンなチュートリアルをやってみること。次にプラグインなどのカスタマイズをして、その時々のエラーをひとつひとつ解決していくこと。という地道な方法が一番理解できるな〜と感じました。

今週のアイカツ格言 千里の道も一歩から


📝 Tips

schema が URL の時 GraphQL が動作している URL を指定しないとエラー

server.ts

app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  })
);
app.listen(4000);

codegen.yml

overwrite: true
schema: 'http://localhost:4000'
generates:
    src/generated/graphql.ts:

graphqlHTTP/graphql に割り当てられているので、schema にローカルサーバーのルートである http://localhost:4000 を指定しているとエラーになる

Run codegen

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml
  ✔ Parse configuration
  ❯ Generate outputs
    ❯ Generate src/generated/graphql.d.ts
      ✖ Load GraphQL schemas
        → Failed to load schema
        Load GraphQL documents
        Generate
    ❯ Generate ./graphql.schema.json
      ✖ Load GraphQL schemas
        → Failed to load schema
        Load GraphQL documents
        Generate

Something went wrong Failed to load schema for "./graphql.schema.json"
        Failed to load schema from http://localhost:4000/:

        Unexpected token < in JSON at position 0
        SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)

=> Failed to load schema from http://localhost:4000/:

👇 graphqlHTTP が動作しているURLにすればOK

codegen.yml

overwrite: true
- schema: 'http://localhost:4000'
+ schema: 'http://localhost:4000/graphql'
generates:
    src/generated/graphql.ts:

Run codegen

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml

  ✔ Parse configuration
  ❯ Generate outputs
    ❯ Generate src/generated/graphql.ts
  ✔ Parse configuration
  ✔ Generate outputs

typescript-graphql-files-modules プラグインを使う場合、生成する型ファイルの拡張子が .d の形でないとエラーになる

codegen.yml

overwrite: true
schema: './schema.graphql'
generates:
  # 生成するファイルの拡張子が `.d` でない場合
  src/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-files-modules'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

Run codegen

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml
✔ Parse configuration
  ❯ Generate outputs
    ❯ Generate src/generated/graphql.ts
      ✔ Load GraphQL schemas
      ✔ Load GraphQL documents
      ✖ Generate
        → Unable to find template plugin matching typescript-graphql-files-modules
    ✔ Generate ./graphql.schema.json

Something went wrong Plugin "typescript-graphql-files-modules" validation failed: for "src/generated/graphql.ts"
            Plugin "typescript-graphql-files-modules" requires extension to be ".d.ts"!

=> Plugin "typescript-graphql-files-modules" requires extension to be ".d.ts"!

👇 codegen.yml

overwrite: true
schema: './schema.graphql'
generates:
# .d.ts に変更
- src/generated/graphql.ts:
- src/generated/graphql.d.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-files-modules'
  ./graphql.schema.json:
    plugins:
      - 'introspection'

Run codegen

$ npm run codegen
> graphql-codegen@1.0.0 codegen
> graphql-codegen --config codegen.yml
  ✔ Parse configuration
  ✔ Generate outputs

cf. Unable to find template plugin matching typescript-operations · Issue #2043 · dotansimha/graphql-code-generator · GitHub


[参考]

やっぱアイカツ格言なんだよな…