graphql-stitching 1.2.3 → 1.2.5

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: 99acc247a0e5e227236b0261b3f975e7277f642a9d76daaf4f6a985bfc4825ea
4
- data.tar.gz: ef5f01132498ca4ba1be6f8593f189645ddf8fb6b6d8ab71f9b7d0e55d6ec682
3
+ metadata.gz: 754e7a61df541f685ef50ec79039df9742e4a2589a30eadf93125d6e4ef47aed
4
+ data.tar.gz: e2ceb89be07b42dbffeaa75416055fa62ffafa001088fd47b2b4f156aee6123a
5
5
  SHA512:
6
- metadata.gz: d1bd0db4c7f6d363231330222006f2bbc552a669227a38aeb7861dde843b59bc3e61b0f301f97d4a5c6b55193d01424cd9f240c0ff7a7ee483e49150686ac3da
7
- data.tar.gz: 348aa663608fd8b0e5544ccf7ec500d4295a63bb9bd19a91031f927165da2a206024ce6cb17111c3ebda2c172e08072be8d01716c11f0a05c3ea8a9a4b99466e
6
+ metadata.gz: 561ecdc36e78b31e4f4fd2f85d4ff5e0f1ab23d44570de7d8ffe63c2e46b5856214eb07c25b98eafbd64857602ebd839e187175e9e3ec347caedf4651a00d332
7
+ data.tar.gz: dd7a98e1c97f7bd5a6ca2585c34b6538e94b6dfaaa83edf9b3f1bc287a79d04f62a29ebe2a6b6fae4d06e8af9f08bb20203a89f1b062f24dc622a66973f87abc
data/README.md CHANGED
@@ -10,12 +10,13 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
10
10
  - Shared objects, fields, enums, and inputs across locations.
11
11
  - Combining local and remote schemas.
12
12
  - File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
13
+ - Tested with all minor versions of `graphql-ruby`.
13
14
 
14
15
  **NOT Supported:**
15
16
  - Computed fields (ie: federation-style `@requires`).
16
17
  - Subscriptions, defer/stream.
17
18
 
18
- This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-throughput API gateway, consider not using Ruby.
19
+ This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-throughput federated reverse proxy, consider not using Ruby.
19
20
 
20
21
  ## Getting started
21
22
 
@@ -152,7 +153,7 @@ type Query {
152
153
  * The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
153
154
  * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#multiple-query-arguments) later).
154
155
 
155
- Each location that provides a unique variant of a type must provide at least one stitching query for the type. The exception to this requirement are [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain only a single key field:
156
+ Each location that provides a unique variant of a type must provide at least one resolver query for the type. The exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) and/or [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain no exclusive data:
156
157
 
157
158
  ```graphql
158
159
  type Product {
@@ -160,11 +161,11 @@ type Product {
160
161
  }
161
162
  ```
162
163
 
163
- The above representation of a `Product` type provides no unique data beyond a key that is available in other locations. Thus, this representation will never require an inbound request to fetch it, and its stitching query may be omitted.
164
+ The above representation of a `Product` type contains nothing but a key that is available in other locations. Therefore, this representation will never require an inbound request to fetch it, and its resolver query may be omitted.
164
165
 
165
166
  #### List queries
166
167
 
167
- It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a stitching query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
168
+ It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a resolver query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
168
169
 
169
170
  ```graphql
170
171
  type Query {
@@ -179,7 +180,7 @@ See [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.
179
180
 
180
181
  #### Abstract queries
181
182
 
182
- It's okay for stitching queries to be implemented through abstract types. An abstract query will provide access to all of its possible types. For interfaces, the key selection should match a field within the interface. For unions, all possible types must implement the key selection individually.
183
+ It's okay for resolver queries to be implemented through abstract types. An abstract query will provide access to all of its possible types by default, each of which must implement the key.
183
184
 
184
185
  ```graphql
185
186
  interface Node {
@@ -194,13 +195,33 @@ type Query {
194
195
  }
195
196
  ```
196
197
 
198
+ To customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.
199
+
200
+ ```graphql
201
+ directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
202
+
203
+ type Product { sku: ID! }
204
+ type Order { id: ID! }
205
+ type Customer { id: ID! } # << not stitched
206
+ union Entity = Product | Order | Customer
207
+
208
+ type Query {
209
+ entity(key: ID!): Entity
210
+ @stitch(key: "sku", typeName: "Product")
211
+ @stitch(key: "id", typeName: "Order")
212
+ }
213
+ ```
214
+
197
215
  #### Multiple query arguments
198
216
 
199
- Stitching infers which argument to use for queries with a single argument. For queries that accept multiple arguments, the key must provide an argument mapping specified as `"<arg>:<key>"`. Note the `"id:id"` key:
217
+ Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For queries that accept multiple arguments with unmatched names, the key should provide an argument alias specified as `"<arg>:<key>"`.
200
218
 
201
219
  ```graphql
220
+ type Product {
221
+ id: ID!
222
+ }
202
223
  type Query {
203
- product(id: ID, upc: ID): Product @stitch(key: "id:id")
224
+ product(byId: ID, bySku: ID): Product @stitch(key: "byId:id")
204
225
  }
205
226
  ```
206
227
 
@@ -210,20 +231,20 @@ A type may exist in multiple locations across the graph using different keys, fo
210
231
 
211
232
  ```graphql
212
233
  type Product { id:ID! } # storefronts location
213
- type Product { id:ID! upc:ID! } # products location
214
- type Product { upc:ID! } # catelog location
234
+ type Product { id:ID! sku:ID! } # products location
235
+ type Product { sku:ID! } # catelog location
215
236
  ```
216
237
 
217
- In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides stitching queries for each possible key:
238
+ In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides resolver queries for each possible key:
218
239
 
219
240
  ```graphql
220
241
  type Product {
221
242
  id: ID!
222
- upc: ID!
243
+ sku: ID!
223
244
  }
224
245
  type Query {
225
246
  productById(id: ID!): Product @stitch(key: "id")
226
- productByUpc(upc: ID!): Product @stitch(key: "upc")
247
+ productBySku(sku: ID!): Product @stitch(key: "sku")
227
248
  }
228
249
  ```
229
250
 
@@ -232,10 +253,10 @@ The `@stitch` directive is also repeatable, allowing a single query to associate
232
253
  ```graphql
233
254
  type Product {
234
255
  id: ID!
235
- upc: ID!
256
+ sku: ID!
236
257
  }
237
258
  type Query {
238
- product(id: ID, upc: ID): Product @stitch(key: "id:id") @stitch(key: "upc:upc")
259
+ product(id: ID, sku: ID): Product @stitch(key: "id") @stitch(key: "sku")
239
260
  }
240
261
  ```
241
262
 
@@ -269,11 +290,11 @@ A clean SDL string may also have stitching directives applied via static configu
269
290
  sdl_string = <<~GRAPHQL
270
291
  type Product {
271
292
  id: ID!
272
- upc: ID!
293
+ sku: ID!
273
294
  }
274
295
  type Query {
275
296
  productById(id: ID!): Product
276
- productByUpc(upc: ID!): Product
297
+ productBySku(sku: ID!): Product
277
298
  }
278
299
  GRAPHQL
279
300
 
@@ -283,7 +304,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
283
304
  executable: ->() { ... },
284
305
  stitch: [
285
306
  { field_name: "productById", key: "id" },
286
- { field_name: "productByUpc", key: "upc" },
307
+ { field_name: "productBySku", key: "sku" },
287
308
  ]
288
309
  },
289
310
  # ...
data/docs/mechanics.md CHANGED
@@ -303,3 +303,45 @@ And produces this result:
303
303
  ```
304
304
 
305
305
  Location B is allowed to return `null` here because its one unique field, `rating`, is nullable (the `id` field can be provided by Location A). If `rating` were non-null, then null bubbling would invalidate the response data.
306
+
307
+ ### Outbound-only merged types
308
+
309
+ Merged types do not always require a resolver query. For example:
310
+
311
+ ```graphql
312
+ # -- Location A
313
+
314
+ type Widget {
315
+ id: ID!
316
+ name: String
317
+ }
318
+
319
+ type Query {
320
+ widgetA(id: ID!): Widget @stitch(key: "id")
321
+ }
322
+
323
+ # -- Location B
324
+
325
+ type Widget {
326
+ id: ID!
327
+ size: Float
328
+ }
329
+
330
+ type Query {
331
+ widgetB(id: ID!): Widget @stitch(key: "id")
332
+ }
333
+
334
+ # -- Location C
335
+
336
+ type Widget {
337
+ id: ID!
338
+ name: String
339
+ size: Float
340
+ }
341
+
342
+ type Query {
343
+ featuredWidget: Widget
344
+ }
345
+ ```
346
+
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.
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class BoundaryConfig
6
+ ENTITY_TYPENAME = "_Entity"
7
+ ENTITIES_QUERY = "_entities"
8
+
9
+ class << self
10
+ def extract_directive_assignments(schema, location, assignments)
11
+ return EMPTY_OBJECT unless assignments && assignments.any?
12
+
13
+ assignments.each_with_object({}) do |kwargs, memo|
14
+ type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
15
+ raise ComposerError, "Invalid stitch directive type `#{kwargs[:parent_type_name]}`" unless type
16
+
17
+ field = type.get_field(kwargs[:field_name])
18
+ raise ComposerError, "Invalid stitch directive field `#{kwargs[:field_name]}`" unless field
19
+
20
+ field_path = "#{location}.#{field.name}"
21
+ memo[field_path] ||= []
22
+ memo[field_path] << from_kwargs(kwargs)
23
+ end
24
+ end
25
+
26
+ def extract_federation_entities(schema, location)
27
+ return EMPTY_OBJECT unless federation_entities_schema?(schema)
28
+
29
+ schema.possible_types(schema.get_type(ENTITY_TYPENAME)).each_with_object({}) do |entity_type, memo|
30
+ entity_type.directives.each do |directive|
31
+ next unless directive.graphql_name == "key"
32
+
33
+ key = directive.arguments.keyword_arguments.fetch(:fields).strip
34
+ raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
35
+
36
+ field_path = "#{location}._entities"
37
+ memo[field_path] ||= []
38
+ memo[field_path] << new(
39
+ key: key,
40
+ type_name: entity_type.graphql_name,
41
+ federation: true,
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ def from_kwargs(kwargs)
48
+ new(
49
+ key: kwargs[:key],
50
+ type_name: kwargs[:type_name] || kwargs[:typeName],
51
+ federation: kwargs[:federation] || false,
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def federation_entities_schema?(schema)
58
+ entity_type = schema.get_type(ENTITY_TYPENAME)
59
+ entities_query = schema.query.get_field(ENTITIES_QUERY)
60
+ entity_type && entity_type.kind.union? && entities_query && entities_query.type.unwrap == entity_type
61
+ end
62
+ end
63
+
64
+ attr_reader :key, :type_name, :federation
65
+
66
+ def initialize(key:, type_name:, federation: false)
67
+ @key = key
68
+ @type_name = type_name
69
+ @federation = federation
70
+ end
71
+ end
72
+ end
73
+ end
@@ -4,29 +4,29 @@ module GraphQL::Stitching
4
4
  class Composer
5
5
  class ValidateBoundaries < BaseValidator
6
6
 
7
- def perform(ctx, composer)
8
- ctx.schema.types.each do |type_name, type|
7
+ def perform(supergraph, composer)
8
+ supergraph.schema.types.each do |type_name, type|
9
9
  # objects and interfaces that are not the root operation types
10
10
  next unless type.kind.object? || type.kind.interface?
11
- next if ctx.schema.query == type || ctx.schema.mutation == type
11
+ next if supergraph.schema.query == type || supergraph.schema.mutation == type
12
12
  next if type.graphql_name.start_with?("__")
13
13
 
14
14
  # multiple subschemas implement the type
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 = ctx.boundaries[type_name]
18
+ boundaries = supergraph.boundaries[type_name]
19
19
  if boundaries&.any?
20
- validate_as_boundary(ctx, type, candidate_types_by_location, boundaries)
20
+ validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
21
21
  elsif type.kind.object?
22
- validate_as_shared(ctx, type, candidate_types_by_location)
22
+ validate_as_shared(supergraph, type, candidate_types_by_location)
23
23
  end
24
24
  end
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def validate_as_boundary(ctx, type, candidate_types_by_location, boundaries)
29
+ def validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
30
30
  # abstract boundaries are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
31
31
  return if type.kind.abstract?
32
32
 
@@ -41,25 +41,30 @@ module GraphQL::Stitching
41
41
  memo[boundary.location][boundary.key] = boundary
42
42
  end
43
43
 
44
- boundary_keys = boundaries.map { _1.key }.uniq
45
- key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
46
- subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
47
- end
44
+ boundary_keys = boundaries.map(&:key).to_set
45
+
46
+ # All non-key fields must be resolvable in at least one boundary location
47
+ supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
48
+ next if boundary_keys.include?(field_name)
48
49
 
49
- # all locations have a boundary, or else are key-only
50
- candidate_types_by_location.each do |location, subschema_type|
51
- unless boundaries_by_location_and_key[location] || key_only_types_by_location[location]
52
- raise Composer::ValidationError, "A boundary query is required for `#{type.graphql_name}` in #{location} because it provides unique fields."
50
+ if locations.none? { boundaries_by_location_and_key[_1] }
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}`."
53
53
  end
54
54
  end
55
55
 
56
- outbound_access_locations = key_only_types_by_location.keys
57
- bidirectional_access_locations = candidate_types_by_location.keys - outbound_access_locations
56
+ # All locations of a boundary type must include at least one key field
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."
60
+ end
61
+ end
58
62
 
59
63
  # verify that all outbound locations can access all inbound locations
60
- (outbound_access_locations + bidirectional_access_locations).each do |location|
61
- remote_locations = bidirectional_access_locations.reject { _1 == location }
62
- paths = ctx.route_type_to_locations(type.graphql_name, location, remote_locations)
64
+ resolver_locations = boundaries_by_location_and_key.keys
65
+ candidate_types_by_location.each_key do |location|
66
+ remote_locations = resolver_locations.reject { _1 == location }
67
+ paths = supergraph.route_type_to_locations(type.graphql_name, location, remote_locations)
63
68
  if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
64
69
  raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` boundaries in #{location} to all other locations. "\
65
70
  "All locations must provide a boundary accessor that uses a conjoining key."
@@ -67,7 +72,7 @@ module GraphQL::Stitching
67
72
  end
68
73
  end
69
74
 
70
- def validate_as_shared(ctx, type, candidate_types_by_location)
75
+ def validate_as_shared(supergraph, type, candidate_types_by_location)
71
76
  expected_fields = begin
72
77
  type.fields.keys.sort
73
78
  rescue StandardError => e
@@ -79,8 +84,8 @@ module GraphQL::Stitching
79
84
  end
80
85
  end
81
86
 
82
- candidate_types_by_location.each do |location, subschema_type|
83
- if subschema_type.fields.keys.sort != expected_fields
87
+ candidate_types_by_location.each do |location, candidate_type|
88
+ if candidate_type.fields.keys.sort != expected_fields
84
89
  raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
85
90
  "or else define boundary queries so that its unique fields may be accessed remotely."
86
91
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "./composer/base_validator"
4
4
  require_relative "./composer/validate_interfaces"
5
5
  require_relative "./composer/validate_boundaries"
6
+ require_relative "./composer/boundary_config"
6
7
 
7
8
  module GraphQL
8
9
  module Stitching
@@ -61,12 +62,13 @@ module GraphQL
61
62
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
62
63
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
63
64
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
64
- @stitch_directives = {}
65
+ @boundary_configs = {}
65
66
 
66
67
  @field_map = nil
67
68
  @boundary_map = nil
68
69
  @mapped_type_names = nil
69
70
  @candidate_directives_by_name_and_location = nil
71
+ @candidate_types_by_name_and_location = nil
70
72
  @schema_directives = nil
71
73
  end
72
74
 
@@ -187,37 +189,8 @@ module GraphQL
187
189
  raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
188
190
  end
189
191
 
190
- input.fetch(:stitch, GraphQL::Stitching::EMPTY_ARRAY).each do |dir|
191
- type = dir[:parent_type_name] ? schema.types[dir[:parent_type_name]] : schema.query
192
- raise ComposerError, "Invalid stitch directive type `#{dir[:parent_type_name]}`" unless type
193
-
194
- field = type.fields[dir[:field_name]]
195
- raise ComposerError, "Invalid stitch directive field `#{dir[:field_name]}`" unless field
196
-
197
- field_path = "#{location}.#{field.name}"
198
- @stitch_directives[field_path] ||= []
199
- @stitch_directives[field_path] << dir.slice(:key, :type_name)
200
- end
201
-
202
- federation_entity_type = schema.types["_Entity"]
203
- if federation_entity_type && federation_entity_type.kind.union? && schema.query.fields["_entities"]&.type&.unwrap == federation_entity_type
204
- schema.possible_types(federation_entity_type).each do |entity_type|
205
- entity_type.directives.each do |directive|
206
- next unless directive.graphql_name == "key"
207
-
208
- key = directive.arguments.keyword_arguments.fetch(:fields).strip
209
- raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
210
-
211
- field_path = "#{location}._entities"
212
- @stitch_directives[field_path] ||= []
213
- @stitch_directives[field_path] << {
214
- key: key,
215
- type_name: entity_type.graphql_name,
216
- federation: true,
217
- }
218
- end
219
- end
220
- end
192
+ @boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
193
+ @boundary_configs.merge!(BoundaryConfig.extract_federation_entities(schema, location))
221
194
 
222
195
  schemas[location.to_s] = schema
223
196
  executables[location.to_s] = input[:executable] || schema
@@ -557,19 +530,17 @@ module GraphQL
557
530
  def extract_boundaries(type_name, types_by_location)
558
531
  types_by_location.each do |location, type_candidate|
559
532
  type_candidate.fields.each do |field_name, field_candidate|
560
- boundary_type_name = field_candidate.type.unwrap.graphql_name
533
+ boundary_type = field_candidate.type.unwrap
561
534
  boundary_structure = Util.flatten_type_structure(field_candidate.type)
562
- boundary_kwargs = @stitch_directives["#{location}.#{field_name}"] || []
535
+ boundary_configs = @boundary_configs.fetch("#{location}.#{field_name}", [])
563
536
 
564
537
  field_candidate.directives.each do |directive|
565
538
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
566
- boundary_kwargs << directive.arguments.keyword_arguments
539
+ boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
567
540
  end
568
541
 
569
- boundary_kwargs.each do |kwargs|
570
- key = kwargs.fetch(:key)
571
- impl_type_name = kwargs.fetch(:type_name, boundary_type_name)
572
- key_selections = GraphQL.parse("{ #{key} }").definitions[0].selections
542
+ boundary_configs.each do |config|
543
+ key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
573
544
 
574
545
  if key_selections.length != 1
575
546
  raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
@@ -578,6 +549,8 @@ module GraphQL
578
549
  argument_name = key_selections[0].alias
579
550
  argument_name ||= if field_candidate.arguments.size == 1
580
551
  field_candidate.arguments.keys.first
552
+ elsif field_candidate.arguments[config.key]
553
+ config.key
581
554
  end
582
555
 
583
556
  argument = field_candidate.arguments[argument_name]
@@ -591,15 +564,26 @@ module GraphQL
591
564
  raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
592
565
  end
593
566
 
594
- @boundary_map[impl_type_name] ||= []
595
- @boundary_map[impl_type_name] << Boundary.new(
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}`."
572
+ end
573
+ config.type_name
574
+ else
575
+ boundary_type.graphql_name
576
+ end
577
+
578
+ @boundary_map[boundary_type_name] ||= []
579
+ @boundary_map[boundary_type_name] << Boundary.new(
596
580
  location: location,
597
- type_name: impl_type_name,
581
+ type_name: boundary_type_name,
598
582
  key: key_selections[0].name,
599
583
  field: field_candidate.name,
600
584
  arg: argument_name,
601
585
  list: boundary_structure.first.list?,
602
- federation: kwargs[:federation] || false,
586
+ federation: config.federation,
603
587
  )
604
588
  end
605
589
  end
@@ -5,6 +5,7 @@ module GraphQL::Stitching
5
5
  class ResolverDirective < GraphQL::Schema::Directive
6
6
  graphql_name "resolver"
7
7
  locations OBJECT, INTERFACE, UNION
8
+ argument :type_name, String, required: false
8
9
  argument :location, String, required: true
9
10
  argument :key, String, required: true
10
11
  argument :field, String, required: true
@@ -31,7 +31,7 @@ module GraphQL
31
31
  kwargs = directive.arguments.keyword_arguments
32
32
  boundary_map[type_name] ||= []
33
33
  boundary_map[type_name] << Boundary.new(
34
- type_name: type_name,
34
+ type_name: kwargs.fetch(:type_name, type_name),
35
35
  location: kwargs[:location],
36
36
  key: kwargs[:key],
37
37
  field: kwargs[:field],
@@ -134,6 +134,7 @@ module GraphQL
134
134
  end
135
135
 
136
136
  type.directive(ResolverDirective, **{
137
+ type_name: (boundary.type_name if boundary.type_name != type_name),
137
138
  location: boundary.location,
138
139
  key: boundary.key,
139
140
  field: boundary.field,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.2.3"
5
+ VERSION = "1.2.5"
6
6
  end
7
7
  end
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.3
4
+ version: 1.2.5
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-04-06 00:00:00.000000000 Z
11
+ date: 2024-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -114,6 +114,7 @@ files:
114
114
  - lib/graphql/stitching/client.rb
115
115
  - lib/graphql/stitching/composer.rb
116
116
  - lib/graphql/stitching/composer/base_validator.rb
117
+ - lib/graphql/stitching/composer/boundary_config.rb
117
118
  - lib/graphql/stitching/composer/validate_boundaries.rb
118
119
  - lib/graphql/stitching/composer/validate_interfaces.rb
119
120
  - lib/graphql/stitching/executor.rb