graphql-stitching 1.1.1 → 1.2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -0
  3. data/Gemfile +0 -3
  4. data/README.md +10 -32
  5. data/docs/README.md +1 -0
  6. data/docs/client.md +2 -2
  7. data/docs/composer.md +1 -1
  8. data/docs/executor.md +8 -15
  9. data/docs/http_executable.md +51 -0
  10. data/docs/planner.md +12 -14
  11. data/docs/request.md +2 -0
  12. data/docs/supergraph.md +2 -2
  13. data/examples/file_uploads/Gemfile +9 -0
  14. data/examples/file_uploads/Procfile +2 -0
  15. data/examples/file_uploads/README.md +37 -0
  16. data/examples/file_uploads/file.txt +1 -0
  17. data/examples/file_uploads/gateway.rb +37 -0
  18. data/examples/file_uploads/helpers.rb +62 -0
  19. data/examples/file_uploads/remote.rb +21 -0
  20. data/examples/merged_types/Gemfile +8 -0
  21. data/examples/merged_types/Procfile +3 -0
  22. data/examples/merged_types/README.md +33 -0
  23. data/{example → examples/merged_types}/gateway.rb +4 -5
  24. data/examples/merged_types/remote1.rb +22 -0
  25. data/examples/merged_types/remote2.rb +22 -0
  26. data/lib/graphql/stitching/client.rb +9 -19
  27. data/lib/graphql/stitching/composer/base_validator.rb +3 -3
  28. data/lib/graphql/stitching/composer/validate_boundaries.rb +3 -3
  29. data/lib/graphql/stitching/composer/validate_interfaces.rb +3 -4
  30. data/lib/graphql/stitching/composer.rb +66 -9
  31. data/lib/graphql/stitching/executor/boundary_source.rb +4 -6
  32. data/lib/graphql/stitching/executor/root_source.rb +4 -4
  33. data/lib/graphql/stitching/executor.rb +16 -13
  34. data/lib/graphql/stitching/export_selection.rb +6 -1
  35. data/lib/graphql/stitching/http_executable.rb +145 -4
  36. data/lib/graphql/stitching/planner.rb +3 -3
  37. data/lib/graphql/stitching/request.rb +66 -4
  38. data/lib/graphql/stitching/shaper.rb +4 -3
  39. data/lib/graphql/stitching/skip_include.rb +4 -3
  40. data/lib/graphql/stitching/supergraph/resolver_directive.rb +17 -0
  41. data/lib/graphql/stitching/supergraph/source_directive.rb +12 -0
  42. data/lib/graphql/stitching/supergraph.rb +27 -34
  43. data/lib/graphql/stitching/util.rb +0 -9
  44. data/lib/graphql/stitching/version.rb +1 -1
  45. metadata +20 -7
  46. data/Procfile +0 -3
  47. data/example/remote1.rb +0 -26
  48. data/example/remote2.rb +0 -26
  49. /data/{example → examples/merged_types}/graphiql.html +0 -0
@@ -6,9 +6,32 @@ module GraphQL
6
6
  SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
7
  SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
8
8
 
9
- attr_reader :document, :variables, :operation_name, :context
9
+ # @return [Supergraph] supergraph instance that resolves the request.
10
+ attr_reader :supergraph
10
11
 
11
- def initialize(document, operation_name: nil, variables: nil, context: nil)
12
+ # @return [GraphQL::Language::Nodes::Document] the parsed GraphQL AST document.
13
+ attr_reader :document
14
+
15
+ # @return [Hash] input variables for the request.
16
+ attr_reader :variables
17
+
18
+ # @return [String] operation name selected for the request.
19
+ attr_reader :operation_name
20
+
21
+ # @return [Hash] contextual object passed through resolver flows.
22
+ attr_reader :context
23
+
24
+ # @return [GraphQL::Schema::Warden] a visibility warden for this request.
25
+ attr_reader :warden
26
+
27
+ # Creates a new supergraph request.
28
+ # @param supergraph [Supergraph] supergraph instance that resolves the request.
29
+ # @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
30
+ # @param operation_name [String, nil] operation name selected for the request.
31
+ # @param variables [Hash, nil] input variables for the request.
32
+ # @param context [Hash, nil] a contextual object passed through resolver flows.
33
+ def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
34
+ @supergraph = supergraph
12
35
  @string = nil
13
36
  @digest = nil
14
37
  @normalized_string = nil
@@ -17,6 +40,7 @@ module GraphQL
17
40
  @operation_directives = nil
18
41
  @variable_definitions = nil
19
42
  @fragment_definitions = nil
43
+ @plan = nil
20
44
 
21
45
  @document = if document.is_a?(String)
22
46
  @string = document
@@ -27,25 +51,34 @@ module GraphQL
27
51
 
28
52
  @operation_name = operation_name
29
53
  @variables = variables || {}
30
- @context = context || GraphQL::Stitching::EMPTY_OBJECT
54
+
55
+ @query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
56
+ @warden = @query.warden
57
+ @context = @query.context
58
+ @context[:request] = self
31
59
  end
32
60
 
61
+ # @return [String] the original document string, or a print of the parsed AST document.
33
62
  def string
34
63
  @string || normalized_string
35
64
  end
36
65
 
66
+ # @return [String] a print of the parsed AST document with consistent whitespace.
37
67
  def normalized_string
38
68
  @normalized_string ||= @document.to_query_string
39
69
  end
40
70
 
71
+ # @return [String] a digest of the original document string. Generally faster but less consistent.
41
72
  def digest
42
73
  @digest ||= Digest::SHA2.hexdigest(string)
43
74
  end
44
75
 
76
+ # @return [String] a digest of the normalized document string. Slower but more consistent.
45
77
  def normalized_digest
46
78
  @normalized_digest ||= Digest::SHA2.hexdigest(normalized_string)
47
79
  end
48
80
 
81
+ # @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
49
82
  def operation
50
83
  @operation ||= begin
51
84
  operation_defs = @document.definitions.select do |d|
@@ -64,6 +97,7 @@ module GraphQL
64
97
  end
65
98
  end
66
99
 
100
+ # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
67
101
  def operation_directives
68
102
  @operation_directives ||= if operation.directives.any?
69
103
  printer = GraphQL::Language::Printer.new
@@ -71,18 +105,27 @@ module GraphQL
71
105
  end
72
106
  end
73
107
 
108
+ # @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
74
109
  def variable_definitions
75
110
  @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
76
111
  memo[v.name] = v.type
77
112
  end
78
113
  end
79
114
 
115
+ # @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
80
116
  def fragment_definitions
81
117
  @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
82
118
  memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
83
119
  end
84
120
  end
85
121
 
122
+ # Validates the request using the combined supergraph schema.
123
+ def validate
124
+ result = @supergraph.static_validator.validate(@query)
125
+ result[:errors]
126
+ end
127
+
128
+ # Prepares the request for stitching by rendering variable defaults and applying @skip/@include conditionals.
86
129
  def prepare!
87
130
  operation.variables.each do |v|
88
131
  @variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
@@ -93,12 +136,31 @@ module GraphQL
93
136
  @document = modified_ast
94
137
  @string = @normalized_string = nil
95
138
  @digest = @normalized_digest = nil
96
- @operation = @operation_directives = @variable_definitions = nil
139
+ @operation = @operation_directives = @variable_definitions = @plan = nil
97
140
  end
98
141
  end
99
142
 
100
143
  self
101
144
  end
145
+
146
+ # Gets and sets the query plan for the request. Assigned query plans may pull from cache.
147
+ # @param new_plan [Plan, nil] a cached query plan for the request.
148
+ # @return [Plan] query plan for the request.
149
+ def plan(new_plan = nil)
150
+ if new_plan
151
+ raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
152
+ @plan = new_plan
153
+ else
154
+ @plan ||= GraphQL::Stitching::Planner.new(self).perform
155
+ end
156
+ end
157
+
158
+ # Executes the request and returns the rendered response.
159
+ # @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
160
+ # @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
161
+ def execute(raw: false)
162
+ GraphQL::Stitching::Executor.new(self).perform(raw: raw)
163
+ end
102
164
  end
103
165
  end
104
166
  end
@@ -5,10 +5,11 @@ module GraphQL
5
5
  module Stitching
6
6
  # Shapes the final results payload to the request selection and schema definition.
7
7
  # This eliminates unrequested export selections and applies null bubbling.
8
+ # @api private
8
9
  class Shaper
9
- def initialize(supergraph:, request:)
10
- @supergraph = supergraph
10
+ def initialize(request)
11
11
  @request = request
12
+ @supergraph = request.supergraph
12
13
  @root_type = nil
13
14
  end
14
15
 
@@ -117,7 +118,7 @@ module GraphQL
117
118
  def typename_in_type?(typename, type)
118
119
  return true if type.graphql_name == typename
119
120
 
120
- type.kind.abstract? && @supergraph.memoized_schema_possible_types(type.graphql_name).any? do |t|
121
+ type.kind.abstract? && @request.warden.possible_types(type).any? do |t|
121
122
  t.graphql_name == typename
122
123
  end
123
124
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # Faster implementation of an AST visitor for prerendering
6
+ # @skip and @include conditional directives into a document.
7
+ # This avoids unnecessary planning steps, and prepares result shaping.
8
+ # @api private
5
9
  class SkipInclude
6
10
  class << self
7
- # Faster implementation of an AST visitor for prerendering
8
- # @skip and @include conditional directives into a document.
9
- # This avoids unnecessary planning steps, and prepares result shaping.
10
11
  def render(document, variables)
11
12
  changed = false
12
13
  definitions = document.definitions.map do |original_definition|
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Supergraph
5
+ class ResolverDirective < GraphQL::Schema::Directive
6
+ graphql_name "resolver"
7
+ locations OBJECT, INTERFACE, UNION
8
+ argument :location, String, required: true
9
+ argument :key, String, required: true
10
+ argument :field, String, required: true
11
+ argument :arg, String, required: true
12
+ argument :list, Boolean, required: false
13
+ argument :federation, Boolean, required: false
14
+ repeatable true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Supergraph
5
+ class SourceDirective < GraphQL::Schema::Directive
6
+ graphql_name "source"
7
+ locations FIELD_DEFINITION
8
+ argument :location, String, required: true
9
+ repeatable true
10
+ end
11
+ end
12
+ end
@@ -1,29 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "./supergraph/resolver_directive"
4
+ require_relative "./supergraph/source_directive"
5
+
3
6
  module GraphQL
4
7
  module Stitching
5
8
  class Supergraph
6
9
  SUPERGRAPH_LOCATION = "__super"
7
10
 
8
- class ResolverDirective < GraphQL::Schema::Directive
9
- graphql_name "resolver"
10
- locations OBJECT, INTERFACE, UNION
11
- argument :location, String, required: true
12
- argument :key, String, required: true
13
- argument :field, String, required: true
14
- argument :arg, String, required: true
15
- argument :list, Boolean, required: false
16
- argument :federation, Boolean, required: false
17
- repeatable true
18
- end
19
-
20
- class SourceDirective < GraphQL::Schema::Directive
21
- graphql_name "source"
22
- locations FIELD_DEFINITION
23
- argument :location, String, required: true
24
- repeatable true
25
- end
26
-
27
11
  class << self
28
12
  def validate_executable!(location, executable)
29
13
  return true if executable.is_a?(Class) && executable <= GraphQL::Schema
@@ -88,19 +72,27 @@ module GraphQL
88
72
  end
89
73
  end
90
74
 
91
- attr_reader :schema, :boundaries, :locations_by_type_and_field, :executables
75
+ # @return [GraphQL::Schema] the composed schema for the supergraph.
76
+ attr_reader :schema
77
+
78
+ # @return [Hash<String, Executable>] a map of executable resources by location.
79
+ attr_reader :executables
92
80
 
93
- def initialize(schema:, fields:, boundaries:, executables:)
81
+ attr_reader :boundaries, :locations_by_type_and_field
82
+
83
+ def initialize(schema:, fields: {}, boundaries: {}, executables: {})
94
84
  @schema = schema
85
+ @schema.use(GraphQL::Schema::AlwaysVisible)
86
+
95
87
  @boundaries = boundaries
96
- @possible_keys_by_type = {}
97
- @possible_keys_by_type_and_location = {}
98
- @memoized_schema_possible_types = {}
99
- @memoized_schema_fields = {}
100
- @memoized_introspection_types = nil
101
- @memoized_schema_types = nil
102
88
  @fields_by_type_and_location = nil
103
89
  @locations_by_type = nil
90
+ @memoized_introspection_types = nil
91
+ @memoized_schema_fields = {}
92
+ @memoized_schema_types = nil
93
+ @possible_keys_by_type = {}
94
+ @possible_keys_by_type_and_location = {}
95
+ @static_validator = nil
104
96
 
105
97
  # add introspection types into the fields mapping
106
98
  @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
@@ -172,6 +164,11 @@ module GraphQL
172
164
  @schema.to_definition
173
165
  end
174
166
 
167
+ # @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
168
+ def static_validator
169
+ @static_validator ||= @schema.static_validator
170
+ end
171
+
175
172
  def fields
176
173
  @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
177
174
  end
@@ -188,10 +185,6 @@ module GraphQL
188
185
  @memoized_schema_types ||= @schema.types
189
186
  end
190
187
 
191
- def memoized_schema_possible_types(type_name)
192
- @memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
193
- end
194
-
195
188
  def memoized_schema_fields(type_name)
196
189
  @memoized_schema_fields[type_name] ||= begin
197
190
  fields = memoized_schema_types[type_name].fields
@@ -209,7 +202,7 @@ module GraphQL
209
202
  end
210
203
  end
211
204
 
212
- def execute_at_location(location, source, variables, context)
205
+ def execute_at_location(location, source, variables, request)
213
206
  executable = executables[location]
214
207
 
215
208
  if executable.nil?
@@ -218,11 +211,11 @@ module GraphQL
218
211
  executable.execute(
219
212
  query: source,
220
213
  variables: variables,
221
- context: context.frozen? ? context.dup : context,
214
+ context: request.context.frozen? ? request.context.dup : request.context,
222
215
  validate: false,
223
216
  )
224
217
  elsif executable.respond_to?(:call)
225
- executable.call(location, source, variables, context)
218
+ executable.call(request, source, variables)
226
219
  else
227
220
  raise StitchingError, "Missing valid executable for #{location} location."
228
221
  end
@@ -12,16 +12,7 @@ module GraphQL
12
12
  end
13
13
  end
14
14
 
15
- GRAPHQL_VERSION = GraphQL::VERSION.split(".").map(&:to_i).freeze
16
-
17
15
  class << self
18
- def graphql_version?(major, minor = nil, patch = nil)
19
- result = GRAPHQL_VERSION[0] >= major
20
- result &&= GRAPHQL_VERSION[1] >= minor if minor
21
- result &&= GRAPHQL_VERSION[2] >= patch if patch
22
- result
23
- end
24
-
25
16
  # specifies if a type is a primitive leaf value
26
17
  def is_leaf_type?(type)
27
18
  type.kind.scalar? || type.kind.enum?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.1.1"
5
+ VERSION = "1.2.1"
6
6
  end
7
7
  end
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.1.1
4
+ version: 1.2.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-12-26 00:00:00.000000000 Z
11
+ date: 2024-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -74,15 +74,16 @@ extra_rdoc_files: []
74
74
  files:
75
75
  - ".github/workflows/ci.yml"
76
76
  - ".gitignore"
77
+ - ".yardopts"
77
78
  - Gemfile
78
79
  - LICENSE
79
- - Procfile
80
80
  - README.md
81
81
  - Rakefile
82
82
  - docs/README.md
83
83
  - docs/client.md
84
84
  - docs/composer.md
85
85
  - docs/executor.md
86
+ - docs/http_executable.md
86
87
  - docs/images/library.png
87
88
  - docs/images/merging.png
88
89
  - docs/images/stitching.png
@@ -90,10 +91,20 @@ files:
90
91
  - docs/planner.md
91
92
  - docs/request.md
92
93
  - docs/supergraph.md
93
- - example/gateway.rb
94
- - example/graphiql.html
95
- - example/remote1.rb
96
- - example/remote2.rb
94
+ - examples/file_uploads/Gemfile
95
+ - examples/file_uploads/Procfile
96
+ - examples/file_uploads/README.md
97
+ - examples/file_uploads/file.txt
98
+ - examples/file_uploads/gateway.rb
99
+ - examples/file_uploads/helpers.rb
100
+ - examples/file_uploads/remote.rb
101
+ - examples/merged_types/Gemfile
102
+ - examples/merged_types/Procfile
103
+ - examples/merged_types/README.md
104
+ - examples/merged_types/gateway.rb
105
+ - examples/merged_types/graphiql.html
106
+ - examples/merged_types/remote1.rb
107
+ - examples/merged_types/remote2.rb
97
108
  - gemfiles/graphql_1.13.9.gemfile
98
109
  - graphql-stitching.gemspec
99
110
  - lib/graphql/stitching.rb
@@ -115,6 +126,8 @@ files:
115
126
  - lib/graphql/stitching/shaper.rb
116
127
  - lib/graphql/stitching/skip_include.rb
117
128
  - lib/graphql/stitching/supergraph.rb
129
+ - lib/graphql/stitching/supergraph/resolver_directive.rb
130
+ - lib/graphql/stitching/supergraph/source_directive.rb
118
131
  - lib/graphql/stitching/util.rb
119
132
  - lib/graphql/stitching/version.rb
120
133
  homepage: https://github.com/gmac/graphql-stitching-ruby
data/Procfile DELETED
@@ -1,3 +0,0 @@
1
- gateway: bundle exec ruby example/gateway.rb
2
- remote1: bundle exec ruby example/remote1.rb
3
- remote2: bundle exec ruby example/remote2.rb
data/example/remote1.rb DELETED
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rackup'
4
- require 'json'
5
- require 'graphql'
6
- require_relative '../test/schemas/example'
7
-
8
- class FirstRemoteApp
9
- def call(env)
10
- req = Rack::Request.new(env)
11
- case req.path_info
12
- when /graphql/
13
- params = JSON.parse(req.body.read)
14
- result = Schemas::Example::Storefronts.execute(
15
- query: params["query"],
16
- variables: params["variables"],
17
- operation_name: params["operationName"],
18
- )
19
- [200, {"content-type" => "application/json"}, [JSON.generate(result)]]
20
- else
21
- [404, {"content-type" => "text/html"}, ["not found"]]
22
- end
23
- end
24
- end
25
-
26
- Rackup::Handler.default.run(FirstRemoteApp.new, :Port => 3001)
data/example/remote2.rb DELETED
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rackup'
4
- require 'json'
5
- require 'graphql'
6
- require_relative '../test/schemas/example'
7
-
8
- class SecondRemoteApp
9
- def call(env)
10
- req = Rack::Request.new(env)
11
- case req.path_info
12
- when /graphql/
13
- params = JSON.parse(req.body.read)
14
- result = Schemas::Example::Manufacturers.execute(
15
- query: params["query"],
16
- variables: params["variables"],
17
- operation_name: params["operationName"],
18
- )
19
- [200, {"content-type" => "application/json"}, [JSON.generate(result)]]
20
- else
21
- [404, {"content-type" => "text/html"}, ["not found"]]
22
- end
23
- end
24
- end
25
-
26
- Rackup::Handler.default.run(SecondRemoteApp.new, :Port => 3002)
File without changes