Apollo CLIでGraphQLのSchemaからTypeScriptの型定義を自動生成する

GraphQL(Apollo Client)をTypeScriptで利用する際に、TypeScriptの型定義と
Schemaの定義とで二重に定義を記述する手間を省くため、Apollo CLI(apollo-tooling)を利用してSchemaからTypeScriptの型定義を自動生成させます。

[The Apollo CLI - APOLLO DOCS](https://www.apollographql.com/docs/devtools/cli/)
[Apollo CLI - Github](https://github.com/apollographql/apollo-tooling)

名称は「Apollo CLI」の様ですが、Githubのリポジトリ名は「apollo-tooling」、npm上では「apollo」で、また「Apollo Client」とも似ていて少々紛らわしいです。
本記事内では、「apollo-tooling」の名称を用いることにします。

# apollo-tooling(Apollo CLI) のインストール

apollo-tooling自体は、Apolloで提供されている各種ライブラリをコマンドライン上で操作することを可能にするためのツールです。

[Apollo CLI - npm](https://www.npmjs.com/package/apollo)

## Error: Cannot use GraphQLSchema "[object GraphQLSchema]" from another module or realm.

既にdependenciesの方で`"graphql": "^14.5.8"`をインストールしている状態で、
追加で`"apollo": "^2.23.0"`をインストールしたところ、後にapollo-toolingのコマンドを実行する際に
下記エラーに遭遇しました。

```
Error: Cannot use GraphQLSchema "[object GraphQLSchema]" from another module or realm.
Ensure that there is only one instance of "graphql" in the node_modules
directory. If different versions of "graphql" are the dependencies of other
relied on modules, use "resolutions" to ensure only one version is installed.
https://yarnpkg.com/en/docs/selective-version-resolutions
Duplicate "graphql" modules cannot be used at the same time since different
versions may have different capabilities and behavior. The data from one
version used in the function from another could produce confusing and
spurious results.
```

node_modules > apollo > package.json を確認したところ、dependenciesに`"graphql": "14.0.2 - 14.2.0 || ^14.3.1"`の記載があり、
node_modules > apollo > node_modules 配下にgraphqlフォルダがありました。
これが、既存のものと衝突してしまっていたようです。
エラーメッセージにある「[(yarnの)resolutions](https://classic.yarnpkg.com/ja/docs/selective-version-resolutions/)を使え」という指示に従い、package.jsonに下記項目を追加して node_modules を入れなおしたところ、無事エラーが解消されました。
同様のエラーが発生する可能性がある場合は、インストールを実行する前に予め追記しておくことをお勧めします。

npmをご利用の場合は、直接バージョンを書き換えることになりそうです。(筆者は実際に試してはいません。)
https://github.com/apollographql/apollo-tooling/issues/1296#issuecomment-497862004

```js
"resolutions": {
  "graphql": "^14.5.8",
  "graphql-tag": "^2.10.1"
},
// graphql-tagの重複エラーは見かけませんでしたが、重複インストールは発生していたのでついでに追加しました。
```

エラー回避の下準備が完了したら、下記コマンドでインストールします。

```sh
yarn add -D apollo

# OR

npm install -D apollo
```

# apollo.config.js の準備

公式サイトの説明に従って、プロジェクトのルートディレクトリに「apollo.config.js」を準備します。
[Configuring Apollo projects](https://www.apollographql.com/docs/devtools/apollo-config/)
今回は、リモートエンドポイントからSchema情報を取得します。

```js:apollo.config.js
module.exports = {
  client: {
    name: 'client',
    includes: ['src/**/*.ts'],
    tagName: 'gql',
    addTypename: true,
    service: {
      // remote endpoint
      name: 'sever',
      url: 'https://server/graphql',
      headers: {
        authorization: 'nanikashiranohituyounatoken...',
      },
    },
  },
};
```

`tagName`はGraphQLクエリを記述したテンプレートリテラルを引き渡す関数名です。(デフォルト値`gql`)
上記では`src`配下の`.ts`ファイル内で、`gql`関数の引数値にあるテンプレートリテラルを参照しろという指示になっています。
また、`addTypename`にて型定義ファイル生成の際にSchemaで定義されているType名を追記するとしています。(デフォルト値`true`)

`client.service`にて、参照先のサーバを指定しています。

# 型定義生成実行

package.jsonにscriptを追加して実行します。
オプションが色々と用意されています。
https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output

型定義出力のコマンドは`apollo client:codegen [OPTIONS] [OUTPUT]`です。
`[OUTPUT]`は出力ファイルのパスです。特に指定しない場合、元ファイルと同じディレクトリ内に`__generated__`というフォルダを生成し、クエリ毎に定義ファイルを出力します。
この際、クエリの名前が重複しているとエラーになります。

```json
"scripts": {
  "apollo:codegen": "apollo client:codegen --target=typescript --globalTypesFile=src/types/globalTypes.ts"
}
```

typescript以外にも、swift, flow, scala に対応している為、`--target=typescript`で明記します。
共通で利用する型定義ファイル(`globalTypesFile`)が、そのままだとルートフォルダ上(`/__generated__/globalTypes.ts`)に生成された為、`/src/types/`配下に指定しています。

## 予めSchemaファイルをダウンロードしておく

サーバアクセス時のトークンの期限が短い等々毎回エンドポイント参照が面倒な場合は、事前にローカルにダウンロードしておきます。

```json
"scripts": {
  "apollo:download": "apollo client:download-schema src/type/schema.json",
  "apollo:codegen": "apollo client:codegen --target=typescript --localSchemaFile=src/type/schema.json"
}
```

`apollo:download`でダウンロードして`src/type/schema.json`に保存し、`apollo:codegen`実行時にはこのファイルを参照させます。

# Custom Scalarの対応

日付などの独自に定義した型(Custom Scalar)はデフォルトで`any`になります。
`apollo:codegen`のオプションに`--passthroughCustomScalars`を指定すると、Custom Scalarの名前がそのまま出力されます。

ただ、該当する型がglobalTypesFileで定義さることはないので、予め自分で用意しておく必要があります。

```ts
// 日付型の Custom Scalar の名前が GqlDate の場合
declare global {
  type GqlDate = Date;
}
```

また、実際に受け取ったデータがDate型に変換されるわけではないので、別途データ型変換処理を実施する必要はあります。

```ts
// 「GqlDate」が、「ISO 8601」形式の文字列型で送信されるとした場合

// GqlDate（ISO 8601）判定
export const isGqlDate = (arg: any): arg is GqlDate => {
  return (
    typeof arg === 'string' &&
    /^\d{4}(-\d\d){2}T\d{2}(:\d\d){2}.\d{2,3}Z$/.test(arg)
  );
};

// GqlDate文字列をDateオブジェクトに変換
export const convertGqlDateToDate = <T extends Object = {}>(arg: T): T => {
  let res: { [key: string]: any } = {};

  for (const [key, value] of Object.entries(arg)) {
    if (isGqlDate(value)) {
      res[key] = new Date(value);
    } else if (Object.prototype.toString.call(value) === '[object Object]') {
      res[key] = convertGqlDateToDate(value);
    } else if (Array.isArray(value)) {
      // GqlDate[] には対応していない……
      res[key] = value.map(item => {
        if (Object.prototype.toString.call(value) === '[object Object]') {
          return convertGqlDateToDate(item);
        }
        return item;
      });
    } else {
      res[key] = value;
    }
  }
  return res as T;
};

// DateオブジェクトをGqlDate（ISO 8601）に変換
export const convertDateToGqlDate = <T extends Object = {}>(arg: T): T => {
  let res: { [key: string]: any } = {};

  // ...略

  return res as T;
};

// variablesとonCompletedで変換処理を挟む

const [state, setState] = useState<StateType | null>(null);

const {loading, error} = useQuery<ResultType, VariablesType>(
  ANY_QUERY,
  {
    variables: convertDateToGqlDate<VariablesType>(anyData),
    onCompleted(result) {
      setState(
        convertGqlDateToDate<StateType>(result.something)
      );
    },
  },
);

```

Apollo Client側でのCustom Scalarの導入に関しては、ライブラリを作成している方もいらっしゃるようです。
正式導入まだかな。。。

https://github.com/apollographql/apollo-feature-requests/issues/2


# 参考情報
[Automatically Generate TypeScript Definitions for GraphQL Queries with Apollo Codegen](https://medium.com/open-graphql/automatically-generate-typescript-definitions-for-graphql-queries-with-apollo-codegen-e73eae72b561)
[TypeScript + Apollo ClientでGraphQLのデータに型を付ける](https://blog.shibayu36.org/entry/2019/10/28/193000)
[Apollo toolingでTypeScriptのクライアントの型定義を自動生成](https://scrapbox.io/kadoyau/Apollo_tooling%E3%81%A7TypeScript%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%AE%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90)