きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

GraphQL.js の graphql で使われる引数(variables, context) の動作を確認する

(=˘ ꒳ ˘=) GraphQL って「何をするもの」であって「どう実装される」想定のものなのか、まだなんかよくわからない...

少し前に graphcool-framework がオープンソース化しましたし、そういうのを見ればベストプラクティスのようなものをトップダウンに得られるのかもしれません。が、どうやら自分の興味は graphql とはそもそも何なのだ、というボトムアップ的な箇所に向いてしまっているようです。

商業面や実用面は一旦置いておいて、GraphQL は趣味プログラミングの題材としてはとても面白いです。 きみはデータオブジェクトの枝葉を駆ける猫となるのだ。

そしてそのボトムアップの起点として今気になっているのが graphql の graphql メソッドです。

graphql(schema, '{ hello }', rootValue, ...<未踏の領域>)

サーバミドルウェアバインディングされようと、ここでできないことはできないでしょうし、逆にここさえ把握してしまえば graphql がどの範囲の問題をどう解決できるものなのか、根本的に把握できる気がするのです。あくまで仕様としての GraphQL ではなくライブラリとしての graphql の、ですが。

lightbulbcat.hatenablog.com

の続きです。前回は graphql を実行する際の引数のうち、schemarootvalue について簡単に眺めました。今回は 残りの引数について眺めていきたいと思います。

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 の動作に触れてみる

variablesoperationName の挙動を確認するために以下のような、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 の動作を確認してみます。

contextrootValueresolver に値が渡るという点では似ていますが、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 的な語彙も強化されたので、今後はいろんなドキュメントが読みやすくなるのでは 、と言う感じで今回はここまでにしておきます。