graphql-stitching 1.7.2 → 1.7.3
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 +1 -1
- data/docs/performance.md +28 -0
- data/lib/graphql/stitching/client.rb +2 -2
- data/lib/graphql/stitching/composer/type_resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer.rb +1 -3
- data/lib/graphql/stitching/executor/root_source.rb +15 -11
- data/lib/graphql/stitching/executor/shaper.rb +11 -6
- data/lib/graphql/stitching/executor/type_resolver_source.rb +45 -29
- data/lib/graphql/stitching/executor.rb +1 -1
- data/lib/graphql/stitching/http_executable.rb +1 -2
- data/lib/graphql/stitching/plan.rb +46 -13
- data/lib/graphql/stitching/planner.rb +28 -27
- data/lib/graphql/stitching/request/skip_include.rb +2 -2
- data/lib/graphql/stitching/request.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +16 -6
- data/lib/graphql/stitching/type_resolver.rb +12 -1
- data/lib/graphql/stitching/util.rb +21 -4
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +3 -3
- /data/docs/{introduction.md → README.md} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65951348a6c34feff379cdbfc7483380dce295dbd86cc24870eee264de079a46
|
4
|
+
data.tar.gz: c16673030f3be9728c986b8e4fe5e8084ca7a4e0240ac7e495da26261f1f0a3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 255495cde4ac822edc3b93ac67af6eb3b49ab657adedebd2870058468d6b49d241fca5e01c494fd3f345e134b21755e1a3f3ce2d2d43bad6e078b1833f47768c
|
7
|
+
data.tar.gz: ac375cfe4ac73d9b1ce732180efada3d56aab8fa6e5cca75b40e0a6a8d133a45e67637090b9e49a76f9b2b20a81de9014bde03f88f03cdf277f85b5aa435504a
|
data/README.md
CHANGED
@@ -21,7 +21,7 @@ This Ruby implementation is designed as a generic library to join basic spec-com
|
|
21
21
|
|
22
22
|
## Documentation
|
23
23
|
|
24
|
-
1. [Introduction](./docs/
|
24
|
+
1. [Introduction](./docs/README.md)
|
25
25
|
1. [Composing a supergraph](./docs/composing_a_supergraph.md)
|
26
26
|
1. [Merged types](./docs/merged_types.md)
|
27
27
|
1. [Executables & file uploads](./docs/executables.md)
|
data/docs/performance.md
CHANGED
@@ -57,6 +57,34 @@ query($id: ID!) {
|
|
57
57
|
# variables: { "id" => "1" }
|
58
58
|
```
|
59
59
|
|
60
|
+
### Subgraph validations
|
61
|
+
|
62
|
+
Requests are validated by the supergraph, and should always divide into valid subgraph documents. Therefore, you can skip redundant subgraph validations for requests sent by the supergraph, ex:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
exe = GraphQL::Stitching::HttpExecutable.new(
|
66
|
+
url: "http://localhost:3001",
|
67
|
+
headers: {
|
68
|
+
"Authorization" => "...",
|
69
|
+
"X-Supergraph-Secret" => "<shared-secret>",
|
70
|
+
},
|
71
|
+
)
|
72
|
+
```
|
73
|
+
|
74
|
+
A shared secret allows a subgraph location to trust the supergraph origin, at which time it can disable validations:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
def query
|
78
|
+
sg_header = request.headers["X-Supergraph-Secret"]
|
79
|
+
MySchema.execute(
|
80
|
+
query: params[:query],
|
81
|
+
variables: params[:variables],
|
82
|
+
operation_name: params[:operationName],
|
83
|
+
validate: sg_header.nil? || sg_header != Rails.env.credentials.supergraph,
|
84
|
+
)
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
60
88
|
### Digests
|
61
89
|
|
62
90
|
All computed digests use SHA2 hashing by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base state by reconfiguring `Stitching.digest`:
|
@@ -25,7 +25,7 @@ module GraphQL
|
|
25
25
|
raise ArgumentError, "Cannot provide both locations and a supergraph."
|
26
26
|
elsif supergraph && !supergraph.is_a?(Supergraph)
|
27
27
|
raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
28
|
-
elsif supergraph && composer_options.
|
28
|
+
elsif supergraph && !composer_options.empty?
|
29
29
|
raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
|
30
30
|
elsif supergraph
|
31
31
|
supergraph
|
@@ -50,7 +50,7 @@ module GraphQL
|
|
50
50
|
|
51
51
|
if validate
|
52
52
|
validation_errors = request.validate
|
53
|
-
return error_result(request, validation_errors)
|
53
|
+
return error_result(request, validation_errors) unless validation_errors.empty?
|
54
54
|
end
|
55
55
|
|
56
56
|
load_plan(request)
|
@@ -8,7 +8,7 @@ module GraphQL::Stitching
|
|
8
8
|
|
9
9
|
class << self
|
10
10
|
def extract_directive_assignments(schema, location, assignments)
|
11
|
-
return EMPTY_OBJECT unless assignments && assignments.
|
11
|
+
return EMPTY_OBJECT unless assignments && !assignments.empty?
|
12
12
|
|
13
13
|
assignments.each_with_object({}) do |kwargs, memo|
|
14
14
|
type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
|
@@ -423,7 +423,7 @@ module GraphQL
|
|
423
423
|
memo[location] = argument.default_value
|
424
424
|
end
|
425
425
|
|
426
|
-
|
426
|
+
unless default_values_by_location.empty?
|
427
427
|
kwargs[:default_value] = @default_value_merger.call(default_values_by_location, {
|
428
428
|
type_name: type_name,
|
429
429
|
field_name: field_name,
|
@@ -468,8 +468,6 @@ module GraphQL
|
|
468
468
|
kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
|
469
469
|
directive.arguments.keyword_arguments.each do |key, value|
|
470
470
|
key = key.to_s
|
471
|
-
next unless directive_class.arguments[key]
|
472
|
-
|
473
471
|
memo[key] ||= {}
|
474
472
|
memo[key][location] = value
|
475
473
|
end
|
@@ -21,7 +21,7 @@ module GraphQL::Stitching
|
|
21
21
|
@executor.query_count += 1
|
22
22
|
|
23
23
|
if result["data"]
|
24
|
-
|
24
|
+
unless op.path.empty?
|
25
25
|
# Nested root scopes must expand their pathed origin set
|
26
26
|
origin_set = op.path.reduce([@executor.data]) do |set, ns|
|
27
27
|
set.flat_map { |obj| obj && obj[ns] }.tap(&:compact!)
|
@@ -44,24 +44,28 @@ module GraphQL::Stitching
|
|
44
44
|
# Builds root source documents
|
45
45
|
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
46
46
|
def build_document(op, operation_name = nil, operation_directives = nil)
|
47
|
-
|
48
|
-
|
47
|
+
doc_buffer = String.new
|
48
|
+
doc_buffer << op.operation_type
|
49
49
|
|
50
50
|
if operation_name
|
51
|
-
|
51
|
+
doc_buffer << " " << operation_name << "_" << op.step.to_s
|
52
52
|
end
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
unless op.variables.empty?
|
55
|
+
doc_buffer << "("
|
56
|
+
op.variables.each_with_index do |(k, v), i|
|
57
|
+
doc_buffer << "," unless i.zero?
|
58
|
+
doc_buffer << "$" << k << ":" << v
|
59
|
+
end
|
60
|
+
doc_buffer << ")"
|
57
61
|
end
|
58
62
|
|
59
63
|
if operation_directives
|
60
|
-
|
64
|
+
doc_buffer << " " << operation_directives << " "
|
61
65
|
end
|
62
66
|
|
63
|
-
|
64
|
-
|
67
|
+
doc_buffer << op.selections
|
68
|
+
doc_buffer
|
65
69
|
end
|
66
70
|
|
67
71
|
# Format response errors without a document location (because it won't match the request doc),
|
@@ -69,7 +73,7 @@ module GraphQL::Stitching
|
|
69
73
|
def format_errors!(errors, path)
|
70
74
|
errors.each do |err|
|
71
75
|
err.delete("locations")
|
72
|
-
err["path"].unshift(*path) if err["path"] && path.
|
76
|
+
err["path"].unshift(*path) if err["path"] && !path.empty?
|
73
77
|
end
|
74
78
|
errors
|
75
79
|
end
|
@@ -30,10 +30,15 @@ module GraphQL::Stitching
|
|
30
30
|
case node
|
31
31
|
when GraphQL::Language::Nodes::Field
|
32
32
|
field_name = node.alias || node.name
|
33
|
+
raw_value = raw_object.delete(field_name)
|
33
34
|
|
34
35
|
if @request.query.get_field(parent_type, node.name).introspection?
|
35
|
-
if
|
36
|
-
|
36
|
+
next if TypeResolver.export_key?(field_name)
|
37
|
+
|
38
|
+
raw_object[field_name] = if node.name == TYPENAME && parent_type == @root_type
|
39
|
+
@root_type.graphql_name
|
40
|
+
else
|
41
|
+
raw_value
|
37
42
|
end
|
38
43
|
next
|
39
44
|
end
|
@@ -42,14 +47,14 @@ module GraphQL::Stitching
|
|
42
47
|
named_type = node_type.unwrap
|
43
48
|
|
44
49
|
raw_object[field_name] = if node_type.list?
|
45
|
-
resolve_list_scope(
|
50
|
+
resolve_list_scope(raw_value, Util.unwrap_non_null(node_type), node.selections)
|
46
51
|
elsif Util.is_leaf_type?(named_type)
|
47
|
-
|
52
|
+
raw_value
|
48
53
|
else
|
49
|
-
resolve_object_scope(
|
54
|
+
resolve_object_scope(raw_value, named_type, node.selections)
|
50
55
|
end
|
51
56
|
|
52
|
-
return nil if raw_object[field_name].nil?
|
57
|
+
return nil if node_type.non_null? && raw_object[field_name].nil?
|
53
58
|
|
54
59
|
when GraphQL::Language::Nodes::InlineFragment
|
55
60
|
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
@@ -20,10 +20,10 @@ module GraphQL::Stitching
|
|
20
20
|
origin_set.select! { _1[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
|
21
21
|
end
|
22
22
|
|
23
|
-
memo[op] = origin_set
|
23
|
+
memo[op] = origin_set unless origin_set.empty?
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
unless origin_sets_by_operation.empty?
|
27
27
|
query_document, variable_names = build_document(
|
28
28
|
origin_sets_by_operation,
|
29
29
|
@executor.request.operation_name,
|
@@ -51,61 +51,76 @@ module GraphQL::Stitching
|
|
51
51
|
# }"
|
52
52
|
def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
|
53
53
|
variable_defs = {}
|
54
|
-
|
54
|
+
fields_buffer = String.new
|
55
|
+
|
56
|
+
origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
|
55
57
|
variable_defs.merge!(op.variables)
|
56
58
|
resolver = @executor.request.supergraph.resolvers_by_version[op.resolver]
|
59
|
+
fields_buffer << " " unless batch_index.zero?
|
57
60
|
|
58
61
|
if resolver.list?
|
59
|
-
|
62
|
+
fields_buffer << "_" << batch_index.to_s << "_result: " << resolver.field << "("
|
63
|
+
|
64
|
+
resolver.arguments.each_with_index do |arg, i|
|
65
|
+
fields_buffer << "," unless i.zero?
|
60
66
|
if arg.key?
|
61
67
|
variable_name = "_#{batch_index}_key_#{i}".freeze
|
62
68
|
@variables[variable_name] = origin_set.map { arg.build(_1) }
|
63
69
|
variable_defs[variable_name] = arg.to_type_signature
|
64
|
-
|
70
|
+
fields_buffer << arg.name << ":$" << variable_name
|
65
71
|
else
|
66
|
-
|
72
|
+
fields_buffer << arg.name << ":" << arg.value.print
|
67
73
|
end
|
68
74
|
end
|
69
75
|
|
70
|
-
|
76
|
+
fields_buffer << ") " << op.selections
|
71
77
|
else
|
72
|
-
origin_set.
|
73
|
-
|
78
|
+
origin_set.each_with_index do |origin_obj, index|
|
79
|
+
fields_buffer << " " unless index.zero?
|
80
|
+
fields_buffer << "_" << batch_index.to_s << "_" << index.to_s << "_result: " << resolver.field << "("
|
81
|
+
|
82
|
+
resolver.arguments.each_with_index do |arg, i|
|
83
|
+
fields_buffer << "," unless i.zero?
|
74
84
|
if arg.key?
|
75
85
|
variable_name = "_#{batch_index}_#{index}_key_#{i}".freeze
|
76
86
|
@variables[variable_name] = arg.build(origin_obj)
|
77
87
|
variable_defs[variable_name] = arg.to_type_signature
|
78
|
-
|
88
|
+
fields_buffer << arg.name << ":$" << variable_name
|
79
89
|
else
|
80
|
-
|
90
|
+
fields_buffer << arg.name << ":" << arg.value.print
|
81
91
|
end
|
82
92
|
end
|
83
93
|
|
84
|
-
|
94
|
+
fields_buffer << ") " << op.selections
|
85
95
|
end
|
86
96
|
end
|
87
97
|
end
|
88
98
|
|
89
|
-
|
99
|
+
doc_buffer = String.new(QUERY_OP) # << resolver fulfillment always uses query
|
90
100
|
|
91
101
|
if operation_name
|
92
|
-
|
102
|
+
doc_buffer << " " << operation_name
|
93
103
|
origin_sets_by_operation.each_key do |op|
|
94
|
-
|
104
|
+
doc_buffer << "_" << op.step.to_s
|
95
105
|
end
|
96
106
|
end
|
97
107
|
|
98
|
-
|
99
|
-
|
108
|
+
unless variable_defs.empty?
|
109
|
+
doc_buffer << "("
|
110
|
+
variable_defs.each_with_index do |(k, v), i|
|
111
|
+
doc_buffer << "," unless i.zero?
|
112
|
+
doc_buffer << "$" << k << ":" << v
|
113
|
+
end
|
114
|
+
doc_buffer << ")"
|
100
115
|
end
|
101
116
|
|
102
117
|
if operation_directives
|
103
|
-
|
118
|
+
doc_buffer << " " << operation_directives << " "
|
104
119
|
end
|
105
120
|
|
106
|
-
|
121
|
+
doc_buffer << "{ " << fields_buffer << " }"
|
107
122
|
|
108
|
-
return
|
123
|
+
return doc_buffer, variable_defs.keys.tap do |names|
|
109
124
|
names.reject! { @variables.key?(_1) }
|
110
125
|
end
|
111
126
|
end
|
@@ -120,10 +135,11 @@ module GraphQL::Stitching
|
|
120
135
|
origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
|
121
136
|
end
|
122
137
|
|
123
|
-
next
|
138
|
+
next if results.nil? || results.empty?
|
124
139
|
|
125
140
|
origin_set.each_with_index do |origin_obj, index|
|
126
|
-
|
141
|
+
result = results[index]
|
142
|
+
origin_obj.merge!(result) if result
|
127
143
|
end
|
128
144
|
end
|
129
145
|
end
|
@@ -132,7 +148,7 @@ module GraphQL::Stitching
|
|
132
148
|
def extract_errors!(origin_sets_by_operation, errors)
|
133
149
|
ops = origin_sets_by_operation.keys
|
134
150
|
origin_sets = origin_sets_by_operation.values
|
135
|
-
pathed_errors_by_op_index_and_object_id = {}
|
151
|
+
pathed_errors_by_op_index_and_object_id = Hash.new { |h, k| h[k] = {} }
|
136
152
|
|
137
153
|
errors_result = errors.each_with_object([]) do |err, memo|
|
138
154
|
err.delete("locations")
|
@@ -151,8 +167,8 @@ module GraphQL::Stitching
|
|
151
167
|
end
|
152
168
|
|
153
169
|
if origin_obj
|
154
|
-
|
155
|
-
by_object_id =
|
170
|
+
pathed_errors_by_op_index = pathed_errors_by_op_index_and_object_id[result_alias[1].to_i]
|
171
|
+
by_object_id = pathed_errors_by_op_index[origin_obj.object_id] ||= []
|
156
172
|
by_object_id << err
|
157
173
|
next
|
158
174
|
end
|
@@ -162,9 +178,9 @@ module GraphQL::Stitching
|
|
162
178
|
memo << err
|
163
179
|
end
|
164
180
|
|
165
|
-
|
181
|
+
unless pathed_errors_by_op_index_and_object_id.empty?
|
166
182
|
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
167
|
-
repath_errors!(pathed_errors_by_object_id, ops.
|
183
|
+
repath_errors!(pathed_errors_by_object_id, ops[op_index].path)
|
168
184
|
errors_result.push(*pathed_errors_by_object_id.each_value)
|
169
185
|
end
|
170
186
|
end
|
@@ -180,7 +196,7 @@ module GraphQL::Stitching
|
|
180
196
|
current_path.push(forward_path.shift)
|
181
197
|
scope = root[current_path.last]
|
182
198
|
|
183
|
-
if forward_path.
|
199
|
+
if !forward_path.empty? && scope.is_a?(Array)
|
184
200
|
scope.each_with_index do |element, index|
|
185
201
|
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
186
202
|
inner_elements.each do |inner_element|
|
@@ -190,7 +206,7 @@ module GraphQL::Stitching
|
|
190
206
|
end
|
191
207
|
end
|
192
208
|
|
193
|
-
elsif forward_path.
|
209
|
+
elsif !forward_path.empty?
|
194
210
|
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
195
211
|
|
196
212
|
elsif scope.is_a?(Array)
|
@@ -80,7 +80,7 @@ module GraphQL
|
|
80
80
|
|
81
81
|
return if files_by_path.none?
|
82
82
|
|
83
|
-
map = {}
|
83
|
+
map = Hash.new { |h, k| h[k] = [] }
|
84
84
|
files = files_by_path.values.tap(&:uniq!)
|
85
85
|
variables_copy = variables.dup
|
86
86
|
|
@@ -90,7 +90,6 @@ module GraphQL
|
|
90
90
|
path.each_with_index do |key, i|
|
91
91
|
if i == path.length - 1
|
92
92
|
file_index = files.index(copy[key]).to_s
|
93
|
-
map[file_index] ||= []
|
94
93
|
map[file_index] << "variables.#{path.join(".")}"
|
95
94
|
copy[key] = nil
|
96
95
|
elsif orig[key].object_id == copy[key].object_id
|
@@ -2,21 +2,42 @@
|
|
2
2
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
|
-
# Immutable
|
5
|
+
# Immutable structures representing a query plan.
|
6
6
|
# May serialize to/from JSON.
|
7
7
|
class Plan
|
8
|
-
Op
|
9
|
-
:step
|
10
|
-
:after
|
11
|
-
:location
|
12
|
-
:operation_type
|
13
|
-
:selections
|
14
|
-
:variables
|
15
|
-
:path
|
16
|
-
:if_type
|
17
|
-
:resolver
|
18
|
-
|
19
|
-
|
8
|
+
class Op
|
9
|
+
attr_reader :step
|
10
|
+
attr_reader :after
|
11
|
+
attr_reader :location
|
12
|
+
attr_reader :operation_type
|
13
|
+
attr_reader :selections
|
14
|
+
attr_reader :variables
|
15
|
+
attr_reader :path
|
16
|
+
attr_reader :if_type
|
17
|
+
attr_reader :resolver
|
18
|
+
|
19
|
+
def initialize(
|
20
|
+
step:,
|
21
|
+
after:,
|
22
|
+
location:,
|
23
|
+
operation_type:,
|
24
|
+
selections:,
|
25
|
+
variables: nil,
|
26
|
+
path: nil,
|
27
|
+
if_type: nil,
|
28
|
+
resolver: nil
|
29
|
+
)
|
30
|
+
@step = step
|
31
|
+
@after = after
|
32
|
+
@location = location
|
33
|
+
@operation_type = operation_type
|
34
|
+
@selections = selections
|
35
|
+
@variables = variables
|
36
|
+
@path = path
|
37
|
+
@if_type = if_type
|
38
|
+
@resolver = resolver
|
39
|
+
end
|
40
|
+
|
20
41
|
def as_json
|
21
42
|
{
|
22
43
|
step: step,
|
@@ -30,6 +51,18 @@ module GraphQL
|
|
30
51
|
resolver: resolver
|
31
52
|
}.tap(&:compact!)
|
32
53
|
end
|
54
|
+
|
55
|
+
def ==(other)
|
56
|
+
step == other.step &&
|
57
|
+
after == other.after &&
|
58
|
+
location == other.location &&
|
59
|
+
operation_type == other.operation_type &&
|
60
|
+
selections == other.selections &&
|
61
|
+
variables == other.variables &&
|
62
|
+
path == other.path &&
|
63
|
+
if_type == other.if_type &&
|
64
|
+
resolver == other.resolver
|
65
|
+
end
|
33
66
|
end
|
34
67
|
|
35
68
|
class << self
|
@@ -10,6 +10,15 @@ module GraphQL
|
|
10
10
|
SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
|
11
11
|
ROOT_INDEX = 0
|
12
12
|
|
13
|
+
class ScopePartition
|
14
|
+
attr_reader :location, :selections
|
15
|
+
|
16
|
+
def initialize(location:, selections:)
|
17
|
+
@location = location
|
18
|
+
@selections = selections
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
13
22
|
def initialize(request)
|
14
23
|
@request = request
|
15
24
|
@supergraph = request.supergraph
|
@@ -76,11 +85,15 @@ module GraphQL
|
|
76
85
|
resolver: nil
|
77
86
|
)
|
78
87
|
# coalesce repeat parameters into a single entrypoint
|
79
|
-
entrypoint =
|
88
|
+
entrypoint = String.new
|
89
|
+
entrypoint << parent_index.to_s << "/" << location << "/" << parent_type.graphql_name
|
90
|
+
entrypoint << "/" << (resolver&.key&.to_s || "") << "/#"
|
91
|
+
path.each { entrypoint << "/" << _1 }
|
92
|
+
|
80
93
|
step = @steps_by_entrypoint[entrypoint]
|
81
94
|
next_index = step ? parent_index : @planning_index += 1
|
82
95
|
|
83
|
-
|
96
|
+
unless selections.empty?
|
84
97
|
selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
|
85
98
|
end
|
86
99
|
|
@@ -102,8 +115,6 @@ module GraphQL
|
|
102
115
|
end
|
103
116
|
end
|
104
117
|
|
105
|
-
ScopePartition = Struct.new(:location, :selections, keyword_init: true)
|
106
|
-
|
107
118
|
# A) Group all root selections by their preferred entrypoint locations.
|
108
119
|
def build_root_entrypoints
|
109
120
|
parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
|
@@ -111,10 +122,9 @@ module GraphQL
|
|
111
122
|
case @request.operation.operation_type
|
112
123
|
when QUERY_OP
|
113
124
|
# A.1) Group query fields by location for parallel execution.
|
114
|
-
selections_by_location = {}
|
125
|
+
selections_by_location = Hash.new { |h, k| h[k] = [] }
|
115
126
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
116
127
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
117
|
-
selections_by_location[locations.first] ||= []
|
118
128
|
selections_by_location[locations.first] << node
|
119
129
|
end
|
120
130
|
|
@@ -213,7 +223,7 @@ module GraphQL
|
|
213
223
|
input_selections.each do |node|
|
214
224
|
case node
|
215
225
|
when GraphQL::Language::Nodes::Field
|
216
|
-
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node != TypeResolver::TYPENAME_EXPORT_NODE
|
226
|
+
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node.object_id != TypeResolver::TYPENAME_EXPORT_NODE.object_id
|
217
227
|
raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
|
218
228
|
elsif node.name == TYPENAME
|
219
229
|
locale_selections << node
|
@@ -229,7 +239,8 @@ module GraphQL
|
|
229
239
|
|
230
240
|
# B.3) Collect all variable definitions used within the filtered selection.
|
231
241
|
extract_node_variables(node, locale_variables)
|
232
|
-
|
242
|
+
schema_fields = @supergraph.memoized_schema_fields(parent_type.graphql_name)
|
243
|
+
field_type = schema_fields[node.name].type.unwrap
|
233
244
|
|
234
245
|
if Util.is_leaf_type?(field_type)
|
235
246
|
locale_selections << node
|
@@ -377,9 +388,11 @@ module GraphQL
|
|
377
388
|
end
|
378
389
|
|
379
390
|
# C.2) Distribute non-unique fields among locations that were added during C.1.
|
380
|
-
if selections_by_location.
|
391
|
+
if !selections_by_location.empty? && !remote_selections.empty?
|
392
|
+
available_locations = Set.new(selections_by_location.each_key)
|
393
|
+
|
381
394
|
remote_selections.reject! do |node|
|
382
|
-
used_location = possible_locations_by_field[node.name].find {
|
395
|
+
used_location = possible_locations_by_field[node.name].find { available_locations.include?(_1) }
|
383
396
|
if used_location
|
384
397
|
selections_by_location[used_location] << node
|
385
398
|
true
|
@@ -388,29 +401,17 @@ module GraphQL
|
|
388
401
|
end
|
389
402
|
|
390
403
|
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
391
|
-
if remote_selections.
|
392
|
-
field_count_by_location =
|
404
|
+
if !remote_selections.empty?
|
405
|
+
field_count_by_location = Hash.new(0)
|
406
|
+
remote_selections.each do |node|
|
393
407
|
possible_locations_by_field[node.name].each do |location|
|
394
|
-
|
395
|
-
memo[location] += 1
|
408
|
+
field_count_by_location[location] += 1
|
396
409
|
end
|
397
410
|
end
|
398
411
|
|
399
412
|
remote_selections.each do |node|
|
400
413
|
possible_locations = possible_locations_by_field[node.name]
|
401
|
-
preferred_location = possible_locations.first
|
402
|
-
|
403
|
-
possible_locations.reduce(0) do |max_availability, possible_location|
|
404
|
-
availability = field_count_by_location.fetch(possible_location, 0)
|
405
|
-
|
406
|
-
if availability > max_availability
|
407
|
-
preferred_location = possible_location
|
408
|
-
availability
|
409
|
-
else
|
410
|
-
max_availability
|
411
|
-
end
|
412
|
-
end
|
413
|
-
|
414
|
+
preferred_location = possible_locations.max_by { field_count_by_location[_1] } || possible_locations.first
|
414
415
|
selections_by_location[preferred_location] ||= []
|
415
416
|
selections_by_location[preferred_location] << node
|
416
417
|
end
|
@@ -34,7 +34,7 @@ module GraphQL::Stitching
|
|
34
34
|
next nil
|
35
35
|
end
|
36
36
|
|
37
|
-
node = render_node(node, variables)
|
37
|
+
node = render_node(node, variables) unless node.selections.empty?
|
38
38
|
changed ||= node.object_id != original_node.object_id
|
39
39
|
node
|
40
40
|
end
|
@@ -51,7 +51,7 @@ module GraphQL::Stitching
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def prune_node(node, variables)
|
54
|
-
return node
|
54
|
+
return node if node.directives.empty?
|
55
55
|
|
56
56
|
delete_node = false
|
57
57
|
filtered_directives = node.directives.reject do |directive|
|
@@ -101,7 +101,7 @@ module GraphQL
|
|
101
101
|
|
102
102
|
# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
|
103
103
|
def operation_directives
|
104
|
-
@operation_directives ||=
|
104
|
+
@operation_directives ||= unless operation.directives.empty?
|
105
105
|
printer = GraphQL::Language::Printer.new
|
106
106
|
operation.directives.map { printer.print(_1) }.join(" ")
|
107
107
|
end
|
@@ -50,11 +50,11 @@ module GraphQL
|
|
50
50
|
end
|
51
51
|
end.freeze
|
52
52
|
|
53
|
-
if visibility_profiles.
|
53
|
+
if visibility_profiles.empty?
|
54
|
+
@schema.use(GraphQL::Schema::AlwaysVisible)
|
55
|
+
else
|
54
56
|
profiles = visibility_profiles.each_with_object({ nil => {} }) { |p, m| m[p.to_s] = {} }
|
55
57
|
@schema.use(GraphQL::Schema::Visibility, profiles: profiles)
|
56
|
-
else
|
57
|
-
@schema.use(GraphQL::Schema::AlwaysVisible)
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
@@ -187,7 +187,17 @@ module GraphQL
|
|
187
187
|
|
188
188
|
private
|
189
189
|
|
190
|
-
PathNode
|
190
|
+
class PathNode
|
191
|
+
attr_reader :location, :key, :resolver
|
192
|
+
attr_accessor :cost
|
193
|
+
|
194
|
+
def initialize(location:, key:, resolver: nil, cost: 0)
|
195
|
+
@location = location
|
196
|
+
@key = key
|
197
|
+
@resolver = resolver
|
198
|
+
@cost = cost
|
199
|
+
end
|
200
|
+
end
|
191
201
|
|
192
202
|
# tunes A* search to favor paths with fewest joining locations, ie:
|
193
203
|
# favor longer paths through target locations over shorter paths with additional locations.
|
@@ -196,10 +206,10 @@ module GraphQL
|
|
196
206
|
costs = {}
|
197
207
|
|
198
208
|
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
199
|
-
[PathNode.new(location: start_location, key: possible_key
|
209
|
+
[PathNode.new(location: start_location, key: possible_key)]
|
200
210
|
end
|
201
211
|
|
202
|
-
while paths.
|
212
|
+
while !paths.empty?
|
203
213
|
path = paths.pop
|
204
214
|
current_location = path.last.location
|
205
215
|
current_key = path.last.key
|
@@ -10,6 +10,13 @@ module GraphQL
|
|
10
10
|
extend ArgumentsParser
|
11
11
|
extend KeysParser
|
12
12
|
|
13
|
+
class << self
|
14
|
+
# only intended for testing...
|
15
|
+
def use_static_version?
|
16
|
+
@use_static_version ||= false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
13
20
|
# location name providing the resolver query.
|
14
21
|
attr_reader :location
|
15
22
|
|
@@ -47,7 +54,11 @@ module GraphQL
|
|
47
54
|
end
|
48
55
|
|
49
56
|
def version
|
50
|
-
@version ||=
|
57
|
+
@version ||= if self.class.use_static_version?
|
58
|
+
[location, field, key.to_definition, type_name].join(".")
|
59
|
+
else
|
60
|
+
Stitching.digest.call("#{Stitching::VERSION}/#{as_json.to_json}")
|
61
|
+
end
|
51
62
|
end
|
52
63
|
|
53
64
|
def ==(other)
|
@@ -4,12 +4,29 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
# General utilities to aid with stitching.
|
6
6
|
class Util
|
7
|
-
TypeStructure
|
8
|
-
|
9
|
-
|
7
|
+
class TypeStructure
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(list:, null:, name:)
|
11
|
+
@list = list
|
12
|
+
@null = null
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def list?
|
17
|
+
@list
|
18
|
+
end
|
19
|
+
|
20
|
+
def null?
|
21
|
+
@null
|
22
|
+
end
|
10
23
|
|
11
24
|
def non_null?
|
12
|
-
|
25
|
+
!@null
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
@list == other.list? && @null == other.null? && @name == other.name
|
13
30
|
end
|
14
31
|
end
|
15
32
|
|
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.7.
|
4
|
+
version: 1.7.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -79,13 +79,13 @@ files:
|
|
79
79
|
- LICENSE
|
80
80
|
- README.md
|
81
81
|
- Rakefile
|
82
|
+
- docs/README.md
|
82
83
|
- docs/composing_a_supergraph.md
|
83
84
|
- docs/error_handling.md
|
84
85
|
- docs/executables.md
|
85
86
|
- docs/images/library.png
|
86
87
|
- docs/images/merging.png
|
87
88
|
- docs/images/stitching.png
|
88
|
-
- docs/introduction.md
|
89
89
|
- docs/merged_types.md
|
90
90
|
- docs/merged_types_apollo.md
|
91
91
|
- docs/performance.md
|
File without changes
|