2024.01.12
GraphQL:Apollo ServerでOperation中のQueryの数を制限する
Nest.js + Apolloの環境で,ApolloのValidationRulesを独自に実装することで,様々なバリデーションを追加する方法を紹介します。アイキャッチはDALLE 3で生成しました。スペルが間違っていますね。
1.GraphQLのQueryのバリデーション
GraphQLはクライアントアプリケーションが非常に柔軟にデータをフェッチできるプロトコルです。その柔軟性ゆえに,一度に大量のデータをフェッチしたり,ネストが深いQueryを発行したりすることで,サーバに負荷をかけるような攻撃を行うことが可能です。
このような想定外のQueryの実行を回避するには,アプリケーションのロジックを実行する前にQuery自体をバリデーションすることが重要です。代表的なGraphQL serverであるApollo serverは,初期化時にバリデーション用の関数の配列を渡すことで,受信してQueryを自動的にバリデーションする機能が備わっています。
ネストの深さをバリデーションするgraphql-depth-limitや,Queryの複雑さをバリデーションするgraphql-validation-complexityのように,ライブラリとして提供されているものも多くあります。
しかし,これらのライブラリは総じて古く,最新バージョンのApollo serverでは動作しない場合があります。また,あるQueryの組み合わせだけブロックしたい,という需要をgraphql-validation-complexityで満たすためには,Fieldごとに細かくコスト設定を行う必要があり,煩雑です。
そこで,今回はApollo serverで任意のバリデーション関数を追加する方法を紹介します。想定する環境はNest.js+Apollo serverです(記事執筆時点の最新バージョン)。
2.validationRules
Nest.jsでApollo serverを使用するには,アプリケーションのルートモジュールのimportに
GraphQLModule.forRoot%3CApolloDriverConfig%3E({ driver: ApolloDriver, 各種オプション })
を追加します。ここで,「validationRules」というオプションを指定することができます。
validationRulesは,名前の通りバリデーションのルールを指定するもので,validationContextクラスのインスタンスを受け取る関数の配列を指定します。バリデーションを行う関数を実装し,validationRulesに追加すれば,任意の条件でQuery自体をバリデーションすることが可能です。
3.validationContext
validationContextは,graphql-jsパッケージに含まれるクラスです。Queryをパースし,バリデーションに必要な情報をまとめたクラスになっています。
validationContextの親クラスであるASTValidationContextに実装されているgetDocument()を呼び出すことにより,DocumentNodeのインスタンスを取得することができます。DocumentNodeには,受信したOperationやVariablesの情報が格納されています。
Queryの内容をバリデーションするためには,DocumentNodeからOperationの定義を取得する必要があります。これは,DocumentNode内のdefinitionsから取得することができます。definitionsはDefinitionNodeクラスのインスタンスの配列になっています。DefinitionNodeはkindフィールドを持っており,この値がenumで定義されている「Kind.OPERATION_DEFINITION」になっているインスタンスが,Operationの定義です。
Operationの定義を取得したら,その中のselectionSet.selectionsにFieldの情報が格納されています。selectionsは配列になっており,各要素のうちkindが「kind.FIELD」のものがFieldNodeインスタンスであり,Fieldの情報をもっています。
FieldNodeはselectionsをネストしており,QueryのトップレベルからResolveFieldを辿っていくことが可能です。あとはselectionsの長さやネストの深さ,nameやaliasを見てQueryしているFieldの確認処理を実装すれば,任意のバリデーションを行うことが可能です。
4.関数の実装例
ここまでに紹介した内容を用いて,トップレベルのFieldのQueryを最大10個に制限するバリデーション用関数を作成してみました。
(validationContext: ValidationContext) => { const { definitions } = validationContext.getDocument(); definitions.forEach((definition) => { if (definition.kind === Kind.OPERATION_DEFINITION) { if (definition.selectionSet.selections.filter((s) => s.kind === Kind.FIELD).length > 10) { validationContext.reportError( new GraphQLError( 'The request exceeds the maximum number of query', ), ); } } }); return validationContext; };
これまでの解説のように,validationContextから順にFieldNodeを取得して,その長さをチェックすることでQueryしているトップレベルのField数の制限を実現しています。
エラーにする場合は,validationContext.reportErrorを呼び出します。引数はGraphQLErrorで,クライアントに返したいメッセージを渡します。バリデーションに問題がない場合は,validationContextをそのまま返します。
あとはこの関数をApollo server初期化時のvalidationRulesに追加すれば,実際のリクエストにも適用されます。
5.まとめ
今回は,Nest.js + Apollo serverでGraphQLのQuery自体をバリデーションする方法を紹介しました。この方法を応用すれば,バリデーション以外にもロギング等にも活用できそうです。
次世代システム研究室では,グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方,次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら,ぜひ募集職種一覧からご応募をお願いします。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD