GraphQL の Custom Directive について
(=˘ ꒳ ˘=) GraphQL のラストフロンティア Custom Directive にもそろそろ手を出してみたい...
ここまで趣味で GraphQL のいろいろな要素を見てきました。
実行時のパラメータとして GraphQLSchema、queryString、rootValue、context、variables、 スキーマを構成する要素として Query、Mutation、Subscription、Custom Scalar と見てきて最後に残った Custom Directive について見ていきたいと思います。 色々できそうで楽しそうですよね。
しかしこの Custom Directive、意外と魔窟なのでした...
- GraphQL の Directive について
- Directive の処理はどこにあるのか
- GraphQLDirective で定義してみる
- フィールドに付加したディレクティブの情報はどこで取得できるのか
- graphql-custom-directive
- 追記
GraphQL の Directive について
とりあえず定義から眺めていきます
- GraphQL.org
- Working Draft – October 2016
と、さてここから型情報を頼りに実装をしてみようかと思ったのですが、意外と難航して...
Directive の処理はどこにあるのか
てっきり GraphQLObjectType
のフィールド定義の resolve
のようなものがあるのかと思っていたのですが見つかりません。
GraphQLDirective
のコンストラクタ引数を見てみると以下のようになっています。
export interface GraphQLDirectiveConfig { name: string; description?: string; locations: DirectiveLocationEnum[]; args?: GraphQLFieldConfigArgumentMap; astNode?: DirectiveDefinitionNode; }
他の箇所も探してみたのですが、フィルタ処理や変換処理の実装を記述する箇所が見つかりません。
GraphQL.js の @skip
ディレクティブの定義を見てみると
export const GraphQLSkipDirective = new GraphQLDirective({ name: 'skip', description: 'Directs the executor to skip this field or fragment when the `if` ' + 'argument is true.', locations: [ DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, ], args: { if: { type: GraphQLNonNull(GraphQLBoolean), description: 'Skipped when true.', }, }, });
https://github.com/graphql/graphql-js/blob/master/src/type/directives.js#L111-L127
と、どこにも処理の内容が書かれていません。ならばと思い execution/execution.js のコードを覗いてみると shouldIncludeNode として フィールドをフィルタリングする処理が定義されていました。
つまり Directive の処理はスキーマではなく、それを解釈する実行関数側に寄せられて実装されているのですね。
これを素直に受け取れば Custom Directives を実装したければ、実行関数である graphql
のカスタム実装を自前で書け、という事になります。
それはちょっと常人にはつらい道ですね。
GraphQLDirective で定義してみる
とはいえ一度自分で行けるところまで実装してみようと GraphQLDirective
を使ってコードをこねくり回してみました。
import { graphql, GraphQLSchema, GraphQLDirective, GraphQLObjectType, GraphQLString, } from 'graphql' // @myDirective というディレクティブを定義する const myDirective = new GraphQLDirective({ name: 'myDirective', // どこで使うディレクティブかを指定 // FIELD、FRAGMENT_DEFINITION、FRAGMENT_SPREAD...など色々指定できる locations: ['FIELD'], // ディレクティブの引数を定義 args: { name: { type: GraphQLString, }, }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'rootQuery', fields: { hello: { type: GraphQLString, }, }, }), directives: [myDirective], // スキーマに @myDirective を組み込む }) // クエリ内で @myDirective を使ってもエラーにならない const query = `{ hello @myDirective(name: "asa-taka") }` const rootValue = { hello: 'world', } graphql(schema, query, rootValue).then(console.log, console.error)
これで一応 @myDirective
という Custom Directive がクエリ内で利用できるようになりました。
正確には「クエリ内で利用してもエラーが出ない」ようにはなりました。
しかし肝心の処理がまだ定義できていません。
値の変換やフィルタ処理など、いろいろ定義したいですよね。
フィールドに付加したディレクティブの情報はどこで取得できるのか
resolve
を定義して console.log
で探し回った結論だけ書いておきますと、第4引数 info
の fieldNodes
の directives
でフィールドに付加したディレクティブの情報にアクセスできるようです。
hello: { type: GraphQLString, resolve(src, args, ctx, info) { console.log(info.fieldNodes[0].directives[0]) } }
しかしこれ、ここまでやってディレクティブの name
と args
しか取得できず、ここから更にそのディレクティブについての処理を書かなければなりません。
このディレクティブが存在すれば resolve
の処理をこう変える、という処理ですね。
更にこれだと特定のフィールドに対してのみの実装ででしかない、ということも忘れてはなりません。
ディレクティブは複数フィールドで使いたい場合がほとんどだと思いますが、その分の実装を複数フィールドに行うのはとても現実的ではありませんね。
graphql-custom-directive
graphql
の再実装なんてできるわけなく、フィールド毎の resolve
のディレクティブ実装も現実的ではない、ということで
graphql-custom-directive
という便利ライブラリがあります。
使い方はREADMEに書いてある通りで、パブリックなAPIは以下のふたつです。
GraphQLCustomDirective
resolve
が追加されたGraphQLDirective
を定義できる- オリジナルには存在しない
GraphQLDirective.resolve
を Directive の処理実装置き場としてしれっと追加している
applySchemaCustomDirectives
GraphQLSchema
にDirective
の処理を追加するGraphQLSchema
には予めdirectives
にGraphQLCustomDirectives
を列挙しておく必要がある
内部処理の大枠としては以下の通りです。
wrapFieldsWithMiddleware
GraphQLSchema
の各フィールドにresolveMiddlewareWrapper
を適用する
resolveMiddlewareWrapper
- フィールドの
resolve
に Directive のresolve
を絡めるラッパー info
からディレクティブの情報を読み取っている
- フィールドの
resolveWithDirective(resolve, source, directive, context, info)
- 通常のフィールドの
resolve
に対して、第1引数に Directive のresolve
が拡張されたもの
- 通常のフィールドの
コードを見ればわかるように _queryType
へのアクセスなど GraphQL.js のプライベートインタフェースにかなり依存した実装となっており、バージョンアップに対してかなり無防備な実装になっています。とは言え現状 GraphQLSchema
クラスの仕様上、一旦作成したスキーマを変更する手段が他にないため仕方がないともいえますね。
つまりここまで拡張しなければ Custom Directives は定義できないということで、ひとまず納得しました。
追記
そしてこの graphql-custom-directive を使ってみたところうまく動きませんでした。実は graphql-custom-directives の READMEに書いてあったところから、この s がつかない graphql-custom-directive を見つけたのですが、graphql-custom-directives の方でも同様の applySchemaCustomDirectives
の実装があり、更に内部でアクセスしている GraphQLSchema
のプライベートインタフェースが異なるという、どうも GraphQL.js の内部実装の変更に追従できていない様子でした。
GraphQL の Directive はあまり一利用者が実装するような段階のものではないのかもしれません...