graphql-stitching 1.4.2 → 1.5.0

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/README.md +4 -2
  4. data/docs/README.md +1 -0
  5. data/docs/composer.md +1 -1
  6. data/docs/subscriptions.md +208 -0
  7. data/examples/subscriptions/.gitattributes +9 -0
  8. data/examples/subscriptions/.gitignore +35 -0
  9. data/examples/subscriptions/Gemfile +65 -0
  10. data/examples/subscriptions/README.md +38 -0
  11. data/examples/subscriptions/Rakefile +6 -0
  12. data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
  13. data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
  14. data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
  15. data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
  16. data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
  17. data/examples/subscriptions/app/models/repository.rb +39 -0
  18. data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
  19. data/examples/subscriptions/bin/bundle +109 -0
  20. data/examples/subscriptions/bin/docker-entrypoint +8 -0
  21. data/examples/subscriptions/bin/importmap +4 -0
  22. data/examples/subscriptions/bin/rails +4 -0
  23. data/examples/subscriptions/bin/rake +4 -0
  24. data/examples/subscriptions/bin/setup +33 -0
  25. data/examples/subscriptions/config/application.rb +14 -0
  26. data/examples/subscriptions/config/boot.rb +4 -0
  27. data/examples/subscriptions/config/cable.yml +10 -0
  28. data/examples/subscriptions/config/credentials.yml.enc +1 -0
  29. data/examples/subscriptions/config/database.yml +25 -0
  30. data/examples/subscriptions/config/environment.rb +5 -0
  31. data/examples/subscriptions/config/environments/development.rb +74 -0
  32. data/examples/subscriptions/config/environments/production.rb +91 -0
  33. data/examples/subscriptions/config/environments/test.rb +64 -0
  34. data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
  35. data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
  36. data/examples/subscriptions/config/initializers/inflections.rb +16 -0
  37. data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
  38. data/examples/subscriptions/config/locales/en.yml +31 -0
  39. data/examples/subscriptions/config/master.key +1 -0
  40. data/examples/subscriptions/config/puma.rb +35 -0
  41. data/examples/subscriptions/config/routes.rb +8 -0
  42. data/examples/subscriptions/config/storage.yml +34 -0
  43. data/examples/subscriptions/config.ru +6 -0
  44. data/examples/subscriptions/db/seeds.rb +9 -0
  45. data/examples/subscriptions/public/404.html +17 -0
  46. data/examples/subscriptions/public/422.html +17 -0
  47. data/examples/subscriptions/public/500.html +16 -0
  48. data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
  49. data/examples/subscriptions/public/apple-touch-icon.png +0 -0
  50. data/examples/subscriptions/public/favicon.ico +0 -0
  51. data/examples/subscriptions/public/robots.txt +1 -0
  52. data/lib/graphql/stitching/client.rb +18 -11
  53. data/lib/graphql/stitching/composer/resolver_config.rb +1 -1
  54. data/lib/graphql/stitching/composer/validate_resolvers.rb +7 -1
  55. data/lib/graphql/stitching/composer.rb +30 -27
  56. data/lib/graphql/stitching/{shaper.rb → executor/shaper.rb} +3 -3
  57. data/lib/graphql/stitching/executor.rb +20 -11
  58. data/lib/graphql/stitching/http_executable.rb +3 -0
  59. data/lib/graphql/stitching/plan.rb +1 -1
  60. data/lib/graphql/stitching/{planner_step.rb → planner/step.rb} +3 -3
  61. data/lib/graphql/stitching/planner.rb +27 -7
  62. data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +2 -2
  63. data/lib/graphql/stitching/request.rb +42 -4
  64. data/lib/graphql/stitching/resolver/arguments.rb +2 -2
  65. data/lib/graphql/stitching/resolver/keys.rb +2 -3
  66. data/lib/graphql/stitching/resolver.rb +4 -4
  67. data/lib/graphql/stitching/supergraph.rb +5 -2
  68. data/lib/graphql/stitching/util.rb +1 -0
  69. data/lib/graphql/stitching/version.rb +1 -1
  70. data/lib/graphql/stitching.rb +18 -4
  71. metadata +51 -5
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "planner/step"
4
+
3
5
  module GraphQL
4
6
  module Stitching
7
+ # Planner partitions request selections by best-fit graph locations,
8
+ # and provides a query plan with sequential execution steps.
5
9
  class Planner
6
10
  SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
7
- TYPENAME = "__typename"
8
- QUERY_OP = "query"
9
- MUTATION_OP = "mutation"
10
11
  ROOT_INDEX = 0
11
12
 
12
13
  def initialize(request)
@@ -85,7 +86,7 @@ module GraphQL
85
86
  end
86
87
 
87
88
  if step.nil?
88
- @steps_by_entrypoint[entrypoint] = PlannerStep.new(
89
+ @steps_by_entrypoint[entrypoint] = Step.new(
89
90
  index: next_index,
90
91
  after: parent_index,
91
92
  location: location,
@@ -124,6 +125,7 @@ module GraphQL
124
125
  parent_index: ROOT_INDEX,
125
126
  parent_type: parent_type,
126
127
  selections: selections,
128
+ operation_type: QUERY_OP,
127
129
  )
128
130
  end
129
131
 
@@ -152,6 +154,22 @@ module GraphQL
152
154
  ).index
153
155
  end
154
156
 
157
+ when SUBSCRIPTION_OP
158
+ parent_type = @supergraph.schema.subscription
159
+
160
+ each_field_in_scope(parent_type, @request.operation.selections) do |node|
161
+ raise StitchingError, "Too many root fields for subscription." unless @steps_by_entrypoint.empty?
162
+
163
+ locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
164
+ add_step(
165
+ location: locations.first,
166
+ parent_index: ROOT_INDEX,
167
+ parent_type: parent_type,
168
+ selections: [node],
169
+ operation_type: SUBSCRIPTION_OP,
170
+ )
171
+ end
172
+
155
173
  else
156
174
  raise StitchingError, "Invalid operation type."
157
175
  end
@@ -261,7 +279,7 @@ module GraphQL
261
279
 
262
280
  # B.4) Add a `__typename` export to abstracts and types that implement
263
281
  # fragments so that resolved type information is available during execution.
264
- if requires_typename
282
+ if requires_typename && !locale_selections.include?(Resolver::TYPENAME_EXPORT_NODE)
265
283
  locale_selections << Resolver::TYPENAME_EXPORT_NODE
266
284
  end
267
285
 
@@ -277,6 +295,10 @@ module GraphQL
277
295
  route.reduce(locale_selections) do |parent_selections, resolver|
278
296
  # E.1) Add the key of each resolver query into the prior location's selection set.
279
297
  parent_selections.push(*resolver.key.export_nodes) if resolver.key
298
+ parent_selections.uniq! do |node|
299
+ export_node = node.is_a?(GraphQL::Language::Nodes::Field) && Resolver.export_key?(node.alias)
300
+ export_node ? node.alias : node.object_id
301
+ end
280
302
 
281
303
  # E.2) Add a planner step for each new entrypoint location.
282
304
  add_step(
@@ -289,8 +311,6 @@ module GraphQL
289
311
  ).selections
290
312
  end
291
313
  end
292
-
293
- locale_selections.uniq! { _1.alias || _1.name }
294
314
  end
295
315
 
296
316
  locale_selections
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
3
+ module GraphQL::Stitching
4
+ class Request
5
5
  # Faster implementation of an AST visitor for prerendering
6
6
  # @skip and @include conditional directives into a document.
7
7
  # This avoids unnecessary planning steps, and prepares result shaping.
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "request/skip_include"
4
+
3
5
  module GraphQL
4
6
  module Stitching
7
+ # Request combines a supergraph, GraphQL document, variables,
8
+ # variable/fragment definitions, and the selected operation.
9
+ # It provides the lifecycle of validating, preparing,
10
+ # planning, and executing upon these inputs.
5
11
  class Request
6
- SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
12
  SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
8
13
 
9
14
  # @return [Supergraph] supergraph instance that resolves the request.
@@ -83,7 +88,6 @@ module GraphQL
83
88
  @operation ||= begin
84
89
  operation_defs = @document.definitions.select do |d|
85
90
  next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
86
- next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
87
91
  @operation_name ? d.name == @operation_name : true
88
92
  end
89
93
 
@@ -97,6 +101,21 @@ module GraphQL
97
101
  end
98
102
  end
99
103
 
104
+ # @return [Boolean] true if operation type is a query
105
+ def query?
106
+ operation.operation_type == QUERY_OP
107
+ end
108
+
109
+ # @return [Boolean] true if operation type is a mutation
110
+ def mutation?
111
+ operation.operation_type == MUTATION_OP
112
+ end
113
+
114
+ # @return [Boolean] true if operation type is a subscription
115
+ def subscription?
116
+ operation.operation_type == SUBSCRIPTION_OP
117
+ end
118
+
100
119
  # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
101
120
  def operation_directives
102
121
  @operation_directives ||= if operation.directives.any?
@@ -162,7 +181,7 @@ module GraphQL
162
181
  raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
163
182
  @plan = new_plan
164
183
  else
165
- @plan ||= GraphQL::Stitching::Planner.new(self).perform
184
+ @plan ||= Planner.new(self).perform
166
185
  end
167
186
  end
168
187
 
@@ -170,7 +189,26 @@ module GraphQL
170
189
  # @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
171
190
  # @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
172
191
  def execute(raw: false)
173
- GraphQL::Stitching::Executor.new(self).perform(raw: raw)
192
+ add_subscription_update_handler if subscription?
193
+ Executor.new(self).perform(raw: raw)
194
+ end
195
+
196
+ private
197
+
198
+ # Adds a handler into context for enriching subscription updates with stitched data
199
+ def add_subscription_update_handler
200
+ request = self
201
+ @context[:stitch_subscription_update] = -> (result) {
202
+ stitched_result = Executor.new(
203
+ request,
204
+ data: result.to_h["data"] || {},
205
+ errors: result.to_h["errors"] || [],
206
+ after: request.plan.ops.first.step,
207
+ ).perform
208
+
209
+ result.to_h.merge!(stitched_result.to_h)
210
+ result
211
+ }
174
212
  end
175
213
  end
176
214
  end
@@ -130,8 +130,8 @@ module GraphQL::Stitching
130
130
 
131
131
  def verify_key(arg, key)
132
132
  key_field = value.reduce(Resolver::KeyField.new("", inner: key)) do |field, ns|
133
- if ns == Resolver::TYPE_NAME
134
- Resolver::KeyField.new(Resolver::TYPE_NAME)
133
+ if ns == TYPENAME
134
+ Resolver::KeyField.new(TYPENAME)
135
135
  elsif field
136
136
  field.inner.find { _1.name == ns }
137
137
  end
@@ -3,7 +3,6 @@
3
3
  module GraphQL::Stitching
4
4
  class Resolver
5
5
  EXPORT_PREFIX = "_export_"
6
- TYPE_NAME = "__typename"
7
6
 
8
7
  class FieldNode
9
8
  # GraphQL Ruby changed the argument assigning Field.alias from
@@ -59,8 +58,8 @@ module GraphQL::Stitching
59
58
 
60
59
  EMPTY_FIELD_SET = KeyFieldSet.new(GraphQL::Stitching::EMPTY_ARRAY)
61
60
  TYPENAME_EXPORT_NODE = FieldNode.build(
62
- field_alias: "#{EXPORT_PREFIX}#{TYPE_NAME}",
63
- field_name: TYPE_NAME,
61
+ field_alias: "#{EXPORT_PREFIX}#{TYPENAME}",
62
+ field_name: TYPENAME,
64
63
  )
65
64
 
66
65
  class Key < KeyFieldSet
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./resolver/arguments"
4
- require_relative "./resolver/keys"
3
+ require_relative "resolver/arguments"
4
+ require_relative "resolver/keys"
5
5
 
6
6
  module GraphQL
7
7
  module Stitching
8
- # Defines a root resolver query that provides direct access to an entity type.
8
+ # Defines a type resolver query that provides direct access to an entity type.
9
9
  class Resolver
10
10
  extend ArgumentsParser
11
11
  extend KeysParser
@@ -47,7 +47,7 @@ module GraphQL
47
47
  end
48
48
 
49
49
  def version
50
- @version ||= Digest::SHA2.hexdigest(as_json.to_json)
50
+ @version ||= Digest::SHA2.hexdigest("#{Stitching::VERSION}/#{as_json.to_json}")
51
51
  end
52
52
 
53
53
  def ==(other)
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./supergraph/to_definition"
3
+ require_relative "supergraph/to_definition"
4
4
 
5
5
  module GraphQL
6
6
  module Stitching
7
+ # Supergraph is the singuar representation of a stitched graph.
8
+ # It provides the combined GraphQL schema and delegation maps
9
+ # used to route selections across subgraph locations.
7
10
  class Supergraph
8
11
  SUPERGRAPH_LOCATION = "__super"
9
12
 
@@ -100,7 +103,7 @@ module GraphQL
100
103
  executable.execute(
101
104
  query: source,
102
105
  variables: variables,
103
- context: request.context.frozen? ? request.context.dup : request.context,
106
+ context: request.context.to_h,
104
107
  validate: false,
105
108
  )
106
109
  elsif executable.respond_to?(:call)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
+ # General utilities to aid with stitching.
5
6
  class Util
6
7
  TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
7
8
  alias_method :list?, :list
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.4.2"
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
@@ -4,7 +4,22 @@ require "graphql"
4
4
 
5
5
  module GraphQL
6
6
  module Stitching
7
+ # scope name of query operations.
8
+ QUERY_OP = "query"
9
+
10
+ # scope name of mutation operations.
11
+ MUTATION_OP = "mutation"
12
+
13
+ # scope name of subscription operations.
14
+ SUBSCRIPTION_OP = "subscription"
15
+
16
+ # introspection typename field.
17
+ TYPENAME = "__typename"
18
+
19
+ # @api private
7
20
  EMPTY_OBJECT = {}.freeze
21
+
22
+ # @api private
8
23
  EMPTY_ARRAY = [].freeze
9
24
 
10
25
  class StitchingError < StandardError; end
@@ -18,6 +33,8 @@ module GraphQL
18
33
 
19
34
  attr_writer :stitch_directive
20
35
 
36
+ # Names of stitching directives to omit from the composed supergraph.
37
+ # @returns [Array<String>] list of stitching directive names.
21
38
  def stitching_directive_names
22
39
  [stitch_directive]
23
40
  end
@@ -26,16 +43,13 @@ module GraphQL
26
43
  end
27
44
 
28
45
  require_relative "stitching/supergraph"
29
- require_relative "stitching/resolver"
30
46
  require_relative "stitching/client"
31
47
  require_relative "stitching/composer"
32
48
  require_relative "stitching/executor"
33
49
  require_relative "stitching/http_executable"
34
50
  require_relative "stitching/plan"
35
- require_relative "stitching/planner_step"
36
51
  require_relative "stitching/planner"
37
52
  require_relative "stitching/request"
38
- require_relative "stitching/shaper"
39
- require_relative "stitching/skip_include"
53
+ require_relative "stitching/resolver"
40
54
  require_relative "stitching/util"
41
55
  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: 1.4.2
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-03 00:00:00.000000000 Z
11
+ date: 2024-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -90,6 +90,7 @@ files:
90
90
  - docs/mechanics.md
91
91
  - docs/request.md
92
92
  - docs/resolver.md
93
+ - docs/subscriptions.md
93
94
  - docs/supergraph.md
94
95
  - examples/file_uploads/Gemfile
95
96
  - examples/file_uploads/Procfile
@@ -105,6 +106,51 @@ files:
105
106
  - examples/merged_types/graphiql.html
106
107
  - examples/merged_types/remote1.rb
107
108
  - examples/merged_types/remote2.rb
109
+ - examples/subscriptions/.gitattributes
110
+ - examples/subscriptions/.gitignore
111
+ - examples/subscriptions/Gemfile
112
+ - examples/subscriptions/README.md
113
+ - examples/subscriptions/Rakefile
114
+ - examples/subscriptions/app/channels/graphql_channel.rb
115
+ - examples/subscriptions/app/controllers/graphql_controller.rb
116
+ - examples/subscriptions/app/graphql/entities_schema.rb
117
+ - examples/subscriptions/app/graphql/stitched_schema.rb
118
+ - examples/subscriptions/app/graphql/subscriptions_schema.rb
119
+ - examples/subscriptions/app/models/repository.rb
120
+ - examples/subscriptions/app/views/graphql/client.html.erb
121
+ - examples/subscriptions/bin/bundle
122
+ - examples/subscriptions/bin/docker-entrypoint
123
+ - examples/subscriptions/bin/importmap
124
+ - examples/subscriptions/bin/rails
125
+ - examples/subscriptions/bin/rake
126
+ - examples/subscriptions/bin/setup
127
+ - examples/subscriptions/config.ru
128
+ - examples/subscriptions/config/application.rb
129
+ - examples/subscriptions/config/boot.rb
130
+ - examples/subscriptions/config/cable.yml
131
+ - examples/subscriptions/config/credentials.yml.enc
132
+ - examples/subscriptions/config/database.yml
133
+ - examples/subscriptions/config/environment.rb
134
+ - examples/subscriptions/config/environments/development.rb
135
+ - examples/subscriptions/config/environments/production.rb
136
+ - examples/subscriptions/config/environments/test.rb
137
+ - examples/subscriptions/config/initializers/content_security_policy.rb
138
+ - examples/subscriptions/config/initializers/filter_parameter_logging.rb
139
+ - examples/subscriptions/config/initializers/inflections.rb
140
+ - examples/subscriptions/config/initializers/permissions_policy.rb
141
+ - examples/subscriptions/config/locales/en.yml
142
+ - examples/subscriptions/config/master.key
143
+ - examples/subscriptions/config/puma.rb
144
+ - examples/subscriptions/config/routes.rb
145
+ - examples/subscriptions/config/storage.yml
146
+ - examples/subscriptions/db/seeds.rb
147
+ - examples/subscriptions/public/404.html
148
+ - examples/subscriptions/public/422.html
149
+ - examples/subscriptions/public/500.html
150
+ - examples/subscriptions/public/apple-touch-icon-precomposed.png
151
+ - examples/subscriptions/public/apple-touch-icon.png
152
+ - examples/subscriptions/public/favicon.ico
153
+ - examples/subscriptions/public/robots.txt
108
154
  - gemfiles/graphql_1.13.9.gemfile
109
155
  - gemfiles/graphql_2.0.0.gemfile
110
156
  - gemfiles/graphql_2.1.0.gemfile
@@ -120,16 +166,16 @@ files:
120
166
  - lib/graphql/stitching/executor.rb
121
167
  - lib/graphql/stitching/executor/resolver_source.rb
122
168
  - lib/graphql/stitching/executor/root_source.rb
169
+ - lib/graphql/stitching/executor/shaper.rb
123
170
  - lib/graphql/stitching/http_executable.rb
124
171
  - lib/graphql/stitching/plan.rb
125
172
  - lib/graphql/stitching/planner.rb
126
- - lib/graphql/stitching/planner_step.rb
173
+ - lib/graphql/stitching/planner/step.rb
127
174
  - lib/graphql/stitching/request.rb
175
+ - lib/graphql/stitching/request/skip_include.rb
128
176
  - lib/graphql/stitching/resolver.rb
129
177
  - lib/graphql/stitching/resolver/arguments.rb
130
178
  - lib/graphql/stitching/resolver/keys.rb
131
- - lib/graphql/stitching/shaper.rb
132
- - lib/graphql/stitching/skip_include.rb
133
179
  - lib/graphql/stitching/supergraph.rb
134
180
  - lib/graphql/stitching/supergraph/key_directive.rb
135
181
  - lib/graphql/stitching/supergraph/resolver_directive.rb