graphql-guardとポリシーオブジェクトをgraphql-ruby 1.8で使うための方法

これの続きです。

blog.kymmt.com

問題

graphql-guardはインラインでポリシーを書く方法の他に、ポリシーオブジェクトを定義して GraphQL::Guard.new に渡すという方法があります。使いかたはREADMEに書いてあるとおりです。

class GraphqlPolicy
  RULES = {
    Types::QueryType => {
      viewer: ->(obj, args, ctx) { !ctx[:current_user].nil? }
    }
  }

  def self.guard(type, field)
    type.introspection? ? ->(_, _, _) { true } : RULES.dig(type, field)
  end
end

MySchema = GraphQL::Schema.define do
  query Types::QueryType

  use GraphQL::Guard.new(policy_object: GraphqlPolicy)
end

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :viewer, !Types::UserType do
    resolver ->(obj, args, ctx) { ctx[:current_user] }
  end
end

# 認証に失敗すると GraphQL::Guard::NotAuthorizedError がraiseされる

で、graphql-ruby 1.8でclass-based APIを使ってスキーマを書き直すと、残念ながらこのポリシーオブジェクトは動かなくなります。

class GraphqlPolicy
  RULES = {
    Types::Query => {
      viewer: ->(obj, args, ctx) { !ctx[:current_user].nil? }
    }
  }

  def self.guard(type, field)
    type.introspection? ? ->(_, _, _) { true } : RULES.dig(type.name, field)
  end
end

class MySchema < GraphQL::Schema
  query Types::Query

  use GraphQL::Guard.new(policy_object: GraphqlPolicy)
end

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

  def viewer
    context[:current_user]
  end
end

# 認証に失敗すると GraphQL::Guard::NotAuthorizedError がraiseされず、
# viewerのresolverの結果がnilになるので、次のエラーになってしまう
# {
#   "data": null,
#   "errors": [
#     {
#       "message": "Cannot return null for non-nullable field Query.viewer"
#     }
#   ]
# }

解決策

上の問題の原因はHash GraphqlPolicy::RULE のキーの等価比較にあります。

graphql-guardではフィールドのinstrumentationを使っており、GraphqlPolicy.guard に渡る type は、graphql-guardの差し込むinstrumentationで取得した、class-based API以前のオブジェクト型を表す GraphQL::ObjectType のインスタンスです。一方、RULE のキーはclass-based APIで導入された GraphQL::Schema::Object のインスタンスなので、クラスがそもそも一致していません。なので、これを GraphQL::ObjectType のような古いほうのクラスのインスタンスに変換するためのメソッド #to_graphql を呼ぶ必要があります。

また、GraphQL::ObjectType では eql? をオーバーライドしていないので、Hashのキーの比較にはオブジェクトIDが使われます。ここで、#to_graphql は内部で GraphQL::ObjectType.new しているので、これで作ったインスタンスと RULE のキーになっているインスタンスはオブジェクトIDが異なってしまいます。よって、キーが一致せず、RULE からguard Procを見つけられないので、望むような動作をしなくなっています。

GraphqlPolicy を次のように定義し直せば動きます。GraphQLの型をキーとするのは諦めて、その型の名前 name をキーとすることで、Hashのキー探索がうまくハマるようにしています。

class GraphqlPolicy
  RULES = {
    Types::Query.to_graphql.name => {
      viewer: ->(obj, args, ctx) { !ctx[:current_user].nil? }
    }
  }

  def self.guard(type, field)
    type.introspection? ? ->(_, _, _) { true } : RULES.dig(type.name, field)
  end
end

# 認証に失敗すると GraphQL::Guard::NotAuthorizedError がraiseされる

雑感

この件はgemのREADMEを改善したほうがよさそう。また、なにかもうちょっといい方法があれば教えてください。