graphql-stitching 1.2.5 → 1.4.0

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -17
  3. data/docs/README.md +2 -1
  4. data/docs/mechanics.md +2 -1
  5. data/docs/resolver.md +101 -0
  6. data/lib/graphql/stitching/client.rb +5 -1
  7. data/lib/graphql/stitching/composer/{boundary_config.rb → resolver_config.rb} +18 -13
  8. data/lib/graphql/stitching/composer/validate_interfaces.rb +4 -4
  9. data/lib/graphql/stitching/composer/validate_resolvers.rb +97 -0
  10. data/lib/graphql/stitching/composer.rb +107 -112
  11. data/lib/graphql/stitching/executor/{boundary_source.rb → resolver_source.rb} +40 -32
  12. data/lib/graphql/stitching/executor.rb +3 -3
  13. data/lib/graphql/stitching/plan.rb +3 -4
  14. data/lib/graphql/stitching/planner.rb +30 -41
  15. data/lib/graphql/stitching/planner_step.rb +6 -6
  16. data/lib/graphql/stitching/resolver/arguments.rb +284 -0
  17. data/lib/graphql/stitching/resolver/keys.rb +206 -0
  18. data/lib/graphql/stitching/resolver.rb +70 -0
  19. data/lib/graphql/stitching/shaper.rb +3 -3
  20. data/lib/graphql/stitching/skip_include.rb +1 -1
  21. data/lib/graphql/stitching/supergraph/key_directive.rb +13 -0
  22. data/lib/graphql/stitching/supergraph/resolver_directive.rb +4 -4
  23. data/lib/graphql/stitching/supergraph/to_definition.rb +165 -0
  24. data/lib/graphql/stitching/supergraph.rb +31 -144
  25. data/lib/graphql/stitching/util.rb +28 -0
  26. data/lib/graphql/stitching/version.rb +1 -1
  27. data/lib/graphql/stitching.rb +3 -2
  28. metadata +11 -7
  29. data/lib/graphql/stitching/boundary.rb +0 -29
  30. data/lib/graphql/stitching/composer/validate_boundaries.rb +0 -96
  31. data/lib/graphql/stitching/export_selection.rb +0 -42
@@ -2,15 +2,12 @@
2
2
 
3
3
  require_relative "./composer/base_validator"
4
4
  require_relative "./composer/validate_interfaces"
5
- require_relative "./composer/validate_boundaries"
6
- require_relative "./composer/boundary_config"
5
+ require_relative "./composer/validate_resolvers"
6
+ require_relative "./composer/resolver_config"
7
7
 
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
@@ -31,7 +28,7 @@ module GraphQL
31
28
  # @api private
32
29
  VALIDATORS = [
33
30
  "ValidateInterfaces",
34
- "ValidateBoundaries",
31
+ "ValidateResolvers",
35
32
  ].freeze
36
33
 
37
34
  # @return [String] name of the Query type in the composed schema.
@@ -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
@@ -62,13 +59,13 @@ module GraphQL
62
59
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
63
60
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
64
61
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
65
- @boundary_configs = {}
62
+ @resolver_configs = {}
66
63
 
67
64
  @field_map = nil
68
- @boundary_map = nil
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,47 +82,47 @@ 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
- extract_boundaries(type_name, types_by_location) if type_name == @query_name
125
+ extract_resolvers(type_name, types_by_location) if type_name == @query_name
129
126
 
130
127
  memo[type_name] = case kinds.first
131
128
  when "SCALAR"
@@ -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
 
@@ -157,12 +154,12 @@ module GraphQL
157
154
  end
158
155
 
159
156
  select_root_field_locations(schema)
160
- expand_abstract_boundaries(schema)
157
+ expand_abstract_resolvers(schema)
161
158
 
162
159
  supergraph = Supergraph.new(
163
160
  schema: schema,
164
161
  fields: @field_map,
165
- boundaries: @boundary_map,
162
+ resolvers: @resolver_map,
166
163
  executables: executables,
167
164
  )
168
165
 
@@ -184,13 +181,13 @@ 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
- @boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
193
- @boundary_configs.merge!(BoundaryConfig.extract_federation_entities(schema, location))
189
+ @resolver_configs.merge!(ResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
190
+ @resolver_configs.merge!(ResolverConfig.extract_federation_entities(schema, location))
194
191
 
195
192
  schemas[location.to_s] = schema
196
193
  executables[location.to_s] = input[:executable] || schema
@@ -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
 
@@ -527,63 +524,61 @@ module GraphQL
527
524
 
528
525
  # @!scope class
529
526
  # @!visibility private
530
- def extract_boundaries(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
- boundary_type = field_candidate.type.unwrap
534
- boundary_structure = Util.flatten_type_structure(field_candidate.type)
535
- boundary_configs = @boundary_configs.fetch("#{location}.#{field_name}", [])
536
-
537
- field_candidate.directives.each do |directive|
527
+ def extract_resolvers(type_name, types_by_location)
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)
532
+ resolver_configs = @resolver_configs.fetch("#{location}.#{field_name}", [])
533
+
534
+ subgraph_field.directives.each do |directive|
538
535
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
539
- boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
536
+ resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
540
537
  end
541
538
 
542
- boundary_configs.each do |config|
543
- key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
544
-
545
- if key_selections.length != 1
546
- raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
547
- end
548
-
549
- argument_name = key_selections[0].alias
550
- argument_name ||= if field_candidate.arguments.size == 1
551
- field_candidate.arguments.keys.first
552
- elsif field_candidate.arguments[config.key]
553
- config.key
539
+ resolver_configs.each do |config|
540
+ resolver_type_name = if config.type_name
541
+ if !resolver_type.kind.abstract?
542
+ raise CompositionError, "Resolver config may only specify a type name for abstract resolvers."
543
+ elsif !resolver_type.possible_types.find { _1.graphql_name == config.type_name }
544
+ raise CompositionError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
545
+ end
546
+ config.type_name
547
+ else
548
+ resolver_type.graphql_name
554
549
  end
555
550
 
556
- argument = field_candidate.arguments[argument_name]
557
- unless argument
558
- # contextualize this... "boundaries with multiple args need mapping aliases."
559
- raise ComposerError, "Invalid boundary argument `#{argument_name}` for #{type_name}.#{field_name}."
560
- end
551
+ key = Resolver.parse_key_with_types(
552
+ config.key,
553
+ @subgraph_types_by_name_and_location[resolver_type_name],
554
+ )
561
555
 
562
- argument_structure = Util.flatten_type_structure(argument.type)
563
- if argument_structure.length != boundary_structure.length
564
- raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
565
- end
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
566
562
 
567
- boundary_type_name = if config.type_name
568
- if !boundary_type.kind.abstract?
569
- raise ComposerError, "Resolver config may only specify a type name for abstract resolvers."
570
- elsif !boundary_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}`."
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."
572
566
  end
573
- config.type_name
574
- else
575
- boundary_type.graphql_name
567
+
568
+ "#{argument.graphql_name}: $.#{key.default_argument_name}"
576
569
  end
577
570
 
578
- @boundary_map[boundary_type_name] ||= []
579
- @boundary_map[boundary_type_name] << Boundary.new(
571
+ arguments = Resolver.parse_arguments_with_field(arguments_format, subgraph_field)
572
+ arguments.each { _1.verify_key(key) }
573
+
574
+ @resolver_map[resolver_type_name] ||= []
575
+ @resolver_map[resolver_type_name] << Resolver.new(
580
576
  location: location,
581
- type_name: boundary_type_name,
582
- key: key_selections[0].name,
583
- field: field_candidate.name,
584
- arg: argument_name,
585
- list: boundary_structure.first.list?,
586
- federation: config.federation,
577
+ type_name: resolver_type_name,
578
+ field: subgraph_field.name,
579
+ list: resolver_structure.first.list?,
580
+ key: key,
581
+ arguments: arguments,
587
582
  )
588
583
  end
589
584
  end
@@ -612,15 +607,15 @@ module GraphQL
612
607
 
613
608
  # @!scope class
614
609
  # @!visibility private
615
- def expand_abstract_boundaries(schema)
616
- @boundary_map.keys.each do |type_name|
617
- boundary_type = schema.types[type_name]
618
- next unless boundary_type.kind.abstract?
619
-
620
- expanded_types = Util.expand_abstract_type(schema, boundary_type)
621
- expanded_types.select { @candidate_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
622
- @boundary_map[expanded_type.graphql_name] ||= []
623
- @boundary_map[expanded_type.graphql_name].push(*@boundary_map[type_name])
610
+ def expand_abstract_resolvers(schema)
611
+ @resolver_map.keys.each do |type_name|
612
+ resolver_type = schema.types[type_name]
613
+ next unless resolver_type.kind.abstract?
614
+
615
+ expanded_types = Util.expand_abstract_type(schema, resolver_type)
616
+ expanded_types.select { @subgraph_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
617
+ @resolver_map[expanded_type.graphql_name] ||= []
618
+ @resolver_map[expanded_type.graphql_name].push(*@resolver_map[type_name])
624
619
  end
625
620
  end
626
621
  end
@@ -670,9 +665,9 @@ module GraphQL
670
665
 
671
666
  def reset!
672
667
  @field_map = {}
673
- @boundary_map = {}
668
+ @resolver_map = {}
674
669
  @mapped_type_names = {}
675
- @candidate_directives_by_name_and_location = nil
670
+ @subgraph_directives_by_name_and_location = nil
676
671
  @schema_directives = nil
677
672
  end
678
673
  end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module GraphQL::Stitching
4
4
  class Executor
5
- class BoundarySource < GraphQL::Dataloader::Source
5
+ class ResolverSource < GraphQL::Dataloader::Source
6
6
  def initialize(executor, location)
7
7
  @executor = executor
8
8
  @location = location
9
+ @variables = {}
9
10
  end
10
11
 
11
12
  def fetch(ops)
@@ -16,7 +17,7 @@ module GraphQL::Stitching
16
17
 
17
18
  if op.if_type
18
19
  # operations planned around unused fragment conditions should not trigger requests
19
- origin_set.select! { _1[ExportSelection.typename_node.alias] == op.if_type }
20
+ origin_set.select! { _1[Resolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
20
21
  end
21
22
 
22
23
  memo[op] = origin_set if origin_set.any?
@@ -28,7 +29,7 @@ module GraphQL::Stitching
28
29
  @executor.request.operation_name,
29
30
  @executor.request.operation_directives,
30
31
  )
31
- variables = @executor.request.variables.slice(*variable_names)
32
+ variables = @variables.merge!(@executor.request.variables.slice(*variable_names))
32
33
  raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
33
34
  @executor.query_count += 1
34
35
 
@@ -41,36 +42,51 @@ module GraphQL::Stitching
41
42
  ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
42
43
  end
43
44
 
44
- # Builds batched boundary queries
45
- # "query MyOperation_2_3($var:VarType) {
46
- # _0_result: list(keys:["a","b","c"]) { boundarySelections... }
47
- # _1_0_result: item(key:"x") { boundarySelections... }
48
- # _1_1_result: item(key:"y") { boundarySelections... }
49
- # _1_2_result: item(key:"z") { boundarySelections... }
45
+ # Builds batched resolver queries
46
+ # "query MyOperation_2_3($var:VarType, $_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {
47
+ # _0_result: list(keys: $_0_key) { resolverSelections... }
48
+ # _1_0_result: item(key: $_1_0_key) { resolverSelections... }
49
+ # _1_1_result: item(key: $_1_1_key) { resolverSelections... }
50
+ # _1_2_result: item(key: $_1_2_key) { resolverSelections... }
50
51
  # }"
51
52
  def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
52
53
  variable_defs = {}
53
54
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
54
55
  variable_defs.merge!(op.variables)
55
- boundary = op.boundary
56
-
57
- if boundary.list
58
- input = origin_set.each_with_index.reduce(String.new) do |memo, (origin_obj, index)|
59
- memo << "," if index > 0
60
- memo << build_key(boundary.key, origin_obj, federation: boundary.federation)
61
- memo
56
+ resolver = @executor.request.supergraph.resolvers_by_version[op.resolver]
57
+
58
+ if resolver.list?
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
62
68
  end
63
69
 
64
- "_#{batch_index}_result: #{boundary.field}(#{boundary.arg}:[#{input}]) #{op.selections}"
70
+ "_#{batch_index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
65
71
  else
66
72
  origin_set.map.with_index do |origin_obj, index|
67
- input = build_key(boundary.key, origin_obj, federation: boundary.federation)
68
- "_#{batch_index}_#{index}_result: #{boundary.field}(#{boundary.arg}:#{input}) #{op.selections}"
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
83
+
84
+ "_#{batch_index}_#{index}_result: #{resolver.field}(#{arguments.join(",")}) #{op.selections}"
69
85
  end
70
86
  end
71
87
  end
72
88
 
73
- doc = String.new("query") # << boundary fulfillment always uses query
89
+ doc = String.new("query") # << resolver fulfillment always uses query
74
90
 
75
91
  if operation_name
76
92
  doc << " #{operation_name}"
@@ -80,8 +96,7 @@ module GraphQL::Stitching
80
96
  end
81
97
 
82
98
  if variable_defs.any?
83
- variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
84
- doc << "(#{variable_str})"
99
+ doc << "(#{variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")})"
85
100
  end
86
101
 
87
102
  if operation_directives
@@ -90,15 +105,8 @@ module GraphQL::Stitching
90
105
 
91
106
  doc << "{ #{query_fields.join(" ")} }"
92
107
 
93
- return doc, variable_defs.keys
94
- end
95
-
96
- def build_key(key, origin_obj, federation: false)
97
- key_value = JSON.generate(origin_obj[ExportSelection.key(key)])
98
- if federation
99
- "{ __typename: \"#{origin_obj[ExportSelection.typename_node.alias]}\", #{key}: #{key_value} }"
100
- else
101
- key_value
108
+ return doc, variable_defs.keys.tap do |names|
109
+ names.reject! { @variables.key?(_1) }
102
110
  end
103
111
  end
104
112
 
@@ -106,7 +114,7 @@ module GraphQL::Stitching
106
114
  return unless raw_result
107
115
 
108
116
  origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
109
- results = if op.dig("boundary", "list")
117
+ results = if @executor.request.supergraph.resolvers_by_version[op.resolver].list?
110
118
  raw_result["_#{batch_index}_result"]
111
119
  else
112
120
  origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "./executor/boundary_source"
4
+ require_relative "./executor/resolver_source"
5
5
  require_relative "./executor/root_source"
6
6
 
7
7
  module GraphQL
@@ -55,9 +55,9 @@ module GraphQL
55
55
  tasks = @request.plan
56
56
  .ops
57
57
  .select { next_steps.include?(_1.after) }
58
- .group_by { [_1.location, _1.boundary.nil?] }
58
+ .group_by { [_1.location, _1.resolver.nil?] }
59
59
  .map do |(location, root_source), ops|
60
- source_type = root_source ? RootSource : BoundarySource
60
+ source_type = root_source ? RootSource : ResolverSource
61
61
  @dataloader.with(source_type, self, location).request_all(ops)
62
62
  end
63
63
 
@@ -14,7 +14,7 @@ module GraphQL
14
14
  :variables,
15
15
  :path,
16
16
  :if_type,
17
- :boundary,
17
+ :resolver,
18
18
  keyword_init: true
19
19
  ) do
20
20
  def as_json
@@ -27,7 +27,7 @@ module GraphQL
27
27
  variables: variables,
28
28
  path: path,
29
29
  if_type: if_type,
30
- boundary: boundary&.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
- boundary = op["boundary"]
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
- boundary: boundary ? GraphQL::Stitching::Boundary.new(**boundary) : nil,
48
+ resolver: op["resolver"],
50
49
  )
51
50
  end
52
51
  new(ops: ops)