graphql-stitching 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -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-04
|
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
|