これまでにGraphQLサーバーを書いたことがあるなら、リゾルバーに渡される`info`オブジェクトを目にしたことがあるでしょう。幸いなことに、ほとんどの場合、それが実際に何をするのか、クエリ解決中の役割が何であるかを理解する必要はありません。
しかし、`info`オブジェクトが多くの混乱や誤解の原因となるエッジケースがいくつか存在します。この記事の目的は、`info`オブジェクトの内部を詳しく調べ、GraphQLの実行プロセスにおけるその役割を明らかにすることです。
この記事は、GraphQLのクエリとミューテーションがどのように解決されるかの基本に既に精通していることを前提としています。この点で少し不安がある場合は、このシリーズの以前の記事をぜひ確認してください。パートI:GraphQLスキーマ(必須)パートII:ネットワーク層(任意)
info
オブジェクトの構造
復習:GraphQLリゾルバーのシグネチャ
簡単に復習すると、GraphQL.jsでGraphQLサーバーを構築する際には、主に2つのタスクがあります。
- あなたのGraphQLスキーマを定義する(SDLまたはプレーンなJSオブジェクトのいずれかで)
- スキーマ内の各フィールドについて、そのフィールドの値を返す方法を知っているリゾルバー関数を実装する
リゾルバー関数は(この順序で)4つの引数を取ります
parent
: 以前のリゾルバー呼び出しの結果(詳細はこちら)。args
: リゾルバーのフィールドの引数。context
: 各リゾルバーが読み書きできるカスタムオブジェクト。info
: これについてはこの記事で説明します。
ここでは、シンプルなGraphQLクエリの実行プロセスと、それに属するリゾルバーの呼び出しの概要を示します。第2リゾルバーレベルの解決は自明であるため、これらのリゾルバーを実際に実装する必要はありません。それらの戻り値はGraphQL.jsによって自動的に推論されます。

GraphQLリゾルバーチェーンにおけるparent
およびargs
引数の概要
info
にはクエリのASTとその他の実行情報が含まれる
`info`オブジェクトの構造と役割について興味がある人は、情報が不足しています。公式の仕様もドキュメントも、それについて一切言及していません。以前は、より良いドキュメントを求めるGitHubのissueがありましたが、特に進展なくクローズされました。そのため、コードを掘り下げるしかありません。
非常に高レベルで見ると、`info`オブジェクトには、入力されたGraphQLクエリのASTが含まれていると言えます。これにより、リゾルバーはどのフィールドを返す必要があるかを知ることができます。
クエリASTがどのようなものかについて詳しく知るには、Christian Joudreyによる素晴らしい記事Life of a GraphQL Query — Lexing/Parsing、そしてEric Baerによる素晴らしい講演GraphQL Under the Hoodをぜひご覧ください。
info
の構造を理解するために、そのFlow型定義を見てみましょう。
以下に、これらの各キーの概要と簡単な説明を示します
fieldName
: 前述のとおり、GraphQLスキーマの各フィールドはリゾルバーによってサポートされる必要があります。fieldName
には、現在のリゾルバーに属するフィールドの名前が含まれます。fieldNodes
: 各オブジェクトが残りの選択セット内のフィールドを表す配列。returnType
: 対応するフィールドのGraphQL型。parentType
: このフィールドが属するGraphQL型。path
: 現在のフィールド(つまりリゾルバー)に到達するまでに辿られたフィールドを追跡します。schema
: 実行可能なスキーマを表すGraphQLSchema
インスタンス。fragments
: クエリドキュメントの一部であったフラグメントのマップ。rootValue
: 実行に渡されたrootValue
引数。operation
: クエリ全体のAST。variableValues
: クエリと一緒に提供された変数のマップ。variableValues引数に対応します。
これらがまだ抽象的に思えても心配しないでください。すぐにすべての例を見ることができます。
フィールド固有 vs グローバル
上記のキーに関して、興味深い観察が1つあります。info
オブジェクト上のキーは、フィールド固有であるか、グローバルであるかのいずれかです。
フィールド固有とは、そのキーの値が、info
オブジェクトが渡されるフィールド(およびその基盤となるリゾルバー)に依存することを意味します。明らかな例としては、fieldName
、returnType
、parentType
が挙げられます。以下のGraphQL型のauthor
フィールドを考えてみましょう。
そのフィールドのfieldName
は単にauthor
であり、returnType
はUser!
、parentType
もQuery
です。
さて、feed
の場合、これらの値は当然異なります。fieldName
はfeed
、returnType
は[Post!]!
、そしてparentType
もQuery
です。
したがって、これら3つのキーの値はフィールド固有です。さらにフィールド固有のキーは、fieldNodes
とpath
です。実質的に、上記のFlow定義の最初の5つのキーはフィールド固有です。
一方、グローバルとは、これらのキーの値が、どのリゾルバーを扱っているかに関わらず変化しないことを意味します。schema
、fragments
、rootValue
、operation
、そしてvariableValues
は、すべてのリゾルバーで常に同じ値を持ちます。
簡単な例
では、info
オブジェクトの内容の例を見ていきましょう。この例のために、ここで使用するスキーマ定義を示します。
このスキーマのリゾルバーが以下のように実装されていると仮定します。
Post.title
リゾルバーは実際には必須ではありませんが、リゾルバーが呼び出されたときにinfo
オブジェクトがどのように見えるかを確認するために、ここに含まれています。
次に、以下のクエリを考えます。
簡潔にするため、Query.author
フィールドのリゾルバーのみを議論し、Post.title
のリゾルバー(上記のクエリが実行される際にやはり呼び出されます)については議論しません。
この例を試してみたい場合は、上記のスキーマの動作バージョンを含むリポジトリを用意しましたので、試すことができます!
次に、info
オブジェクト内の各キーを見て、Query.author
リゾルバーが呼び出されたときにそれらがどのように見えるかを確認しましょう(info
オブジェクトの完全なログ出力はこちらで見ることができます)。
fieldName
fieldName
は単にauthor
です。
fieldNodes
fieldNodes
がフィールド固有であることを思い出してください。それは実質的にクエリASTの抜粋を含んでいます。この抜粋はクエリのルートからではなく、現在のフィールド(つまりauthor
)から始まります。(ルートから始まるクエリAST全体はoperation
に格納されます。下記を参照してください)。
returnType
& parentType
以前に見たように、returnType
とparentType
はかなり自明です。
path
path
は、現在のフィールドに到達するまでに辿られたフィールドを追跡します。Query.author
の場合、単に"path": { "key": "author" }
のようになります。
比較のために、Post.title
リゾルバーでは、path
は以下のようになります。
残りの5つのフィールドは「グローバル」カテゴリに属し、したがって
Post.title
リゾルバーでも同じになります。
schema
schema
は実行可能なスキーマへの参照です。
fragments
fragments
にはフラグメント定義が含まれますが、クエリドキュメントにはそのようなものがないため、単なる空のマップ{}
です。
rootValue
前述のとおり、rootValue
キーの値は、そもそもgraphql実行関数に渡されるrootValue
引数に対応します。この例の場合、それは単にnull
です。
operation
operation
には、入力されたクエリの完全なクエリASTが含まれています。他の情報に加えて、これには上で見たfieldNodes
と同じ値が含まれていることを思い出してください。
variableValues
このキーは、クエリに渡された変数を表します。この例には変数が含まれていないため、この値も単なる空のマップ{}
です。
クエリが変数で記述された場合
variableValues
キーは単に以下の値を持つことになります。
GraphQLバインディングを使用する際のinfo
の役割
記事の冒頭で述べたように、ほとんどのシナリオではinfo
オブジェクトについてまったく心配する必要はありません。それは単にリゾルバーのシグネチャの一部であるだけで、実際には何も使っていないからです。では、それがいつ重要になるのでしょうか?
バインディング関数へのinfo
の受け渡し
以前にGraphQLバインディングを扱ったことがあるなら、生成されたバインディング関数の一部としてinfo
オブジェクトを見たことがあるでしょう。以下のスキーマを考えてみましょう。
graphql-binding
を使用すると、生(raw)のクエリやミューテーションを送信する代わりに、専用のバインディング関数を呼び出すことで、利用可能なクエリやミューテーションを送信できるようになります。
例えば、特定のUser
を取得する以下の生(raw)のクエリを考えてみましょう。
バインディング関数で同じことを行うと、以下のようになります。
バインディングインスタンスのuser
関数の呼び出しと、対応する引数を渡すことで、上記の生(raw)のGraphQLクエリとまったく同じ情報を伝達します。
graphql-binding
のバインディング関数は3つの引数を取ります。
args
: フィールドの引数を含みます(例:上記のcreateUser
ミューテーションのusername
)。context
: リゾルバーチェーンを介して渡されるcontext
オブジェクト。info
:info
オブジェクト。`info`の型であるGraphQLResolveInfo
のインスタンスではなく、選択セットを単に定義する文字列を渡すこともできることに注意してください。
Prismaを用いたアプリケーションスキーマとデータベーススキーマのマッピング
`info`オブジェクトが混乱を招く可能性のあるもう一つの一般的なユースケースは、Prismaおよびprisma-bindingに基づいてGraphQLサーバーを実装する場合です。
その文脈では、2つのGraphQLレイヤーを持つという考え方です。
- The データベースレイヤーはPrismaによって自動生成され、汎用的で強力なCRUD APIを提供します。
- The* アプリケーションレイヤー*は、クライアントアプリケーションに公開され、アプリケーションのニーズに合わせて調整されたGraphQL APIを定義します。
バックエンド開発者として、あなたはアプリケーションレイヤーのアプリケーションスキーマを定義し、そのリゾルバーを実装する責任があります。prisma-binding
のおかげで、リゾルバーの実装は、大きなオーバーヘッドなしで、受信クエリを下位のデータベースAPIに委任するプロセスに過ぎません。
簡単な例を考えてみましょう。Prismaデータベースサービスに以下のデータモデルから開始するとします。
Prismaがこのデータモデルに基づいて生成するデータベーススキーマは、次のようになります。
さて、次のようなアプリケーションスキーマを構築したいと仮定します。
feed
クエリは、Post
要素のリストを返すだけでなく、リストのcount
(件数)も返すことができます。オプションでauthorId
を受け取り、特定のUser
によって書かれたPost
要素のみを返すようにフィードをフィルタリングすることに注意してください。
このアプリケーションスキーマを実装する際の最初の直感は、次のようになるかもしれません。
実装1:この実装は正しく見えますが、微妙な欠陥があります
この実装は十分に妥当に見えます。feed
リゾルバー内で、入力される可能性のあるauthorId
に基づいてauthorFilter
を構築しています。authorFilter
はその後、posts
クエリを実行してPost
要素を取得し、さらにリストのcount
へのアクセスを提供するpostsConnection
クエリを実行するために使用されます。
実際のPost要素は、postsConnectionクエリのみを使用して取得することも可能です。物事を単純にするために、ここではpostsクエリを使用し、他のアプローチは注意深い読者の練習問題として残しておきます。
実際、この実装でGraphQLサーバーを起動すると、最初はうまくいくように見えるでしょう。シンプルなクエリは適切に処理され、例えば以下のクエリは成功します。
Post
要素のauthor
を取得しようとすると、問題に遭遇します。
なるほど!どういうわけか、実装がauthor
を返さず、そのため「nullを非null可能なPost.authorに返すことはできません。」というエラーが発生します。これは、アプリケーションスキーマでPost.author
フィールドが必須としてマークされているためです。
実装の関連部分をもう一度見てみましょう。
ここでPost要素を取得します。しかし、選択セットをpostsバインディング関数に渡していません。Prismaバインディング関数に2番目の引数が渡されない場合、デフォルトの動作はその型のすべてのスカラーフィールドをクエリすることです。
これは確かにその振る舞いを説明します。ctx.db.query.posts
の呼び出しは、正しいPost
要素のセットを返しますが、author
に関するリレーショナルデータはなく、そのid
とtitle
の値のみです。
では、どうすればこれを修正できるでしょうか?明らかに必要なのは、posts
バインディング関数にどのフィールドを返す必要があるかを伝える方法です。しかし、その情報はfeed
リゾルバーのコンテキストのどこに存在するのでしょうか?推測できますか?
正解:info
オブジェクトの中です!Prismaバインディング関数の2番目の引数は、文字列またはinfo
オブジェクトのいずれかであるため、feed
リゾルバーに渡されるinfo
オブジェクトをそのままposts
バインディング関数に渡しましょう。
このクエリは実装2で失敗します:「タイプ'Post'のフィールド'posts'はサブ選択を持たなければなりません。」
しかし、この実装では、どのリクエストも適切に処理されません。例として、以下のクエリを考えてみましょう。
エラーメッセージ「タイプ'Post'のフィールド'posts'はサブ選択を持たなければなりません。」は、上記実装の8行目で生成されます。
では、ここで何が起こっているのでしょうか?これが失敗する理由は、info
オブジェクト内のフィールド固有のキーがposts
クエリと一致しないためです。
feed
リゾルバー内でinfo
オブジェクトを出力すると、状況がより明確になります。fieldNodes
内のフィールド固有の情報のみを考慮してみましょう。
このJSONオブジェクトは、文字列の選択セットとしても表現できます。
これですべてが分かりました!上記の選択セットをPrismaデータベーススキーマのposts
クエリに送信しているのですが、もちろんPrismaはfeed
フィールドとcount
フィールドを認識していません。確かに生成されるエラーメッセージはあまり役に立ちませんが、少なくとも今では何が起こっているのかを理解できます。
では、この問題の解決策は何でしょうか?この問題に取り組む一つの方法は、fieldNodes
の選択セットの正しい部分を手動でパースして、それをposts
バインディング関数に渡すこと(例えば文字列として)でしょう。
しかし、この問題にははるかにエレガントな解決策があり、それはアプリケーションスキーマからFeed
型専用のリゾルバーを実装することです。適切な実装は次のようになります。
実装3:この実装は上記の問題を修正します
この実装は、上で議論されたすべての問題を解決します。いくつか注意すべき点があります。
- 8行目で、文字列の選択セット(
{ id }
)を2番目の引数として渡しています。これは効率のためであり、そうでなければすべてのスカラー値がフェッチされてしまいますが(この例では大きな違いはありません)、ここではIDのみが必要です。 Query.feed
リゾルバーからposts
を返すのではなく、IDの配列(文字列として表現)であるpostIds
を返しています。Feed.posts
リゾルバーでは、親リゾルバーによって返されたpostIds
にアクセスできるようになりました。今回は、入力されたinfo
オブジェクトを利用して、それをそのままposts
バインディング関数に渡すことができます。
この例を試してみたい場合は、上記の例の動作バージョンを含むこのリポジトリを確認してください。この記事で言及されている異なる実装を自由に試して、ご自身で動作を確認してください!
まとめ
この記事では、GraphQL.jsに基づいてGraphQL APIを実装する際に使用されるinfo
オブジェクトについて深く掘り下げました。
info
オブジェクトは公式には文書化されていません。詳細を知るにはコードを深く掘り下げる必要があります。このチュートリアルでは、その内部構造を概説し、GraphQLリゾルバー関数におけるその役割を理解することから始めました。次に、info
に関するより深い理解が必要となるいくつかのエッジケースと潜在的な落とし穴について説明しました。
この記事で示されたすべてのコードは、対応するGitHubリポジトリで見つけることができます。ご自身で`info`オブジェクトの動作を試して観察してみてください。
次の投稿をお見逃しなく!
Prismaニュースレターに登録する