GraphQLのCustom Scalarsを使って再帰的なデータ構造を扱ってみる
サマリ
- GraphQLでは再帰的なデータ構造を定義することができるが、再帰的なクエリを書くことはできない
- カスタムスカラーを定義すると、再起的なデータ構造を扱うことができる
※ただし、クライアント側で型を再定義する必要がある。型の二重定義にならないように工夫が必要。
※どうしても再帰的なデータ構造を扱いたい時のみお使いください。
GraphQLで再帰的なデータ構造を扱えるか
GraphQLでは再起的なデータ構造を定義することが可能です。
type Category {
id: ID!
name: String!
children: [Category!]!
}
type Query {
categories(): [Category!]!
}
しかし、再帰的なクエリを上手く書くことはできません。
再帰を扱うためにfragmentを使って書きたいところですが、以下のようなエラーとなります。
fragment CategoryFields on Category {
id
name
children {
...CategoryFields
# Cannot spread fragment "CategoryFields" within itself.GraphQL: Validation
}
}
# ネストしたクエリを自前で書くことはできる
query fetchCategories() {
categories {
id
name
children {
id
name
children {
id
name
}
}
}
}
参考
カスタムスカラーを用いる
GraphQLではデフォルトで Int
String
などの型が定義されていますが、これらの型は自由に拡張することが可能です。これを用いて、再起的なデータ構造をもつ型をカスタムスカラーとして定義してみましょう。
ここでは、以下の構成でのサンプルコードを紹介します。
- 言語:TypeScript
- サーバーサイド:Nexus(Apollo Server)
- フロントエンド:Apollo Client(Graphql Codegen)
Nexus(Apollo Server)
公式ドキュメントはこちら。
公式ドキュメントだけだとやや分かりづらかったので、以下にコメント付きのサンプルコードを示します。
import { Kind } from 'graphql'
import { scalarType } from 'nexus'
export type Category = {
id: string
name: string
children: Category[]
}
export const CategoryScalar = scalarType({
name: 'Category', // スカラー型の名前
asNexusMethod: 'category', // nexusで扱う際の名前。t.category()のメソッドが定義される。
description: '', // 説明
sourceType: { // ここで対応するTypeScriptの型を定義
module: __filename, // TypeScriptの型が定義されているファイルの絶対パスを指定。ここでは、このファイル自身とする
export: `Category`, // TypeScriptの型の名前(4行目でexportしているCategory型を指定)
},
serialize(value) { // レスポンスをserialize
// validationして、valueがCategory型かどうかをチェック(省略)
// 必要があればvalueを整形してから返す
return value
},
parseValue(value) { // リクエストをdeserialize
// validationして、valueがCategory型かどうかをチェック(省略)
// 必要があればvalueを整形してから返す
return value
},
parseLiteral(ast) {
// parseLiteralがどう使われているのかよくわからなかった… :pray:
const { value } = ast
return value
},
})
nexusの場合、これをmakeSchemaのtypesで指定すればOKです。
export const schema = makeSchema({
types: [Query, Mutation, ...types, CategoryScalar],
// ...
これでCategory型を定義することができました。
Graphql Codegen (Apollo Client)
クライアント側でGraphQL Code Generatorを使っている場合、サーバー側の定義が終わった時点でクライアントにも Category
型が定義されますが、any型となってしまうためこのままでは使えません。
正しく型を定義するために、 codegen.yml
に以下を追記します。
-
scalars
にCategory
を追加 - addプラグインをインストールし、生成されるファイルにimport文を追記する
-
src/graphql/types/Category.ts
で型を再定義する。
overwrite: true
schema: 'http://localhost:3000/graphql'
documents: 'src/graphql/**/*.graphql'
generates:
src/graphql/generated.ts:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
- 'add'
config:
withComponent: false
withHooks: true
withHOC: false
scalars:
Category: Category
content:
- "import { Category } from './types/Category';"
このままだと src/graphql/types/Category.ts
型を再定義する必要があり、サーバーサイドでの型定義と二重定義になってしまい微妙です。
サーバーサイドの型定義ファイルを直接参照できるようなプロジェクト構造でない限り根本的な解決策はありませんが、自分の場合は -> 後々モノレポ化して解決しました👏。graphql-codegen
の実行時にサーバーサイドのリポジトリからファイルを cp
するようなスクリプトを書いて一旦の解決としました
※ここでは最低限の設定を紹介しましたが、より堅牢な書き方もできます。以下を参考にどうぞ。
理想的な解決策ではありませんが、どうしても再帰的なデータ構造を扱いたくなった際に検討してみてください。
Discussion