graphql-stitching 1.0.0 → 1.0.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/README.md +1 -1
- data/lib/graphql/stitching/boundary.rb +28 -0
- data/lib/graphql/stitching/client.rb +5 -5
- data/lib/graphql/stitching/composer/validate_boundaries.rb +6 -6
- data/lib/graphql/stitching/composer/validate_interfaces.rb +2 -2
- data/lib/graphql/stitching/composer.rb +14 -14
- data/lib/graphql/stitching/executor/boundary_source.rb +14 -14
- data/lib/graphql/stitching/executor/root_source.rb +8 -8
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +65 -0
- data/lib/graphql/stitching/planner.rb +70 -89
- data/lib/graphql/stitching/planner_step.rb +63 -0
- data/lib/graphql/stitching/request.rb +2 -51
- data/lib/graphql/stitching/selection_hint.rb +29 -0
- data/lib/graphql/stitching/shaper.rb +2 -2
- data/lib/graphql/stitching/skip_include.rb +81 -0
- data/lib/graphql/stitching/supergraph.rb +29 -23
- data/lib/graphql/stitching/util.rb +49 -38
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -2
- metadata +7 -3
- data/lib/graphql/stitching/planner_operation.rb +0 -63
@@ -21,10 +21,14 @@ module GraphQL
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
boundaries = delegation_map["boundaries"].map do |k, b|
|
25
|
+
[k, b.map { Boundary.new(**_1) }]
|
26
|
+
end
|
27
|
+
|
24
28
|
new(
|
25
29
|
schema: schema,
|
26
30
|
fields: delegation_map["fields"],
|
27
|
-
boundaries:
|
31
|
+
boundaries: boundaries.to_h,
|
28
32
|
executables: executables,
|
29
33
|
)
|
30
34
|
end
|
@@ -68,7 +72,7 @@ module GraphQL
|
|
68
72
|
return GraphQL::Schema::Printer.print_schema(@schema), {
|
69
73
|
"locations" => locations,
|
70
74
|
"fields" => fields,
|
71
|
-
"boundaries" => @boundaries,
|
75
|
+
"boundaries" => @boundaries.map { |k, b| [k, b.map(&:as_json)] }.to_h,
|
72
76
|
}
|
73
77
|
end
|
74
78
|
|
@@ -144,7 +148,7 @@ module GraphQL
|
|
144
148
|
# ("Type") => ["id", ...]
|
145
149
|
def possible_keys_for_type(type_name)
|
146
150
|
@possible_keys_by_type[type_name] ||= begin
|
147
|
-
@boundaries[type_name].map
|
151
|
+
@boundaries[type_name].map(&:key).tap(&:uniq!)
|
148
152
|
end
|
149
153
|
end
|
150
154
|
|
@@ -162,74 +166,76 @@ module GraphQL
|
|
162
166
|
# used to connect a partial type across locations via boundary queries
|
163
167
|
def route_type_to_locations(type_name, start_location, goal_locations)
|
164
168
|
if possible_keys_for_type(type_name).length > 1
|
165
|
-
# multiple keys use an
|
169
|
+
# multiple keys use an A* search to traverse intermediary locations
|
166
170
|
return route_type_to_locations_via_search(type_name, start_location, goal_locations)
|
167
171
|
end
|
168
172
|
|
169
173
|
# types with a single key attribute must all be within a single hop of each other,
|
170
174
|
# so can use a simple match to collect boundaries for the goal locations.
|
171
175
|
@boundaries[type_name].each_with_object({}) do |boundary, memo|
|
172
|
-
if goal_locations.include?(boundary
|
173
|
-
memo[boundary
|
176
|
+
if goal_locations.include?(boundary.location)
|
177
|
+
memo[boundary.location] = [boundary]
|
174
178
|
end
|
175
179
|
end
|
176
180
|
end
|
177
181
|
|
178
182
|
private
|
179
183
|
|
180
|
-
|
184
|
+
PathNode = Struct.new(:location, :key, :cost, :boundary, keyword_init: true)
|
185
|
+
|
186
|
+
# tunes A* search to favor paths with fewest joining locations, ie:
|
181
187
|
# favor longer paths through target locations over shorter paths with additional locations.
|
182
188
|
def route_type_to_locations_via_search(type_name, start_location, goal_locations)
|
183
189
|
results = {}
|
184
190
|
costs = {}
|
185
191
|
|
186
192
|
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
187
|
-
[
|
193
|
+
[PathNode.new(location: start_location, key: possible_key, cost: 0)]
|
188
194
|
end
|
189
195
|
|
190
196
|
while paths.any?
|
191
197
|
path = paths.pop
|
192
|
-
current_location = path.last
|
193
|
-
current_key = path.last
|
194
|
-
current_cost = path.last
|
198
|
+
current_location = path.last.location
|
199
|
+
current_key = path.last.key
|
200
|
+
current_cost = path.last.cost
|
195
201
|
|
196
202
|
@boundaries[type_name].each do |boundary|
|
197
|
-
forward_location = boundary
|
198
|
-
next if current_key != boundary
|
199
|
-
next if path.any? { _1
|
203
|
+
forward_location = boundary.location
|
204
|
+
next if current_key != boundary.key
|
205
|
+
next if path.any? { _1.location == forward_location }
|
200
206
|
|
201
207
|
best_cost = costs[forward_location] || Float::INFINITY
|
202
208
|
next if best_cost < current_cost
|
203
209
|
|
204
210
|
path.pop
|
205
|
-
path <<
|
211
|
+
path << PathNode.new(
|
206
212
|
location: current_location,
|
207
213
|
key: current_key,
|
208
214
|
cost: current_cost,
|
209
215
|
boundary: boundary,
|
210
|
-
|
216
|
+
)
|
211
217
|
|
212
218
|
if goal_locations.include?(forward_location)
|
213
219
|
current_result = results[forward_location]
|
214
220
|
if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
|
215
|
-
results[forward_location] = path.map
|
221
|
+
results[forward_location] = path.map(&:boundary)
|
216
222
|
end
|
217
223
|
else
|
218
|
-
path.last
|
224
|
+
path.last.cost += 1
|
219
225
|
end
|
220
226
|
|
221
|
-
forward_cost = path.last
|
227
|
+
forward_cost = path.last.cost
|
222
228
|
costs[forward_location] = forward_cost if forward_cost < best_cost
|
223
229
|
|
224
230
|
possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
|
225
|
-
paths << [*path,
|
231
|
+
paths << [*path, PathNode.new(location: forward_location, key: possible_key, cost: forward_cost)]
|
226
232
|
end
|
227
233
|
end
|
228
234
|
|
229
235
|
paths.sort! do |a, b|
|
230
|
-
cost_diff =
|
231
|
-
cost_diff.zero? ?
|
232
|
-
end
|
236
|
+
cost_diff = b.last.cost - a.last.cost
|
237
|
+
cost_diff.zero? ? b.length - a.length : cost_diff
|
238
|
+
end
|
233
239
|
end
|
234
240
|
|
235
241
|
results
|
@@ -3,54 +3,65 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
5
|
class Util
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
|
7
|
+
alias_method :list?, :list
|
8
|
+
alias_method :null?, :null
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
type
|
10
|
+
def non_null?
|
11
|
+
!null
|
12
|
+
end
|
15
13
|
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
class << self
|
16
|
+
# specifies if a type is a primitive leaf value
|
17
|
+
def is_leaf_type?(type)
|
18
|
+
type.kind.scalar? || type.kind.enum?
|
19
|
+
end
|
20
|
+
|
21
|
+
# strips non-null wrappers from a type
|
22
|
+
def unwrap_non_null(type)
|
23
|
+
type = type.of_type while type.non_null?
|
24
|
+
type
|
25
|
+
end
|
26
|
+
|
27
|
+
# builds a single-dimensional representation of a wrapped type structure
|
28
|
+
def flatten_type_structure(type)
|
29
|
+
structure = []
|
30
|
+
|
31
|
+
while type.list?
|
32
|
+
structure << TypeStructure.new(
|
33
|
+
list: true,
|
34
|
+
null: !type.non_null?,
|
35
|
+
name: nil,
|
36
|
+
)
|
37
|
+
|
38
|
+
type = unwrap_non_null(type).of_type
|
39
|
+
end
|
20
40
|
|
21
|
-
|
22
|
-
|
23
|
-
list: true,
|
41
|
+
structure << TypeStructure.new(
|
42
|
+
list: false,
|
24
43
|
null: !type.non_null?,
|
25
|
-
name:
|
26
|
-
|
44
|
+
name: type.unwrap.graphql_name,
|
45
|
+
)
|
27
46
|
|
28
|
-
|
47
|
+
structure
|
29
48
|
end
|
30
49
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
structure
|
38
|
-
end
|
50
|
+
# expands interfaces and unions to an array of their memberships
|
51
|
+
# like `schema.possible_types`, but includes child interfaces
|
52
|
+
def expand_abstract_type(schema, parent_type)
|
53
|
+
return [] unless parent_type.kind.abstract?
|
54
|
+
return parent_type.possible_types if parent_type.kind.union?
|
39
55
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
49
|
-
next unless type.interfaces.include?(parent_type)
|
50
|
-
result << type
|
51
|
-
result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
|
56
|
+
result = []
|
57
|
+
schema.types.values.each do |type|
|
58
|
+
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
59
|
+
next unless type.interfaces.include?(parent_type)
|
60
|
+
result << type
|
61
|
+
result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
|
62
|
+
end
|
63
|
+
result.uniq
|
52
64
|
end
|
53
|
-
result.uniq
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
data/lib/graphql/stitching.rb
CHANGED
@@ -10,7 +10,6 @@ module GraphQL
|
|
10
10
|
class StitchingError < StandardError; end
|
11
11
|
|
12
12
|
class << self
|
13
|
-
|
14
13
|
def stitch_directive
|
15
14
|
@stitch_directive ||= "stitch"
|
16
15
|
end
|
@@ -25,13 +24,17 @@ module GraphQL
|
|
25
24
|
end
|
26
25
|
|
27
26
|
require_relative "stitching/supergraph"
|
27
|
+
require_relative "stitching/boundary"
|
28
28
|
require_relative "stitching/client"
|
29
29
|
require_relative "stitching/composer"
|
30
30
|
require_relative "stitching/executor"
|
31
31
|
require_relative "stitching/http_executable"
|
32
|
-
require_relative "stitching/
|
32
|
+
require_relative "stitching/plan"
|
33
|
+
require_relative "stitching/planner_step"
|
33
34
|
require_relative "stitching/planner"
|
34
35
|
require_relative "stitching/request"
|
36
|
+
require_relative "stitching/selection_hint"
|
35
37
|
require_relative "stitching/shaper"
|
38
|
+
require_relative "stitching/skip_include"
|
36
39
|
require_relative "stitching/util"
|
37
40
|
require_relative "stitching/version"
|
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.0.
|
4
|
+
version: 1.0.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: 2023-
|
11
|
+
date: 2023-10-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- gemfiles/graphql_1.13.9.gemfile
|
98
98
|
- graphql-stitching.gemspec
|
99
99
|
- lib/graphql/stitching.rb
|
100
|
+
- lib/graphql/stitching/boundary.rb
|
100
101
|
- lib/graphql/stitching/client.rb
|
101
102
|
- lib/graphql/stitching/composer.rb
|
102
103
|
- lib/graphql/stitching/composer/base_validator.rb
|
@@ -106,10 +107,13 @@ files:
|
|
106
107
|
- lib/graphql/stitching/executor/boundary_source.rb
|
107
108
|
- lib/graphql/stitching/executor/root_source.rb
|
108
109
|
- lib/graphql/stitching/http_executable.rb
|
110
|
+
- lib/graphql/stitching/plan.rb
|
109
111
|
- lib/graphql/stitching/planner.rb
|
110
|
-
- lib/graphql/stitching/
|
112
|
+
- lib/graphql/stitching/planner_step.rb
|
111
113
|
- lib/graphql/stitching/request.rb
|
114
|
+
- lib/graphql/stitching/selection_hint.rb
|
112
115
|
- lib/graphql/stitching/shaper.rb
|
116
|
+
- lib/graphql/stitching/skip_include.rb
|
113
117
|
- lib/graphql/stitching/supergraph.rb
|
114
118
|
- lib/graphql/stitching/util.rb
|
115
119
|
- lib/graphql/stitching/version.rb
|
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Stitching
|
5
|
-
class PlannerOperation
|
6
|
-
LANGUAGE_PRINTER = GraphQL::Language::Printer.new
|
7
|
-
|
8
|
-
attr_reader :order, :location, :parent_type, :if_type, :operation_type, :path
|
9
|
-
attr_accessor :after, :selections, :variables, :boundary
|
10
|
-
|
11
|
-
def initialize(
|
12
|
-
location:,
|
13
|
-
parent_type:,
|
14
|
-
order:,
|
15
|
-
after: nil,
|
16
|
-
operation_type: "query",
|
17
|
-
selections: [],
|
18
|
-
variables: [],
|
19
|
-
path: [],
|
20
|
-
if_type: nil,
|
21
|
-
boundary: nil
|
22
|
-
)
|
23
|
-
@location = location
|
24
|
-
@parent_type = parent_type
|
25
|
-
@order = order
|
26
|
-
@after = after
|
27
|
-
@operation_type = operation_type
|
28
|
-
@selections = selections
|
29
|
-
@variables = variables
|
30
|
-
@path = path
|
31
|
-
@if_type = if_type
|
32
|
-
@boundary = boundary
|
33
|
-
end
|
34
|
-
|
35
|
-
def selection_set
|
36
|
-
op = GraphQL::Language::Nodes::OperationDefinition.new(selections: @selections)
|
37
|
-
LANGUAGE_PRINTER.print(op).gsub!(/\s+/, " ").strip!
|
38
|
-
end
|
39
|
-
|
40
|
-
def variable_set
|
41
|
-
@variables.each_with_object({}) do |(variable_name, value_type), memo|
|
42
|
-
memo[variable_name] = LANGUAGE_PRINTER.print(value_type)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def to_h
|
47
|
-
data = {
|
48
|
-
"order" => @order,
|
49
|
-
"after" => @after,
|
50
|
-
"location" => @location,
|
51
|
-
"operation_type" => @operation_type,
|
52
|
-
"selections" => selection_set,
|
53
|
-
"variables" => variable_set,
|
54
|
-
"path" => @path,
|
55
|
-
}
|
56
|
-
|
57
|
-
data["if_type"] = @if_type if @if_type
|
58
|
-
data["boundary"] = @boundary if @boundary
|
59
|
-
data
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|