graphql-stitching 1.2.4 → 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: 5c1c6ae95fd6ec036a7678356c8bc48191d8cf5cfd9e4b2936e79fdb72b52f48
4
- data.tar.gz: 7f8002d013e26f69df5a7023e147f98b86a46cc3027d2753b6d704bfe75a835b
3
+ metadata.gz: d33ec469a7f4598bd9f198b25102bcb3453d034b124fb18bcfd8ec595b78a6ce
4
+ data.tar.gz: a6ef6c8e218045a508c22ade74fb83504d762c0a6cadb5ed3cd4f58a89bddcea
5
5
  SHA512:
6
- metadata.gz: 734fe2a73f5412f30ccc6d89a20e62c1da376422de65a816a9680f02330cb2df93c39803cc182c5eed1c310624509beccb0a69162c1dad407ccaf6ada69178dd
7
- data.tar.gz: c293a3de403d0888d15047e2de120a8187e4fee598229572f37905dd4e4b3429017ee958ae8ab2a2c16561d25761128b9a6ffb954cbf49b5a310c92aa77f7ffe
6
+ metadata.gz: dc753d25c70045d1f2f995a0bbc8329ab426e1c5426943fa88e03fa71128fe8c1da928a8c905c997396c4d248e26e4400a02e2b2bc56d9e26b4916b5c63a8a7b
7
+ data.tar.gz: fdea7099a1cb486df512bdb93e7d90cceef42e7ff875e4c72c8e4b1fb46836d9e15cf5ae6124ca81132ec2a0707aca08a0d2a80d35fc40b18cf036c12576ff43
data/README.md CHANGED
@@ -16,7 +16,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
16
16
  - Computed fields (ie: federation-style `@requires`).
17
17
  - Subscriptions, defer/stream.
18
18
 
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-performance 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.
20
20
 
21
21
  ## Getting started
22
22
 
@@ -153,7 +153,7 @@ type Query {
153
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.
154
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).
155
155
 
156
- 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:
157
157
 
158
158
  ```graphql
159
159
  type Product {
@@ -161,11 +161,11 @@ type Product {
161
161
  }
162
162
  ```
163
163
 
164
- 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.
165
165
 
166
166
  #### List queries
167
167
 
168
- 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.
169
169
 
170
170
  ```graphql
171
171
  type Query {
@@ -180,7 +180,7 @@ See [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.
180
180
 
181
181
  #### Abstract queries
182
182
 
183
- It's okay for stitching 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
+ 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.
184
184
 
185
185
  ```graphql
186
186
  interface Node {
@@ -214,14 +214,14 @@ type Query {
214
214
 
215
215
  #### Multiple query arguments
216
216
 
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 may provide an argument mapping specified as `"<arg>:<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>"`.
218
218
 
219
219
  ```graphql
220
220
  type Product {
221
221
  id: ID!
222
222
  }
223
223
  type Query {
224
- product(by_id: ID, by_sku: ID): Product @stitch(key: "by_id:id")
224
+ product(byId: ID, bySku: ID): Product @stitch(key: "byId:id")
225
225
  }
226
226
  ```
227
227
 
@@ -235,7 +235,7 @@ type Product { id:ID! sku:ID! } # products location
235
235
  type Product { sku:ID! } # catelog location
236
236
  ```
237
237
 
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 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:
239
239
 
240
240
  ```graphql
241
241
  type Product {
@@ -244,7 +244,7 @@ type Product {
244
244
  }
245
245
  type Query {
246
246
  productById(id: ID!): Product @stitch(key: "id")
247
- productByUpc(sku: ID!): Product @stitch(key: "sku")
247
+ productBySku(sku: ID!): Product @stitch(key: "sku")
248
248
  }
249
249
  ```
250
250
 
@@ -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
@@ -294,7 +294,7 @@ sdl_string = <<~GRAPHQL
294
294
  }
295
295
  type Query {
296
296
  productById(id: ID!): Product
297
- productByUpc(sku: ID!): Product
297
+ productBySku(sku: ID!): Product
298
298
  }
299
299
  GRAPHQL
300
300
 
@@ -304,7 +304,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
304
304
  executable: ->() { ... },
305
305
  stitch: [
306
306
  { field_name: "productById", key: "id" },
307
- { field_name: "productByUpc", key: "sku" },
307
+ { field_name: "productBySku", key: "sku" },
308
308
  ]
309
309
  },
310
310
  # ...
@@ -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
@@ -303,3 +303,46 @@ 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
+ price: Float
318
+ }
319
+
320
+ type Query {
321
+ widgetA(id: ID!): Widget @stitch(key: "id")
322
+ }
323
+
324
+ # -- Location B
325
+
326
+ type Widget {
327
+ id: ID!
328
+ size: Float
329
+ }
330
+
331
+ type Query {
332
+ widgetB(id: ID!): Widget @stitch(key: "id")
333
+ }
334
+
335
+ # -- Location C
336
+
337
+ type Widget {
338
+ id: ID!
339
+ name: String
340
+ size: Float
341
+ }
342
+
343
+ type Query {
344
+ featuredWidget: Widget
345
+ }
346
+ ```
347
+
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
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class ValidateResolvers < BaseValidator
6
+
7
+ def perform(supergraph, composer)
8
+ supergraph.schema.types.each do |type_name, type|
9
+ # objects and interfaces that are not the root operation types
10
+ next unless type.kind.object? || type.kind.interface?
11
+ next if supergraph.schema.query == type || supergraph.schema.mutation == type
12
+ next if type.graphql_name.start_with?("__")
13
+
14
+ # multiple subschemas implement the type
15
+ candidate_types_by_location = composer.candidate_types_by_name_and_location[type_name]
16
+ next unless candidate_types_by_location.length > 1
17
+
18
+ resolvers = supergraph.resolvers[type_name]
19
+ if resolvers&.any?
20
+ validate_as_resolver(supergraph, type, candidate_types_by_location, resolvers)
21
+ elsif type.kind.object?
22
+ validate_as_shared(supergraph, type, candidate_types_by_location)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
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
+ return if type.kind.abstract?
32
+
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
+ end
40
+ memo[resolver.location] ||= {}
41
+ memo[resolver.location][resolver.key] = resolver
42
+ end
43
+
44
+ resolver_keys = resolvers.map(&:key).to_set
45
+
46
+ # All non-key fields must be resolvable in at least one resolver location
47
+ supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
48
+ next if resolver_keys.include?(field_name)
49
+
50
+ if locations.none? { resolvers_by_location_and_key[_1] }
51
+ where = locations.length > 1 ? "one of #{locations.join(", ")} locations" : locations.first
52
+ raise Composer::ValidationError, "A resolver query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
53
+ end
54
+ end
55
+
56
+ # All locations of a resolver 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? { 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
+ end
61
+ end
62
+
63
+ # verify that all outbound locations can access all inbound locations
64
+ resolver_locations = resolvers_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)
68
+ if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
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
+ end
72
+ end
73
+ end
74
+
75
+ def validate_as_shared(supergraph, type, candidate_types_by_location)
76
+ expected_fields = begin
77
+ type.fields.keys.sort
78
+ rescue StandardError => e
79
+ # bug with inherited interfaces in older versions of GraphQL
80
+ if type.interfaces.any? { _1.is_a?(GraphQL::Schema::LateBoundType) }
81
+ raise Composer::ComposerError, "Merged interface inheritance requires GraphQL >= v2.0.3"
82
+ else
83
+ raise e
84
+ end
85
+ end
86
+
87
+ candidate_types_by_location.each do |location, candidate_type|
88
+ if candidate_type.fields.keys.sort != expected_fields
89
+ raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
90
+ "or else define resolver queries so that its unique fields may be accessed remotely."
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ 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,12 +62,13 @@ 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
+ @candidate_types_by_name_and_location = nil
71
72
  @schema_directives = nil
72
73
  end
73
74
 
@@ -124,7 +125,7 @@ module GraphQL
124
125
  raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
125
126
  end
126
127
 
127
- 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
128
129
 
129
130
  memo[type_name] = case kinds.first
130
131
  when "SCALAR"
@@ -156,12 +157,12 @@ module GraphQL
156
157
  end
157
158
 
158
159
  select_root_field_locations(schema)
159
- expand_abstract_boundaries(schema)
160
+ expand_abstract_resolvers(schema)
160
161
 
161
162
  supergraph = Supergraph.new(
162
163
  schema: schema,
163
164
  fields: @field_map,
164
- boundaries: @boundary_map,
165
+ resolvers: @resolver_map,
165
166
  executables: executables,
166
167
  )
167
168
 
@@ -188,8 +189,8 @@ module GraphQL
188
189
  raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
189
190
  end
190
191
 
191
- @boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
192
- @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))
193
194
 
194
195
  schemas[location.to_s] = schema
195
196
  executables[location.to_s] = input[:executable] || schema
@@ -526,63 +527,64 @@ module GraphQL
526
527
 
527
528
  # @!scope class
528
529
  # @!visibility private
529
- def extract_boundaries(type_name, types_by_location)
530
+ def extract_resolvers(type_name, types_by_location)
530
531
  types_by_location.each do |location, type_candidate|
531
532
  type_candidate.fields.each do |field_name, field_candidate|
532
- boundary_type = field_candidate.type.unwrap
533
- boundary_structure = Util.flatten_type_structure(field_candidate.type)
534
- 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}", [])
535
536
 
536
537
  field_candidate.directives.each do |directive|
537
538
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
538
- boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
539
+ resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
539
540
  end
540
541
 
541
- boundary_configs.each do |config|
542
+ resolver_configs.each do |config|
542
543
  key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
543
544
 
544
545
  if key_selections.length != 1
545
- 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."
546
547
  end
547
548
 
548
- argument_name = key_selections[0].alias
549
- argument_name ||= if field_candidate.arguments.size == 1
550
- field_candidate.arguments.keys.first
551
- elsif field_candidate.arguments[config.key]
552
- 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]
553
554
  end
554
555
 
555
- argument = field_candidate.arguments[argument_name]
556
556
  unless argument
557
- # contextualize this... "boundaries with multiple args need mapping aliases."
558
- 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`"
559
559
  end
560
560
 
561
561
  argument_structure = Util.flatten_type_structure(argument.type)
562
- if argument_structure.length != boundary_structure.length
563
- 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."
564
565
  end
565
566
 
566
- boundary_type_name = if config.type_name
567
- if !boundary_type.kind.abstract?
567
+ resolver_type_name = if config.type_name
568
+ if !resolver_type.kind.abstract?
568
569
  raise ComposerError, "Resolver config may only specify a type name for abstract resolvers."
569
- 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 }
570
571
  raise ComposerError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
571
572
  end
572
573
  config.type_name
573
574
  else
574
- boundary_type.graphql_name
575
+ resolver_type.graphql_name
575
576
  end
576
577
 
577
- @boundary_map[boundary_type_name] ||= []
578
- @boundary_map[boundary_type_name] << Boundary.new(
578
+ @resolver_map[resolver_type_name] ||= []
579
+ @resolver_map[resolver_type_name] << Resolver.new(
579
580
  location: location,
580
- type_name: boundary_type_name,
581
+ type_name: resolver_type_name,
581
582
  key: key_selections[0].name,
582
583
  field: field_candidate.name,
583
- arg: argument_name,
584
- list: boundary_structure.first.list?,
585
- 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,
586
588
  )
587
589
  end
588
590
  end
@@ -611,15 +613,15 @@ module GraphQL
611
613
 
612
614
  # @!scope class
613
615
  # @!visibility private
614
- def expand_abstract_boundaries(schema)
615
- @boundary_map.keys.each do |type_name|
616
- boundary_type = schema.types[type_name]
617
- 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?
618
620
 
619
- expanded_types = Util.expand_abstract_type(schema, boundary_type)
621
+ expanded_types = Util.expand_abstract_type(schema, resolver_type)
620
622
  expanded_types.select { @candidate_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
621
- @boundary_map[expanded_type.graphql_name] ||= []
622
- @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])
623
625
  end
624
626
  end
625
627
  end
@@ -669,7 +671,7 @@ module GraphQL
669
671
 
670
672
  def reset!
671
673
  @field_map = {}
672
- @boundary_map = {}
674
+ @resolver_map = {}
673
675
  @mapped_type_names = {}
674
676
  @candidate_directives_by_name_and_location = nil
675
677
  @schema_directives = nil