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

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

GraphQL の Custom Directive について

(=˘ ꒳ ˘=) GraphQL のラストフロンティア Custom Directive にもそろそろ手を出してみたい...

ここまで趣味で GraphQL のいろいろな要素を見てきました。

実行時のパラメータとして GraphQLSchema、queryString、rootValue、context、variables、 スキーマを構成する要素として Query、Mutation、Subscription、Custom Scalar と見てきて最後に残った Custom Directive について見ていきたいと思います。 色々できそうで楽しそうですよね。

しかしこの Custom Directive、意外と魔窟なのでした...

GraphQL の Directive について

とりあえず定義から眺めていきます

と、さてここから型情報を頼りに実装をしてみようかと思ったのですが、意外と難航して...

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引数 infofieldNodesdirectives でフィールドに付加したディレクティブの情報にアクセスできるようです。

      hello: {
        type: GraphQLString,
        resolve(src, args, ctx, info) {
          console.log(info.fieldNodes[0].directives[0])
        }
      }

しかしこれ、ここまでやってディレクティブの nameargs しか取得できず、ここから更にそのディレクティブについての処理を書かなければなりません。 このディレクティブが存在すれば resolve の処理をこう変える、という処理ですね。 更にこれだと特定のフィールドに対してのみの実装ででしかない、ということも忘れてはなりません。 ディレクティブは複数フィールドで使いたい場合がほとんどだと思いますが、その分の実装を複数フィールドに行うのはとても現実的ではありませんね。

graphql-custom-directive

graphql の再実装なんてできるわけなく、フィールド毎の resolve のディレクティブ実装も現実的ではない、ということで graphql-custom-directive という便利ライブラリがあります。

使い方はREADMEに書いてある通りで、パブリックなAPIは以下のふたつです。

  • GraphQLCustomDirective
    • resolve が追加された GraphQLDirective を定義できる
    • オリジナルには存在しない GraphQLDirective.resolve を Directive の処理実装置き場としてしれっと追加している
  • applySchemaCustomDirectives
    • GraphQLSchemaDirective の処理を追加する
    • GraphQLSchema には予め directivesGraphQLCustomDirectives を列挙しておく必要がある

内部処理の大枠としては以下の通りです。

  • 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 はあまり一利用者が実装するような段階のものではないのかもしれません...