この記事は一休.com Advent Calendar 2024 2日目の記事です。
概要
GraphQLアプリケーションにおいて、ランタイムに影響を与えずに型定義のみ上書きしたいケースがあり、出力時に力業でスキーマを上書きすることによって解決したという事例を紹介します。
構成
- スキーマビルダー(バックエンド): Pothos
- クライアント(フロントエンド): Relay
問題
type RestaurantGroup {
customers(after: ID, first: Int): CustomerConnection!
}
上記のフィールドを例として説明します。
first
は取得件数を表す引数で、メインのユースケースではRelayが想定するように 20
などを指定してページネーションに利用しています。
一方、(本当はよくないのですが便宜上)使用する場所によっては first
に大きな数字を与えることで全件取得の普通のリストとしても使っていました。しかし、first
はnullableなため、全件取得するつもりが指定を忘れてデフォルトの件数しか取得していないことによるバグがときどき発生していました。
そこで、first
をnon-nullableに変更してlintで指定忘れを防ぐことを考えました。その際、古いクライアントでエラーが出ないようにランタイムの振る舞いは保持したいという要件もあったので、リゾルバのコードは変更せずスキーマファイル出力時にのみ first
の型を上書きすることにしました。
解決方法
手動でASTをいじることも覚悟していましたが、幸い graphql-tools というライブラリの mapSchema
という便利な関数のおかげで簡単にスキーマの編集ができました。
この関数は公式ドキュメントに示されている型から読み取れるように、スキーマに対してさまざまな操作が可能です。
今回は first
の type
を上書きするだけなので下記のように書きました。
/* ビルド時に実行するスクリプト */
import { writeFileSync } from 'node:fs'
import { MapperKind, mapSchema } from '@graphql-tools/utils'
import { GraphQLInt, GraphQLNonNull, printSchema } from 'graphql'
import { builder } from './graphql.js'
// ビルダーからスキーマオブジェクトを生成
const schema = builder.toSchema()
// 編集
const modifiedSchema = mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) =>
fieldConfig.args && 'first' in fieldConfig.args ?
{
...fieldConfig,
args: {
...fieldConfig.args,
first: {
...fieldConfig.args.first,
type: new GraphQLNonNull(GraphQLInt),
},
},
}
: fieldConfig,
})
// ファイルに出力
writeFileSync('schema.graphql', printSchema(modifiedSchema))
出力したスキーマファイルにもしっかり反映されていました。
type RestaurantGroup {
customers(after: ID, first: Int!): CustomerConnection!
}
参考