graphql-stitching 0.3.6 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,10 +21,14 @@ module GraphQL
21
21
  end
22
22
  end
23
23
 
24
+ boundaries = delegation_map["boundaries"].map do |k, b|
25
+ [k, b.map { Boundary.new(**_1) }]
26
+ end
27
+
24
28
  new(
25
29
  schema: schema,
26
30
  fields: delegation_map["fields"],
27
- boundaries: delegation_map["boundaries"],
31
+ boundaries: boundaries.to_h,
28
32
  executables: executables,
29
33
  )
30
34
  end
@@ -68,7 +72,7 @@ module GraphQL
68
72
  return GraphQL::Schema::Printer.print_schema(@schema), {
69
73
  "locations" => locations,
70
74
  "fields" => fields,
71
- "boundaries" => @boundaries,
75
+ "boundaries" => @boundaries.map { |k, b| [k, b.map(&:as_json)] }.to_h,
72
76
  }
73
77
  end
74
78
 
@@ -144,7 +148,7 @@ module GraphQL
144
148
  # ("Type") => ["id", ...]
145
149
  def possible_keys_for_type(type_name)
146
150
  @possible_keys_by_type[type_name] ||= begin
147
- @boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
151
+ @boundaries[type_name].map(&:key).tap(&:uniq!)
148
152
  end
149
153
  end
150
154
 
@@ -162,74 +166,76 @@ module GraphQL
162
166
  # used to connect a partial type across locations via boundary queries
163
167
  def route_type_to_locations(type_name, start_location, goal_locations)
164
168
  if possible_keys_for_type(type_name).length > 1
165
- # multiple keys use an a-star search to traverse intermediary locations
169
+ # multiple keys use an A* search to traverse intermediary locations
166
170
  return route_type_to_locations_via_search(type_name, start_location, goal_locations)
167
171
  end
168
172
 
169
173
  # types with a single key attribute must all be within a single hop of each other,
170
174
  # so can use a simple match to collect boundaries for the goal locations.
171
175
  @boundaries[type_name].each_with_object({}) do |boundary, memo|
172
- if goal_locations.include?(boundary["location"])
173
- memo[boundary["location"]] = [boundary]
176
+ if goal_locations.include?(boundary.location)
177
+ memo[boundary.location] = [boundary]
174
178
  end
175
179
  end
176
180
  end
177
181
 
178
182
  private
179
183
 
180
- # tunes a-star search to favor paths with fewest joining locations, ie:
184
+ PathNode = Struct.new(:location, :key, :cost, :boundary, keyword_init: true)
185
+
186
+ # tunes A* search to favor paths with fewest joining locations, ie:
181
187
  # favor longer paths through target locations over shorter paths with additional locations.
182
188
  def route_type_to_locations_via_search(type_name, start_location, goal_locations)
183
189
  results = {}
184
190
  costs = {}
185
191
 
186
192
  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
187
- [{ location: start_location, key: possible_key, cost: 0 }]
193
+ [PathNode.new(location: start_location, key: possible_key, cost: 0)]
188
194
  end
189
195
 
190
196
  while paths.any?
191
197
  path = paths.pop
192
- current_location = path.last[:location]
193
- current_key = path.last[:key]
194
- current_cost = path.last[:cost]
198
+ current_location = path.last.location
199
+ current_key = path.last.key
200
+ current_cost = path.last.cost
195
201
 
196
202
  @boundaries[type_name].each do |boundary|
197
- forward_location = boundary["location"]
198
- next if current_key != boundary["key"]
199
- next if path.any? { _1[:location] == forward_location }
203
+ forward_location = boundary.location
204
+ next if current_key != boundary.key
205
+ next if path.any? { _1.location == forward_location }
200
206
 
201
207
  best_cost = costs[forward_location] || Float::INFINITY
202
208
  next if best_cost < current_cost
203
209
 
204
210
  path.pop
205
- path << {
211
+ path << PathNode.new(
206
212
  location: current_location,
207
213
  key: current_key,
208
214
  cost: current_cost,
209
215
  boundary: boundary,
210
- }
216
+ )
211
217
 
212
218
  if goal_locations.include?(forward_location)
213
219
  current_result = results[forward_location]
214
220
  if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
215
- results[forward_location] = path.map { _1[:boundary] }
221
+ results[forward_location] = path.map(&:boundary)
216
222
  end
217
223
  else
218
- path.last[:cost] += 1
224
+ path.last.cost += 1
219
225
  end
220
226
 
221
- forward_cost = path.last[:cost]
227
+ forward_cost = path.last.cost
222
228
  costs[forward_location] = forward_cost if forward_cost < best_cost
223
229
 
224
230
  possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
225
- paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
231
+ paths << [*path, PathNode.new(location: forward_location, key: possible_key, cost: forward_cost)]
226
232
  end
227
233
  end
228
234
 
229
235
  paths.sort! do |a, b|
230
- cost_diff = a.last[:cost] - b.last[:cost]
231
- cost_diff.zero? ? a.length - b.length : cost_diff
232
- end.reverse!
236
+ cost_diff = b.last.cost - a.last.cost
237
+ cost_diff.zero? ? b.length - a.length : cost_diff
238
+ end
233
239
  end
234
240
 
235
241
  results
@@ -3,54 +3,65 @@
3
3
  module GraphQL
4
4
  module Stitching
5
5
  class Util
6
- # specifies if a type is a primitive leaf value
7
- def self.is_leaf_type?(type)
8
- type.kind.scalar? || type.kind.enum?
9
- end
6
+ TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
7
+ alias_method :list?, :list
8
+ alias_method :null?, :null
10
9
 
11
- # strips non-null wrappers from a type
12
- def self.unwrap_non_null(type)
13
- type = type.of_type while type.non_null?
14
- type
10
+ def non_null?
11
+ !null
12
+ end
15
13
  end
16
14
 
17
- # builds a single-dimensional representation of a wrapped type structure
18
- def self.flatten_type_structure(type)
19
- structure = []
15
+ class << self
16
+ # specifies if a type is a primitive leaf value
17
+ def is_leaf_type?(type)
18
+ type.kind.scalar? || type.kind.enum?
19
+ end
20
+
21
+ # strips non-null wrappers from a type
22
+ def unwrap_non_null(type)
23
+ type = type.of_type while type.non_null?
24
+ type
25
+ end
26
+
27
+ # builds a single-dimensional representation of a wrapped type structure
28
+ def flatten_type_structure(type)
29
+ structure = []
30
+
31
+ while type.list?
32
+ structure << TypeStructure.new(
33
+ list: true,
34
+ null: !type.non_null?,
35
+ name: nil,
36
+ )
37
+
38
+ type = unwrap_non_null(type).of_type
39
+ end
20
40
 
21
- while type.list?
22
- structure << {
23
- list: true,
41
+ structure << TypeStructure.new(
42
+ list: false,
24
43
  null: !type.non_null?,
25
- name: nil,
26
- }
44
+ name: type.unwrap.graphql_name,
45
+ )
27
46
 
28
- type = unwrap_non_null(type).of_type
47
+ structure
29
48
  end
30
49
 
31
- structure << {
32
- list: false,
33
- null: !type.non_null?,
34
- name: type.unwrap.graphql_name,
35
- }
36
-
37
- structure
38
- end
50
+ # expands interfaces and unions to an array of their memberships
51
+ # like `schema.possible_types`, but includes child interfaces
52
+ def expand_abstract_type(schema, parent_type)
53
+ return [] unless parent_type.kind.abstract?
54
+ return parent_type.possible_types if parent_type.kind.union?
39
55
 
40
- # expands interfaces and unions to an array of their memberships
41
- # like `schema.possible_types`, but includes child interfaces
42
- def self.expand_abstract_type(schema, parent_type)
43
- return [] unless parent_type.kind.abstract?
44
- return parent_type.possible_types if parent_type.kind.union?
45
-
46
- result = []
47
- schema.types.values.each do |type|
48
- next unless type <= GraphQL::Schema::Interface && type != parent_type
49
- next unless type.interfaces.include?(parent_type)
50
- result << type
51
- result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
56
+ result = []
57
+ schema.types.values.each do |type|
58
+ next unless type <= GraphQL::Schema::Interface && type != parent_type
59
+ next unless type.interfaces.include?(parent_type)
60
+ result << type
61
+ result.push(*expand_abstract_type(schema, type)) if type.kind.interface?
62
+ end
63
+ result.uniq
52
64
  end
53
- result.uniq
54
65
  end
55
66
  end
56
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.3.6"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end
@@ -5,11 +5,11 @@ 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
 
11
12
  class << self
12
-
13
13
  def stitch_directive
14
14
  @stitch_directive ||= "stitch"
15
15
  end
@@ -23,14 +23,18 @@ module GraphQL
23
23
  end
24
24
  end
25
25
 
26
- require_relative "stitching/gateway"
27
26
  require_relative "stitching/supergraph"
27
+ require_relative "stitching/boundary"
28
+ require_relative "stitching/client"
28
29
  require_relative "stitching/composer"
29
30
  require_relative "stitching/executor"
30
- require_relative "stitching/planner_operation"
31
+ require_relative "stitching/http_executable"
32
+ require_relative "stitching/plan"
33
+ require_relative "stitching/planner_step"
31
34
  require_relative "stitching/planner"
32
- require_relative "stitching/remote_client"
33
35
  require_relative "stitching/request"
36
+ require_relative "stitching/selection_hint"
34
37
  require_relative "stitching/shaper"
38
+ require_relative "stitching/skip_include"
35
39
  require_relative "stitching/util"
36
40
  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.3.6
4
+ version: 1.0.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-04-11 00:00:00.000000000 Z
11
+ date: 2023-10-04 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,17 +97,23 @@ files:
97
97
  - gemfiles/graphql_1.13.9.gemfile
98
98
  - graphql-stitching.gemspec
99
99
  - lib/graphql/stitching.rb
100
+ - lib/graphql/stitching/boundary.rb
101
+ - lib/graphql/stitching/client.rb
100
102
  - lib/graphql/stitching/composer.rb
101
103
  - lib/graphql/stitching/composer/base_validator.rb
102
104
  - lib/graphql/stitching/composer/validate_boundaries.rb
103
105
  - lib/graphql/stitching/composer/validate_interfaces.rb
104
106
  - lib/graphql/stitching/executor.rb
105
- - lib/graphql/stitching/gateway.rb
107
+ - lib/graphql/stitching/executor/boundary_source.rb
108
+ - lib/graphql/stitching/executor/root_source.rb
109
+ - lib/graphql/stitching/http_executable.rb
110
+ - lib/graphql/stitching/plan.rb
106
111
  - lib/graphql/stitching/planner.rb
107
- - lib/graphql/stitching/planner_operation.rb
108
- - lib/graphql/stitching/remote_client.rb
112
+ - lib/graphql/stitching/planner_step.rb
109
113
  - lib/graphql/stitching/request.rb
114
+ - lib/graphql/stitching/selection_hint.rb
110
115
  - lib/graphql/stitching/shaper.rb
116
+ - lib/graphql/stitching/skip_include.rb
111
117
  - lib/graphql/stitching/supergraph.rb
112
118
  - lib/graphql/stitching/util.rb
113
119
  - lib/graphql/stitching/version.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
- ```
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- class PlannerOperation
6
- LANGUAGE_PRINTER = GraphQL::Language::Printer.new
7
-
8
- attr_reader :order, :location, :parent_type, :if_type, :operation_type, :path
9
- attr_accessor :after, :selections, :variables, :boundary
10
-
11
- def initialize(
12
- location:,
13
- parent_type:,
14
- order:,
15
- after: nil,
16
- operation_type: "query",
17
- selections: [],
18
- variables: [],
19
- path: [],
20
- if_type: nil,
21
- boundary: nil
22
- )
23
- @location = location
24
- @parent_type = parent_type
25
- @order = order
26
- @after = after
27
- @operation_type = operation_type
28
- @selections = selections
29
- @variables = variables
30
- @path = path
31
- @if_type = if_type
32
- @boundary = boundary
33
- end
34
-
35
- def selection_set
36
- op = GraphQL::Language::Nodes::OperationDefinition.new(selections: @selections)
37
- LANGUAGE_PRINTER.print(op).gsub!(/\s+/, " ").strip!
38
- end
39
-
40
- def variable_set
41
- @variables.each_with_object({}) do |(variable_name, value_type), memo|
42
- memo[variable_name] = LANGUAGE_PRINTER.print(value_type)
43
- end
44
- end
45
-
46
- def to_h
47
- data = {
48
- "order" => @order,
49
- "after" => @after,
50
- "location" => @location,
51
- "operation_type" => @operation_type,
52
- "selections" => selection_set,
53
- "variables" => variable_set,
54
- "path" => @path,
55
- }
56
-
57
- data["if_type"] = @if_type if @if_type
58
- data["boundary"] = @boundary if @boundary
59
- data
60
- end
61
- end
62
- end
63
- end