graphql-stitching 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -17
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +31 -12
- data/docs/images/library.png +0 -0
- data/docs/planner.md +11 -7
- data/docs/request.md +50 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +1 -1
- data/lib/graphql/stitching/composer.rb +98 -16
- data/lib/graphql/stitching/executor.rb +88 -45
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +204 -151
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +7 -7
- data/lib/graphql/stitching/supergraph.rb +44 -10
- data/lib/graphql/stitching/util.rb +28 -35
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +7 -1
- metadata +6 -6
- data/docs/document.md +0 -15
- data/lib/graphql/stitching/document.rb +0 -59
@@ -4,14 +4,14 @@
|
|
4
4
|
module GraphQL
|
5
5
|
module Stitching
|
6
6
|
class Shaper
|
7
|
-
def initialize(schema:,
|
7
|
+
def initialize(schema:, request:)
|
8
8
|
@schema = schema
|
9
|
-
@
|
9
|
+
@request = request
|
10
10
|
end
|
11
11
|
|
12
12
|
def perform!(raw)
|
13
|
-
|
14
|
-
raw
|
13
|
+
root_type = @schema.public_send(@request.operation.operation_type)
|
14
|
+
resolve_object_scope(raw, root_type, @request.operation.selections)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
@@ -29,7 +29,7 @@ module GraphQL
|
|
29
29
|
|
30
30
|
field_name = node.alias || node.name
|
31
31
|
node_type = parent_type.fields[node.name].type
|
32
|
-
named_type = Util.
|
32
|
+
named_type = Util.named_type_for_field_node(@schema, parent_type, node)
|
33
33
|
|
34
34
|
raw_object[field_name] = if node_type.list?
|
35
35
|
resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
|
@@ -48,7 +48,7 @@ module GraphQL
|
|
48
48
|
return nil if result.nil?
|
49
49
|
|
50
50
|
when GraphQL::Language::Nodes::FragmentSpread
|
51
|
-
fragment = @
|
51
|
+
fragment = @request.fragment_definitions[node.name]
|
52
52
|
fragment_type = @schema.types[fragment.type.name]
|
53
53
|
next unless typename == fragment_type.graphql_name
|
54
54
|
|
@@ -67,7 +67,7 @@ module GraphQL
|
|
67
67
|
return nil if raw_list.nil?
|
68
68
|
|
69
69
|
next_node_type = Util.unwrap_non_null(current_node_type).of_type
|
70
|
-
named_type =
|
70
|
+
named_type = next_node_type.unwrap
|
71
71
|
contains_null = false
|
72
72
|
|
73
73
|
resolved_list = raw_list.map! do |raw_list_element|
|
@@ -29,6 +29,7 @@ module GraphQL
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
@possible_keys_by_type = {}
|
32
33
|
@possible_keys_by_type_and_location = {}
|
33
34
|
@executables = { LOCATION => @schema }.merge!(executables)
|
34
35
|
end
|
@@ -57,31 +58,33 @@ module GraphQL
|
|
57
58
|
def assign_executable(location, executable = nil, &block)
|
58
59
|
executable ||= block
|
59
60
|
unless executable.is_a?(Class) && executable <= GraphQL::Schema
|
60
|
-
raise "A client or block handler must be provided." unless executable
|
61
|
-
raise "A client must be callable" unless executable.respond_to?(:call)
|
61
|
+
raise StitchingError, "A client or block handler must be provided." unless executable
|
62
|
+
raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
|
62
63
|
end
|
63
64
|
@executables[location] = executable
|
64
65
|
end
|
65
66
|
|
66
|
-
def execute_at_location(location, query, variables)
|
67
|
+
def execute_at_location(location, query, variables, context)
|
67
68
|
executable = executables[location]
|
68
69
|
|
69
70
|
if executable.nil?
|
70
|
-
raise "No executable assigned for #{location} location."
|
71
|
+
raise StitchingError, "No executable assigned for #{location} location."
|
71
72
|
elsif executable.is_a?(Class) && executable <= GraphQL::Schema
|
72
73
|
executable.execute(
|
73
74
|
query: query,
|
74
75
|
variables: variables,
|
76
|
+
context: context.frozen? ? context.dup : context,
|
75
77
|
validate: false,
|
76
78
|
)
|
77
79
|
elsif executable.respond_to?(:call)
|
78
|
-
executable.call(location, query, variables)
|
80
|
+
executable.call(location, query, variables, context)
|
79
81
|
else
|
80
|
-
raise "Missing valid executable for #{location} location."
|
82
|
+
raise StitchingError, "Missing valid executable for #{location} location."
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
84
86
|
# inverts fields map to provide fields for a type/location
|
87
|
+
# "Type" => "location" => ["field1", "field2", ...]
|
85
88
|
def fields_by_type_and_location
|
86
89
|
@fields_by_type_and_location ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
|
87
90
|
memo[type_name] = fields.each_with_object({}) do |(field_name, locations), memo|
|
@@ -93,24 +96,55 @@ module GraphQL
|
|
93
96
|
end
|
94
97
|
end
|
95
98
|
|
99
|
+
# { "Type" => ["location1", "location2", ...] }
|
96
100
|
def locations_by_type
|
97
101
|
@locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
|
98
102
|
memo[type_name] = fields.values.flatten.uniq
|
99
103
|
end
|
100
104
|
end
|
101
105
|
|
106
|
+
# collects all possible boundary keys for a given type
|
107
|
+
# { "Type" => ["id", ...] }
|
108
|
+
def possible_keys_for_type(type_name)
|
109
|
+
@possible_keys_by_type[type_name] ||= begin
|
110
|
+
keys = @boundaries[type_name].map { _1["selection"] }
|
111
|
+
keys.uniq!
|
112
|
+
keys
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# collects possible boundary keys for a given type and location
|
117
|
+
# ("Type", "location") => ["id", ...]
|
102
118
|
def possible_keys_for_type_and_location(type_name, location)
|
103
119
|
possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
|
104
120
|
possible_keys_by_type[location] ||= begin
|
105
121
|
location_fields = fields_by_type_and_location[type_name][location] || []
|
106
|
-
location_fields &
|
122
|
+
location_fields & possible_keys_for_type(type_name)
|
107
123
|
end
|
108
124
|
end
|
109
125
|
|
110
|
-
# For a given type, route from one origin
|
111
|
-
#
|
112
|
-
# favor longer paths through target locations over shorter paths with additional locations.
|
126
|
+
# For a given type, route from one origin location to one or more remote locations
|
127
|
+
# used to connect a partial type across locations via boundary queries
|
113
128
|
def route_type_to_locations(type_name, start_location, goal_locations)
|
129
|
+
if possible_keys_for_type(type_name).length > 1
|
130
|
+
# multiple keys use an a-star search to traverse intermediary locations
|
131
|
+
return route_type_to_locations_via_search(type_name, start_location, goal_locations)
|
132
|
+
end
|
133
|
+
|
134
|
+
# types with a single key attribute must all be within a single hop of each other,
|
135
|
+
# so can use a simple match to collect boundaries for the goal locations.
|
136
|
+
@boundaries[type_name].each_with_object({}) do |boundary, memo|
|
137
|
+
if goal_locations.include?(boundary["location"])
|
138
|
+
memo[boundary["location"]] = [boundary]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# tunes a-star search to favor paths with fewest joining locations, ie:
|
146
|
+
# favor longer paths through target locations over shorter paths with additional locations.
|
147
|
+
def route_type_to_locations_via_search(type_name, start_location, goal_locations)
|
114
148
|
results = {}
|
115
149
|
costs = {}
|
116
150
|
|
@@ -3,13 +3,9 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
5
|
class Util
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
while type.respond_to?(:of_type)
|
10
|
-
type = type.of_type
|
11
|
-
end
|
12
|
-
type
|
6
|
+
# specifies if a type is a primitive leaf value
|
7
|
+
def self.is_leaf_type?(type)
|
8
|
+
type.kind.scalar? || type.kind.enum?
|
13
9
|
end
|
14
10
|
|
15
11
|
# strips non-null wrappers from a type
|
@@ -20,6 +16,31 @@ module GraphQL
|
|
20
16
|
type
|
21
17
|
end
|
22
18
|
|
19
|
+
# gets a named type for a field node, including hidden root introspections
|
20
|
+
def self.named_type_for_field_node(schema, parent_type, node)
|
21
|
+
if node.name == "__schema" && parent_type == schema.query
|
22
|
+
schema.types["__Schema"]
|
23
|
+
else
|
24
|
+
parent_type.fields[node.name].type.unwrap
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# expands interfaces and unions to an array of their memberships
|
29
|
+
# like `schema.possible_types`, but includes child interfaces
|
30
|
+
def self.expand_abstract_type(schema, parent_type)
|
31
|
+
return [] unless parent_type.kind.abstract?
|
32
|
+
return parent_type.possible_types if parent_type.kind.union?
|
33
|
+
|
34
|
+
result = []
|
35
|
+
schema.types.values.each do |type|
|
36
|
+
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
37
|
+
next unless type.interfaces.include?(parent_type)
|
38
|
+
result << type
|
39
|
+
result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
|
40
|
+
end
|
41
|
+
result.uniq
|
42
|
+
end
|
43
|
+
|
23
44
|
# gets a deep structural description of a list value type
|
24
45
|
def self.get_list_structure(type)
|
25
46
|
structure = []
|
@@ -38,34 +59,6 @@ module GraphQL
|
|
38
59
|
end
|
39
60
|
structure
|
40
61
|
end
|
41
|
-
|
42
|
-
# Gets all objects and interfaces that implement a given interface
|
43
|
-
def self.get_possible_types(schema, parent_type)
|
44
|
-
return [parent_type] unless parent_type.kind.abstract?
|
45
|
-
return parent_type.possible_types if parent_type.kind.union?
|
46
|
-
|
47
|
-
result = []
|
48
|
-
schema.types.values.each do |type|
|
49
|
-
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
50
|
-
next unless type.interfaces.include?(parent_type)
|
51
|
-
result << type
|
52
|
-
result.push(*get_possible_types(schema, type)) if type.kind.interface?
|
53
|
-
end
|
54
|
-
result.uniq
|
55
|
-
end
|
56
|
-
|
57
|
-
# Specifies if a type is a leaf node (no children)
|
58
|
-
def self.is_leaf_type?(type)
|
59
|
-
type.kind.scalar? || type.kind.enum?
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.get_named_type_for_field_node(schema, parent_type, node)
|
63
|
-
if node.name == "__schema" && parent_type == schema.query
|
64
|
-
schema.types["__Schema"] # type mapped to phantom introspection field
|
65
|
-
else
|
66
|
-
Util.get_named_type(parent_type.fields[node.name].type)
|
67
|
-
end
|
68
|
-
end
|
69
62
|
end
|
70
63
|
end
|
71
64
|
end
|
data/lib/graphql/stitching.rb
CHANGED
@@ -4,6 +4,8 @@ require "graphql"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
|
+
EMPTY_OBJECT = {}.freeze
|
8
|
+
|
7
9
|
class StitchingError < StandardError; end
|
8
10
|
|
9
11
|
class << self
|
@@ -13,6 +15,10 @@ module GraphQL
|
|
13
15
|
end
|
14
16
|
|
15
17
|
attr_writer :stitch_directive
|
18
|
+
|
19
|
+
def stitching_directive_names
|
20
|
+
[stitch_directive]
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -20,11 +26,11 @@ end
|
|
20
26
|
require_relative "stitching/gateway"
|
21
27
|
require_relative "stitching/supergraph"
|
22
28
|
require_relative "stitching/composer"
|
23
|
-
require_relative "stitching/document"
|
24
29
|
require_relative "stitching/executor"
|
25
30
|
require_relative "stitching/planner_operation"
|
26
31
|
require_relative "stitching/planner"
|
27
32
|
require_relative "stitching/remote_client"
|
33
|
+
require_relative "stitching/request"
|
28
34
|
require_relative "stitching/shaper"
|
29
35
|
require_relative "stitching/util"
|
30
36
|
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: 0.
|
4
|
+
version: 0.2.2
|
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-02-
|
11
|
+
date: 2023-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.0.
|
19
|
+
version: 2.0.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.0.
|
26
|
+
version: 2.0.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,13 +81,13 @@ files:
|
|
81
81
|
- Rakefile
|
82
82
|
- docs/README.md
|
83
83
|
- docs/composer.md
|
84
|
-
- docs/document.md
|
85
84
|
- docs/executor.md
|
86
85
|
- docs/gateway.md
|
87
86
|
- docs/images/library.png
|
88
87
|
- docs/images/merging.png
|
89
88
|
- docs/images/stitching.png
|
90
89
|
- docs/planner.md
|
90
|
+
- docs/request.md
|
91
91
|
- docs/supergraph.md
|
92
92
|
- example/gateway.rb
|
93
93
|
- example/graphiql.html
|
@@ -99,12 +99,12 @@ files:
|
|
99
99
|
- lib/graphql/stitching/composer/base_validator.rb
|
100
100
|
- lib/graphql/stitching/composer/validate_boundaries.rb
|
101
101
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
102
|
-
- lib/graphql/stitching/document.rb
|
103
102
|
- lib/graphql/stitching/executor.rb
|
104
103
|
- lib/graphql/stitching/gateway.rb
|
105
104
|
- lib/graphql/stitching/planner.rb
|
106
105
|
- lib/graphql/stitching/planner_operation.rb
|
107
106
|
- lib/graphql/stitching/remote_client.rb
|
107
|
+
- lib/graphql/stitching/request.rb
|
108
108
|
- lib/graphql/stitching/shaper.rb
|
109
109
|
- lib/graphql/stitching/supergraph.rb
|
110
110
|
- lib/graphql/stitching/util.rb
|
data/docs/document.md
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
## GraphQL::Stitching::Document
|
2
|
-
|
3
|
-
A `Document` wraps a parsed GraphQL request, and handles the logistics of extracting its appropriate operation, variable definitions, and fragments. A `Document` should be built once for a request and passed through to other stitching components that utilize document information.
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
query = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
|
7
|
-
document = GraphQL::Stitching::Document.new(query, operation_name: "FetchMovie")
|
8
|
-
|
9
|
-
document.ast # parsed AST via GraphQL.parse
|
10
|
-
document.string # normalized printed string
|
11
|
-
document.digest # SHA digest of the normalized string
|
12
|
-
|
13
|
-
document.variables # mapping of variable names to type definitions
|
14
|
-
document.fragments # mapping of fragment names to fragment definitions
|
15
|
-
```
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Stitching
|
5
|
-
class Document
|
6
|
-
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
|
-
|
8
|
-
attr_reader :ast, :operation_name
|
9
|
-
|
10
|
-
def initialize(string_or_ast, operation_name: nil)
|
11
|
-
@ast = if string_or_ast.is_a?(String)
|
12
|
-
GraphQL.parse(string_or_ast)
|
13
|
-
else
|
14
|
-
string_or_ast
|
15
|
-
end
|
16
|
-
|
17
|
-
@operation_name = operation_name
|
18
|
-
end
|
19
|
-
|
20
|
-
def string
|
21
|
-
@string ||= GraphQL::Language::Printer.new.print(@ast)
|
22
|
-
end
|
23
|
-
|
24
|
-
def digest
|
25
|
-
@digest ||= Digest::SHA2.hexdigest(string)
|
26
|
-
end
|
27
|
-
|
28
|
-
def operation
|
29
|
-
@operation ||= begin
|
30
|
-
operation_defs = @ast.definitions.select do |d|
|
31
|
-
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
32
|
-
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
33
|
-
@operation_name ? d.name == @operation_name : true
|
34
|
-
end
|
35
|
-
|
36
|
-
if operation_defs.length < 1
|
37
|
-
raise GraphQL::ExecutionError, "Invalid root operation."
|
38
|
-
elsif operation_defs.length > 1
|
39
|
-
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
40
|
-
end
|
41
|
-
|
42
|
-
operation_defs.first
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def variable_definitions
|
47
|
-
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
48
|
-
memo[v.name] = v.type
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def fragment_definitions
|
53
|
-
@fragment_definitions ||= @ast.definitions.each_with_object({}) do |d, memo|
|
54
|
-
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|