graphql-stitching 0.3.6 → 1.0.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.
@@ -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