GraphQL-Ruby1.9+から子fieldが選択されたかによって処理を変更できるようになった

プロダクトでGraphQL-Rubyのgemを最新バージョンにアップデートしました!

1.9ではfieldにextras:[:lookahead]を追加できます。

この機能は、子field要素がリクエスト対象であるかで条件分岐を可能にします。

ドキュメントを見るだけでは挙動がわかりにくいので実際にリクエストした時に内部では、どのような処理を行っているかをソースコードを追いながら紹介します。

実際のソースコードは、GitHubにあります。

messageはroom_idという外部キーを持ち、roomとhas_manyの関係です。

[room_type.rb]
module Types
    class RoomType < Types::BaseObject
      field :messages, [Types::MessageType], null: true, extras: [:lookahead]
    
      def messages(lookahead:)
        binding.pry
        object.messages
      end
    end
end

[message_type.rb]
module Types
    class MessageType < Types::BaseObject
      field :id, ID, null: false
      field :body, String, null: false
    end
end

リクエストするクエリは以下の通りです。

query {
  rooms {
    messages {
      id
    }
  }
}

リクエストを送るとbinding.pryで処理が止まるので、lookahead.selects?(:id)lookahead.selects?(:body)を実行すると返り値はそれぞれtureとfalseです。

lookahead.selects?(:body)がfalseとなるのは、リクエスト対象にbodyを含んでいないからです。

それでは、GraphQL::Execution::Lookaheadselects?メソッドを追っていきます。

Add Query#lookahead #1931を確認すると、GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])からGraphQL::Execution::Lookaheadオブジェクトが生成されることがわかります。

def selects?(field_name, arguments: nil)
    selection(field_name, arguments: arguments).selected?
end

GraphQL::Execution::Lookaheadのpublicメソッドであるselects?は、filed_nameとargumentsを引数として渡すことができます。そのまま、selectionに受け取った引数を渡しているのでselectionメソッドを確認します。

def selection(field_name, selected_type: @selected_type, arguments: nil)
    next_field_name = normalize_name(field_name)
    
    next_field_defn = FieldHelpers.get_field(@query.schema, selected_type, next_field_name)
    if next_field_defn
      next_nodes = []
      @ast_nodes.each do |ast_node|
        ast_node.selections.each do |selection|
          find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
        end
      end
    
      if next_nodes.any?
        Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
      else
        NULL_LOOKAHEAD
      end
    else
      NULL_LOOKAHEAD
    end
end

初めのnormalize_nameは、privateメソッドで実装を見るとシンボルの時にはキャメルケースのStringオブジェクトにして返します。

このことから、lookahead.selects?("roomId")lookahead.selects?(:room_id)どちらで渡してもいいことがわかります。

def normalize_name(name)
    if name.is_a?(Symbol)
      Schema::Member::BuildType.camelize(name.to_s)
    else
      name
    end
end

残りの行は、子filedにfield_nameが存在するかを判定しています。存在しない場合は、NullLookaheadオブジェクトを生成します。

NULL_LOOKAHEAD = NullLookahead.new

つまり、selectionメソッドの返り値はGraphQL::Execution::Lookaheadまたは、GraphQL::Execution::Lookahead::NullLookaheadのオブジェクトです。

selects?に戻るとselectionメソッドの返り値に対してselected?メソッドを実行しています。

GraphQL::Execution::Lookaheadselected?メソッドはtrue、GraphQL::Execution::Lookahead::NullLookaheadは、falseを返します。

よって、ドキュメントに記載されているように子fieldが選択されたかによって処理を変更できます。

def files(lookahead:)
  if lookahead.selects?(:full_path)
    # This is a query like `files { fullPath ... }`
  else
    # This query doesn't have `fullPath`
  end
end

参考URL

Add a basic lookahead object #1894

Class: GraphQL::Execution::Lookahead