graphql-stitching 0.3.3 → 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/graphql/stitching/composer/validate_boundaries.rb +4 -4
- data/lib/graphql/stitching/composer.rb +5 -3
- data/lib/graphql/stitching/executor.rb +18 -19
- data/lib/graphql/stitching/planner.rb +286 -191
- data/lib/graphql/stitching/planner_operation.rb +18 -16
- data/lib/graphql/stitching/shaper.rb +32 -11
- data/lib/graphql/stitching/supergraph.rb +42 -25
- data/lib/graphql/stitching/util.rb +0 -9
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3cb133db35cdb705f296a2fe67909bfe2f07baefc873cf502ba48dbd53e8c6a
|
4
|
+
data.tar.gz: c81040a57f364a1a8a045bf079428cfae3c8a5886d2f7d0aaae45844c055b92e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc2a8d99942c2a5e1558ded0fbb467dd96d4bdb30c5a2ff9aa2348161ae28073cf36b2b56e51b0d92b757f68a87366ab1750e716bf0939d1645bf1ddee2049f5
|
7
|
+
data.tar.gz: 599760134f3f59f6ec5285e0d0976e1e9c07ba4103a74341bb35d1dacb89d0026383da2f4c2ca47d33ce3971510d9dddcc61d90a5248342406a4787f650d6126
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
## GraphQL Stitching for Ruby
|
2
2
|
|
3
|
-
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly
|
3
|
+
GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
|
4
4
|
|
5
5
|
![Stitched graph](./docs/images/stitching.png)
|
6
6
|
|
@@ -14,7 +14,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
14
14
|
- Computed fields (ie: federation-style `@requires`).
|
15
15
|
- Subscriptions, defer/stream.
|
16
16
|
|
17
|
-
This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. While Ruby is not the fastest language for a high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local
|
17
|
+
This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. While Ruby is not the fastest language for a purely high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language.
|
18
18
|
|
19
19
|
## Getting started
|
20
20
|
|
@@ -32,16 +32,16 @@ module GraphQL
|
|
32
32
|
|
33
33
|
# only one boundary allowed per type/location/key
|
34
34
|
boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
|
35
|
-
if memo.dig(boundary["location"], boundary["
|
36
|
-
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["
|
35
|
+
if memo.dig(boundary["location"], boundary["key"])
|
36
|
+
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["key"]}` "\
|
37
37
|
"found in #{boundary["location"]}. Limit one boundary query per type and key in each location. "\
|
38
38
|
"Abstract boundaries provide all possible types."
|
39
39
|
end
|
40
40
|
memo[boundary["location"]] ||= {}
|
41
|
-
memo[boundary["location"]][boundary["
|
41
|
+
memo[boundary["location"]][boundary["key"]] = boundary
|
42
42
|
end
|
43
43
|
|
44
|
-
boundary_keys = boundaries.map { _1["
|
44
|
+
boundary_keys = boundaries.map { _1["key"] }.uniq
|
45
45
|
key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
|
46
46
|
subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
|
47
47
|
end
|
@@ -56,8 +56,9 @@ module GraphQL
|
|
56
56
|
raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
|
57
57
|
raise ComposerError, "The subscription operation is not supported." if schema.subscription
|
58
58
|
|
59
|
+
introspection_types = schema.introspection_system.types.keys
|
59
60
|
schema.types.each do |type_name, type_candidate|
|
60
|
-
next if
|
61
|
+
next if introspection_types.include?(type_name)
|
61
62
|
|
62
63
|
if type_name == @query_name && type_candidate != schema.query
|
63
64
|
raise ComposerError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
|
@@ -491,7 +492,7 @@ module GraphQL
|
|
491
492
|
@boundary_map[boundary_type_name] ||= []
|
492
493
|
@boundary_map[boundary_type_name] << {
|
493
494
|
"location" => location,
|
494
|
-
"
|
495
|
+
"key" => key_selections[0].name,
|
495
496
|
"field" => field_candidate.name,
|
496
497
|
"arg" => argument_name,
|
497
498
|
"list" => boundary_structure.first[:list],
|
@@ -538,8 +539,9 @@ module GraphQL
|
|
538
539
|
writes = []
|
539
540
|
|
540
541
|
schemas.each do |schema|
|
542
|
+
introspection_types = schema.introspection_system.types.keys
|
541
543
|
schema.types.values.each do |type|
|
542
|
-
next if
|
544
|
+
next if introspection_types.include?(type.graphql_name)
|
543
545
|
|
544
546
|
if type.kind.object? || type.kind.interface?
|
545
547
|
type.fields.values.each do |field|
|
@@ -26,7 +26,7 @@ module GraphQL
|
|
26
26
|
@executor.errors.concat(result["errors"])
|
27
27
|
end
|
28
28
|
|
29
|
-
ops.map { op["
|
29
|
+
ops.map { op["order"] }
|
30
30
|
end
|
31
31
|
|
32
32
|
# Builds root source documents
|
@@ -36,12 +36,12 @@ module GraphQL
|
|
36
36
|
doc << op["operation_type"]
|
37
37
|
|
38
38
|
if operation_name
|
39
|
-
doc << "
|
39
|
+
doc << " #{operation_name}_#{op["order"]}"
|
40
40
|
end
|
41
41
|
|
42
42
|
if op["variables"].any?
|
43
43
|
variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
|
44
|
-
doc << "(
|
44
|
+
doc << "(#{variable_defs})"
|
45
45
|
end
|
46
46
|
|
47
47
|
doc << op["selections"]
|
@@ -57,13 +57,13 @@ module GraphQL
|
|
57
57
|
|
58
58
|
def fetch(ops)
|
59
59
|
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
|
60
|
-
origin_set = op["
|
60
|
+
origin_set = op["path"].reduce([@executor.data]) do |set, path_segment|
|
61
61
|
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
62
62
|
end
|
63
63
|
|
64
|
-
if op["
|
64
|
+
if op["if_type"]
|
65
65
|
# operations planned around unused fragment conditions should not trigger requests
|
66
|
-
origin_set.select! { _1["_STITCH_typename"] == op["
|
66
|
+
origin_set.select! { _1["_STITCH_typename"] == op["if_type"] }
|
67
67
|
end
|
68
68
|
|
69
69
|
memo[op] = origin_set if origin_set.any?
|
@@ -81,7 +81,7 @@ module GraphQL
|
|
81
81
|
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
|
82
82
|
end
|
83
83
|
|
84
|
-
ops.map { origin_sets_by_operation[_1] ? _1["
|
84
|
+
ops.map { origin_sets_by_operation[_1] ? _1["order"] : nil }
|
85
85
|
end
|
86
86
|
|
87
87
|
# Builds batched boundary queries
|
@@ -96,7 +96,7 @@ module GraphQL
|
|
96
96
|
query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
|
97
97
|
variable_defs.merge!(op["variables"])
|
98
98
|
boundary = op["boundary"]
|
99
|
-
key_selection = "_STITCH_#{boundary["
|
99
|
+
key_selection = "_STITCH_#{boundary["key"]}"
|
100
100
|
|
101
101
|
if boundary["list"]
|
102
102
|
input = JSON.generate(origin_set.map { _1[key_selection] })
|
@@ -113,18 +113,18 @@ module GraphQL
|
|
113
113
|
doc << "query" # << boundary fulfillment always uses query
|
114
114
|
|
115
115
|
if operation_name
|
116
|
-
doc << " "
|
116
|
+
doc << " #{operation_name}"
|
117
117
|
origin_sets_by_operation.each_key do |op|
|
118
|
-
doc << "_
|
118
|
+
doc << "_#{op["order"]}"
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
122
|
if variable_defs.any?
|
123
123
|
variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
|
124
|
-
doc << "(
|
124
|
+
doc << "(#{variable_str})"
|
125
125
|
end
|
126
126
|
|
127
|
-
doc << "{
|
127
|
+
doc << "{ #{query_fields.join(" ")} }"
|
128
128
|
|
129
129
|
return doc, variable_defs.keys
|
130
130
|
end
|
@@ -183,7 +183,7 @@ module GraphQL
|
|
183
183
|
|
184
184
|
if pathed_errors_by_op_index_and_object_id.any?
|
185
185
|
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
186
|
-
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "
|
186
|
+
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
|
187
187
|
errors_result.concat(pathed_errors_by_object_id.values)
|
188
188
|
end
|
189
189
|
end
|
@@ -251,7 +251,7 @@ module GraphQL
|
|
251
251
|
|
252
252
|
if @data && @data.length > 0
|
253
253
|
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
254
|
-
|
254
|
+
supergraph: @supergraph,
|
255
255
|
request: @request,
|
256
256
|
).perform!(@data)
|
257
257
|
end
|
@@ -265,7 +265,7 @@ module GraphQL
|
|
265
265
|
|
266
266
|
private
|
267
267
|
|
268
|
-
def exec!(
|
268
|
+
def exec!(next_ordinals = [0])
|
269
269
|
if @exec_cycles > @queue.length
|
270
270
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
271
271
|
raise StitchingError, "Too many execution requests attempted."
|
@@ -273,7 +273,7 @@ module GraphQL
|
|
273
273
|
|
274
274
|
@dataloader.append_job do
|
275
275
|
tasks = @queue
|
276
|
-
.select {
|
276
|
+
.select { next_ordinals.include?(_1["after"]) }
|
277
277
|
.group_by { [_1["location"], _1["boundary"].nil?] }
|
278
278
|
.map do |(location, root_source), ops|
|
279
279
|
if root_source
|
@@ -291,9 +291,8 @@ module GraphQL
|
|
291
291
|
end
|
292
292
|
|
293
293
|
def exec_task(task)
|
294
|
-
|
295
|
-
|
296
|
-
exec!(next_keys) if next_keys.any?
|
294
|
+
next_ordinals = task.load.tap(&:compact!)
|
295
|
+
exec!(next_ordinals) if next_ordinals.any?
|
297
296
|
end
|
298
297
|
end
|
299
298
|
end
|
@@ -5,22 +5,23 @@ module GraphQL
|
|
5
5
|
class Planner
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
7
|
TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
|
8
|
+
ROOT_ORDER = 0
|
8
9
|
|
9
10
|
def initialize(supergraph:, request:)
|
10
11
|
@supergraph = supergraph
|
11
12
|
@request = request
|
12
|
-
@
|
13
|
-
@
|
13
|
+
@planning_order = ROOT_ORDER
|
14
|
+
@operations_by_entrypoint = {}
|
14
15
|
end
|
15
16
|
|
16
17
|
def perform
|
17
|
-
|
18
|
+
build_root_entrypoints
|
18
19
|
expand_abstract_boundaries
|
19
20
|
self
|
20
21
|
end
|
21
22
|
|
22
23
|
def operations
|
23
|
-
@
|
24
|
+
@operations_by_entrypoint.values.sort_by!(&:order)
|
24
25
|
end
|
25
26
|
|
26
27
|
def to_h
|
@@ -29,45 +30,137 @@ module GraphQL
|
|
29
30
|
|
30
31
|
private
|
31
32
|
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
33
|
+
# **
|
34
|
+
# Algorithm:
|
35
|
+
#
|
36
|
+
# A) Group all root selections by their preferred entrypoint locations.
|
37
|
+
# A.1) Group query fields by location for parallel execution.
|
38
|
+
# A.2) Partition mutation fields by consecutive location for serial execution.
|
39
|
+
#
|
40
|
+
# B) Extract contiguous selections for each entrypoint location.
|
41
|
+
#
|
42
|
+
# B.1) Selections on interface types that do not belong to the interface at the
|
43
|
+
# entrypoint location are expanded into concrete type fragments prior to extraction.
|
44
|
+
#
|
45
|
+
# B.2) Filter the selection tree down to just fields of the entrypoint location.
|
46
|
+
# Adjoining selections not available here get split off into new entrypoints (C).
|
47
|
+
#
|
48
|
+
# B.3) Collect all variable definitions used within the filtered selection.
|
49
|
+
# These specify which request variables to pass along with the selection.
|
50
|
+
#
|
51
|
+
# B.4) Add a `__typename` selection to concrete types and abstracts that implement
|
52
|
+
# fragments. This provides resolved type information used during execution.
|
53
|
+
#
|
54
|
+
# C) Delegate adjoining selections to new entrypoint locations.
|
55
|
+
# C.1) Distribute unique fields among their required locations.
|
56
|
+
# C.2) Distribute non-unique fields among locations that were added during C.1.
|
57
|
+
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
58
|
+
#
|
59
|
+
# D) Create paths routing to new entrypoint locations via boundary queries.
|
60
|
+
# D.1) Types joining through multiple keys route using a-star search.
|
61
|
+
# D.2) Types joining through a single key route via quick location match.
|
62
|
+
# (D.2 is an optional optimization of D.1)
|
63
|
+
#
|
64
|
+
# E) Translate boundary pathways into new entrypoints.
|
65
|
+
# E.1) Add the key of each boundary query into the prior location's selection set.
|
66
|
+
# E.2) Add a planner operation for each new entrypoint location, then extract it (B).
|
67
|
+
#
|
68
|
+
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
69
|
+
# **
|
70
|
+
|
71
|
+
# adds an entrypoint for fetching and inserting data into the aggregate result.
|
72
|
+
def add_entrypoint(
|
73
|
+
location:,
|
74
|
+
parent_order:,
|
75
|
+
parent_type:,
|
76
|
+
selections:,
|
77
|
+
variables: {},
|
78
|
+
path: [],
|
79
|
+
operation_type: "query",
|
80
|
+
boundary: nil
|
81
|
+
)
|
82
|
+
# coalesce repeat parameters into a single entrypoint
|
83
|
+
boundary_key = boundary ? boundary["key"] : "_"
|
84
|
+
entrypoint = String.new("#{parent_order}/#{location}/#{parent_type.graphql_name}/#{boundary_key}")
|
85
|
+
path.each { entrypoint << "/#{_1}" }
|
86
|
+
|
87
|
+
op = @operations_by_entrypoint[entrypoint]
|
88
|
+
next_order = op ? parent_order : @planning_order += 1
|
89
|
+
|
90
|
+
if selections.any?
|
91
|
+
selections = extract_locale_selections(location, parent_type, next_order, selections, path, variables)
|
92
|
+
end
|
93
|
+
|
94
|
+
if op.nil?
|
95
|
+
# concrete types that are not root Query/Mutation report themselves as a type condition
|
96
|
+
# executor must check the __typename of loaded objects to see if they match subsequent operations
|
97
|
+
# this prevents the executor from taking action on unused fragment selections
|
98
|
+
conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.root_type_for_operation(operation_type)
|
99
|
+
|
100
|
+
@operations_by_entrypoint[entrypoint] = PlannerOperation.new(
|
101
|
+
order: next_order,
|
102
|
+
after: parent_order,
|
103
|
+
location: location,
|
104
|
+
parent_type: parent_type,
|
105
|
+
operation_type: operation_type,
|
106
|
+
selections: selections,
|
107
|
+
variables: variables,
|
108
|
+
path: path,
|
109
|
+
if_type: conditional ? parent_type.graphql_name : nil,
|
110
|
+
boundary: boundary,
|
111
|
+
)
|
112
|
+
else
|
113
|
+
op.selections.concat(selections)
|
114
|
+
op
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# A) Group all root selections by their preferred entrypoint locations.
|
119
|
+
def build_root_entrypoints
|
36
120
|
case @request.operation.operation_type
|
37
121
|
when "query"
|
122
|
+
# A.1) Group query fields by location for parallel execution.
|
38
123
|
parent_type = @supergraph.schema.query
|
39
124
|
|
40
|
-
selections_by_location =
|
125
|
+
selections_by_location = {}
|
126
|
+
each_selection_in_type(parent_type, @request.operation.selections) do |node|
|
41
127
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
42
|
-
|
43
|
-
|
128
|
+
selections_by_location[locations.first] ||= []
|
129
|
+
selections_by_location[locations.first] << node
|
44
130
|
end
|
45
131
|
|
46
132
|
selections_by_location.each do |location, selections|
|
47
|
-
|
133
|
+
add_entrypoint(
|
134
|
+
location: location,
|
135
|
+
parent_order: ROOT_ORDER,
|
136
|
+
parent_type: parent_type,
|
137
|
+
selections: selections,
|
138
|
+
)
|
48
139
|
end
|
49
140
|
|
50
141
|
when "mutation"
|
142
|
+
# A.2) Partition mutation fields by consecutive location for serial execution.
|
51
143
|
parent_type = @supergraph.schema.mutation
|
52
144
|
|
53
|
-
|
145
|
+
partitions = []
|
146
|
+
each_selection_in_type(parent_type, @request.operation.selections) do |node|
|
54
147
|
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
|
55
148
|
|
56
|
-
if
|
57
|
-
|
149
|
+
if partitions.none? || partitions.last[:location] != next_location
|
150
|
+
partitions << { location: next_location, selections: [] }
|
58
151
|
end
|
59
152
|
|
60
|
-
|
153
|
+
partitions.last[:selections] << node
|
61
154
|
end
|
62
155
|
|
63
|
-
|
64
|
-
|
65
|
-
location:
|
66
|
-
|
67
|
-
operation_type: "mutation",
|
156
|
+
partitions.reduce(ROOT_ORDER) do |parent_order, partition|
|
157
|
+
add_entrypoint(
|
158
|
+
location: partition[:location],
|
159
|
+
parent_order: parent_order,
|
68
160
|
parent_type: parent_type,
|
69
|
-
|
70
|
-
|
161
|
+
selections: partition[:selections],
|
162
|
+
operation_type: "mutation",
|
163
|
+
).order
|
71
164
|
end
|
72
165
|
|
73
166
|
else
|
@@ -75,70 +168,44 @@ module GraphQL
|
|
75
168
|
end
|
76
169
|
end
|
77
170
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
parent_type:,
|
84
|
-
selections:,
|
85
|
-
insertion_path: [],
|
86
|
-
operation_type: "query",
|
87
|
-
after_key: 0,
|
88
|
-
boundary: nil
|
89
|
-
)
|
90
|
-
parent_key = @sequence_key += 1
|
91
|
-
locale_variables = {}
|
92
|
-
locale_selections = if selections.any?
|
93
|
-
extract_locale_selections(location, parent_type, selections, insertion_path, parent_key, locale_variables)
|
94
|
-
else
|
95
|
-
selections
|
96
|
-
end
|
171
|
+
def each_selection_in_type(parent_type, input_selections, &block)
|
172
|
+
input_selections.each do |node|
|
173
|
+
case node
|
174
|
+
when GraphQL::Language::Nodes::Field
|
175
|
+
yield(node)
|
97
176
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
grouping = String.new
|
102
|
-
grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
|
103
|
-
grouping = insertion_path.reduce(grouping) do |memo, segment|
|
104
|
-
memo << "/" << segment
|
105
|
-
end
|
177
|
+
when GraphQL::Language::Nodes::InlineFragment
|
178
|
+
next unless node.type.nil? || parent_type.graphql_name == node.type.name
|
179
|
+
each_selection_in_type(parent_type, node.selections, &block)
|
106
180
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
else
|
112
|
-
# concrete types that are not root Query/Mutation report themselves as a type condition
|
113
|
-
# executor must check the __typename of loaded objects to see if they match subsequent operations
|
114
|
-
# this prevents the executor from taking action on unused fragment selections
|
115
|
-
type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
|
181
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
182
|
+
fragment = @request.fragment_definitions[node.name]
|
183
|
+
next unless parent_type.graphql_name == fragment.type.name
|
184
|
+
each_selection_in_type(parent_type, fragment.selections, &block)
|
116
185
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
location: location,
|
121
|
-
parent_type: parent_type,
|
122
|
-
operation_type: operation_type,
|
123
|
-
insertion_path: insertion_path,
|
124
|
-
type_condition: type_conditional ? parent_type.graphql_name : nil,
|
125
|
-
selections: locale_selections,
|
126
|
-
variables: locale_variables,
|
127
|
-
boundary: boundary,
|
128
|
-
)
|
186
|
+
else
|
187
|
+
raise "Unexpected node of type #{node.class.name} in selection set."
|
188
|
+
end
|
129
189
|
end
|
130
190
|
end
|
131
191
|
|
132
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
192
|
+
# B) Contiguous selections are extracted for each entrypoint location.
|
193
|
+
def extract_locale_selections(
|
194
|
+
current_location,
|
195
|
+
parent_type,
|
196
|
+
parent_order,
|
197
|
+
input_selections,
|
198
|
+
path,
|
199
|
+
locale_variables,
|
136
200
|
locale_selections = []
|
137
|
-
|
201
|
+
)
|
202
|
+
# B.1) Expand selections on interface types that do not belong to this location.
|
203
|
+
input_selections = expand_interface_selections(current_location, parent_type, input_selections)
|
138
204
|
|
139
|
-
|
140
|
-
|
141
|
-
|
205
|
+
# B.2) Filter the selection tree down to just fields of the entrypoint location.
|
206
|
+
# Adjoining selections not available here get split off into new entrypoints (C).
|
207
|
+
remote_selections = nil
|
208
|
+
requires_typename = parent_type.kind.abstract?
|
142
209
|
|
143
210
|
input_selections.each do |node|
|
144
211
|
case node
|
@@ -155,68 +222,172 @@ module GraphQL
|
|
155
222
|
next
|
156
223
|
end
|
157
224
|
|
158
|
-
|
225
|
+
# B.3) Collect all variable definitions used within the filtered selection.
|
159
226
|
extract_node_variables(node, locale_variables)
|
227
|
+
field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
|
160
228
|
|
161
229
|
if Util.is_leaf_type?(field_type)
|
162
230
|
locale_selections << node
|
163
231
|
else
|
164
|
-
|
165
|
-
selection_set = extract_locale_selections(current_location, field_type, node.selections,
|
166
|
-
|
232
|
+
path.push(node.alias || node.name)
|
233
|
+
selection_set = extract_locale_selections(current_location, field_type, parent_order, node.selections, path, locale_variables)
|
234
|
+
path.pop
|
167
235
|
|
168
236
|
locale_selections << node.merge(selections: selection_set)
|
169
237
|
end
|
170
238
|
|
171
239
|
when GraphQL::Language::Nodes::InlineFragment
|
172
|
-
|
240
|
+
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
241
|
+
next unless @supergraph.locations_by_type[fragment_type.graphql_name].include?(current_location)
|
173
242
|
|
174
|
-
|
175
|
-
selection_set =
|
176
|
-
|
177
|
-
|
243
|
+
is_same_scope = fragment_type == parent_type
|
244
|
+
selection_set = is_same_scope ? locale_selections : []
|
245
|
+
extract_locale_selections(current_location, fragment_type, parent_order, node.selections, path, locale_variables, selection_set)
|
246
|
+
|
247
|
+
unless is_same_scope
|
248
|
+
locale_selections << node.merge(selections: selection_set)
|
249
|
+
requires_typename = true
|
250
|
+
end
|
178
251
|
|
179
252
|
when GraphQL::Language::Nodes::FragmentSpread
|
180
253
|
fragment = @request.fragment_definitions[node.name]
|
181
254
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
182
255
|
|
183
|
-
fragment_type = @supergraph.
|
184
|
-
|
185
|
-
|
186
|
-
|
256
|
+
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
257
|
+
is_same_scope = fragment_type == parent_type
|
258
|
+
selection_set = is_same_scope ? locale_selections : []
|
259
|
+
extract_locale_selections(current_location, fragment_type, parent_order, fragment.selections, path, locale_variables, selection_set)
|
260
|
+
|
261
|
+
unless is_same_scope
|
262
|
+
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
263
|
+
requires_typename = true
|
264
|
+
end
|
187
265
|
|
188
266
|
else
|
189
267
|
raise "Unexpected node of type #{node.class.name} in selection set."
|
190
268
|
end
|
191
269
|
end
|
192
270
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
locale_selections,
|
198
|
-
remote_selections,
|
199
|
-
insertion_path,
|
200
|
-
after_key
|
201
|
-
)
|
271
|
+
# B.4) Add a `__typename` selection to concrete types and abstracts that implement
|
272
|
+
# fragments so that resolved type information is available during execution.
|
273
|
+
if requires_typename
|
274
|
+
locale_selections << TYPENAME_NODE
|
202
275
|
end
|
203
276
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
277
|
+
if remote_selections
|
278
|
+
# C) Delegate adjoining selections to new entrypoint locations.
|
279
|
+
remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
|
280
|
+
|
281
|
+
# D) Create paths routing to new entrypoint locations via boundary queries.
|
282
|
+
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.keys)
|
283
|
+
|
284
|
+
# E) Translate boundary pathways into new entrypoints.
|
285
|
+
routes.each_value do |route|
|
286
|
+
route.reduce(locale_selections) do |parent_selections, boundary|
|
287
|
+
# E.1) Add the key of each boundary query into the prior location's selection set.
|
288
|
+
foreign_key = "_STITCH_#{boundary["key"]}"
|
289
|
+
has_key = false
|
290
|
+
has_typename = false
|
291
|
+
|
292
|
+
parent_selections.each do |selection|
|
293
|
+
next unless selection.is_a?(GraphQL::Language::Nodes::Field)
|
294
|
+
case selection.alias
|
295
|
+
when foreign_key
|
296
|
+
has_key = true
|
297
|
+
when TYPENAME_NODE.alias
|
298
|
+
has_typename = true
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
parent_selections << GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["key"]) unless has_key
|
303
|
+
parent_selections << TYPENAME_NODE unless has_typename
|
304
|
+
|
305
|
+
# E.2) Add a planner operation for each new entrypoint location.
|
306
|
+
location = boundary["location"]
|
307
|
+
add_entrypoint(
|
308
|
+
location: location,
|
309
|
+
parent_order: parent_order,
|
310
|
+
parent_type: parent_type,
|
311
|
+
selections: remote_selections_by_location[location] || [],
|
312
|
+
path: path.dup,
|
313
|
+
boundary: boundary,
|
314
|
+
).selections
|
315
|
+
end
|
316
|
+
end
|
208
317
|
end
|
209
318
|
|
210
319
|
locale_selections
|
211
320
|
end
|
212
321
|
|
213
|
-
#
|
214
|
-
#
|
215
|
-
def
|
322
|
+
# B.1) Selections on interface types that do not belong to the interface at the
|
323
|
+
# entrypoint location are expanded into concrete type fragments prior to extraction.
|
324
|
+
def expand_interface_selections(current_location, parent_type, input_selections)
|
325
|
+
return input_selections unless parent_type.kind.interface?
|
326
|
+
|
327
|
+
local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
|
328
|
+
|
329
|
+
expanded_selections = nil
|
330
|
+
input_selections = input_selections.filter_map do |node|
|
331
|
+
case node
|
332
|
+
when GraphQL::Language::Nodes::Field
|
333
|
+
if node.name != "__typename" && !local_interface_fields.include?(node.name)
|
334
|
+
expanded_selections ||= []
|
335
|
+
expanded_selections << node
|
336
|
+
next nil
|
337
|
+
end
|
338
|
+
|
339
|
+
when GraphQL::Language::Nodes::InlineFragment
|
340
|
+
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
341
|
+
selection_set = expand_interface_selections(current_location, fragment_type, node.selections)
|
342
|
+
node = node.merge(selections: selection_set)
|
343
|
+
|
344
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
345
|
+
fragment = @request.fragment_definitions[node.name]
|
346
|
+
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
347
|
+
selection_set = expand_interface_selections(current_location, fragment_type, fragment.selections)
|
348
|
+
node = GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
349
|
+
|
350
|
+
end
|
351
|
+
node
|
352
|
+
end
|
353
|
+
|
354
|
+
if expanded_selections
|
355
|
+
@supergraph.memoized_schema_possible_types(parent_type.graphql_name).each do |possible_type|
|
356
|
+
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
357
|
+
|
358
|
+
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
359
|
+
input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
input_selections
|
364
|
+
end
|
365
|
+
|
366
|
+
# B.3) Collect all variable definitions used within the filtered selection.
|
367
|
+
# These specify which request variables to pass along with the selection.
|
368
|
+
def extract_node_variables(node_with_args, variable_definitions)
|
369
|
+
node_with_args.arguments.each do |argument|
|
370
|
+
case argument.value
|
371
|
+
when GraphQL::Language::Nodes::InputObject
|
372
|
+
extract_node_variables(argument.value, variable_definitions)
|
373
|
+
when GraphQL::Language::Nodes::VariableIdentifier
|
374
|
+
variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
if node_with_args.respond_to?(:directives)
|
379
|
+
node_with_args.directives.each do |directive|
|
380
|
+
extract_node_variables(directive, variable_definitions)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# C) Delegate adjoining selections to new entrypoint locations.
|
386
|
+
def delegate_remote_selections(parent_type, remote_selections)
|
216
387
|
possible_locations_by_field = @supergraph.locations_by_type_and_field[parent_type.graphql_name]
|
217
388
|
selections_by_location = {}
|
218
389
|
|
219
|
-
# 1
|
390
|
+
# C.1) Distribute unique fields among their required locations.
|
220
391
|
remote_selections.reject! do |node|
|
221
392
|
possible_locations = possible_locations_by_field[node.name]
|
222
393
|
if possible_locations.length == 1
|
@@ -226,7 +397,7 @@ module GraphQL
|
|
226
397
|
end
|
227
398
|
end
|
228
399
|
|
229
|
-
# 2
|
400
|
+
# C.2) Distribute non-unique fields among locations that were added during C.1.
|
230
401
|
if selections_by_location.any? && remote_selections.any?
|
231
402
|
remote_selections.reject! do |node|
|
232
403
|
used_location = possible_locations_by_field[node.name].find { selections_by_location[_1] }
|
@@ -237,7 +408,7 @@ module GraphQL
|
|
237
408
|
end
|
238
409
|
end
|
239
410
|
|
240
|
-
# 3
|
411
|
+
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
241
412
|
if remote_selections.any?
|
242
413
|
field_count_by_location = if remote_selections.length > 1
|
243
414
|
remote_selections.each_with_object({}) do |node, memo|
|
@@ -270,91 +441,15 @@ module GraphQL
|
|
270
441
|
end
|
271
442
|
end
|
272
443
|
|
273
|
-
|
274
|
-
# then translate those routes into planner operations
|
275
|
-
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
|
276
|
-
routes.values.each_with_object({}) do |route, ops_by_location|
|
277
|
-
route.reduce(nil) do |parent_op, boundary|
|
278
|
-
location = boundary["location"]
|
279
|
-
|
280
|
-
unless op = ops_by_location[location]
|
281
|
-
op = ops_by_location[location] = add_operation(
|
282
|
-
location: location,
|
283
|
-
# routing locations added as intermediaries have no initial selections,
|
284
|
-
# but will be given foreign keys by subsequent operations
|
285
|
-
selections: selections_by_location[location] || [],
|
286
|
-
parent_type: parent_type,
|
287
|
-
insertion_path: insertion_path.dup,
|
288
|
-
boundary: boundary,
|
289
|
-
after_key: after_key,
|
290
|
-
)
|
291
|
-
end
|
292
|
-
|
293
|
-
foreign_key = "_STITCH_#{boundary["selection"]}"
|
294
|
-
parent_selections = parent_op ? parent_op.selections : locale_selections
|
295
|
-
|
296
|
-
if parent_selections.none? { _1.is_a?(GraphQL::Language::Nodes::Field) && _1.alias == foreign_key }
|
297
|
-
foreign_key_node = GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["selection"])
|
298
|
-
parent_selections << foreign_key_node << TYPENAME_NODE
|
299
|
-
end
|
300
|
-
|
301
|
-
op
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
# extracts variable definitions used by a node
|
307
|
-
# (each operation tracks the specific variables used in its tree)
|
308
|
-
def extract_node_variables(node_with_args, variable_definitions)
|
309
|
-
node_with_args.arguments.each do |argument|
|
310
|
-
case argument.value
|
311
|
-
when GraphQL::Language::Nodes::InputObject
|
312
|
-
extract_node_variables(argument.value, variable_definitions)
|
313
|
-
when GraphQL::Language::Nodes::VariableIdentifier
|
314
|
-
variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
if node_with_args.respond_to?(:directives)
|
319
|
-
node_with_args.directives.each do |directive|
|
320
|
-
extract_node_variables(directive, variable_definitions)
|
321
|
-
end
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
# fields of a merged interface may not belong to the interface at the local level,
|
326
|
-
# so any non-local interface fields get expanded into typed fragments before planning
|
327
|
-
def expand_interface_selections(current_location, parent_type, input_selections)
|
328
|
-
local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
|
329
|
-
|
330
|
-
expanded_selections = nil
|
331
|
-
input_selections = input_selections.reject do |node|
|
332
|
-
if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
|
333
|
-
expanded_selections ||= []
|
334
|
-
expanded_selections << node
|
335
|
-
true
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
if expanded_selections
|
340
|
-
@supergraph.schema.possible_types(parent_type).each do |possible_type|
|
341
|
-
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
342
|
-
|
343
|
-
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
344
|
-
input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
|
-
input_selections
|
444
|
+
selections_by_location
|
349
445
|
end
|
350
446
|
|
351
|
-
#
|
352
|
-
# this shifts all loose selection fields into a wrapping concrete type fragment
|
447
|
+
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
353
448
|
def expand_abstract_boundaries
|
354
|
-
@
|
449
|
+
@operations_by_entrypoint.each_value do |op|
|
355
450
|
next unless op.boundary
|
356
451
|
|
357
|
-
boundary_type = @supergraph.
|
452
|
+
boundary_type = @supergraph.memoized_schema_types[op.boundary["type_name"]]
|
358
453
|
next unless boundary_type.kind.abstract?
|
359
454
|
next if boundary_type == op.parent_type
|
360
455
|
|
@@ -5,30 +5,30 @@ module GraphQL
|
|
5
5
|
class PlannerOperation
|
6
6
|
LANGUAGE_PRINTER = GraphQL::Language::Printer.new
|
7
7
|
|
8
|
-
attr_reader :
|
9
|
-
attr_accessor :
|
8
|
+
attr_reader :order, :location, :parent_type, :if_type, :operation_type, :path
|
9
|
+
attr_accessor :after, :selections, :variables, :boundary
|
10
10
|
|
11
11
|
def initialize(
|
12
|
-
key:,
|
13
12
|
location:,
|
14
13
|
parent_type:,
|
14
|
+
order:,
|
15
|
+
after: nil,
|
15
16
|
operation_type: "query",
|
16
|
-
insertion_path: [],
|
17
|
-
type_condition: nil,
|
18
|
-
after_key: nil,
|
19
17
|
selections: [],
|
20
18
|
variables: [],
|
19
|
+
path: [],
|
20
|
+
if_type: nil,
|
21
21
|
boundary: nil
|
22
22
|
)
|
23
|
-
@key = key
|
24
|
-
@after_key = after_key
|
25
23
|
@location = location
|
26
24
|
@parent_type = parent_type
|
25
|
+
@order = order
|
26
|
+
@after = after
|
27
27
|
@operation_type = operation_type
|
28
|
-
@insertion_path = insertion_path
|
29
|
-
@type_condition = type_condition
|
30
28
|
@selections = selections
|
31
29
|
@variables = variables
|
30
|
+
@path = path
|
31
|
+
@if_type = if_type
|
32
32
|
@boundary = boundary
|
33
33
|
end
|
34
34
|
|
@@ -44,17 +44,19 @@ module GraphQL
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def to_h
|
47
|
-
{
|
48
|
-
"
|
49
|
-
"
|
47
|
+
data = {
|
48
|
+
"order" => @order,
|
49
|
+
"after" => @after,
|
50
50
|
"location" => @location,
|
51
51
|
"operation_type" => @operation_type,
|
52
|
-
"insertion_path" => @insertion_path,
|
53
|
-
"type_condition" => @type_condition,
|
54
52
|
"selections" => selection_set,
|
55
53
|
"variables" => variable_set,
|
56
|
-
"
|
54
|
+
"path" => @path,
|
57
55
|
}
|
56
|
+
|
57
|
+
data["if_type"] = @if_type if @if_type
|
58
|
+
data["boundary"] = @boundary if @boundary
|
59
|
+
data
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
@@ -4,14 +4,14 @@
|
|
4
4
|
module GraphQL
|
5
5
|
module Stitching
|
6
6
|
class Shaper
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(supergraph:, request:)
|
8
|
+
@supergraph = supergraph
|
9
9
|
@request = request
|
10
10
|
end
|
11
11
|
|
12
12
|
def perform!(raw)
|
13
|
-
root_type = @schema.
|
14
|
-
resolve_object_scope(raw, root_type, @request.operation.selections)
|
13
|
+
@root_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
|
14
|
+
resolve_object_scope(raw, @root_type, @request.operation.selections, @root_type.graphql_name)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
@@ -25,11 +25,14 @@ module GraphQL
|
|
25
25
|
selections.each do |node|
|
26
26
|
case node
|
27
27
|
when GraphQL::Language::Nodes::Field
|
28
|
-
next if node.name.start_with?("__")
|
29
|
-
|
30
28
|
field_name = node.alias || node.name
|
31
|
-
|
32
|
-
|
29
|
+
|
30
|
+
next if introspection_field?(parent_type, node) do |is_root_typename|
|
31
|
+
raw_object[field_name] = @root_type.graphql_name if is_root_typename
|
32
|
+
end
|
33
|
+
|
34
|
+
node_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type
|
35
|
+
named_type = node_type.unwrap
|
33
36
|
|
34
37
|
raw_object[field_name] = if node_type.list?
|
35
38
|
resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
|
@@ -42,7 +45,7 @@ module GraphQL
|
|
42
45
|
return nil if raw_object[field_name].nil? && node_type.non_null?
|
43
46
|
|
44
47
|
when GraphQL::Language::Nodes::InlineFragment
|
45
|
-
fragment_type = @
|
48
|
+
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
46
49
|
next unless typename_in_type?(typename, fragment_type)
|
47
50
|
|
48
51
|
result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
|
@@ -50,7 +53,7 @@ module GraphQL
|
|
50
53
|
|
51
54
|
when GraphQL::Language::Nodes::FragmentSpread
|
52
55
|
fragment = @request.fragment_definitions[node.name]
|
53
|
-
fragment_type = @
|
56
|
+
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
54
57
|
next unless typename_in_type?(typename, fragment_type)
|
55
58
|
|
56
59
|
result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
|
@@ -93,9 +96,27 @@ module GraphQL
|
|
93
96
|
resolved_list
|
94
97
|
end
|
95
98
|
|
99
|
+
def introspection_field?(parent_type, node)
|
100
|
+
return false unless node.name.start_with?("__")
|
101
|
+
is_root = parent_type == @root_type
|
102
|
+
|
103
|
+
case node.name
|
104
|
+
when "__typename"
|
105
|
+
yield(is_root)
|
106
|
+
true
|
107
|
+
when "__schema", "__type"
|
108
|
+
is_root && @request.operation.operation_type == "query"
|
109
|
+
else
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
96
114
|
def typename_in_type?(typename, type)
|
97
115
|
return true if type.graphql_name == typename
|
98
|
-
|
116
|
+
|
117
|
+
type.kind.abstract? && @supergraph.memoized_schema_possible_types(type.graphql_name).any? do |t|
|
118
|
+
t.graphql_name == typename
|
119
|
+
end
|
99
120
|
end
|
100
121
|
end
|
101
122
|
end
|
@@ -4,16 +4,6 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Supergraph
|
6
6
|
LOCATION = "__super"
|
7
|
-
INTROSPECTION_TYPES = [
|
8
|
-
"__Schema",
|
9
|
-
"__Type",
|
10
|
-
"__Field",
|
11
|
-
"__Directive",
|
12
|
-
"__EnumValue",
|
13
|
-
"__InputValue",
|
14
|
-
"__TypeKind",
|
15
|
-
"__DirectiveLocation",
|
16
|
-
].freeze
|
17
7
|
|
18
8
|
def self.validate_executable!(location, executable)
|
19
9
|
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
@@ -46,13 +36,14 @@ module GraphQL
|
|
46
36
|
@boundaries = boundaries
|
47
37
|
@possible_keys_by_type = {}
|
48
38
|
@possible_keys_by_type_and_location = {}
|
39
|
+
@memoized_schema_possible_types = {}
|
40
|
+
@memoized_schema_fields = {}
|
49
41
|
|
50
42
|
# add introspection types into the fields mapping
|
51
|
-
@locations_by_type_and_field =
|
52
|
-
|
53
|
-
next unless introspection_type.kind.fields?
|
43
|
+
@locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
44
|
+
next unless type.kind.fields?
|
54
45
|
|
55
|
-
memo[type_name] =
|
46
|
+
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
|
56
47
|
m[field_name] = [LOCATION]
|
57
48
|
end
|
58
49
|
end.freeze
|
@@ -66,7 +57,7 @@ module GraphQL
|
|
66
57
|
end
|
67
58
|
|
68
59
|
def fields
|
69
|
-
@locations_by_type_and_field.reject { |k, _v|
|
60
|
+
@locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
|
70
61
|
end
|
71
62
|
|
72
63
|
def locations
|
@@ -81,6 +72,35 @@ module GraphQL
|
|
81
72
|
}
|
82
73
|
end
|
83
74
|
|
75
|
+
def memoized_introspection_types
|
76
|
+
@memoized_introspection_types ||= schema.introspection_system.types
|
77
|
+
end
|
78
|
+
|
79
|
+
def memoized_schema_types
|
80
|
+
@memoized_schema_types ||= @schema.types
|
81
|
+
end
|
82
|
+
|
83
|
+
def memoized_schema_possible_types(type_name)
|
84
|
+
@memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
|
85
|
+
end
|
86
|
+
|
87
|
+
def memoized_schema_fields(type_name)
|
88
|
+
@memoized_schema_fields[type_name] ||= begin
|
89
|
+
fields = memoized_schema_types[type_name].fields
|
90
|
+
@schema.introspection_system.dynamic_fields.each do |field|
|
91
|
+
fields[field.name] ||= field # adds __typename
|
92
|
+
end
|
93
|
+
|
94
|
+
if type_name == @schema.query.graphql_name
|
95
|
+
@schema.introspection_system.entry_points.each do |field|
|
96
|
+
fields[field.name] ||= field # adds __schema, __type
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
fields
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
84
104
|
def execute_at_location(location, source, variables, context)
|
85
105
|
executable = executables[location]
|
86
106
|
|
@@ -124,9 +144,7 @@ module GraphQL
|
|
124
144
|
# ("Type") => ["id", ...]
|
125
145
|
def possible_keys_for_type(type_name)
|
126
146
|
@possible_keys_by_type[type_name] ||= begin
|
127
|
-
|
128
|
-
keys.uniq!
|
129
|
-
keys
|
147
|
+
@boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
|
130
148
|
end
|
131
149
|
end
|
132
150
|
|
@@ -166,18 +184,18 @@ module GraphQL
|
|
166
184
|
costs = {}
|
167
185
|
|
168
186
|
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
169
|
-
[{ location: start_location,
|
187
|
+
[{ location: start_location, key: possible_key, cost: 0 }]
|
170
188
|
end
|
171
189
|
|
172
190
|
while paths.any?
|
173
191
|
path = paths.pop
|
174
192
|
current_location = path.last[:location]
|
175
|
-
|
193
|
+
current_key = path.last[:key]
|
176
194
|
current_cost = path.last[:cost]
|
177
195
|
|
178
196
|
@boundaries[type_name].each do |boundary|
|
179
197
|
forward_location = boundary["location"]
|
180
|
-
next if
|
198
|
+
next if current_key != boundary["key"]
|
181
199
|
next if path.any? { _1[:location] == forward_location }
|
182
200
|
|
183
201
|
best_cost = costs[forward_location] || Float::INFINITY
|
@@ -186,7 +204,7 @@ module GraphQL
|
|
186
204
|
path.pop
|
187
205
|
path << {
|
188
206
|
location: current_location,
|
189
|
-
|
207
|
+
key: current_key,
|
190
208
|
cost: current_cost,
|
191
209
|
boundary: boundary,
|
192
210
|
}
|
@@ -204,14 +222,13 @@ module GraphQL
|
|
204
222
|
costs[forward_location] = forward_cost if forward_cost < best_cost
|
205
223
|
|
206
224
|
possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
|
207
|
-
paths << [*path, { location: forward_location,
|
225
|
+
paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
|
208
226
|
end
|
209
227
|
end
|
210
228
|
|
211
229
|
paths.sort! do |a, b|
|
212
230
|
cost_diff = a.last[:cost] - b.last[:cost]
|
213
|
-
|
214
|
-
a.length - b.length
|
231
|
+
cost_diff.zero? ? a.length - b.length : cost_diff
|
215
232
|
end.reverse!
|
216
233
|
end
|
217
234
|
|
@@ -37,15 +37,6 @@ module GraphQL
|
|
37
37
|
structure
|
38
38
|
end
|
39
39
|
|
40
|
-
# gets a named type for a field node, including hidden root introspections
|
41
|
-
def self.named_type_for_field_node(schema, parent_type, node)
|
42
|
-
if node.name == "__schema" && parent_type == schema.query
|
43
|
-
schema.types["__Schema"]
|
44
|
-
else
|
45
|
-
parent_type.fields[node.name].type.unwrap
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
40
|
# expands interfaces and unions to an array of their memberships
|
50
41
|
# like `schema.possible_types`, but includes child interfaces
|
51
42
|
def self.expand_abstract_type(schema, parent_type)
|
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: 0.3.
|
4
|
+
version: 0.3.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|