graphql-stitching 0.3.4 → 1.0.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.
@@ -4,16 +4,6 @@ module GraphQL
4
4
  module Stitching
5
5
  class Supergraph
6
6
  LOCATION = "__super"
7
- INTROSPECTION_TYPES = [
8
- "__Schema",
9
- "__Type",
10
- "__Field",
11
- "__Directive",
12
- "__EnumValue",
13
- "__InputValue",
14
- "__TypeKind",
15
- "__DirectiveLocation",
16
- ].freeze
17
7
 
18
8
  def self.validate_executable!(location, executable)
19
9
  return true if executable.is_a?(Class) && executable <= GraphQL::Schema
@@ -50,11 +40,10 @@ module GraphQL
50
40
  @memoized_schema_fields = {}
51
41
 
52
42
  # add introspection types into the fields mapping
53
- @locations_by_type_and_field = INTROSPECTION_TYPES.each_with_object(fields) do |type_name, memo|
54
- introspection_type = schema.get_type(type_name)
55
- next unless introspection_type.kind.fields?
43
+ @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
44
+ next unless type.kind.fields?
56
45
 
57
- memo[type_name] = introspection_type.fields.keys.each_with_object({}) do |field_name, m|
46
+ memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
58
47
  m[field_name] = [LOCATION]
59
48
  end
60
49
  end.freeze
@@ -68,7 +57,7 @@ module GraphQL
68
57
  end
69
58
 
70
59
  def fields
71
- @locations_by_type_and_field.reject { |k, _v| INTROSPECTION_TYPES.include?(k) }
60
+ @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
72
61
  end
73
62
 
74
63
  def locations
@@ -83,6 +72,10 @@ module GraphQL
83
72
  }
84
73
  end
85
74
 
75
+ def memoized_introspection_types
76
+ @memoized_introspection_types ||= schema.introspection_system.types
77
+ end
78
+
86
79
  def memoized_schema_types
87
80
  @memoized_schema_types ||= @schema.types
88
81
  end
@@ -94,11 +87,14 @@ module GraphQL
94
87
  def memoized_schema_fields(type_name)
95
88
  @memoized_schema_fields[type_name] ||= begin
96
89
  fields = memoized_schema_types[type_name].fields
97
- fields["__typename"] = @schema.introspection_system.dynamic_field(name: "__typename")
90
+ @schema.introspection_system.dynamic_fields.each do |field|
91
+ fields[field.name] ||= field # adds __typename
92
+ end
98
93
 
99
94
  if type_name == @schema.query.graphql_name
100
- fields["__schema"] = @schema.introspection_system.entry_point(name: "__schema")
101
- fields["__type"] = @schema.introspection_system.entry_point(name: "__type")
95
+ @schema.introspection_system.entry_points.each do |field|
96
+ fields[field.name] ||= field # adds __schema, __type
97
+ end
102
98
  end
103
99
 
104
100
  fields
@@ -148,9 +144,7 @@ module GraphQL
148
144
  # ("Type") => ["id", ...]
149
145
  def possible_keys_for_type(type_name)
150
146
  @possible_keys_by_type[type_name] ||= begin
151
- keys = @boundaries[type_name].map { _1["selection"] }
152
- keys.uniq!
153
- keys
147
+ @boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
154
148
  end
155
149
  end
156
150
 
@@ -190,18 +184,18 @@ module GraphQL
190
184
  costs = {}
191
185
 
192
186
  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
193
- [{ location: start_location, selection: possible_key, cost: 0 }]
187
+ [{ location: start_location, key: possible_key, cost: 0 }]
194
188
  end
195
189
 
196
190
  while paths.any?
197
191
  path = paths.pop
198
192
  current_location = path.last[:location]
199
- current_selection = path.last[:selection]
193
+ current_key = path.last[:key]
200
194
  current_cost = path.last[:cost]
201
195
 
202
196
  @boundaries[type_name].each do |boundary|
203
197
  forward_location = boundary["location"]
204
- next if current_selection != boundary["selection"]
198
+ next if current_key != boundary["key"]
205
199
  next if path.any? { _1[:location] == forward_location }
206
200
 
207
201
  best_cost = costs[forward_location] || Float::INFINITY
@@ -210,7 +204,7 @@ module GraphQL
210
204
  path.pop
211
205
  path << {
212
206
  location: current_location,
213
- selection: current_selection,
207
+ key: current_key,
214
208
  cost: current_cost,
215
209
  boundary: boundary,
216
210
  }
@@ -228,14 +222,13 @@ module GraphQL
228
222
  costs[forward_location] = forward_cost if forward_cost < best_cost
229
223
 
230
224
  possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
231
- paths << [*path, { location: forward_location, selection: possible_key, cost: forward_cost }]
225
+ paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
232
226
  end
233
227
  end
234
228
 
235
229
  paths.sort! do |a, b|
236
230
  cost_diff = a.last[:cost] - b.last[:cost]
237
- next cost_diff unless cost_diff.zero?
238
- a.length - b.length
231
+ cost_diff.zero? ? a.length - b.length : cost_diff
239
232
  end.reverse!
240
233
  end
241
234
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.3.4"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
@@ -5,6 +5,7 @@ require "graphql"
5
5
  module GraphQL
6
6
  module Stitching
7
7
  EMPTY_OBJECT = {}.freeze
8
+ EMPTY_ARRAY = [].freeze
8
9
 
9
10
  class StitchingError < StandardError; end
10
11
 
@@ -23,13 +24,13 @@ module GraphQL
23
24
  end
24
25
  end
25
26
 
26
- require_relative "stitching/gateway"
27
27
  require_relative "stitching/supergraph"
28
+ require_relative "stitching/client"
28
29
  require_relative "stitching/composer"
29
30
  require_relative "stitching/executor"
31
+ require_relative "stitching/http_executable"
30
32
  require_relative "stitching/planner_operation"
31
33
  require_relative "stitching/planner"
32
- require_relative "stitching/remote_client"
33
34
  require_relative "stitching/request"
34
35
  require_relative "stitching/shaper"
35
36
  require_relative "stitching/util"
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.3.4
4
+ version: 1.0.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: 2023-03-27 00:00:00.000000000 Z
11
+ date: 2023-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -80,9 +80,9 @@ files:
80
80
  - README.md
81
81
  - Rakefile
82
82
  - docs/README.md
83
+ - docs/client.md
83
84
  - docs/composer.md
84
85
  - docs/executor.md
85
- - docs/gateway.md
86
86
  - docs/images/library.png
87
87
  - docs/images/merging.png
88
88
  - docs/images/stitching.png
@@ -97,15 +97,17 @@ files:
97
97
  - gemfiles/graphql_1.13.9.gemfile
98
98
  - graphql-stitching.gemspec
99
99
  - lib/graphql/stitching.rb
100
+ - lib/graphql/stitching/client.rb
100
101
  - lib/graphql/stitching/composer.rb
101
102
  - lib/graphql/stitching/composer/base_validator.rb
102
103
  - lib/graphql/stitching/composer/validate_boundaries.rb
103
104
  - lib/graphql/stitching/composer/validate_interfaces.rb
104
105
  - lib/graphql/stitching/executor.rb
105
- - lib/graphql/stitching/gateway.rb
106
+ - lib/graphql/stitching/executor/boundary_source.rb
107
+ - lib/graphql/stitching/executor/root_source.rb
108
+ - lib/graphql/stitching/http_executable.rb
106
109
  - lib/graphql/stitching/planner.rb
107
110
  - lib/graphql/stitching/planner_operation.rb
108
- - lib/graphql/stitching/remote_client.rb
109
111
  - lib/graphql/stitching/request.rb
110
112
  - lib/graphql/stitching/shaper.rb
111
113
  - lib/graphql/stitching/supergraph.rb
data/docs/gateway.md DELETED
@@ -1,103 +0,0 @@
1
- ## GraphQL::Stitching::Gateway
2
-
3
- The `Gateway` is an out-of-the-box convenience with all stitching components assembled into a default workflow. A gateway is designed to work for most common needs, though you're welcome to assemble the component parts into your own configuration. A Gateway is constructed with the same [location settings](./composer.md#performing-composition) used to perform supergraph composition:
4
-
5
- ```ruby
6
- movies_schema = "type Query { ..."
7
- showtimes_schema = "type Query { ..."
8
-
9
- gateway = GraphQL::Stitching::Gateway.new(locations: {
10
- products: {
11
- schema: GraphQL::Schema.from_definition(movies_schema),
12
- executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3000"),
13
- stitch: [{ field_name: "products", key: "id" }],
14
- },
15
- showtimes: {
16
- schema: GraphQL::Schema.from_definition(showtimes_schema),
17
- executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3001"),
18
- },
19
- my_local: {
20
- schema: MyLocal::GraphQL::Schema,
21
- },
22
- })
23
- ```
24
-
25
- Alternatively, you may pass a prebuilt `Supergraph` instance to the Gateway constructor. This is useful when [exporting and rehydrating](./supergraph.md#export-and-caching) supergraph instances, which bypasses the need for runtime composition:
26
-
27
- ```ruby
28
- exported_schema = "type Query { ..."
29
- exported_mapping = JSON.parse("{ ... }")
30
- supergraph = GraphQL::Stitching::Supergraph.from_export(
31
- schema: exported_schema,
32
- delegation_map: exported_mapping,
33
- executables: { ... },
34
- )
35
-
36
- gateway = GraphQL::Stitching::Gateway.new(supergraph: supergraph)
37
- ```
38
-
39
- ### Execution
40
-
41
- A gateway provides an `execute` method with a subset of arguments provided by [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries). Executing requests on a stitched gateway becomes mostly a drop-in replacement to executing on a `GraphQL::Schema` instance:
42
-
43
- ```ruby
44
- result = gateway.execute(
45
- query: "query MyProduct($id: ID!) { product(id: $id) { name } }",
46
- variables: { "id" => "1" },
47
- operation_name: "MyProduct",
48
- )
49
- ```
50
-
51
- Arguments for the `execute` method include:
52
-
53
- * `query`: a query (or mutation) as a string or parsed AST.
54
- * `variables`: a hash of variables for the request.
55
- * `operation_name`: the name of the operation to execute (when multiple are provided).
56
- * `validate`: true if static validation should run on the supergraph schema before execution.
57
- * `context`: an object passed through to executable calls and gateway hooks.
58
-
59
- ### Cache hooks
60
-
61
- The gateway provides cache hooks to enable caching query plans across requests. Without caching, every request made the the gateway will be planned individually. With caching, a query may be planned once, cached, and then executed from cache for subsequent requests. Cache keys are a normalized digest of each query string.
62
-
63
- ```ruby
64
- gateway.on_cache_read do |key, _context|
65
- $redis.get(key) # << 3P code
66
- end
67
-
68
- gateway.on_cache_write do |key, payload, _context|
69
- $redis.set(key, payload) # << 3P code
70
- end
71
- ```
72
-
73
- Note that inlined input data works against caching, so you should _avoid_ this:
74
-
75
- ```graphql
76
- query {
77
- product(id: "1") { name }
78
- }
79
- ```
80
-
81
- Instead, always leverage variables in queries so that the document body remains consistent across requests:
82
-
83
- ```graphql
84
- query($id: ID!) {
85
- product(id: $id) { name }
86
- }
87
-
88
- # variables: { "id" => "1" }
89
- ```
90
-
91
- ### Error hooks
92
-
93
- The gateway also provides an error hook. Any program errors rescued during execution will be passed to the `on_error` handler, which can report on the error as needed and return a formatted error message for the gateway to add to the [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) result.
94
-
95
- ```ruby
96
- gateway.on_error do |err, context|
97
- # log the error
98
- Bugsnag.notify(err)
99
-
100
- # return a formatted message for the public response
101
- "Whoops, please contact support abount request '#{context[:request_id]}'"
102
- end
103
- ```