GraphQL.js の graphql で使われる引数(variables, context) の動作を確認する
(=˘ ꒳ ˘=) GraphQL って「何をするもの」であって「どう実装される」想定のものなのか、まだなんかよくわからない...
少し前に graphcool-framework がオープンソース化しましたし、そういうのを見ればベストプラクティスのようなものをトップダウンに得られるのかもしれません。が、どうやら自分の興味は graphql とはそもそも何なのだ、というボトムアップ的な箇所に向いてしまっているようです。
商業面や実用面は一旦置いておいて、GraphQL は趣味プログラミングの題材としてはとても面白いです。 きみはデータオブジェクトの枝葉を駆ける猫となるのだ。
そしてそのボトムアップの起点として今気になっているのが graphql の graphql メソッドです。
graphql(schema, '{ hello }', rootValue, ...<未踏の領域>)
サーバミドルウェアにバインディングされようと、ここでできないことはできないでしょうし、逆にここさえ把握してしまえば graphql がどの範囲の問題をどう解決できるものなのか、根本的に把握できる気がするのです。あくまで仕様としての GraphQL ではなくライブラリとしての graphql の、ですが。
の続きです。前回は graphql を実行する際の引数のうち、schema
と rootvalue
について簡単に眺めました。今回は 残りの引数について眺めていきたいと思います。
graphql を実行する際の引数
graphql | API Reference によると graphql を実行する際の引数は以下のようになっています。
graphql( schema: GraphQLSchema, requestString: string, rootValue?: ?any, // 実行時に全ての resolve function に渡される contextValue?: ?any, // 実行時に各オペレーションに渡される変数 variableValues?: ?{[key: string]: any}, // requestString 内のどのオペレーションを実行するか指定する operationName?: ?string ): Promise<GraphQLResult>
なので、全ての引数を埋めるとなると以下のようになるでしょうか。
const rootValue = {...} const context = {...} const variables = {...} const operationName = '...' graphql(schema, '{ hello }', rootValue, context, variables, operationName)
長いですね。でも、普段は Apollo を利用してフロントを書くこともあるのですが、意外と見たことのあるパラメータが多いです。今回も簡単なコードを書きながら実際に graphql メソッドを叩いてみましょう。
variables と operationName の動作に触れてみる
variables
と operationName
の挙動を確認するために以下のような、3種類のクエリを実行時に選択できるようなコードを書いてみました。
import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, args: { me: { type: GraphQLString } }, resolve(_, args) { return args.me || 'world' } } } }) }) const requestString = ` # 引数を使用しないパターン query greeting { hello } # 引数がクエリ内に固定されたパターン query greetingForMe { hello(me: "asa-taka") } # 引数を実行時に受け取るパターン query greetingFor($me: String) { hello(me: $me) } ` // operationName と variables を指定して好みのクエリを実行 (*˘꒳˘*) えらべるしあわせ const operationName = 'greetingFor' const variables = { me: 'asa-taka in variables' } // rootValue と context は一旦おやすみ (*˘꒳˘*) スヤァ... const rootValue = undefined const context = undefined graphql(schema, requestString, rootValue, context, variables, operationName) .then(console.log, console.error)
特に予想を外れることはなく、実行していただければ素直に動作すると思います。
例えば、上の例をそのまま実行すると { data: { hello: 'hello, asa-taka in variables' } }
が帰ってきます。
前回の例と比べると GraphQLSchema
には新たに args
を定義しております。
fields: { hello: { type: GraphQLString, - resolve() { - return 'world' + args: { + me: { type: GraphQLString } + }, + resolve(source, args) { + const { me } = args + return me ? `hello, ${me}` : `hello` } } }
context の動作に触れてみる
次は context の動作を確認してみます。
context
も rootValue
も resolver
に値が渡るという点では似ていますが、context
には全ての resolver
に対して同じオブジェクトが渡される、という点で役割が異なります。
rootValue
の場合、resolver
には source
として渡り、渡される値は自身の直上の(というより自身を参照した)オブジェクトになりますね。
またサンプルコードを書いて確認していきましょう。まずは基本的な動作確認として
import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql' const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve(_source, _args, context) { // context を読み取ってご主人様の名前を把握する (*˘꒳˘*) えらい return `my master, ${context.owner.name}` } } } }) }) // context として君のご主人様が誰であるか教えてあげよう (*˘꒳˘*) ふふ const context = { owner: { name: 'asa-taka', favoriteThings: ['wasabi', 'katsuo-bushi'], } } // rootValue は一旦おやすみ (*˘꒳˘*) スヤァ... const rootValue = undefined graphql(schema, '{ hello }', rootValue, context) .then(console.log, console.error)
実行すると { data: { hello: 'my master, asa-taka' } }
と返してくれます。
せっかくなのでネストしたスキーマから context を利用してみる
プレゼントをプランしてくれるスキーマを適当に書いてみます。
import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql' const PresentPlannerType = new GraphQLObjectType({ name: 'PresentPlanner', fields: { plans: { type: new GraphQLList(GraphQLString), resolve(_source, _args, context) { return context.owner.favoriteThings } }, bestOne: { type: GraphQLString, resolve(_source, _args, context) { return context.owner.favoriteThings[0] } }, } }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve(_source, _args, context) { return `my master, ${context.owner.name}` } }, present: { type: PresentPlannerType, resolve(source, args, context) { return {} // <- 必須...? } } } }), types: [PresentPlannerType] }) // 君のご主人様が誰であるか教えてあげよう (*˘꒳˘*) ふふ const context = { owner: { name: 'asa-taka', favoriteThings: ['wasabi', 'katsuo-bushi'], } } // rootValue は一旦おやすみ (*˘꒳˘*) スヤァ... const rootValue = undefined graphql(schema, '{ present { bestOne } }', rootValue, context) .then(console.log, console.error)
{ data: { present: { bestOne: 'wasabi' } } }
と返してくれます。新しく PresentPlannerType
という GraphQLObjectType
を定義しており、RootQueryType
に追加した箇所は以下の通りです。
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve(_source, _args, context) { return `my master, ${context.owner.name}` } }, + present: { + type: PresentPlannerType, + resolve(source, args, context) { + return {} // <- 必須...? + } + } } }), + types: [PresentPlannerType] })
rootValue
に該当する値がなく、かつ resolve
も存在しないと、それ以降の掘り進みは行われず、{ data: { present: null } }
が返ってきます。
ふむ... GraphQL難しいですね。正直 context
よりもネストしたスキーマの振る舞いの方で手間取った感じがしますが、またつまづきながらも前進しました、ということで。
context の利用例
context
についてはWebサーバの apollo-server の方ですでに面識がありまして、そこでは以下のように context
を利用するサンプルコードが紹介されています。
Adding a GraphQL endpoint | Apollo Server
app.use( '/graphql', bodyParser.json(), graphqlExpress(req => { return { schema: myGraphQLSchema, context: { value: req.body.something, }, }; }), );
簡単に説明すると、/graphql
エンドポイントにリクエストが来るたびに後ろで graphql
メソッドを実行するわけですが、その際に graphql
に渡すパラメータとしてリクエストオブジェクトをコンテクストに渡して GraphQLSchema
内の resolve
でリクエストパラメータを参照可能にしているんですね。
他にも context
にはデータベースやリモートリソースを叩くためののクライアントを各 resolver
に渡すために利用できたりします。こうしてみると GraphQL の Context は Angular の依存性注入(DI) や React の Context に近い利用のされ方をするもの、とも言えますね。
まとめ
初めは鬼のように思えた
graphql(schema, requestString, rootValue, context, variables, operationName)
という引数の羅列ですが、今ならその動作のイメージは十分にすることができますね。そこまで変なこともされていなかったと思います。
変数名とそれぞれの動作を確認することで GraphQL 的な語彙も強化されたので、今後はいろんなドキュメントが読みやすくなるのでは 、と言う感じで今回はここまでにしておきます。