graphql-stitching 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -244,6 +244,7 @@ module GraphQL
244
244
  fragment = @request.fragment_definitions[node.name]
245
245
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
246
246
 
247
+ requires_typename = true
247
248
  fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
248
249
  is_same_scope = fragment_type == parent_type
249
250
  selection_set = is_same_scope ? locale_selections : []
@@ -251,18 +252,17 @@ module GraphQL
251
252
 
252
253
  unless is_same_scope
253
254
  locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
254
- requires_typename = true
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