graphql-stitching 1.2.4 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -21
- data/docs/README.md +2 -1
- data/docs/mechanics.md +43 -0
- data/lib/graphql/stitching/composer/{boundary_config.rb → resolver_config.rb} +6 -6
- data/lib/graphql/stitching/composer/validate_resolvers.rb +96 -0
- data/lib/graphql/stitching/composer.rb +47 -45
- data/lib/graphql/stitching/executor/{boundary_source.rb → resolver_source.rb} +34 -25
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +4 -4
- data/lib/graphql/stitching/planner.rb +24 -24
- data/lib/graphql/stitching/planner_step.rb +6 -6
- data/lib/graphql/stitching/resolver.rb +49 -0
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +2 -1
- data/lib/graphql/stitching/supergraph.rb +42 -40
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +1 -1
- metadata +6 -6
- data/lib/graphql/stitching/boundary.rb +0 -29
- data/lib/graphql/stitching/composer/validate_boundaries.rb +0 -91
@@ -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
|