# frozen_string_literal: true module GraphQL module RemoteLoader # Given a list of queries and their caller IDs, generate the merged and labeled # GraphQL query to be sent off to the remote backend. class QueryMerger class << self 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 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 # merges a_query's fragment definitions into b_query 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 # merges a_query into b_query 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 # Are two lists of arguments equal? 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 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 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 # Allows "foo" or "query { foo }" def parse(query) GraphQL.parse(query) rescue GraphQL::ParseError GraphQL.parse("query { #{query} }") end end end end end