graphql-stitching 1.5.2 → 1.6.1
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/.github/workflows/ci.yml +2 -0
- data/README.md +2 -2
- data/docs/request.md +2 -4
- data/gemfiles/graphql_2.4.0.gemfile +9 -0
- data/lib/graphql/stitching/client.rb +0 -1
- data/lib/graphql/stitching/composer.rb +6 -17
- data/lib/graphql/stitching/executor/shaper.rb +7 -19
- data/lib/graphql/stitching/http_executable.rb +1 -1
- data/lib/graphql/stitching/planner.rb +2 -2
- data/lib/graphql/stitching/request.rb +64 -59
- data/lib/graphql/stitching/supergraph/to_definition.rb +1 -2
- data/lib/graphql/stitching/supergraph.rb +0 -2
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da2c14565c38d14e4e49f51ff2027f89a2ee4c0313102083c9ea3baadbcd02d4
|
4
|
+
data.tar.gz: eec311845ed143af3f8d598c22f2b6026a46d34868fd7546abfb585a8fed6ebc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87608b9f5391ec190e0d0f534e1a58447f056b37e37c3f569d908b2c7043d40ef38732b3bff6e4975d81d34b04bb84cd88f809b364d73aeade68675f8c35f35f
|
7
|
+
data.tar.gz: eb1e085ad36c36b52b0c871d9d8f3ff0ca5b6d29ff51c1ea6cd9564c0cdba8e554381615099a331426f4857b82049290287c79cc1338388a36027cb451aa3bb9
|
data/.github/workflows/ci.yml
CHANGED
data/README.md
CHANGED
@@ -74,7 +74,7 @@ result = client.execute(
|
|
74
74
|
|
75
75
|
Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
|
76
76
|
|
77
|
-
|
77
|
+
A Client bundles up the component parts of stitching, which are worth familiarizing with:
|
78
78
|
|
79
79
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
|
80
80
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
|
@@ -87,7 +87,7 @@ While `Client` is sufficient for most usecases, the library offers several discr
|
|
87
87
|
|
88
88
|

|
89
89
|
|
90
|
-
To facilitate this, schemas should be designed around
|
90
|
+
To facilitate this, schemas should be designed around **merged type keys** that stitching can cross-reference and fetch across locations using **type resolver queries** (discussed below). For those in an Apollo ecosystem, there's also _limited_ support for merging types though [federation `_entities`](./docs/federation_entities.md).
|
91
91
|
|
92
92
|
### Merged type keys
|
93
93
|
|
data/docs/request.md
CHANGED
@@ -15,7 +15,6 @@ request = GraphQL::Stitching::Request.new(
|
|
15
15
|
|
16
16
|
A `Request` provides the following information:
|
17
17
|
|
18
|
-
- `req.document`: parsed AST of the GraphQL source.
|
19
18
|
- `req.variables`: a hash of user-submitted variables.
|
20
19
|
- `req.string`: the original GraphQL source string, or printed document.
|
21
20
|
- `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
|
@@ -31,6 +30,5 @@ A request manages the flow of stitching behaviors. These are sequenced by the `C
|
|
31
30
|
component, or you may invoke them manually:
|
32
31
|
|
33
32
|
1. `request.validate`: runs static validations on the request using the combined schema.
|
34
|
-
2. `request.
|
35
|
-
3. `request.
|
36
|
-
4. `request.execute`: executes the request, and returns the resulting data.
|
33
|
+
2. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
|
34
|
+
3. `request.execute`: executes the request, and returns the resulting data.
|
@@ -13,13 +13,11 @@ module GraphQL
|
|
13
13
|
class Composer
|
14
14
|
# @api private
|
15
15
|
NO_DEFAULT_VALUE = begin
|
16
|
-
|
17
|
-
field(:f, String)
|
18
|
-
argument(:a, String)
|
19
|
-
end
|
16
|
+
t = Class.new(GraphQL::Schema::Object) do
|
17
|
+
field(:f, String) { _1.argument(:a, String) }
|
20
18
|
end
|
21
19
|
|
22
|
-
|
20
|
+
t.get_field("f").get_argument("a").default_value
|
23
21
|
end
|
24
22
|
|
25
23
|
# @api private
|
@@ -103,9 +101,8 @@ module GraphQL
|
|
103
101
|
@subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
|
104
102
|
raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
|
105
103
|
|
106
|
-
introspection_types = schema.introspection_system.types.keys
|
107
104
|
schema.types.each do |type_name, subgraph_type|
|
108
|
-
next if
|
105
|
+
next if subgraph_type.introspection?
|
109
106
|
|
110
107
|
if type_name == @query_name && subgraph_type != schema.query
|
111
108
|
raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
|
@@ -373,6 +370,7 @@ module GraphQL
|
|
373
370
|
deprecation_reason: merge_deprecations(type_name, fields_by_location, field_name: field_name),
|
374
371
|
type: Util.unwrap_non_null(type),
|
375
372
|
null: !type.non_null?,
|
373
|
+
connection: false,
|
376
374
|
camelize: false,
|
377
375
|
)
|
378
376
|
|
@@ -403,14 +401,6 @@ module GraphQL
|
|
403
401
|
next
|
404
402
|
end
|
405
403
|
|
406
|
-
# Getting double args sometimes... why?
|
407
|
-
begin
|
408
|
-
next if owner.arguments(GraphQL::Query::NullContext.instance, false).key?(argument_name)
|
409
|
-
rescue ArgumentError
|
410
|
-
# pre- graphql v2.4.5
|
411
|
-
next if owner.arguments.key?(argument_name)
|
412
|
-
end
|
413
|
-
|
414
404
|
kwargs = {}
|
415
405
|
default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
|
416
406
|
next if argument.default_value == NO_DEFAULT_VALUE
|
@@ -648,9 +638,8 @@ module GraphQL
|
|
648
638
|
writes = []
|
649
639
|
|
650
640
|
schemas.each do |schema|
|
651
|
-
introspection_types = schema.introspection_system.types.keys
|
652
641
|
schema.types.each_value do |type|
|
653
|
-
next if
|
642
|
+
next if type.introspection?
|
654
643
|
|
655
644
|
if type.kind.object? || type.kind.interface?
|
656
645
|
type.fields.each_value do |field|
|
@@ -14,7 +14,7 @@ module GraphQL::Stitching
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform!(raw)
|
17
|
-
@root_type = @
|
17
|
+
@root_type = @request.query.root_type_for_operation(@request.operation.operation_type)
|
18
18
|
resolve_object_scope(raw, @root_type, @request.operation.selections, @root_type.graphql_name)
|
19
19
|
end
|
20
20
|
|
@@ -31,8 +31,11 @@ module GraphQL::Stitching
|
|
31
31
|
when GraphQL::Language::Nodes::Field
|
32
32
|
field_name = node.alias || node.name
|
33
33
|
|
34
|
-
|
35
|
-
|
34
|
+
if @request.query.get_field(parent_type, node.name).introspection?
|
35
|
+
if node.name == TYPENAME && parent_type == @root_type
|
36
|
+
raw_object[field_name] = @root_type.graphql_name
|
37
|
+
end
|
38
|
+
next
|
36
39
|
end
|
37
40
|
|
38
41
|
node_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type
|
@@ -100,25 +103,10 @@ module GraphQL::Stitching
|
|
100
103
|
resolved_list
|
101
104
|
end
|
102
105
|
|
103
|
-
def introspection_field?(parent_type, node)
|
104
|
-
return false unless node.name.start_with?("__")
|
105
|
-
is_root = parent_type == @root_type
|
106
|
-
|
107
|
-
case node.name
|
108
|
-
when TYPENAME
|
109
|
-
yield(is_root)
|
110
|
-
true
|
111
|
-
when "__schema", "__type"
|
112
|
-
is_root && @request.operation.operation_type == "query"
|
113
|
-
else
|
114
|
-
false
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
106
|
def typename_in_type?(typename, type)
|
119
107
|
return true if type.graphql_name == typename
|
120
108
|
|
121
|
-
type.kind.abstract? && @
|
109
|
+
type.kind.abstract? && @request.query.possible_types(type).any? do |t|
|
122
110
|
t.graphql_name == typename
|
123
111
|
end
|
124
112
|
end
|
@@ -132,7 +132,7 @@ module GraphQL
|
|
132
132
|
elsif @upload_types.include?(ast_node.name)
|
133
133
|
files_by_path[path.dup] = value
|
134
134
|
else
|
135
|
-
type_def = request.
|
135
|
+
type_def = request.query.get_type(ast_node.name)
|
136
136
|
extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
|
137
137
|
end
|
138
138
|
end
|
@@ -106,7 +106,7 @@ module GraphQL
|
|
106
106
|
|
107
107
|
# A) Group all root selections by their preferred entrypoint locations.
|
108
108
|
def build_root_entrypoints
|
109
|
-
parent_type = @
|
109
|
+
parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
|
110
110
|
|
111
111
|
case @request.operation.operation_type
|
112
112
|
when QUERY_OP
|
@@ -331,7 +331,7 @@ module GraphQL
|
|
331
331
|
end
|
332
332
|
|
333
333
|
if expanded_selections
|
334
|
-
@
|
334
|
+
@request.query.possible_types(parent_type).each do |possible_type|
|
335
335
|
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
336
336
|
|
337
337
|
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
@@ -14,26 +14,21 @@ module GraphQL
|
|
14
14
|
# @return [Supergraph] supergraph instance that resolves the request.
|
15
15
|
attr_reader :supergraph
|
16
16
|
|
17
|
-
# @return [GraphQL::
|
18
|
-
attr_reader :
|
19
|
-
|
20
|
-
# @return [Hash] input variables for the request.
|
21
|
-
attr_reader :variables
|
22
|
-
|
23
|
-
# @return [String] operation name selected for the request.
|
24
|
-
attr_reader :operation_name
|
17
|
+
# @return [GraphQL::Query] query object defining the request.
|
18
|
+
attr_reader :query
|
25
19
|
|
26
20
|
# @return [Hash] contextual object passed through resolver flows.
|
27
21
|
attr_reader :context
|
28
22
|
|
29
23
|
# Creates a new supergraph request.
|
30
24
|
# @param supergraph [Supergraph] supergraph instance that resolves the request.
|
31
|
-
# @param
|
25
|
+
# @param source [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
|
32
26
|
# @param operation_name [String, nil] operation name selected for the request.
|
33
27
|
# @param variables [Hash, nil] input variables for the request.
|
34
28
|
# @param context [Hash, nil] a contextual object passed through resolver flows.
|
35
|
-
def initialize(supergraph,
|
29
|
+
def initialize(supergraph, source, operation_name: nil, variables: nil, context: nil)
|
36
30
|
@supergraph = supergraph
|
31
|
+
@prepared_document = nil
|
37
32
|
@string = nil
|
38
33
|
@digest = nil
|
39
34
|
@normalized_string = nil
|
@@ -44,29 +39,32 @@ module GraphQL
|
|
44
39
|
@fragment_definitions = nil
|
45
40
|
@plan = nil
|
46
41
|
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
params = {
|
43
|
+
operation_name: operation_name,
|
44
|
+
variables: variables,
|
45
|
+
context: context,
|
46
|
+
}
|
47
|
+
|
48
|
+
if source.is_a?(String)
|
49
|
+
@string = source
|
50
|
+
params[:query] = source
|
50
51
|
else
|
51
|
-
document
|
52
|
+
params[:document] = source
|
52
53
|
end
|
53
54
|
|
54
|
-
@
|
55
|
-
@variables = variables || {}
|
56
|
-
|
57
|
-
@query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
|
55
|
+
@query = GraphQL::Query.new(@supergraph.schema, **params)
|
58
56
|
@context = @query.context
|
59
57
|
@context[:request] = self
|
60
58
|
end
|
61
59
|
|
62
60
|
# @return [String] the original document string, or a print of the parsed AST document.
|
63
61
|
def string
|
64
|
-
@string || normalized_string
|
62
|
+
with_prepared_document { @string || normalized_string }
|
65
63
|
end
|
66
64
|
|
67
65
|
# @return [String] a print of the parsed AST document with consistent whitespace.
|
68
66
|
def normalized_string
|
69
|
-
@normalized_string ||=
|
67
|
+
@normalized_string ||= prepared_document.to_query_string
|
70
68
|
end
|
71
69
|
|
72
70
|
# @return [String] a digest of the original document string. Generally faster but less consistent.
|
@@ -81,43 +79,48 @@ module GraphQL
|
|
81
79
|
|
82
80
|
# @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
|
83
81
|
def operation
|
84
|
-
@operation ||=
|
85
|
-
|
82
|
+
@operation ||= with_prepared_document do
|
83
|
+
selected_op = @query.selected_operation
|
84
|
+
raise GraphQL::ExecutionError, "No operation selected" unless selected_op
|
85
|
+
|
86
|
+
@prepared_document.definitions.find do |d|
|
86
87
|
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
87
|
-
@operation_name ? d.name == @operation_name : true
|
88
|
-
end
|
89
88
|
|
90
|
-
|
91
|
-
raise GraphQL::ExecutionError, "Invalid root operation for given name and operation type."
|
92
|
-
elsif operation_defs.length > 1
|
93
|
-
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
89
|
+
selected_op.name.nil? || d.name == selected_op.name
|
94
90
|
end
|
91
|
+
end
|
92
|
+
end
|
95
93
|
|
96
|
-
|
94
|
+
def operation_name
|
95
|
+
operation.name
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
|
99
|
+
def operation_directives
|
100
|
+
@operation_directives ||= if operation.directives.any?
|
101
|
+
printer = GraphQL::Language::Printer.new
|
102
|
+
operation.directives.map { printer.print(_1) }.join(" ")
|
97
103
|
end
|
98
104
|
end
|
99
105
|
|
100
106
|
# @return [Boolean] true if operation type is a query
|
101
107
|
def query?
|
102
|
-
|
108
|
+
@query.query?
|
103
109
|
end
|
104
110
|
|
105
111
|
# @return [Boolean] true if operation type is a mutation
|
106
112
|
def mutation?
|
107
|
-
|
113
|
+
@query.mutation?
|
108
114
|
end
|
109
115
|
|
110
116
|
# @return [Boolean] true if operation type is a subscription
|
111
117
|
def subscription?
|
112
|
-
|
118
|
+
@query.subscription?
|
113
119
|
end
|
114
120
|
|
115
|
-
# @return [String]
|
116
|
-
def
|
117
|
-
@
|
118
|
-
printer = GraphQL::Language::Printer.new
|
119
|
-
operation.directives.map { printer.print(_1) }.join(" ")
|
120
|
-
end
|
121
|
+
# @return [Hash<String, Any>] provided variables hash filled in with default values from definitions
|
122
|
+
def variables
|
123
|
+
@variables || with_prepared_document { @variables }
|
121
124
|
end
|
122
125
|
|
123
126
|
# @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
|
@@ -129,7 +132,7 @@ module GraphQL
|
|
129
132
|
|
130
133
|
# @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
|
131
134
|
def fragment_definitions
|
132
|
-
@fragment_definitions ||=
|
135
|
+
@fragment_definitions ||= prepared_document.definitions.each_with_object({}) do |d, memo|
|
133
136
|
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
134
137
|
end
|
135
138
|
end
|
@@ -137,26 +140,7 @@ module GraphQL
|
|
137
140
|
# Validates the request using the combined supergraph schema.
|
138
141
|
# @return [Array<GraphQL::ExecutionError>] an array of static validation errors
|
139
142
|
def validate
|
140
|
-
|
141
|
-
result[:errors]
|
142
|
-
end
|
143
|
-
|
144
|
-
# Prepares the request for stitching by inserting variable defaults and applying @skip/@include conditionals.
|
145
|
-
def prepare!
|
146
|
-
operation.variables.each do |v|
|
147
|
-
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
|
148
|
-
end
|
149
|
-
|
150
|
-
if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
|
151
|
-
SkipInclude.render(@document, @variables) do |modified_ast|
|
152
|
-
@document = modified_ast
|
153
|
-
@string = @normalized_string = nil
|
154
|
-
@digest = @normalized_digest = nil
|
155
|
-
@operation = @operation_directives = @variable_definitions = @plan = nil
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
self
|
143
|
+
@query.static_errors
|
160
144
|
end
|
161
145
|
|
162
146
|
# Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
|
@@ -191,6 +175,27 @@ module GraphQL
|
|
191
175
|
|
192
176
|
private
|
193
177
|
|
178
|
+
# Prepares the request for stitching by applying @skip/@include conditionals.
|
179
|
+
def prepared_document
|
180
|
+
@prepared_document || with_prepared_document { @prepared_document }
|
181
|
+
end
|
182
|
+
|
183
|
+
def with_prepared_document
|
184
|
+
unless @prepared_document
|
185
|
+
@variables = @query.variables.to_h
|
186
|
+
|
187
|
+
@prepared_document = if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
|
188
|
+
changed = false
|
189
|
+
doc = SkipInclude.render(@query.document, @variables) { changed = true }
|
190
|
+
@string = @normalized_string = doc.to_query_string if changed
|
191
|
+
doc
|
192
|
+
else
|
193
|
+
@query.document
|
194
|
+
end
|
195
|
+
end
|
196
|
+
yield
|
197
|
+
end
|
198
|
+
|
194
199
|
# Adds a handler into context for enriching subscription updates with stitched data
|
195
200
|
def add_subscription_update_handler
|
196
201
|
request = self
|
@@ -17,10 +17,9 @@ module GraphQL::Stitching
|
|
17
17
|
field_map = {}
|
18
18
|
resolver_map = {}
|
19
19
|
possible_locations = {}
|
20
|
-
introspection_types = schema.introspection_system.types.keys
|
21
20
|
|
22
21
|
schema.types.each do |type_name, type|
|
23
|
-
next if
|
22
|
+
next if type.introspection?
|
24
23
|
|
25
24
|
# Collect/build key definitions for each type
|
26
25
|
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
|
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.6.1
|
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-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -155,6 +155,7 @@ files:
|
|
155
155
|
- gemfiles/graphql_2.1.0.gemfile
|
156
156
|
- gemfiles/graphql_2.2.0.gemfile
|
157
157
|
- gemfiles/graphql_2.3.0.gemfile
|
158
|
+
- gemfiles/graphql_2.4.0.gemfile
|
158
159
|
- graphql-stitching.gemspec
|
159
160
|
- lib/graphql/stitching.rb
|
160
161
|
- lib/graphql/stitching/client.rb
|