class GraphQL::RemoteLoader::QueryMerger

Given a list of queries and their caller IDs, generate the merged and labeled GraphQL query to be sent off to the remote backend.

Public Class Methods

merge(queries_and_caller_ids) click to toggle source
# File lib/graphql/remote_loader/query_merger.rb, line 9
def merge(queries_and_caller_ids)
  parsed_queries = queries_and_caller_ids.map do |query, caller_id|
    parsed_query = parse(query)

    parsed_query.definitions.each do |definition|
      attach_caller_id!(definition.children, caller_id)
    end

    parsed_query
  end

  merge_parsed_queries(parsed_queries).to_query_string
end

Private Class Methods

apply_aliases!(query_selections) click to toggle source
# File lib/graphql/remote_loader/query_merger.rb, line 109
def apply_aliases!(query_selections)
  exempt_node_types = [
    GraphQL::Language::Nodes::InlineFragment,
    GraphQL::Language::Nodes::FragmentSpread
  ]

  query_selections.each do |selection|
    unless exempt_node_types.any? { |type| selection.is_a? type }
      binary_id = selection.instance_variable_get(:@binary_id)

      selection.instance_variable_set(:@alias, if selection.alias
        "p#{binary_id}#{selection.alias}"
      else
        "p#{binary_id}#{selection.name}"
      end)
    end

    # Some nodes don't have selections (e.g. fragment spreads)
    apply_aliases!(selection.selections) if selection.respond_to? :selections
  end
end
arguments_equal?(a, b) click to toggle source

Are two lists of arguments equal?

# File lib/graphql/remote_loader/query_merger.rb, line 92
def arguments_equal?(a, b)
  # Return true if both don't have args.
  # Return false if only one doesn't have args
  return true unless a.respond_to?(:arguments) && b.respond_to?(:arguments)
  return false unless a.respond_to?(:arguments) || b.respond_to?(:arguments)

  a.arguments.map { |arg| {name: arg.name, value: arg.value}.to_s }.sort ==
    b.arguments.map { |arg| {name: arg.name, value: arg.value}.to_s }.sort
end
attach_caller_id!(query_fields, caller_id) click to toggle source
# File lib/graphql/remote_loader/query_merger.rb, line 102
def attach_caller_id!(query_fields, caller_id)
  query_fields.each do |field|
    field.instance_variable_set(:@binary_id, 2 ** caller_id)
    attach_caller_id!(field.children, caller_id)
  end
end
merge_fragment_definitions(a_query, b_query) click to toggle source

merges a_query's fragment definitions into b_query

# File lib/graphql/remote_loader/query_merger.rb, line 41
def merge_fragment_definitions(a_query, b_query)
  a_query.definitions[1..-1].each do |a_definition|
    matching_fragment_definition = b_query.definitions.find do |b_definition|
      a_definition.name == b_definition.name
    end

    if matching_fragment_definition
      merge_query_recursive(a_definition, matching_fragment_definition)
    else
      # graphql-ruby Nodes aren't meant to be mutated, but I'd rather slightly abuse graphql-ruby vs
      # maintain my own Ruby data and parsing library implementing the GraphQL spec.
      b_query.instance_variable_set(:@definitions, [b_query.definitions, a_definition].flatten)
    end
  end
end
merge_parsed_queries(parsed_queries) click to toggle source
# File lib/graphql/remote_loader/query_merger.rb, line 25
def merge_parsed_queries(parsed_queries)
  merged_query = parsed_queries.pop

  parsed_queries.each do |query|
    merge_query_recursive(query.definitions[0], merged_query.definitions[0])
    merge_fragment_definitions(query, merged_query)
  end

  merged_query.definitions.each do |definition|
    apply_aliases!(definition.selections)
  end

  merged_query
end
merge_query_recursive(a_query, b_query) click to toggle source

merges a_query into b_query

# File lib/graphql/remote_loader/query_merger.rb, line 58
def merge_query_recursive(a_query, b_query)
  exempt_node_types = [
    GraphQL::Language::Nodes::InlineFragment,
    GraphQL::Language::Nodes::FragmentSpread
  ]

  a_query.selections.each do |a_query_selection|
    matching_field = b_query.selections.find do |b_query_selection|
      next false if (a_query_selection.is_a? GraphQL::Language::Nodes::InlineFragment) &&
        (b_query_selection.is_a? GraphQL::Language::Nodes::InlineFragment)

      same_name = a_query_selection.name == b_query_selection.name

      next same_name if exempt_node_types.any? { |type| b_query_selection.is_a?(type) }

      same_args = arguments_equal?(a_query_selection, b_query_selection)
      same_alias = a_query_selection.alias == b_query_selection.alias

      same_name && same_args && same_alias
    end

    if matching_field
      new_binary_id = matching_field.instance_variable_get(:@binary_id) +
        a_query_selection.instance_variable_get(:@binary_id)

      matching_field.instance_variable_set(:@binary_id, new_binary_id)
      merge_query_recursive(a_query_selection, matching_field) unless exempt_node_types.any? { |type| matching_field.is_a?(type) }
    else
      b_query.instance_variable_set(:@selections, [b_query.selections, a_query_selection].flatten)
    end
  end
end
parse(query) click to toggle source

Allows “foo” or “query { foo }”

# File lib/graphql/remote_loader/query_merger.rb, line 132
def parse(query)
  GraphQL.parse(query)
rescue GraphQL::ParseError
  GraphQL.parse("query { #{query} }")
end