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
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
|