graphql-stitching 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,9 +8,6 @@ require_relative "./composer/resolver_config"
8
8
  module GraphQL
9
9
  module Stitching
10
10
  class Composer
11
- class ComposerError < StitchingError; end
12
- class ValidationError < ComposerError; end
13
-
14
11
  # @api private
15
12
  NO_DEFAULT_VALUE = begin
16
13
  class T < GraphQL::Schema::Object
@@ -41,7 +38,7 @@ module GraphQL
41
38
  attr_reader :mutation_name
42
39
 
43
40
  # @api private
44
- attr_reader :candidate_types_by_name_and_location
41
+ attr_reader :subgraph_types_by_name_and_location
45
42
 
46
43
  # @api private
47
44
  attr_reader :schema_directives
@@ -67,8 +64,8 @@ module GraphQL
67
64
  @field_map = nil
68
65
  @resolver_map = nil
69
66
  @mapped_type_names = nil
70
- @candidate_directives_by_name_and_location = nil
71
- @candidate_types_by_name_and_location = nil
67
+ @subgraph_directives_by_name_and_location = nil
68
+ @subgraph_types_by_name_and_location = nil
72
69
  @schema_directives = nil
73
70
  end
74
71
 
@@ -76,8 +73,8 @@ module GraphQL
76
73
  reset!
77
74
  schemas, executables = prepare_locations_input(locations_input)
78
75
 
79
- # "directive_name" => "location" => candidate_directive
80
- @candidate_directives_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
76
+ # "directive_name" => "location" => subgraph_directive
77
+ @subgraph_directives_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
81
78
  (schema.directives.keys - schema.default_directives.keys - GraphQL::Stitching.stitching_directive_names).each do |directive_name|
82
79
  memo[directive_name] ||= {}
83
80
  memo[directive_name][location] = schema.directives[directive_name]
@@ -85,44 +82,44 @@ module GraphQL
85
82
  end
86
83
 
87
84
  # "directive_name" => merged_directive
88
- @schema_directives = @candidate_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
85
+ @schema_directives = @subgraph_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
89
86
  memo[directive_name] = build_directive(directive_name, directives_by_location)
90
87
  end
91
88
 
92
89
  @schema_directives.merge!(GraphQL::Schema.default_directives)
93
90
 
94
- # "Typename" => "location" => candidate_type
95
- @candidate_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
96
- raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
97
- raise ComposerError, "The subscription operation is not supported." if schema.subscription
91
+ # "Typename" => "location" => subgraph_type
92
+ @subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
93
+ raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
94
+ raise CompositionError, "The subscription operation is not supported." if schema.subscription
98
95
 
99
96
  introspection_types = schema.introspection_system.types.keys
100
- schema.types.each do |type_name, type_candidate|
97
+ schema.types.each do |type_name, subgraph_type|
101
98
  next if introspection_types.include?(type_name)
102
99
 
103
- if type_name == @query_name && type_candidate != schema.query
104
- raise ComposerError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
105
- elsif type_name == @mutation_name && type_candidate != schema.mutation
106
- raise ComposerError, "Mutation name \"#{@mutation_name}\" is used by non-mutation type in #{location} schema."
100
+ if type_name == @query_name && subgraph_type != schema.query
101
+ raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
102
+ elsif type_name == @mutation_name && subgraph_type != schema.mutation
103
+ raise CompositionError, "Mutation name \"#{@mutation_name}\" is used by non-mutation type in #{location} schema."
107
104
  end
108
105
 
109
- type_name = @query_name if type_candidate == schema.query
110
- type_name = @mutation_name if type_candidate == schema.mutation
111
- @mapped_type_names[type_candidate.graphql_name] = type_name if type_candidate.graphql_name != type_name
106
+ type_name = @query_name if subgraph_type == schema.query
107
+ type_name = @mutation_name if subgraph_type == schema.mutation
108
+ @mapped_type_names[subgraph_type.graphql_name] = type_name if subgraph_type.graphql_name != type_name
112
109
 
113
110
  memo[type_name] ||= {}
114
- memo[type_name][location] = type_candidate
111
+ memo[type_name][location] = subgraph_type
115
112
  end
116
113
  end
117
114
 
118
115
  enum_usage = build_enum_usage_map(schemas.values)
119
116
 
120
117
  # "Typename" => merged_type
121
- schema_types = @candidate_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
118
+ schema_types = @subgraph_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
122
119
  kinds = types_by_location.values.map { _1.kind.name }.tap(&:uniq!)
123
120
 
124
121
  if kinds.length > 1
125
- raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
122
+ raise CompositionError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
126
123
  end
127
124
 
128
125
  extract_resolvers(type_name, types_by_location) if type_name == @query_name
@@ -141,7 +138,7 @@ module GraphQL
141
138
  when "INPUT_OBJECT"
142
139
  build_input_object_type(type_name, types_by_location)
143
140
  else
144
- raise ComposerError, "Unexpected kind encountered for `#{type_name}`. Found: #{kinds.first}."
141
+ raise CompositionError, "Unexpected kind encountered for `#{type_name}`. Found: #{kinds.first}."
145
142
  end
146
143
  end
147
144
 
@@ -184,9 +181,9 @@ module GraphQL
184
181
  schema = input[:schema]
185
182
 
186
183
  if schema.nil?
187
- raise ComposerError, "A schema is required for `#{location}` location."
184
+ raise CompositionError, "A schema is required for `#{location}` location."
188
185
  elsif !(schema.is_a?(Class) && schema <= GraphQL::Schema)
189
- raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
186
+ raise CompositionError, "The schema for `#{location}` location must be a GraphQL::Schema class."
190
187
  end
191
188
 
192
189
  @resolver_configs.merge!(ResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
@@ -234,10 +231,10 @@ module GraphQL
234
231
  builder = self
235
232
 
236
233
  # "value" => "location" => enum_value
237
- enum_values_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
238
- type_candidate.enum_values.each do |enum_value_candidate|
239
- memo[enum_value_candidate.graphql_name] ||= {}
240
- memo[enum_value_candidate.graphql_name][location] = enum_value_candidate
234
+ enum_values_by_name_location = types_by_location.each_with_object({}) do |(location, subgraph_type), memo|
235
+ subgraph_type.enum_values.each do |subgraph_enum_value|
236
+ memo[subgraph_enum_value.graphql_name] ||= {}
237
+ memo[subgraph_enum_value.graphql_name][location] = subgraph_enum_value
241
238
  end
242
239
  end
243
240
 
@@ -342,14 +339,14 @@ module GraphQL
342
339
  # @!visibility private
343
340
  def build_merged_fields(type_name, types_by_location, owner)
344
341
  # "field_name" => "location" => field
345
- fields_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
342
+ fields_by_name_location = types_by_location.each_with_object({}) do |(location, subgraph_type), memo|
346
343
  @field_map[type_name] ||= {}
347
- type_candidate.fields.each do |field_name, field_candidate|
348
- @field_map[type_name][field_candidate.name] ||= []
349
- @field_map[type_name][field_candidate.name] << location
344
+ subgraph_type.fields.each do |field_name, subgraph_field|
345
+ @field_map[type_name][subgraph_field.name] ||= []
346
+ @field_map[type_name][subgraph_field.name] << location
350
347
 
351
348
  memo[field_name] ||= {}
352
- memo[field_name][location] = field_candidate
349
+ memo[field_name][location] = subgraph_field
353
350
  end
354
351
  end
355
352
 
@@ -375,8 +372,8 @@ module GraphQL
375
372
  # @!visibility private
376
373
  def build_merged_arguments(type_name, members_by_location, owner, field_name: nil, directive_name: nil)
377
374
  # "argument_name" => "location" => argument
378
- args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
379
- member_candidate.arguments.each do |argument_name, argument|
375
+ args_by_name_location = members_by_location.each_with_object({}) do |(location, subgraph_member), memo|
376
+ subgraph_member.arguments.each do |argument_name, argument|
380
377
  memo[argument_name] ||= {}
381
378
  memo[argument_name][location] = argument
382
379
  end
@@ -388,7 +385,7 @@ module GraphQL
388
385
  if arguments_by_location.length != members_by_location.length
389
386
  if value_types.any?(&:non_null?)
390
387
  path = [type_name, field_name, argument_name].compact.join(".")
391
- raise ComposerError, "Required argument `#{path}` must be defined in all locations." # ...or hidden?
388
+ raise CompositionError, "Required argument `#{path}` must be defined in all locations." # ...or hidden?
392
389
  end
393
390
  next
394
391
  end
@@ -429,8 +426,8 @@ module GraphQL
429
426
  # @!scope class
430
427
  # @!visibility private
431
428
  def build_merged_directives(type_name, members_by_location, owner, field_name: nil, argument_name: nil, enum_value: nil)
432
- directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
433
- member_candidate.directives.each do |directive|
429
+ directives_by_name_location = members_by_location.each_with_object({}) do |(location, subgraph_member), memo|
430
+ subgraph_member.directives.each do |directive|
434
431
  memo[directive.graphql_name] ||= {}
435
432
  memo[directive.graphql_name][location] = directive
436
433
  end
@@ -470,18 +467,18 @@ module GraphQL
470
467
 
471
468
  # @!scope class
472
469
  # @!visibility private
473
- def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
470
+ def merge_value_types(type_name, subgraph_types, field_name: nil, argument_name: nil)
474
471
  path = [type_name, field_name, argument_name].tap(&:compact!).join(".")
475
- alt_structures = type_candidates.map { Util.flatten_type_structure(_1) }
472
+ alt_structures = subgraph_types.map { Util.flatten_type_structure(_1) }
476
473
  basis_structure = alt_structures.shift
477
474
 
478
475
  alt_structures.each do |alt_structure|
479
476
  if alt_structure.length != basis_structure.length
480
- raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
477
+ raise CompositionError, "Cannot compose mixed list structures at `#{path}`."
481
478
  end
482
479
 
483
480
  if alt_structure.last.name != basis_structure.last.name
484
- raise ComposerError, "Cannot compose mixed types at `#{path}`."
481
+ raise CompositionError, "Cannot compose mixed types at `#{path}`."
485
482
  end
486
483
  end
487
484
 
@@ -528,63 +525,60 @@ module GraphQL
528
525
  # @!scope class
529
526
  # @!visibility private
530
527
  def extract_resolvers(type_name, types_by_location)
531
- types_by_location.each do |location, type_candidate|
532
- type_candidate.fields.each do |field_name, field_candidate|
533
- resolver_type = field_candidate.type.unwrap
534
- resolver_structure = Util.flatten_type_structure(field_candidate.type)
528
+ types_by_location.each do |location, subgraph_type|
529
+ subgraph_type.fields.each do |field_name, subgraph_field|
530
+ resolver_type = subgraph_field.type.unwrap
531
+ resolver_structure = Util.flatten_type_structure(subgraph_field.type)
535
532
  resolver_configs = @resolver_configs.fetch("#{location}.#{field_name}", [])
536
533
 
537
- field_candidate.directives.each do |directive|
534
+ subgraph_field.directives.each do |directive|
538
535
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
539
536
  resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
540
537
  end
541
538
 
542
539
  resolver_configs.each do |config|
543
- key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
544
-
545
- if key_selections.length != 1
546
- raise ComposerError, "Resolver key at #{type_name}.#{field_name} must specify exactly one key."
547
- end
548
-
549
- argument = field_candidate.arguments[key_selections[0].alias]
550
- argument ||= if field_candidate.arguments.size == 1
551
- field_candidate.arguments.values.first
552
- else
553
- field_candidate.arguments[config.key]
554
- end
555
-
556
- unless argument
557
- raise ComposerError, "No resolver argument matched for #{type_name}.#{field_name}. " \
558
- "Add an alias to the key that specifies its intended argument, ex: `arg:key`"
559
- end
560
-
561
- argument_structure = Util.flatten_type_structure(argument.type)
562
- if argument_structure.length != resolver_structure.length
563
- raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument.graphql_name} resolver. " \
564
- "Arguments must map directly to results."
565
- end
566
-
567
540
  resolver_type_name = if config.type_name
568
541
  if !resolver_type.kind.abstract?
569
- raise ComposerError, "Resolver config may only specify a type name for abstract resolvers."
542
+ raise CompositionError, "Resolver config may only specify a type name for abstract resolvers."
570
543
  elsif !resolver_type.possible_types.find { _1.graphql_name == config.type_name }
571
- raise ComposerError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
544
+ raise CompositionError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
572
545
  end
573
546
  config.type_name
574
547
  else
575
548
  resolver_type.graphql_name
576
549
  end
577
550
 
551
+ key = Resolver.parse_key_with_types(
552
+ config.key,
553
+ @subgraph_types_by_name_and_location[resolver_type_name],
554
+ )
555
+
556
+ arguments_format = config.arguments || begin
557
+ argument = if subgraph_field.arguments.size == 1
558
+ subgraph_field.arguments.values.first
559
+ else
560
+ subgraph_field.arguments[key.default_argument_name]
561
+ end
562
+
563
+ unless argument
564
+ raise CompositionError, "No resolver argument matched for `#{type_name}.#{field_name}`." \
565
+ "An argument mapping is required for unmatched names and composite keys."
566
+ end
567
+
568
+ "#{argument.graphql_name}: $.#{key.default_argument_name}"
569
+ end
570
+
571
+ arguments = Resolver.parse_arguments_with_field(arguments_format, subgraph_field)
572
+ arguments.each { _1.verify_key(key) }
573
+
578
574
  @resolver_map[resolver_type_name] ||= []
579
575
  @resolver_map[resolver_type_name] << Resolver.new(
580
576
  location: location,
581
577
  type_name: resolver_type_name,
582
- key: key_selections[0].name,
583
- field: field_candidate.name,
584
- arg: argument.graphql_name,
585
- arg_type_name: argument.type.unwrap.graphql_name,
578
+ field: subgraph_field.name,
586
579
  list: resolver_structure.first.list?,
587
- representations: config.representations,
580
+ key: key,
581
+ arguments: arguments,
588
582
  )
589
583
  end
590
584
  end
@@ -619,7 +613,7 @@ module GraphQL
619
613
  next unless resolver_type.kind.abstract?
620
614
 
621
615
  expanded_types = Util.expand_abstract_type(schema, resolver_type)
622
- expanded_types.select { @candidate_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
616
+ expanded_types.select { @subgraph_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
623
617
  @resolver_map[expanded_type.graphql_name] ||= []
624
618
  @resolver_map[expanded_type.graphql_name].push(*@resolver_map[type_name])
625
619
  end
@@ -673,7 +667,7 @@ module GraphQL
673
667
  @field_map = {}
674
668
  @resolver_map = {}
675
669
  @mapped_type_names = {}
676
- @candidate_directives_by_name_and_location = nil
670
+ @subgraph_directives_by_name_and_location = nil
677
671
  @schema_directives = nil
678
672
  end
679
673
  end
@@ -17,7 +17,7 @@ module GraphQL::Stitching
17
17
 
18
18
  if op.if_type
19
19
  # operations planned around unused fragment conditions should not trigger requests
20
- origin_set.select! { _1[ExportSelection.typename_node.alias] == op.if_type }
20
+ origin_set.select! { _1[Resolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
21
21
  end
22
22
 
23
23
  memo[op] = origin_set if origin_set.any?
@@ -53,24 +53,35 @@ module GraphQL::Stitching
53
53
  variable_defs = {}
54
54
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
55
55
  variable_defs.merge!(op.variables)
56
- resolver = op.resolver
56
+ resolver = @executor.request.supergraph.resolvers_by_version[op.resolver]
57
57
 
58
58
  if resolver.list?
59
- variable_name = "_#{batch_index}_key"
60
-
61
- @variables[variable_name] = origin_set.map do |origin_obj|
62
- build_key(resolver.key, origin_obj, as_representation: resolver.representations?)
59
+ arguments = resolver.arguments.map.with_index do |arg, i|
60
+ if arg.key?
61
+ variable_name = "_#{batch_index}_key_#{i}".freeze
62
+ @variables[variable_name] = origin_set.map { arg.build(_1) }
63
+ variable_defs[variable_name] = arg.to_type_signature
64
+ "#{arg.name}:$#{variable_name}"
65
+ else
66
+ "#{arg.name}:#{arg.value.print}"
67
+ end
63
68
  end
64
69
 
65
- variable_defs[variable_name] = "[#{resolver.arg_type_name}!]!"
66
- "_#{batch_index}_result: #{resolver.field}(#{resolver.arg}:$#{variable_name}) #{op.selections}"
70
+ "_#{batch_index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
67
71
  else
68
72
  origin_set.map.with_index do |origin_obj, index|
69
- variable_name = "_#{batch_index}_#{index}_key"
70
- @variables[variable_name] = build_key(resolver.key, origin_obj, as_representation: resolver.representations?)
73
+ arguments = resolver.arguments.map.with_index do |arg, i|
74
+ if arg.key?
75
+ variable_name = "_#{batch_index}_#{index}_key_#{i}".freeze
76
+ @variables[variable_name] = arg.build(origin_obj)
77
+ variable_defs[variable_name] = arg.to_type_signature
78
+ "#{arg.name}:$#{variable_name}"
79
+ else
80
+ "#{arg.name}:#{arg.value.print}"
81
+ end
82
+ end
71
83
 
72
- variable_defs[variable_name] = "#{resolver.arg_type_name}!"
73
- "_#{batch_index}_#{index}_result: #{resolver.field}(#{resolver.arg}:$#{variable_name}) #{op.selections}"
84
+ "_#{batch_index}_#{index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
74
85
  end
75
86
  end
76
87
  end
@@ -85,8 +96,7 @@ module GraphQL::Stitching
85
96
  end
86
97
 
87
98
  if variable_defs.any?
88
- variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
89
- doc << "(#{variable_str})"
99
+ doc << "(#{variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")})"
90
100
  end
91
101
 
92
102
  if operation_directives
@@ -100,22 +110,11 @@ module GraphQL::Stitching
100
110
  end
101
111
  end
102
112
 
103
- def build_key(key, origin_obj, as_representation: false)
104
- if as_representation
105
- {
106
- "__typename" => origin_obj[ExportSelection.typename_node.alias],
107
- key => origin_obj[ExportSelection.key(key)],
108
- }
109
- else
110
- origin_obj[ExportSelection.key(key)]
111
- end
112
- end
113
-
114
113
  def merge_results!(origin_sets_by_operation, raw_result)
115
114
  return unless raw_result
116
115
 
117
116
  origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
118
- results = if op.resolver.list?
117
+ results = if @executor.request.supergraph.resolvers_by_version[op.resolver].list?
119
118
  raw_result["_#{batch_index}_result"]
120
119
  else
121
120
  origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
@@ -27,7 +27,7 @@ module GraphQL
27
27
  variables: variables,
28
28
  path: path,
29
29
  if_type: if_type,
30
- resolver: resolver&.as_json
30
+ resolver: resolver
31
31
  }.tap(&:compact!)
32
32
  end
33
33
  end
@@ -36,7 +36,6 @@ module GraphQL
36
36
  def from_json(json)
37
37
  ops = json["ops"]
38
38
  ops = ops.map do |op|
39
- resolver = op["resolver"]
40
39
  Op.new(
41
40
  step: op["step"],
42
41
  after: op["after"],
@@ -46,7 +45,7 @@ module GraphQL
46
45
  variables: op["variables"],
47
46
  path: op["path"],
48
47
  if_type: op["if_type"],
49
- resolver: resolver ? GraphQL::Stitching::Resolver.new(**resolver) : nil,
48
+ resolver: op["resolver"],
50
49
  )
51
50
  end
52
51
  new(ops: ops)
@@ -74,7 +74,7 @@ module GraphQL
74
74
  resolver: nil
75
75
  )
76
76
  # coalesce repeat parameters into a single entrypoint
77
- entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key}")
77
+ entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key&.to_definition}")
78
78
  path.each { entrypoint << "/#{_1}" }
79
79
 
80
80
  step = @steps_by_entrypoint[entrypoint]
@@ -153,7 +153,7 @@ module GraphQL
153
153
  end
154
154
 
155
155
  else
156
- raise "Invalid operation type."
156
+ raise StitchingError, "Invalid operation type."
157
157
  end
158
158
  end
159
159
 
@@ -173,7 +173,7 @@ module GraphQL
173
173
  each_field_in_scope(parent_type, fragment.selections, &block)
174
174
 
175
175
  else
176
- raise "Unexpected node of type #{node.class.name} in selection set."
176
+ raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
177
177
  end
178
178
  end
179
179
  end
@@ -199,8 +199,8 @@ module GraphQL
199
199
  input_selections.each do |node|
200
200
  case node
201
201
  when GraphQL::Language::Nodes::Field
202
- if node.alias&.start_with?(ExportSelection::EXPORT_PREFIX)
203
- raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{ExportSelection::EXPORT_PREFIX}" is a reserved prefix.)
202
+ if node.alias&.start_with?(Resolver::EXPORT_PREFIX)
203
+ raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{Resolver::EXPORT_PREFIX}" is a reserved prefix.)
204
204
  elsif node.name == TYPENAME
205
205
  locale_selections << node
206
206
  next
@@ -255,14 +255,14 @@ module GraphQL
255
255
  end
256
256
 
257
257
  else
258
- raise "Unexpected node of type #{node.class.name} in selection set."
258
+ raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
259
259
  end
260
260
  end
261
261
 
262
262
  # B.4) Add a `__typename` export to abstracts and types that implement
263
263
  # fragments so that resolved type information is available during execution.
264
264
  if requires_typename
265
- locale_selections << ExportSelection.typename_node
265
+ locale_selections << Resolver::TYPENAME_EXPORT_NODE
266
266
  end
267
267
 
268
268
  if remote_selections
@@ -276,20 +276,7 @@ module GraphQL
276
276
  routes.each_value do |route|
277
277
  route.reduce(locale_selections) do |parent_selections, resolver|
278
278
  # E.1) Add the key of each resolver query into the prior location's selection set.
279
- if resolver.key
280
- foreign_key = ExportSelection.key(resolver.key)
281
- has_key = false
282
- has_typename = false
283
-
284
- parent_selections.each do |node|
285
- next unless node.is_a?(GraphQL::Language::Nodes::Field)
286
- has_key ||= node.alias == foreign_key
287
- has_typename ||= node.alias == ExportSelection.typename_node.alias
288
- end
289
-
290
- parent_selections << ExportSelection.key_node(resolver.key) unless has_key
291
- parent_selections << ExportSelection.typename_node unless has_typename
292
- end
279
+ parent_selections.push(*resolver.key.export_nodes) if resolver.key
293
280
 
294
281
  # E.2) Add a planner step for each new entrypoint location.
295
282
  add_step(
@@ -302,6 +289,8 @@ module GraphQL
302
289
  ).selections
303
290
  end
304
291
  end
292
+
293
+ locale_selections.uniq! { _1.alias || _1.name }
305
294
  end
306
295
 
307
296
  locale_selections
@@ -43,7 +43,7 @@ module GraphQL
43
43
  variables: rendered_variables,
44
44
  path: @path,
45
45
  if_type: type_condition,
46
- resolver: @resolver,
46
+ resolver: @resolver&.version,
47
47
  )
48
48
  end
49
49