graphql-stitching 0.1.0 → 0.2.2
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 +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
|