graphql-stitching 0.3.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- ```