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