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 +4 -4
- data/README.md +38 -17
- data/docs/mechanics.md +42 -0
- data/lib/graphql/stitching/composer/boundary_config.rb +73 -0
- data/lib/graphql/stitching/composer/validate_boundaries.rb +28 -23
- data/lib/graphql/stitching/composer.rb +27 -43
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +1 -0
- data/lib/graphql/stitching/supergraph.rb +2 -1
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 754e7a61df541f685ef50ec79039df9742e4a2589a30eadf93125d6e4ef47aed
|
4
|
+
data.tar.gz: e2ceb89be07b42dbffeaa75416055fa62ffafa001088fd47b2b4f156aee6123a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
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
|
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
|
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(
|
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!
|
214
|
-
type Product {
|
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
|
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
|
-
|
243
|
+
sku: ID!
|
223
244
|
}
|
224
245
|
type Query {
|
225
246
|
productById(id: ID!): Product @stitch(key: "id")
|
226
|
-
|
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
|
-
|
256
|
+
sku: ID!
|
236
257
|
}
|
237
258
|
type Query {
|
238
|
-
product(id: ID,
|
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
|
-
|
293
|
+
sku: ID!
|
273
294
|
}
|
274
295
|
type Query {
|
275
296
|
productById(id: ID!): Product
|
276
|
-
|
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: "
|
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(
|
8
|
-
|
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
|
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 =
|
18
|
+
boundaries = supergraph.boundaries[type_name]
|
19
19
|
if boundaries&.any?
|
20
|
-
validate_as_boundary(
|
20
|
+
validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
|
21
21
|
elsif type.kind.object?
|
22
|
-
validate_as_shared(
|
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(
|
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
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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(
|
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,
|
83
|
-
if
|
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
|
-
@
|
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
|
-
|
191
|
-
|
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
|
-
|
533
|
+
boundary_type = field_candidate.type.unwrap
|
561
534
|
boundary_structure = Util.flatten_type_structure(field_candidate.type)
|
562
|
-
|
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
|
-
|
539
|
+
boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
|
567
540
|
end
|
568
541
|
|
569
|
-
|
570
|
-
|
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
|
-
|
595
|
-
|
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:
|
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:
|
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,
|
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.
|
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-
|
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
|