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 +4 -4
- data/README.md +21 -21
- data/docs/README.md +2 -1
- data/docs/mechanics.md +43 -0
- data/lib/graphql/stitching/composer/{boundary_config.rb → resolver_config.rb} +6 -6
- data/lib/graphql/stitching/composer/validate_resolvers.rb +96 -0
- data/lib/graphql/stitching/composer.rb +47 -45
- data/lib/graphql/stitching/executor/{boundary_source.rb → resolver_source.rb} +34 -25
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +4 -4
- data/lib/graphql/stitching/planner.rb +24 -24
- data/lib/graphql/stitching/planner_step.rb +6 -6
- data/lib/graphql/stitching/resolver.rb +49 -0
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +2 -1
- data/lib/graphql/stitching/supergraph.rb +42 -40
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +1 -1
- metadata +6 -6
- data/lib/graphql/stitching/boundary.rb +0 -29
- data/lib/graphql/stitching/composer/validate_boundaries.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d33ec469a7f4598bd9f198b25102bcb3453d034b124fb18bcfd8ec595b78a6ce
|
4
|
+
data.tar.gz: a6ef6c8e218045a508c22ade74fb83504d762c0a6cadb5ed3cd4f58a89bddcea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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
|
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
|
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
|
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
|
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
|
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(
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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: "
|
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 = "
|
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:
|
370
|
-
_1_0_result: sprocket(id:
|
371
|
-
_1_1_result: sprocket(id:
|
372
|
-
_1_2_result: sprocket(id:
|
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
|
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) -
|
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
|
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
|
-
|
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
|
-
|
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, :
|
64
|
+
attr_reader :key, :type_name, :representations
|
65
65
|
|
66
|
-
def initialize(key:, type_name:,
|
66
|
+
def initialize(key:, type_name:, representations: false)
|
67
67
|
@key = key
|
68
68
|
@type_name = type_name
|
69
|
-
@
|
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/
|
6
|
-
require_relative "./composer/
|
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
|
-
"
|
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
|
-
@
|
65
|
+
@resolver_configs = {}
|
66
66
|
|
67
67
|
@field_map = nil
|
68
|
-
@
|
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
|
-
|
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
|
-
|
160
|
+
expand_abstract_resolvers(schema)
|
160
161
|
|
161
162
|
supergraph = Supergraph.new(
|
162
163
|
schema: schema,
|
163
164
|
fields: @field_map,
|
164
|
-
|
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
|
-
@
|
192
|
-
@
|
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
|
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
|
-
|
533
|
-
|
534
|
-
|
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
|
-
|
539
|
+
resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
|
539
540
|
end
|
540
541
|
|
541
|
-
|
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, "
|
546
|
+
raise ComposerError, "Resolver key at #{type_name}.#{field_name} must specify exactly one key."
|
546
547
|
end
|
547
548
|
|
548
|
-
|
549
|
-
|
550
|
-
field_candidate.arguments.
|
551
|
-
|
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
|
-
|
558
|
-
|
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 !=
|
563
|
-
raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{
|
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
|
-
|
567
|
-
if !
|
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 !
|
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
|
-
|
575
|
+
resolver_type.graphql_name
|
575
576
|
end
|
576
577
|
|
577
|
-
@
|
578
|
-
@
|
578
|
+
@resolver_map[resolver_type_name] ||= []
|
579
|
+
@resolver_map[resolver_type_name] << Resolver.new(
|
579
580
|
location: location,
|
580
|
-
type_name:
|
581
|
+
type_name: resolver_type_name,
|
581
582
|
key: key_selections[0].name,
|
582
583
|
field: field_candidate.name,
|
583
|
-
arg:
|
584
|
-
|
585
|
-
|
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
|
615
|
-
@
|
616
|
-
|
617
|
-
next unless
|
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,
|
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
|
-
@
|
622
|
-
@
|
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
|
-
@
|
674
|
+
@resolver_map = {}
|
673
675
|
@mapped_type_names = {}
|
674
676
|
@candidate_directives_by_name_and_location = nil
|
675
677
|
@schema_directives = nil
|