GraphQLサーバーを書いたことがあれば、リゾルバーに渡されるinfoオブジェクトに既に出会っていることでしょう。幸いなことに、ほとんどの場合、それが実際に何をするのか、クエリ解決中にどのような役割を果たすのかを理解する必要はありません。
しかし、infoオブジェクトが多くの混乱と誤解の原因となるいくつかのエッジケースがあります。この記事の目的は、infoオブジェクトの内側を覗き込み、GraphQLの実行プロセスにおけるその役割に光を当てることです。
この記事は、GraphQLクエリとミューテーションがどのように解決されるかの基本を既にご存知であることを前提としています。もしこの点に不安がある場合は、このシリーズの以前の記事を必ずご確認ください。パートI:GraphQLスキーマ(必須)パートII:ネットワーク層(任意)
info
オブジェクトの構造
再確認:GraphQLリゾルバーのシグネチャ
簡単な再確認です。GraphQL.jsでGraphQL.jsGraphQLサーバーを構築する場合、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氏の素晴らしい記事GraphQLクエリのライフサイクル — 字句解析/構文解析と、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
、rootType
、および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
を使用すると、生のクエリとミューテーションを送信するのではなく、専用のバインディング関数を呼び出すことで、利用可能なクエリとミューテーションを送信できるようになります。
たとえば、特定のUser
を取得する次の生のクエリを考えてみましょう。
バインディング関数で同じことを実現すると、次のようになります。
バインディングインスタンスでuser
関数を呼び出し、対応する引数を渡すことで、上記の生のGraphQLクエリと全く同じ情報を伝えます。
graphql-binding
のバインディング関数は3つの引数を取ります。
args
:フィールドの引数(例:上記のcreateUser
ミューテーションのusername
)が含まれています。context
:リゾルバーチェーンを下に渡されるcontext
オブジェクト。info
:info
オブジェクト。GraphQLResolveInfo
(infoの型)のインスタンスではなく、選択セットを単に定義する文字列を渡すこともできることに注意してください。
Prismaを使用したアプリケーションスキーマからデータベーススキーマへのマッピング
infoオブジェクトが混乱を引き起こす可能性のあるもう1つの一般的なユースケースは、Prismaとprisma-bindingに基づくGraphQLサーバーの実装です。
そのコンテキストでは、アイデアは2つのGraphQLレイヤーを持つことです。
- Prismaによって自動的に生成され、汎用的で強力なCRUD APIを提供するデータベースレイヤー
- クライアントアプリケーションに公開され、アプリケーションのニーズに合わせて調整されたGraphQL APIを定義する* アプリケーションレイヤー*
バックエンド開発者として、アプリケーションレイヤーのアプリケーションスキーマを定義し、そのリゾルバーを実装する責任があります。prisma-binding
のおかげで、リゾルバーの実装は、大きなオーバーヘッドなしに、受信クエリを基盤となるデータベースAPIに委譲するプロセスにすぎません。
簡単な例を考えてみましょう。Prismaデータベースサービスの次のデータモデルから始めるとしましょう。
Prismaがこのデータモデルに基づいて生成するデータベーススキーマは、これに似ています。
さて、これに似たアプリケーションスキーマを構築したいと仮定します。
feed
クエリは、Post
要素のリストを返すだけでなく、リストのcount
も返すことができます。オプションでauthorId
を受け取り、フィードをフィルタリングして、特定のUser
によって書かれたPost
要素のみを返すことに注意してください。
このアプリケーションスキーマを実装するための最初の直感は、次のようになる可能性があります。
実装例1:この実装は正しく見えますが、微妙な欠陥があります
この実装は十分に合理的であるように思われます。feed
リゾルバー内では、潜在的に受信するauthorId
に基づいてauthorFilter
を構築しています。authorFilter
は、posts
クエリを実行し、Post
要素を取得するために使用され、リストのcount
へのアクセスを提供するpostsConnection
クエリも使用されます。
postsConnectionクエリだけを使用して、実際の*Post*要素を取得することも可能です。物事をシンプルにするために、ここではまだ*posts*クエリを使用しており、他のアプローチは注意深い読者への練習問題として残しておきます。
実際、この実装でGraphQLサーバーを起動すると、最初はすべてが順調に見えるでしょう。単純なクエリは適切に処理され、たとえば次のクエリは成功することに気付くでしょう。
Post
要素のauthor
を取得しようとすると、問題が発生します。
なるほど!どういうわけか、実装がauthor
を返さないため、エラー「NonNull型のPost.authorにnullを返すことはできません。」が発生します。これは、Post.author
フィールドがアプリケーションスキーマで必須としてマークされているためです。
実装の関連部分をもう一度見てみましょう。
ここでPost要素を取得します。ただし、選択セットをpostsバインディング関数に渡していません。2番目の引数がPrismaバインディング関数に渡されない場合、デフォルトの動作は、そのタイプのすべてのスカラーフィールドをクエリすることです。
これは確かに動作を説明しています。ctx.db.query.posts
の呼び出しは、Post
要素の正しいセットを返しますが、id
とtitle
の値のみを返します。author
に関するリレーショナルデータはありません。
では、どうすれば修正できるでしょうか?明らかに必要となるのは、posts
バインディング関数に返す必要のあるフィールドを伝える方法です。しかし、その情報はfeed
リゾルバーのコンテキストのどこに存在しますか?推測できますか?
正解:info
オブジェクトの中にあります!Prismaバインディング関数の2番目の引数は、文字列またはinfo
オブジェクトのいずれかであるため、feed
リゾルバーに渡されるinfo
オブジェクトをposts
バインディング関数に渡すだけです。
このクエリは実装例2で失敗します:「タイプ’Post’のフィールド’posts’はサブセレクションを持つ必要があります。」
ただし、この実装では、どのリクエストも適切に処理されません。例として、次のクエリを考えてみましょう。
エラーメッセージ「タイプ’Post’のフィールド’posts’はサブセレクションを持つ必要があります。」は、上記の実装の8行目によって生成されます。
では、ここで何が起こっているのでしょうか?これが失敗する理由は、info
オブジェクトのフィールド固有キーがposts
クエリと一致しないためです。
feed
リゾルバー内のinfo
オブジェクトを出力すると、状況がより明確になります。fieldNodes
内のフィールド固有の情報のみを考えてみましょう。
このJSONオブジェクトは、文字列選択セットとしても表現できます。
これで全てが理にかなっています!上記の選択セットをPrismaデータベーススキーマのposts
クエリに送信していますが、もちろんfeed
フィールドとcount
フィールドは認識されていません。確かに、生成されたエラーメッセージはあまり役に立ちませんが、少なくとも何が起こっているのかは理解できました。
では、この問題の解決策は何でしょうか?この問題への1つのアプローチは、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ニュースレターに登録してください