graphql-stitching 1.0.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ class SkipInclude
6
+ class << self
7
+ # Faster implementation of an AST visitor for prerendering
8
+ # @skip and @include conditional directives into a document.
9
+ # This avoids unnecessary planning steps, and prepares result shaping.
10
+ def render(document, variables)
11
+ changed = false
12
+ definitions = document.definitions.map do |original_definition|
13
+ definition = render_node(original_definition, variables)
14
+ changed ||= definition.object_id != original_definition.object_id
15
+ definition
16
+ end
17
+
18
+ return document.merge(definitions: definitions), changed
19
+ end
20
+
21
+ private
22
+
23
+ def render_node(parent_node, variables)
24
+ changed = false
25
+ filtered_selections = parent_node.selections.filter_map do |original_node|
26
+ node = prune_node(original_node, variables)
27
+ if node.nil?
28
+ changed = true
29
+ next nil
30
+ end
31
+
32
+ node = render_node(node, variables) if node.selections.any?
33
+ changed ||= node.object_id != original_node.object_id
34
+ node
35
+ end
36
+
37
+ if filtered_selections.none?
38
+ filtered_selections << SelectionHint.typename_node
39
+ end
40
+
41
+ if changed
42
+ parent_node.merge(selections: filtered_selections)
43
+ else
44
+ parent_node
45
+ end
46
+ end
47
+
48
+ def prune_node(node, variables)
49
+ return node unless node.directives.any?
50
+
51
+ delete_node = false
52
+ filtered_directives = node.directives.reject do |directive|
53
+ if directive.name == "skip"
54
+ delete_node = assess_condition(directive.arguments.first, variables)
55
+ true
56
+ elsif directive.name == "include"
57
+ delete_node = !assess_condition(directive.arguments.first, variables)
58
+ true
59
+ end
60
+ end
61
+
62
+ if delete_node
63
+ nil
64
+ elsif filtered_directives.length != node.directives.length
65
+ node.merge(directives: filtered_directives)
66
+ else
67
+ node
68
+ end
69
+ end
70
+
71
+ def assess_condition(arg, variables)
72
+ if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
73
+ variables[arg.value.name] || variables[arg.value.name.to_sym]
74
+ else
75
+ arg.value
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -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.tap(&: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 = "1.0.0"
5
+ VERSION = "1.0.2"
6
6
  end
7
7
  end
@@ -10,7 +10,6 @@ module GraphQL
10
10
  class StitchingError < StandardError; end
11
11
 
12
12
  class << self
13
-
14
13
  def stitch_directive
15
14
  @stitch_directive ||= "stitch"
16
15
  end
@@ -25,13 +24,17 @@ module GraphQL
25
24
  end
26
25
 
27
26
  require_relative "stitching/supergraph"
27
+ require_relative "stitching/boundary"
28
28
  require_relative "stitching/client"
29
29
  require_relative "stitching/composer"
30
30
  require_relative "stitching/executor"
31
31
  require_relative "stitching/http_executable"
32
- require_relative "stitching/planner_operation"
32
+ require_relative "stitching/plan"
33
+ require_relative "stitching/planner_step"
33
34
  require_relative "stitching/planner"
34
35
  require_relative "stitching/request"
36
+ require_relative "stitching/selection_hint"
35
37
  require_relative "stitching/shaper"
38
+ require_relative "stitching/skip_include"
36
39
  require_relative "stitching/util"
37
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: 1.0.0
4
+ version: 1.0.2
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-08-01 00:00:00.000000000 Z
11
+ date: 2023-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -97,6 +97,7 @@ 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
100
101
  - lib/graphql/stitching/client.rb
101
102
  - lib/graphql/stitching/composer.rb
102
103
  - lib/graphql/stitching/composer/base_validator.rb
@@ -106,10 +107,13 @@ files:
106
107
  - lib/graphql/stitching/executor/boundary_source.rb
107
108
  - lib/graphql/stitching/executor/root_source.rb
108
109
  - lib/graphql/stitching/http_executable.rb
110
+ - lib/graphql/stitching/plan.rb
109
111
  - lib/graphql/stitching/planner.rb
110
- - lib/graphql/stitching/planner_operation.rb
112
+ - lib/graphql/stitching/planner_step.rb
111
113
  - lib/graphql/stitching/request.rb
114
+ - lib/graphql/stitching/selection_hint.rb
112
115
  - lib/graphql/stitching/shaper.rb
116
+ - lib/graphql/stitching/skip_include.rb
113
117
  - lib/graphql/stitching/supergraph.rb
114
118
  - lib/graphql/stitching/util.rb
115
119
  - lib/graphql/stitching/version.rb
@@ -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