graphql-stitching 1.2.5 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 754e7a61df541f685ef50ec79039df9742e4a2589a30eadf93125d6e4ef47aed
4
- data.tar.gz: e2ceb89be07b42dbffeaa75416055fa62ffafa001088fd47b2b4f156aee6123a
3
+ metadata.gz: d33ec469a7f4598bd9f198b25102bcb3453d034b124fb18bcfd8ec595b78a6ce
4
+ data.tar.gz: a6ef6c8e218045a508c22ade74fb83504d762c0a6cadb5ed3cd4f58a89bddcea
5
5
  SHA512:
6
- metadata.gz: 561ecdc36e78b31e4f4fd2f85d4ff5e0f1ab23d44570de7d8ffe63c2e46b5856214eb07c25b98eafbd64857602ebd839e187175e9e3ec347caedf4651a00d332
7
- data.tar.gz: dd7a98e1c97f7bd5a6ca2585c34b6538e94b6dfaaa83edf9b3f1bc287a79d04f62a29ebe2a6b6fae4d06e8af9f08bb20203a89f1b062f24dc622a66973f87abc
6
+ metadata.gz: dc753d25c70045d1f2f995a0bbc8329ab426e1c5426943fa88e03fa71128fe8c1da928a8c905c997396c4d248e26e4400a02e2b2bc56d9e26b4916b5c63a8a7b
7
+ data.tar.gz: fdea7099a1cb486df512bdb93e7d90cceef42e7ff875e4c72c8e4b1fb46836d9e15cf5ae6124ca81132ec2a0707aca08a0d2a80d35fc40b18cf036c12576ff43
data/README.md CHANGED
@@ -265,7 +265,7 @@ type Query {
265
265
  The `@stitch` directive can be added to class-based schemas with a directive class:
266
266
 
267
267
  ```ruby
268
- class StitchField < GraphQL::Schema::Directive
268
+ class StitchingResolver < GraphQL::Schema::Directive
269
269
  graphql_name "stitch"
270
270
  locations FIELD_DEFINITION
271
271
  repeatable true
@@ -274,7 +274,7 @@ end
274
274
 
275
275
  class Query < GraphQL::Schema::Object
276
276
  field :product, Product, null: false do
277
- directive StitchField, key: "id"
277
+ directive StitchingResolver, key: "id"
278
278
  argument :id, ID, required: true
279
279
  end
280
280
  end
@@ -284,7 +284,7 @@ The `@stitch` directive can be exported from a class-based schema to an SDL stri
284
284
 
285
285
  #### SDL-based schemas
286
286
 
287
- A clean SDL string may also have stitching directives applied via static configuration by passing a `stitch` array in [location settings](./docs/composer.md#performing-composition):
287
+ A clean schema may also have stitching directives applied via static configuration by passing a `stitch` array in [location settings](./docs/composer.md#performing-composition):
288
288
 
289
289
  ```ruby
290
290
  sdl_string = <<~GRAPHQL
@@ -316,7 +316,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
316
316
  The library is configured to use a `@stitch` directive by default. You may customize this by setting a new name during initialization:
317
317
 
318
318
  ```ruby
319
- GraphQL::Stitching.stitch_directive = "merge"
319
+ GraphQL::Stitching.stitch_directive = "resolver"
320
320
  ```
321
321
 
322
322
  ## Executables
@@ -365,17 +365,17 @@ The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executabl
365
365
  The stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data. This is done using batched queries that combine all data access for a given a location. For example:
366
366
 
367
367
  ```graphql
368
- query MyOperation_2 {
369
- _0_result: widgets(ids:["a","b","c"]) { ... } # << 3 Widget
370
- _1_0_result: sprocket(id:"x") { ... } # << 1 Sprocket
371
- _1_1_result: sprocket(id:"y") { ... } # << 1 Sprocket
372
- _1_2_result: sprocket(id:"z") { ... } # << 1 Sprocket
368
+ query MyOperation_2($_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {
369
+ _0_result: widgets(ids: $_0_key) { ... } # << 3 Widget
370
+ _1_0_result: sprocket(id: $_1_0_key) { ... } # << 1 Sprocket
371
+ _1_1_result: sprocket(id: $_1_1_key) { ... } # << 1 Sprocket
372
+ _1_2_result: sprocket(id: $_1_2_key) { ... } # << 1 Sprocket
373
373
  }
374
374
  ```
375
375
 
376
376
  Tips:
377
377
 
378
- * List queries (like the `widgets` selection above) are more compact for accessing multiple records, and are therefore preferable as stitching accessors.
378
+ * List queries (like the `widgets` selection above) are generally preferable as resolver queries because they keep the batched document consistent regardless of set size, and make for smaller documents that parse and validate faster.
379
379
  * Assure that root field resolvers across your subgraph implement batching to anticipate cases like the three `sprocket` selections above.
380
380
 
381
381
  Otherwise, there's no developer intervention necessary (or generally possible) to improve upon data access. Note that multiple generations of data may still force the executor to return to a previous location for more data.
data/docs/README.md CHANGED
@@ -14,4 +14,5 @@ Major components include:
14
14
 
15
15
  Additional topics:
16
16
 
17
- - [Stitching mechanics](./mechanics.md) - learn more about building for stitching.
17
+ - [Stitching mechanics](./mechanics.md) - more about building for stitching and how it operates.
18
+ - [Federation entities](./federation_entities.md) - more about Apollo Federation compatibility.
data/docs/mechanics.md CHANGED
@@ -314,6 +314,7 @@ Merged types do not always require a resolver query. For example:
314
314
  type Widget {
315
315
  id: ID!
316
316
  name: String
317
+ price: Float
317
318
  }
318
319
 
319
320
  type Query {
@@ -344,4 +345,4 @@ type Query {
344
345
  }
345
346
  ```
346
347
 
347
- In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a key field (such as `id` above) that allow them to join with data in other resolver locations.
348
+ In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a shared key field (such as `id` above) that allow them to join with data in other resolver locations (such as `price` above).
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GraphQL::Stitching
4
4
  class Composer
5
- class BoundaryConfig
5
+ class ResolverConfig
6
6
  ENTITY_TYPENAME = "_Entity"
7
7
  ENTITIES_QUERY = "_entities"
8
8
 
@@ -38,7 +38,7 @@ module GraphQL::Stitching
38
38
  memo[field_path] << new(
39
39
  key: key,
40
40
  type_name: entity_type.graphql_name,
41
- federation: true,
41
+ representations: true,
42
42
  )
43
43
  end
44
44
  end
@@ -48,7 +48,7 @@ module GraphQL::Stitching
48
48
  new(
49
49
  key: kwargs[:key],
50
50
  type_name: kwargs[:type_name] || kwargs[:typeName],
51
- federation: kwargs[:federation] || false,
51
+ representations: kwargs[:representations] || false,
52
52
  )
53
53
  end
54
54
 
@@ -61,12 +61,12 @@ module GraphQL::Stitching
61
61
  end
62
62
  end
63
63
 
64
- attr_reader :key, :type_name, :federation
64
+ attr_reader :key, :type_name, :representations
65
65
 
66
- def initialize(key:, type_name:, federation: false)
66
+ def initialize(key:, type_name:, representations: false)
67
67
  @key = key
68
68
  @type_name = type_name
69
- @federation = federation
69
+ @representations = representations
70
70
  end
71
71
  end
72
72
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GraphQL::Stitching
4
4
  class Composer
5
- class ValidateBoundaries < BaseValidator
5
+ class ValidateResolvers < BaseValidator
6
6
 
7
7
  def perform(supergraph, composer)
8
8
  supergraph.schema.types.each do |type_name, type|
@@ -15,9 +15,9 @@ module GraphQL::Stitching
15
15
  candidate_types_by_location = composer.candidate_types_by_name_and_location[type_name]
16
16
  next unless candidate_types_by_location.length > 1
17
17
 
18
- boundaries = supergraph.boundaries[type_name]
19
- if boundaries&.any?
20
- validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
18
+ resolvers = supergraph.resolvers[type_name]
19
+ if resolvers&.any?
20
+ validate_as_resolver(supergraph, type, candidate_types_by_location, resolvers)
21
21
  elsif type.kind.object?
22
22
  validate_as_shared(supergraph, type, candidate_types_by_location)
23
23
  end
@@ -26,48 +26,48 @@ module GraphQL::Stitching
26
26
 
27
27
  private
28
28
 
29
- def validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
30
- # abstract boundaries are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
29
+ def validate_as_resolver(supergraph, type, candidate_types_by_location, resolvers)
30
+ # abstract resolvers are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
31
31
  return if type.kind.abstract?
32
32
 
33
- # only one boundary allowed per type/location/key
34
- boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
35
- if memo.dig(boundary.location, boundary.key)
36
- raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary.key}` "\
37
- "found in #{boundary.location}. Limit one boundary query per type and key in each location. "\
38
- "Abstract boundaries provide all possible types."
33
+ # only one resolver allowed per type/location/key
34
+ resolvers_by_location_and_key = resolvers.each_with_object({}) do |resolver, memo|
35
+ if memo.dig(resolver.location, resolver.key)
36
+ raise Composer::ValidationError, "Multiple resolver queries for `#{type.graphql_name}.#{resolver.key}` "\
37
+ "found in #{resolver.location}. Limit one resolver query per type and key in each location. "\
38
+ "Abstract resolvers provide all possible types."
39
39
  end
40
- memo[boundary.location] ||= {}
41
- memo[boundary.location][boundary.key] = boundary
40
+ memo[resolver.location] ||= {}
41
+ memo[resolver.location][resolver.key] = resolver
42
42
  end
43
43
 
44
- boundary_keys = boundaries.map(&:key).to_set
44
+ resolver_keys = resolvers.map(&:key).to_set
45
45
 
46
- # All non-key fields must be resolvable in at least one boundary location
46
+ # All non-key fields must be resolvable in at least one resolver location
47
47
  supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
48
- next if boundary_keys.include?(field_name)
48
+ next if resolver_keys.include?(field_name)
49
49
 
50
- if locations.none? { boundaries_by_location_and_key[_1] }
50
+ if locations.none? { resolvers_by_location_and_key[_1] }
51
51
  where = locations.length > 1 ? "one of #{locations.join(", ")} locations" : locations.first
52
- raise Composer::ValidationError, "A boundary query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
52
+ raise Composer::ValidationError, "A resolver query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
53
53
  end
54
54
  end
55
55
 
56
- # All locations of a boundary type must include at least one key field
56
+ # All locations of a resolver type must include at least one key field
57
57
  supergraph.fields_by_type_and_location[type.graphql_name].each do |location, field_names|
58
- if field_names.none? { boundary_keys.include?(_1) }
59
- raise Composer::ValidationError, "A boundary key is required for `#{type.graphql_name}` in #{location} to join with other locations."
58
+ if field_names.none? { resolver_keys.include?(_1) }
59
+ raise Composer::ValidationError, "A resolver key is required for `#{type.graphql_name}` in #{location} to join with other locations."
60
60
  end
61
61
  end
62
62
 
63
63
  # verify that all outbound locations can access all inbound locations
64
- resolver_locations = boundaries_by_location_and_key.keys
64
+ resolver_locations = resolvers_by_location_and_key.keys
65
65
  candidate_types_by_location.each_key do |location|
66
66
  remote_locations = resolver_locations.reject { _1 == location }
67
67
  paths = supergraph.route_type_to_locations(type.graphql_name, location, remote_locations)
68
68
  if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
69
- raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` boundaries in #{location} to all other locations. "\
70
- "All locations must provide a boundary accessor that uses a conjoining key."
69
+ raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` resolvers in #{location} to all other locations. "\
70
+ "All locations must provide a resolver query with a joining key."
71
71
  end
72
72
  end
73
73
  end
@@ -87,7 +87,7 @@ module GraphQL::Stitching
87
87
  candidate_types_by_location.each do |location, candidate_type|
88
88
  if candidate_type.fields.keys.sort != expected_fields
89
89
  raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
90
- "or else define boundary queries so that its unique fields may be accessed remotely."
90
+ "or else define resolver queries so that its unique fields may be accessed remotely."
91
91
  end
92
92
  end
93
93
  end
@@ -2,8 +2,8 @@
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
@@ -31,7 +31,7 @@ module GraphQL
31
31
  # @api private
32
32
  VALIDATORS = [
33
33
  "ValidateInterfaces",
34
- "ValidateBoundaries",
34
+ "ValidateResolvers",
35
35
  ].freeze
36
36
 
37
37
  # @return [String] name of the Query type in the composed schema.
@@ -62,10 +62,10 @@ module GraphQL
62
62
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
63
63
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
64
64
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
65
- @boundary_configs = {}
65
+ @resolver_configs = {}
66
66
 
67
67
  @field_map = nil
68
- @boundary_map = nil
68
+ @resolver_map = nil
69
69
  @mapped_type_names = nil
70
70
  @candidate_directives_by_name_and_location = nil
71
71
  @candidate_types_by_name_and_location = nil
@@ -125,7 +125,7 @@ module GraphQL
125
125
  raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
126
126
  end
127
127
 
128
- extract_boundaries(type_name, types_by_location) if type_name == @query_name
128
+ extract_resolvers(type_name, types_by_location) if type_name == @query_name
129
129
 
130
130
  memo[type_name] = case kinds.first
131
131
  when "SCALAR"
@@ -157,12 +157,12 @@ module GraphQL
157
157
  end
158
158
 
159
159
  select_root_field_locations(schema)
160
- expand_abstract_boundaries(schema)
160
+ expand_abstract_resolvers(schema)
161
161
 
162
162
  supergraph = Supergraph.new(
163
163
  schema: schema,
164
164
  fields: @field_map,
165
- boundaries: @boundary_map,
165
+ resolvers: @resolver_map,
166
166
  executables: executables,
167
167
  )
168
168
 
@@ -189,8 +189,8 @@ module GraphQL
189
189
  raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
190
190
  end
191
191
 
192
- @boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
193
- @boundary_configs.merge!(BoundaryConfig.extract_federation_entities(schema, location))
192
+ @resolver_configs.merge!(ResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
193
+ @resolver_configs.merge!(ResolverConfig.extract_federation_entities(schema, location))
194
194
 
195
195
  schemas[location.to_s] = schema
196
196
  executables[location.to_s] = input[:executable] || schema
@@ -527,63 +527,64 @@ module GraphQL
527
527
 
528
528
  # @!scope class
529
529
  # @!visibility private
530
- def extract_boundaries(type_name, types_by_location)
530
+ def extract_resolvers(type_name, types_by_location)
531
531
  types_by_location.each do |location, type_candidate|
532
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}", [])
533
+ resolver_type = field_candidate.type.unwrap
534
+ resolver_structure = Util.flatten_type_structure(field_candidate.type)
535
+ resolver_configs = @resolver_configs.fetch("#{location}.#{field_name}", [])
536
536
 
537
537
  field_candidate.directives.each do |directive|
538
538
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
539
- boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
539
+ resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
540
540
  end
541
541
 
542
- boundary_configs.each do |config|
542
+ resolver_configs.each do |config|
543
543
  key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
544
544
 
545
545
  if key_selections.length != 1
546
- raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
546
+ raise ComposerError, "Resolver key at #{type_name}.#{field_name} must specify exactly one key."
547
547
  end
548
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
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
554
  end
555
555
 
556
- argument = field_candidate.arguments[argument_name]
557
556
  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}."
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`"
560
559
  end
561
560
 
562
561
  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."
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
565
  end
566
566
 
567
- boundary_type_name = if config.type_name
568
- if !boundary_type.kind.abstract?
567
+ resolver_type_name = if config.type_name
568
+ if !resolver_type.kind.abstract?
569
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 }
570
+ elsif !resolver_type.possible_types.find { _1.graphql_name == config.type_name }
571
571
  raise ComposerError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
572
572
  end
573
573
  config.type_name
574
574
  else
575
- boundary_type.graphql_name
575
+ resolver_type.graphql_name
576
576
  end
577
577
 
578
- @boundary_map[boundary_type_name] ||= []
579
- @boundary_map[boundary_type_name] << Boundary.new(
578
+ @resolver_map[resolver_type_name] ||= []
579
+ @resolver_map[resolver_type_name] << Resolver.new(
580
580
  location: location,
581
- type_name: boundary_type_name,
581
+ type_name: resolver_type_name,
582
582
  key: key_selections[0].name,
583
583
  field: field_candidate.name,
584
- arg: argument_name,
585
- list: boundary_structure.first.list?,
586
- federation: config.federation,
584
+ arg: argument.graphql_name,
585
+ arg_type_name: argument.type.unwrap.graphql_name,
586
+ list: resolver_structure.first.list?,
587
+ representations: config.representations,
587
588
  )
588
589
  end
589
590
  end
@@ -612,15 +613,15 @@ module GraphQL
612
613
 
613
614
  # @!scope class
614
615
  # @!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?
616
+ def expand_abstract_resolvers(schema)
617
+ @resolver_map.keys.each do |type_name|
618
+ resolver_type = schema.types[type_name]
619
+ next unless resolver_type.kind.abstract?
619
620
 
620
- expanded_types = Util.expand_abstract_type(schema, boundary_type)
621
+ expanded_types = Util.expand_abstract_type(schema, resolver_type)
621
622
  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])
623
+ @resolver_map[expanded_type.graphql_name] ||= []
624
+ @resolver_map[expanded_type.graphql_name].push(*@resolver_map[type_name])
624
625
  end
625
626
  end
626
627
  end
@@ -670,7 +671,7 @@ module GraphQL
670
671
 
671
672
  def reset!
672
673
  @field_map = {}
673
- @boundary_map = {}
674
+ @resolver_map = {}
674
675
  @mapped_type_names = {}
675
676
  @candidate_directives_by_name_and_location = nil
676
677
  @schema_directives = nil
@@ -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)
@@ -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,40 @@ 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
+ resolver = op.resolver
56
57
 
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
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?)
62
63
  end
63
64
 
64
- "_#{batch_index}_result: #{boundary.field}(#{boundary.arg}:[#{input}]) #{op.selections}"
65
+ variable_defs[variable_name] = "[#{resolver.arg_type_name}!]!"
66
+ "_#{batch_index}_result: #{resolver.field}(#{resolver.arg}:$#{variable_name}) #{op.selections}"
65
67
  else
66
68
  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}"
69
+ variable_name = "_#{batch_index}_#{index}_key"
70
+ @variables[variable_name] = build_key(resolver.key, origin_obj, as_representation: resolver.representations?)
71
+
72
+ variable_defs[variable_name] = "#{resolver.arg_type_name}!"
73
+ "_#{batch_index}_#{index}_result: #{resolver.field}(#{resolver.arg}:$#{variable_name}) #{op.selections}"
69
74
  end
70
75
  end
71
76
  end
72
77
 
73
- doc = String.new("query") # << boundary fulfillment always uses query
78
+ doc = String.new("query") # << resolver fulfillment always uses query
74
79
 
75
80
  if operation_name
76
81
  doc << " #{operation_name}"
@@ -90,15 +95,19 @@ module GraphQL::Stitching
90
95
 
91
96
  doc << "{ #{query_fields.join(" ")} }"
92
97
 
93
- return doc, variable_defs.keys
98
+ return doc, variable_defs.keys.tap do |names|
99
+ names.reject! { @variables.key?(_1) }
100
+ end
94
101
  end
95
102
 
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} }"
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
+ }
100
109
  else
101
- key_value
110
+ origin_obj[ExportSelection.key(key)]
102
111
  end
103
112
  end
104
113
 
@@ -106,7 +115,7 @@ module GraphQL::Stitching
106
115
  return unless raw_result
107
116
 
108
117
  origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
109
- results = if op.dig("boundary", "list")
118
+ results = if op.resolver.list?
110
119
  raw_result["_#{batch_index}_result"]
111
120
  else
112
121
  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&.as_json
31
31
  }.tap(&:compact!)
32
32
  end
33
33
  end
@@ -36,7 +36,7 @@ module GraphQL
36
36
  def from_json(json)
37
37
  ops = json["ops"]
38
38
  ops = ops.map do |op|
39
- boundary = op["boundary"]
39
+ resolver = op["resolver"]
40
40
  Op.new(
41
41
  step: op["step"],
42
42
  after: op["after"],
@@ -46,7 +46,7 @@ module GraphQL
46
46
  variables: op["variables"],
47
47
  path: op["path"],
48
48
  if_type: op["if_type"],
49
- boundary: boundary ? GraphQL::Stitching::Boundary.new(**boundary) : nil,
49
+ resolver: resolver ? GraphQL::Stitching::Resolver.new(**resolver) : nil,
50
50
  )
51
51
  end
52
52
  new(ops: ops)
@@ -18,7 +18,7 @@ module GraphQL
18
18
 
19
19
  def perform
20
20
  build_root_entrypoints
21
- expand_abstract_boundaries
21
+ expand_abstract_resolvers
22
22
  Plan.new(ops: steps.map(&:to_plan_op))
23
23
  end
24
24
 
@@ -50,16 +50,16 @@ module GraphQL
50
50
  # C.2) Distribute non-unique fields among locations that were added during C.1.
51
51
  # C.3) Distribute remaining fields among locations weighted by greatest availability.
52
52
  #
53
- # D) Create paths routing to new entrypoint locations via boundary queries.
53
+ # D) Create paths routing to new entrypoint locations via resolver queries.
54
54
  # D.1) Types joining through multiple keys route using A* search.
55
55
  # D.2) Types joining through a single key route via quick location match.
56
56
  # (D.2 is an optional optimization of D.1)
57
57
  #
58
- # E) Translate boundary pathways into new entrypoints.
59
- # E.1) Add the key of each boundary query into the prior location's selection set.
58
+ # E) Translate resolver pathways into new entrypoints.
59
+ # E.1) Add the key of each resolver query into the prior location's selection set.
60
60
  # E.2) Add a planner step for each new entrypoint location, then extract it (B).
61
61
  #
62
- # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
62
+ # F) Wrap concrete selections targeting abstract resolvers in typed fragments.
63
63
  # **
64
64
 
65
65
  # adds a planning step for fetching and inserting data into the aggregate result.
@@ -71,10 +71,10 @@ module GraphQL
71
71
  variables: {},
72
72
  path: [],
73
73
  operation_type: QUERY_OP,
74
- boundary: nil
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}/#{boundary&.key}")
77
+ entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key}")
78
78
  path.each { entrypoint << "/#{_1}" }
79
79
 
80
80
  step = @steps_by_entrypoint[entrypoint]
@@ -94,7 +94,7 @@ module GraphQL
94
94
  selections: selections,
95
95
  variables: variables,
96
96
  path: path,
97
- boundary: boundary,
97
+ resolver: resolver,
98
98
  )
99
99
  else
100
100
  step.selections.concat(selections)
@@ -269,15 +269,15 @@ module GraphQL
269
269
  # C) Delegate adjoining selections to new entrypoint locations.
270
270
  remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
271
271
 
272
- # D) Create paths routing to new entrypoint locations via boundary queries.
272
+ # D) Create paths routing to new entrypoint locations via resolver queries.
273
273
  routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.keys)
274
274
 
275
- # E) Translate boundary pathways into new entrypoints.
275
+ # E) Translate resolver pathways into new entrypoints.
276
276
  routes.each_value do |route|
277
- route.reduce(locale_selections) do |parent_selections, boundary|
278
- # E.1) Add the key of each boundary query into the prior location's selection set.
279
- if boundary.key
280
- foreign_key = ExportSelection.key(boundary.key)
277
+ route.reduce(locale_selections) do |parent_selections, resolver|
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
281
  has_key = false
282
282
  has_typename = false
283
283
 
@@ -287,18 +287,18 @@ module GraphQL
287
287
  has_typename ||= node.alias == ExportSelection.typename_node.alias
288
288
  end
289
289
 
290
- parent_selections << ExportSelection.key_node(boundary.key) unless has_key
290
+ parent_selections << ExportSelection.key_node(resolver.key) unless has_key
291
291
  parent_selections << ExportSelection.typename_node unless has_typename
292
292
  end
293
293
 
294
294
  # E.2) Add a planner step for each new entrypoint location.
295
295
  add_step(
296
- location: boundary.location,
296
+ location: resolver.location,
297
297
  parent_index: parent_index,
298
298
  parent_type: parent_type,
299
- selections: remote_selections_by_location[boundary.location] || [],
299
+ selections: remote_selections_by_location[resolver.location] || [],
300
300
  path: path.dup,
301
- boundary: boundary.key ? boundary : nil,
301
+ resolver: resolver.key ? resolver : nil,
302
302
  ).selections
303
303
  end
304
304
  end
@@ -414,14 +414,14 @@ module GraphQL
414
414
  selections_by_location
415
415
  end
416
416
 
417
- # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
418
- def expand_abstract_boundaries
417
+ # F) Wrap concrete selections targeting abstract resolvers in typed fragments.
418
+ def expand_abstract_resolvers
419
419
  @steps_by_entrypoint.each_value do |step|
420
- next unless step.boundary
420
+ next unless step.resolver
421
421
 
422
- boundary_type = @supergraph.memoized_schema_types[step.boundary.type_name]
423
- next unless boundary_type.kind.abstract?
424
- next if boundary_type == step.parent_type
422
+ resolver_type = @supergraph.memoized_schema_types[step.resolver.type_name]
423
+ next unless resolver_type.kind.abstract?
424
+ next if resolver_type == step.parent_type
425
425
 
426
426
  expanded_selections = nil
427
427
  step.selections.reject! do |node|
@@ -9,7 +9,7 @@ module GraphQL
9
9
  GRAPHQL_PRINTER = GraphQL::Language::Printer.new
10
10
 
11
11
  attr_reader :index, :location, :parent_type, :operation_type, :path
12
- attr_accessor :after, :selections, :variables, :boundary
12
+ attr_accessor :after, :selections, :variables, :resolver
13
13
 
14
14
  def initialize(
15
15
  location:,
@@ -20,7 +20,7 @@ module GraphQL
20
20
  selections: [],
21
21
  variables: {},
22
22
  path: [],
23
- boundary: nil
23
+ resolver: nil
24
24
  )
25
25
  @location = location
26
26
  @parent_type = parent_type
@@ -30,7 +30,7 @@ module GraphQL
30
30
  @selections = selections
31
31
  @variables = variables
32
32
  @path = path
33
- @boundary = boundary
33
+ @resolver = resolver
34
34
  end
35
35
 
36
36
  def to_plan_op
@@ -43,17 +43,17 @@ module GraphQL
43
43
  variables: rendered_variables,
44
44
  path: @path,
45
45
  if_type: type_condition,
46
- boundary: @boundary,
46
+ resolver: @resolver,
47
47
  )
48
48
  end
49
49
 
50
50
  private
51
51
 
52
- # Concrete types going to a boundary report themselves as a type condition.
52
+ # Concrete types going to a resolver report themselves as a type condition.
53
53
  # This is used by the executor to evalute which planned fragment selections
54
54
  # actually apply to the resolved object types.
55
55
  def type_condition
56
- @parent_type.graphql_name if @boundary && !parent_type.kind.abstract?
56
+ @parent_type.graphql_name if @resolver && !parent_type.kind.abstract?
57
57
  end
58
58
 
59
59
  def rendered_selections
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ # Defines a root resolver query that provides direct access to an entity type.
6
+ Resolver = Struct.new(
7
+ # location name providing the resolver query.
8
+ :location,
9
+
10
+ # name of merged type fulfilled through this resolver.
11
+ :type_name,
12
+
13
+ # a key field to select from prior locations, sent as resolver argument.
14
+ :key,
15
+
16
+ # name of the root field to query.
17
+ :field,
18
+
19
+ # specifies when the resolver is a list query.
20
+ :list,
21
+
22
+ # name of the root field argument used to send the key.
23
+ :arg,
24
+
25
+ # type name of the root field argument used to send the key.
26
+ :arg_type_name,
27
+
28
+ # specifies that keys should be sent as JSON representations with __typename and key.
29
+ :representations,
30
+ keyword_init: true
31
+ ) do
32
+ alias_method :list?, :list
33
+ alias_method :representations?, :representations
34
+
35
+ def as_json
36
+ {
37
+ location: location,
38
+ type_name: type_name,
39
+ key: key,
40
+ field: field,
41
+ list: list,
42
+ arg: arg,
43
+ arg_type_name: arg_type_name,
44
+ representations: representations,
45
+ }.tap(&:compact!)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -10,8 +10,9 @@ module GraphQL::Stitching
10
10
  argument :key, String, required: true
11
11
  argument :field, String, required: true
12
12
  argument :arg, String, required: true
13
+ argument :arg_type_name, String, required: true
13
14
  argument :list, Boolean, required: false
14
- argument :federation, Boolean, required: false
15
+ argument :representations, Boolean, required: false
15
16
  repeatable true
16
17
  end
17
18
  end
@@ -18,7 +18,7 @@ module GraphQL
18
18
  def from_definition(schema, executables:)
19
19
  schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
20
20
  field_map = {}
21
- boundary_map = {}
21
+ resolver_map = {}
22
22
  possible_locations = {}
23
23
  introspection_types = schema.introspection_system.types.keys
24
24
 
@@ -29,15 +29,16 @@ module GraphQL
29
29
  next unless directive.graphql_name == ResolverDirective.graphql_name
30
30
 
31
31
  kwargs = directive.arguments.keyword_arguments
32
- boundary_map[type_name] ||= []
33
- boundary_map[type_name] << Boundary.new(
32
+ resolver_map[type_name] ||= []
33
+ resolver_map[type_name] << Resolver.new(
34
34
  type_name: kwargs.fetch(:type_name, type_name),
35
35
  location: kwargs[:location],
36
36
  key: kwargs[:key],
37
37
  field: kwargs[:field],
38
- arg: kwargs[:arg],
39
38
  list: kwargs[:list] || false,
40
- federation: kwargs[:federation] || false,
39
+ arg: kwargs[:arg],
40
+ arg_type_name: kwargs[:arg_type_name],
41
+ representations: kwargs[:representations] || false,
41
42
  )
42
43
  end
43
44
 
@@ -66,7 +67,7 @@ module GraphQL
66
67
  new(
67
68
  schema: schema,
68
69
  fields: field_map,
69
- boundaries: boundary_map,
70
+ resolvers: resolver_map,
70
71
  executables: executables,
71
72
  )
72
73
  end
@@ -78,13 +79,13 @@ module GraphQL
78
79
  # @return [Hash<String, Executable>] a map of executable resources by location.
79
80
  attr_reader :executables
80
81
 
81
- attr_reader :boundaries, :locations_by_type_and_field
82
+ attr_reader :resolvers, :locations_by_type_and_field
82
83
 
83
- def initialize(schema:, fields: {}, boundaries: {}, executables: {})
84
+ def initialize(schema:, fields: {}, resolvers: {}, executables: {})
84
85
  @schema = schema
85
86
  @schema.use(GraphQL::Schema::AlwaysVisible)
86
87
 
87
- @boundaries = boundaries
88
+ @resolvers = resolvers
88
89
  @fields_by_type_and_location = nil
89
90
  @locations_by_type = nil
90
91
  @memoized_introspection_types = nil
@@ -120,27 +121,28 @@ module GraphQL
120
121
  end
121
122
 
122
123
  @schema.types.each do |type_name, type|
123
- if boundaries_for_type = @boundaries.dig(type_name)
124
- boundaries_for_type.each do |boundary|
124
+ if resolvers_for_type = @resolvers.dig(type_name)
125
+ resolvers_for_type.each do |resolver|
125
126
  existing = type.directives.find do |d|
126
127
  kwargs = d.arguments.keyword_arguments
127
128
  d.graphql_name == ResolverDirective.graphql_name &&
128
- kwargs[:location] == boundary.location &&
129
- kwargs[:key] == boundary.key &&
130
- kwargs[:field] == boundary.field &&
131
- kwargs[:arg] == boundary.arg &&
132
- kwargs.fetch(:list, false) == boundary.list &&
133
- kwargs.fetch(:federation, false) == boundary.federation
129
+ kwargs[:location] == resolver.location &&
130
+ kwargs[:key] == resolver.key &&
131
+ kwargs[:field] == resolver.field &&
132
+ kwargs[:arg] == resolver.arg &&
133
+ kwargs.fetch(:list, false) == resolver.list &&
134
+ kwargs.fetch(:representations, false) == resolver.representations
134
135
  end
135
136
 
136
137
  type.directive(ResolverDirective, **{
137
- type_name: (boundary.type_name if boundary.type_name != type_name),
138
- location: boundary.location,
139
- key: boundary.key,
140
- field: boundary.field,
141
- arg: boundary.arg,
142
- list: boundary.list || nil,
143
- federation: boundary.federation || nil,
138
+ type_name: (resolver.type_name if resolver.type_name != type_name),
139
+ location: resolver.location,
140
+ key: resolver.key,
141
+ field: resolver.field,
142
+ list: resolver.list || nil,
143
+ arg: resolver.arg,
144
+ arg_type_name: resolver.arg_type_name,
145
+ representations: resolver.representations || nil,
144
146
  }.tap(&:compact!)) if existing.nil?
145
147
  end
146
148
  end
@@ -242,19 +244,19 @@ module GraphQL
242
244
  end
243
245
  end
244
246
 
245
- # collects all possible boundary keys for a given type
247
+ # collects all possible resolver keys for a given type
246
248
  # ("Type") => ["id", ...]
247
249
  def possible_keys_for_type(type_name)
248
250
  @possible_keys_by_type[type_name] ||= begin
249
251
  if type_name == @schema.query.graphql_name
250
252
  GraphQL::Stitching::EMPTY_ARRAY
251
253
  else
252
- @boundaries[type_name].map(&:key).tap(&:uniq!)
254
+ @resolvers[type_name].map(&:key).tap(&:uniq!)
253
255
  end
254
256
  end
255
257
  end
256
258
 
257
- # collects possible boundary keys for a given type and location
259
+ # collects possible resolver keys for a given type and location
258
260
  # ("Type", "location") => ["id", ...]
259
261
  def possible_keys_for_type_and_location(type_name, location)
260
262
  possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
@@ -265,14 +267,14 @@ module GraphQL
265
267
  end
266
268
 
267
269
  # For a given type, route from one origin location to one or more remote locations
268
- # used to connect a partial type across locations via boundary queries
270
+ # used to connect a partial type across locations via resolver queries
269
271
  def route_type_to_locations(type_name, start_location, goal_locations)
270
272
  key_count = possible_keys_for_type(type_name).length
271
273
 
272
274
  if key_count.zero?
273
- # nested root scopes have no boundary keys and just return a location
275
+ # nested root scopes have no resolver keys and just return a location
274
276
  goal_locations.each_with_object({}) do |goal_location, memo|
275
- memo[goal_location] = [Boundary.new(location: goal_location)]
277
+ memo[goal_location] = [Resolver.new(location: goal_location)]
276
278
  end
277
279
 
278
280
  elsif key_count > 1
@@ -281,10 +283,10 @@ module GraphQL
281
283
 
282
284
  else
283
285
  # types with a single key attribute must all be within a single hop of each other,
284
- # so can use a simple match to collect boundaries for the goal locations.
285
- @boundaries[type_name].each_with_object({}) do |boundary, memo|
286
- if goal_locations.include?(boundary.location)
287
- memo[boundary.location] = [boundary]
286
+ # so can use a simple match to collect resolvers for the goal locations.
287
+ @resolvers[type_name].each_with_object({}) do |resolver, memo|
288
+ if goal_locations.include?(resolver.location)
289
+ memo[resolver.location] = [resolver]
288
290
  end
289
291
  end
290
292
  end
@@ -292,7 +294,7 @@ module GraphQL
292
294
 
293
295
  private
294
296
 
295
- PathNode = Struct.new(:location, :key, :cost, :boundary, keyword_init: true)
297
+ PathNode = Struct.new(:location, :key, :cost, :resolver, keyword_init: true)
296
298
 
297
299
  # tunes A* search to favor paths with fewest joining locations, ie:
298
300
  # favor longer paths through target locations over shorter paths with additional locations.
@@ -310,9 +312,9 @@ module GraphQL
310
312
  current_key = path.last.key
311
313
  current_cost = path.last.cost
312
314
 
313
- @boundaries[type_name].each do |boundary|
314
- forward_location = boundary.location
315
- next if current_key != boundary.key
315
+ @resolvers[type_name].each do |resolver|
316
+ forward_location = resolver.location
317
+ next if current_key != resolver.key
316
318
  next if path.any? { _1.location == forward_location }
317
319
 
318
320
  best_cost = costs[forward_location] || Float::INFINITY
@@ -323,13 +325,13 @@ module GraphQL
323
325
  location: current_location,
324
326
  key: current_key,
325
327
  cost: current_cost,
326
- boundary: boundary,
328
+ resolver: resolver,
327
329
  )
328
330
 
329
331
  if goal_locations.include?(forward_location)
330
332
  current_result = results[forward_location]
331
333
  if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
332
- results[forward_location] = path.map(&:boundary)
334
+ results[forward_location] = path.map(&:resolver)
333
335
  end
334
336
  else
335
337
  path.last.cost += 1
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.2.5"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -24,7 +24,7 @@ module GraphQL
24
24
  end
25
25
 
26
26
  require_relative "stitching/supergraph"
27
- require_relative "stitching/boundary"
27
+ require_relative "stitching/resolver"
28
28
  require_relative "stitching/client"
29
29
  require_relative "stitching/composer"
30
30
  require_relative "stitching/executor"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-stitching
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-31 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -110,15 +110,14 @@ files:
110
110
  - gemfiles/graphql_2.2.0.gemfile
111
111
  - graphql-stitching.gemspec
112
112
  - lib/graphql/stitching.rb
113
- - lib/graphql/stitching/boundary.rb
114
113
  - lib/graphql/stitching/client.rb
115
114
  - lib/graphql/stitching/composer.rb
116
115
  - lib/graphql/stitching/composer/base_validator.rb
117
- - lib/graphql/stitching/composer/boundary_config.rb
118
- - lib/graphql/stitching/composer/validate_boundaries.rb
116
+ - lib/graphql/stitching/composer/resolver_config.rb
119
117
  - lib/graphql/stitching/composer/validate_interfaces.rb
118
+ - lib/graphql/stitching/composer/validate_resolvers.rb
120
119
  - lib/graphql/stitching/executor.rb
121
- - lib/graphql/stitching/executor/boundary_source.rb
120
+ - lib/graphql/stitching/executor/resolver_source.rb
122
121
  - lib/graphql/stitching/executor/root_source.rb
123
122
  - lib/graphql/stitching/export_selection.rb
124
123
  - lib/graphql/stitching/http_executable.rb
@@ -126,6 +125,7 @@ files:
126
125
  - lib/graphql/stitching/planner.rb
127
126
  - lib/graphql/stitching/planner_step.rb
128
127
  - lib/graphql/stitching/request.rb
128
+ - lib/graphql/stitching/resolver.rb
129
129
  - lib/graphql/stitching/shaper.rb
130
130
  - lib/graphql/stitching/skip_include.rb
131
131
  - lib/graphql/stitching/supergraph.rb
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- # Defines a boundary query that provides direct access to an entity type.
6
- Boundary = Struct.new(
7
- :location,
8
- :type_name,
9
- :key,
10
- :field,
11
- :arg,
12
- :list,
13
- :federation,
14
- keyword_init: true
15
- ) do
16
- def as_json
17
- {
18
- location: location,
19
- type_name: type_name,
20
- key: key,
21
- field: field,
22
- arg: arg,
23
- list: list,
24
- federation: federation,
25
- }.tap(&:compact!)
26
- end
27
- end
28
- end
29
- end