graphql-guardをgraphql-ruby 1.8で使うためのある方法

問題

graphql-ruby 1.8ではclass-based APIが導入されました。フィールド定義は型を表すクラスのコンテキストでクラスメソッド field を呼ぶ形で定義します。

class Types::Query < Types::BaseObject
  field :viewer, Types::User, null: true

  def viewer
    context[:current_user]
  end
end

このときgraphql-guard(今回はv1.1.0)を次のように使おうとすると #<ArgumentError: unknown keyword: guard> になります。

class Types::Query < Types::BaseObject
  field :viewer, Types::User, guard ->(obj, args, ctx) { !ctx[:current_user].nil? }, null: true

  def viewer
    context[:current_user]
  end
end

class-based APIでフィールドを表すクラスが GraphQL::Field から GraphQL::Schema::Field にかわり、従来のように guard をキーとするProcを指定するだけでは、フィールドのメタデータに guard を設定することができなくなったためエラーになっています。

ある解決策

フィールドをカスタマイズして guard をメタデータとして持たせられるようにします。やりかたはこのドキュメントを参照。

GraphQL - Extending the GraphQL-Ruby Type Definition System

class Fields::Guardable < GraphQL::Schema::Field
  def initialize(*args, guard: nil, **kwargs, &block)
    @guard = guard
    super *args, **kwargs, &block
  end

  def to_graphql
    field_defn = super
    field_defn.tap { |d| d.metadata[:guard] = @guard }
  end
end

initialize はこのフィールドを定義するときに guard を設定できるようにしています。また、スキーマ定義が処理される過程で to_graphql が実行されるので、このときにフィールド定義を表す GraphQL::Schema のインスタンスが持つ属性 metadataguard を設定しています。

これで、class-based APIでもフィールド定義時に guard Procが設定できます。guard を定義できるフィールドにするために field_class を明示的に指定します。

class Types::Query < Types::BaseObject
  field_class Fields::Guardable

  field :viewer, Types::User, guard ->(obj, args, ctx) { !ctx[:current_user].nil? }, null: true

  def viewer
    context[:current_user]
  end
end

accepts_definitionを使う[2018-06-10 追記]

accepts_definition を利用すると1.8以前で使っていたオプションをclass-based APIで使うときにgraphql-ruby側でいい感じにハンドリングしてくれます。

GraphQL - Extending the GraphQL-Ruby Type Definition System

なので、Fields::Guardable に対して次のようにすればOKでした。

class Fields::Guardable < GraphQL::Schema::Field
  accepts_definition :guard
end

これを使えば従来どおり guard が使えます。

class Types::Query < Types::BaseObject
  field_class Fields::Guardable

  field :viewer, Types::User, null: true do
    guard ->(obj, args, ctx) { !ctx[:current_user].nil? }
  end

  def viewer
    context[:current_user]
  end
end

内部的には前節の説明に近いことをやっているようです。

移行時に補助的に使うメソッドのようにも見えるので、今後使い続けていいのかがちょっと微妙なところです。

雑感

  • Policyオブジェクトを使うときにどうやるか
  • Fields::Guardable のようなモジュールをgem側で提供できるようにしたほうがよさそうかも