graphql-stitching 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ce8bb1075d536f01aa63487df3e1686bc0c2899b58ca14e95383caef38812e2
|
4
|
+
data.tar.gz: ae39e685bdb31cbf3962216cfce263b2970aa2aeac6be1901a7397cee6eb1656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e718527102969773accf28b2323cbde1d2c467339c44bb876144ae5387a7aaec64143b35b6dfff21401fb087c64ed8742f79cda78e743518ba045871c320f51f
|
7
|
+
data.tar.gz: 74bc97f475b61ea51dd8ce9e42a1ff2783d0c1717a38ff812e256962d64b97ef0ccc092de11e529a22e274d8ad5cf77af9fd7ed2fd680e20e0c058534942a5d6
|
data/README.md
CHANGED
@@ -9,7 +9,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
9
9
|
- Multiple keys per merged type.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
|
-
- Type merging via federation `_entities` protocol.
|
12
|
+
- Type merging via arbitrary queries or federation `_entities` protocol.
|
13
13
|
|
14
14
|
**NOT Supported:**
|
15
15
|
- Computed fields (ie: federation-style `@requires`).
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
Boundary = Struct.new(
|
6
|
+
:location,
|
7
|
+
:type_name,
|
8
|
+
:key,
|
9
|
+
:field,
|
10
|
+
:arg,
|
11
|
+
:list,
|
12
|
+
:federation,
|
13
|
+
keyword_init: true
|
14
|
+
) do
|
15
|
+
def as_json
|
16
|
+
{
|
17
|
+
location: location,
|
18
|
+
type_name: type_name,
|
19
|
+
key: key,
|
20
|
+
field: field,
|
21
|
+
arg: arg,
|
22
|
+
list: list,
|
23
|
+
federation: federation,
|
24
|
+
}.tap(&:compact!)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -41,7 +41,7 @@ module GraphQL
|
|
41
41
|
GraphQL::Stitching::Planner.new(
|
42
42
|
supergraph: @supergraph,
|
43
43
|
request: request,
|
44
|
-
).perform
|
44
|
+
).perform
|
45
45
|
end
|
46
46
|
|
47
47
|
GraphQL::Stitching::Executor.new(
|
@@ -76,16 +76,16 @@ module GraphQL
|
|
76
76
|
def fetch_plan(request)
|
77
77
|
if @on_cache_read
|
78
78
|
cached_plan = @on_cache_read.call(request.digest, request.context)
|
79
|
-
return JSON.parse(cached_plan) if cached_plan
|
79
|
+
return GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan)) if cached_plan
|
80
80
|
end
|
81
81
|
|
82
|
-
|
82
|
+
plan = yield
|
83
83
|
|
84
84
|
if @on_cache_write
|
85
|
-
@on_cache_write.call(request.digest, JSON.generate(
|
85
|
+
@on_cache_write.call(request.digest, JSON.generate(plan.as_json), request.context)
|
86
86
|
end
|
87
87
|
|
88
|
-
|
88
|
+
plan
|
89
89
|
end
|
90
90
|
|
91
91
|
def error_result(errors)
|
@@ -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
|
36
|
-
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary
|
37
|
-
"found in #{boundary
|
35
|
+
if memo.dig(boundary.location, boundary.key)
|
36
|
+
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary.key}` "\
|
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
|
-
memo[boundary
|
41
|
-
memo[boundary
|
40
|
+
memo[boundary.location] ||= {}
|
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
|
@@ -32,12 +32,12 @@ module GraphQL
|
|
32
32
|
interface_type_structure.each_with_index do |interface_struct, index|
|
33
33
|
possible_struct = possible_type_structure[index]
|
34
34
|
|
35
|
-
if possible_struct
|
35
|
+
if possible_struct.name != interface_struct.name
|
36
36
|
raise Composer::ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
|
37
37
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
38
38
|
end
|
39
39
|
|
40
|
-
if possible_struct
|
40
|
+
if possible_struct.null? && interface_struct.non_null?
|
41
41
|
raise Composer::ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
|
42
42
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
43
43
|
end
|
@@ -430,21 +430,21 @@ module GraphQL
|
|
430
430
|
raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
|
431
431
|
end
|
432
432
|
|
433
|
-
if alt_structure.last
|
433
|
+
if alt_structure.last.name != basis_structure.last.name
|
434
434
|
raise ComposerError, "Cannot compose mixed types at `#{path}`."
|
435
435
|
end
|
436
436
|
end
|
437
437
|
|
438
438
|
type = GraphQL::Schema::BUILT_IN_TYPES.fetch(
|
439
|
-
basis_structure.last
|
440
|
-
build_type_binding(basis_structure.last
|
439
|
+
basis_structure.last.name,
|
440
|
+
build_type_binding(basis_structure.last.name)
|
441
441
|
)
|
442
442
|
|
443
443
|
basis_structure.reverse!.each_with_index do |basis, index|
|
444
444
|
rev_index = basis_structure.length - index - 1
|
445
|
-
non_null = alt_structures.each_with_object([
|
445
|
+
non_null = alt_structures.each_with_object([basis.non_null?]) { |s, m| m << s[rev_index].non_null? }
|
446
446
|
|
447
|
-
type = type.to_list_type if basis
|
447
|
+
type = type.to_list_type if basis.list?
|
448
448
|
type = type.to_non_null_type if argument_name ? non_null.any? : non_null.all?
|
449
449
|
end
|
450
450
|
|
@@ -509,15 +509,15 @@ module GraphQL
|
|
509
509
|
end
|
510
510
|
|
511
511
|
@boundary_map[impl_type_name] ||= []
|
512
|
-
@boundary_map[impl_type_name] <<
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
512
|
+
@boundary_map[impl_type_name] << Boundary.new(
|
513
|
+
location: location,
|
514
|
+
type_name: impl_type_name,
|
515
|
+
key: key_selections[0].name,
|
516
|
+
field: field_candidate.name,
|
517
|
+
arg: argument_name,
|
518
|
+
list: boundary_structure.first.list?,
|
519
|
+
federation: kwargs[:federation],
|
520
|
+
)
|
521
521
|
end
|
522
522
|
end
|
523
523
|
end
|
@@ -10,13 +10,13 @@ module GraphQL
|
|
10
10
|
|
11
11
|
def fetch(ops)
|
12
12
|
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
|
13
|
-
origin_set = op
|
13
|
+
origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
|
14
14
|
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
15
15
|
end
|
16
16
|
|
17
|
-
if op
|
17
|
+
if op.if_type
|
18
18
|
# operations planned around unused fragment conditions should not trigger requests
|
19
|
-
origin_set.select! { _1[
|
19
|
+
origin_set.select! { _1[SelectionHint.typename_node.alias] == op.if_type }
|
20
20
|
end
|
21
21
|
|
22
22
|
memo[op] = origin_set if origin_set.any?
|
@@ -34,7 +34,7 @@ module GraphQL
|
|
34
34
|
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
|
35
35
|
end
|
36
36
|
|
37
|
-
ops.map { origin_sets_by_operation[_1] ? _1
|
37
|
+
ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
|
38
38
|
end
|
39
39
|
|
40
40
|
# Builds batched boundary queries
|
@@ -47,21 +47,21 @@ module GraphQL
|
|
47
47
|
def build_document(origin_sets_by_operation, operation_name = nil)
|
48
48
|
variable_defs = {}
|
49
49
|
query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
|
50
|
-
variable_defs.merge!(op
|
51
|
-
boundary = op
|
50
|
+
variable_defs.merge!(op.variables)
|
51
|
+
boundary = op.boundary
|
52
52
|
|
53
|
-
if boundary
|
53
|
+
if boundary.list
|
54
54
|
input = origin_set.each_with_index.reduce(String.new) do |memo, (origin_obj, index)|
|
55
55
|
memo << "," if index > 0
|
56
|
-
memo << build_key(boundary
|
56
|
+
memo << build_key(boundary.key, origin_obj, federation: boundary.federation)
|
57
57
|
memo
|
58
58
|
end
|
59
59
|
|
60
|
-
"_#{batch_index}_result: #{boundary
|
60
|
+
"_#{batch_index}_result: #{boundary.field}(#{boundary.arg}:[#{input}]) #{op.selections}"
|
61
61
|
else
|
62
62
|
origin_set.map.with_index do |origin_obj, index|
|
63
|
-
input = build_key(boundary
|
64
|
-
"_#{batch_index}_#{index}_result: #{boundary
|
63
|
+
input = build_key(boundary.key, origin_obj, federation: boundary.federation)
|
64
|
+
"_#{batch_index}_#{index}_result: #{boundary.field}(#{boundary.arg}:#{input}) #{op.selections}"
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
@@ -71,7 +71,7 @@ module GraphQL
|
|
71
71
|
if operation_name
|
72
72
|
doc << " #{operation_name}"
|
73
73
|
origin_sets_by_operation.each_key do |op|
|
74
|
-
doc << "_#{op
|
74
|
+
doc << "_#{op.step}"
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -86,9 +86,9 @@ module GraphQL
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def build_key(key, origin_obj, federation: false)
|
89
|
-
key_value = JSON.generate(origin_obj[
|
89
|
+
key_value = JSON.generate(origin_obj[SelectionHint.key(key)])
|
90
90
|
if federation
|
91
|
-
"{ __typename: \"#{origin_obj[
|
91
|
+
"{ __typename: \"#{origin_obj[SelectionHint.typename_node.alias]}\", #{key}: #{key_value} }"
|
92
92
|
else
|
93
93
|
key_value
|
94
94
|
end
|
@@ -12,8 +12,8 @@ module GraphQL
|
|
12
12
|
op = ops.first # There should only ever be one per location at a time
|
13
13
|
|
14
14
|
query_document = build_document(op, @executor.request.operation_name)
|
15
|
-
query_variables = @executor.request.variables.slice(*op
|
16
|
-
result = @executor.supergraph.execute_at_location(op
|
15
|
+
query_variables = @executor.request.variables.slice(*op.variables.keys)
|
16
|
+
result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request.context)
|
17
17
|
@executor.query_count += 1
|
18
18
|
|
19
19
|
@executor.data.merge!(result["data"]) if result["data"]
|
@@ -22,25 +22,25 @@ module GraphQL
|
|
22
22
|
@executor.errors.concat(result["errors"])
|
23
23
|
end
|
24
24
|
|
25
|
-
ops.map
|
25
|
+
ops.map(&:step)
|
26
26
|
end
|
27
27
|
|
28
28
|
# Builds root source documents
|
29
29
|
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
30
30
|
def build_document(op, operation_name = nil)
|
31
31
|
doc = String.new
|
32
|
-
doc << op
|
32
|
+
doc << op.operation_type
|
33
33
|
|
34
34
|
if operation_name
|
35
|
-
doc << " #{operation_name}_#{op
|
35
|
+
doc << " #{operation_name}_#{op.step}"
|
36
36
|
end
|
37
37
|
|
38
|
-
if op
|
39
|
-
variable_defs = op
|
38
|
+
if op.variables.any?
|
39
|
+
variable_defs = op.variables.map { |k, v| "$#{k}:#{v}" }.join(",")
|
40
40
|
doc << "(#{variable_defs})"
|
41
41
|
end
|
42
42
|
|
43
|
-
doc << op
|
43
|
+
doc << op.selections
|
44
44
|
doc
|
45
45
|
end
|
46
46
|
end
|
@@ -11,7 +11,7 @@ module GraphQL
|
|
11
11
|
def initialize(supergraph:, request:, plan:, nonblocking: false)
|
12
12
|
@supergraph = supergraph
|
13
13
|
@request = request
|
14
|
-
@queue = plan
|
14
|
+
@queue = plan.ops
|
15
15
|
@data = {}
|
16
16
|
@errors = []
|
17
17
|
@query_count = 0
|
@@ -47,8 +47,8 @@ module GraphQL
|
|
47
47
|
|
48
48
|
@dataloader.append_job do
|
49
49
|
tasks = @queue
|
50
|
-
.select { next_ordinals.include?(_1
|
51
|
-
.group_by { [_1
|
50
|
+
.select { next_ordinals.include?(_1.after) }
|
51
|
+
.group_by { [_1.location, _1.boundary.nil?] }
|
52
52
|
.map do |(location, root_source), ops|
|
53
53
|
if root_source
|
54
54
|
@dataloader.with(RootSource, self, location).request_all(ops)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Plan
|
6
|
+
Op = Struct.new(
|
7
|
+
:step,
|
8
|
+
:after,
|
9
|
+
:location,
|
10
|
+
:operation_type,
|
11
|
+
:selections,
|
12
|
+
:variables,
|
13
|
+
:path,
|
14
|
+
:if_type,
|
15
|
+
:boundary,
|
16
|
+
keyword_init: true
|
17
|
+
) do
|
18
|
+
def as_json
|
19
|
+
{
|
20
|
+
step: step,
|
21
|
+
after: after,
|
22
|
+
location: location,
|
23
|
+
operation_type: operation_type,
|
24
|
+
selections: selections,
|
25
|
+
variables: variables,
|
26
|
+
path: path,
|
27
|
+
if_type: if_type,
|
28
|
+
boundary: boundary&.as_json
|
29
|
+
}.tap(&:compact!)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def from_json(json)
|
35
|
+
ops = json["ops"]
|
36
|
+
ops = ops.map do |op|
|
37
|
+
boundary = op["boundary"]
|
38
|
+
Op.new(
|
39
|
+
step: op["step"],
|
40
|
+
after: op["after"],
|
41
|
+
location: op["location"],
|
42
|
+
operation_type: op["operation_type"],
|
43
|
+
selections: op["selections"],
|
44
|
+
variables: op["variables"],
|
45
|
+
path: op["path"],
|
46
|
+
if_type: op["if_type"],
|
47
|
+
boundary: boundary ? GraphQL::Stitching::Boundary.new(**boundary) : nil,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
new(ops: ops)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :ops
|
55
|
+
|
56
|
+
def initialize(ops: [])
|
57
|
+
@ops = ops
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json
|
61
|
+
{ ops: @ops.map(&:as_json) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|