graphql-stitching 1.0.4 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccc67e1b52f1a0688d25958d1955580c7f88a8115dc6b6b6905f785da94089db
4
- data.tar.gz: 8e0925fb1ad0584bce11672be24868dc261cf680f8f88acdff87a7ca3819d9f8
3
+ metadata.gz: fbeabcd8c2327504cc5ccc613942d163bddddb2087e7eb90c966861777d67b9d
4
+ data.tar.gz: cf03589e91ef96fb8dc9b74ca2703c20095e586bab45f91a89b3579376edbdac
5
5
  SHA512:
6
- metadata.gz: 9213dbd90d0fc0caf10a09b9ba68bfbfe072c3cce9d51c47623b0d5edabb9bfe1eac849fb3135bade04863c2ca9b62c42c2f67b469f1fc268f039fab15f17372
7
- data.tar.gz: c62c51fda4c9210ba71c9be894e7a31fe3346c0e4d49ac57632efa32b8db9809174b6d0fb048eddc1437584c57d7f2bd748e4fab3f1e1440c2d5c6f3647657dd
6
+ metadata.gz: 78b5d6bbf4abccb99795c6225e1a61807ed74f94ad9ce5bdde0be534133e101c7d51f677d3c89aebaad7ebe2b125ff11a192a3d1a29cdacb3d3b51049801212f
7
+ data.tar.gz: ade024c4ace1474d9c7a7cd80fb8820fc71e456e566d89fe656d07bf4b64e60db83561546a908b5d2fcbec9d88ac740a7080946e981a74f7bd904b3da0aeebfb
data/docs/client.md CHANGED
@@ -61,16 +61,16 @@ Arguments for the `execute` method include:
61
61
  The client provides cache hooks to enable caching query plans across requests. Without caching, every request made to the client 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
62
 
63
63
  ```ruby
64
- client.on_cache_read do |key, _context|
64
+ client.on_cache_read do |key, _context, _request|
65
65
  $redis.get(key) # << 3P code
66
66
  end
67
67
 
68
- client.on_cache_write do |key, payload, _context|
68
+ client.on_cache_write do |key, payload, _context, _request|
69
69
  $redis.set(key, payload) # << 3P code
70
70
  end
71
71
  ```
72
72
 
73
- Note that inlined input data works against caching, so you should _avoid_ this:
73
+ Note that inlined input data works against caching, so you should _avoid_ this when possible:
74
74
 
75
75
  ```graphql
76
76
  query {
@@ -78,7 +78,7 @@ query {
78
78
  }
79
79
  ```
80
80
 
81
- Instead, always leverage variables in queries so that the document body remains consistent across requests:
81
+ Instead, leverage query variables so that the document body remains consistent across requests:
82
82
 
83
83
  ```graphql
84
84
  query($id: ID!) {
data/docs/composer.md CHANGED
@@ -12,6 +12,7 @@ composer = GraphQL::Stitching::Composer.new(
12
12
  mutation_name: "Mutation",
13
13
  description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
14
14
  deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
15
+ default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
15
16
  directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
16
17
  root_field_location_selector: ->(locations, info) { locations.last },
17
18
  )
@@ -27,6 +28,8 @@ Constructor arguments:
27
28
 
28
29
  - **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
29
30
 
31
+ - **`default_value_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging argument default values from across locations.
32
+
30
33
  - **`directive_kwarg_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations.
31
34
 
32
35
  - **`root_field_location_selector:`** _optional_, selects a default routing location for root fields with multiple locations. Use this to prioritize sending root fields to their primary data sources (only applies while routing the root operation scope). This handler receives an array of possible locations and an info object with field information, and should return the prioritized location. The last location is used by default.
data/docs/executor.md CHANGED
@@ -50,8 +50,8 @@ The raw result will contain many irregularities from the stitching process, howe
50
50
  "data" => {
51
51
  "product" => {
52
52
  "upc" => "1",
53
- "_STITCH_upc" => "1",
54
- "_STITCH_typename" => "Product",
53
+ "_export_upc" => "1",
54
+ "_export_typename" => "Product",
55
55
  "name" => "iPhone",
56
56
  "price" => nil,
57
57
  }
@@ -75,14 +75,14 @@ module GraphQL
75
75
 
76
76
  def fetch_plan(request)
77
77
  if @on_cache_read
78
- cached_plan = @on_cache_read.call(request.digest, request.context)
78
+ cached_plan = @on_cache_read.call(request.digest, request.context, request)
79
79
  return GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan)) if cached_plan
80
80
  end
81
81
 
82
82
  plan = yield
83
83
 
84
84
  if @on_cache_write
85
- @on_cache_write.call(request.digest, JSON.generate(plan.as_json), request.context)
85
+ @on_cache_write.call(request.digest, JSON.generate(plan.as_json), request.context, request)
86
86
  end
87
87
 
88
88
  plan
@@ -5,9 +5,13 @@ module GraphQL
5
5
  class Composer
6
6
  class ComposerError < StitchingError; end
7
7
  class ValidationError < ComposerError; end
8
+ class ReferenceType < GraphQL::Schema::Object
9
+ field(:f, String) do
10
+ argument(:a, String)
11
+ end
12
+ end
8
13
 
9
- attr_reader :query_name, :mutation_name, :candidate_types_by_name_and_location, :schema_directives
10
-
14
+ NO_DEFAULT_VALUE = ReferenceType.get_field("f").get_argument("a").default_value
11
15
  BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
12
16
  BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
13
17
 
@@ -16,6 +20,8 @@ module GraphQL
16
20
  "ValidateBoundaries",
17
21
  ].freeze
18
22
 
23
+ attr_reader :query_name, :mutation_name, :candidate_types_by_name_and_location, :schema_directives
24
+
19
25
  def initialize(
20
26
  query_name: "Query",
21
27
  mutation_name: "Mutation",
@@ -47,7 +53,7 @@ module GraphQL
47
53
  end
48
54
  end
49
55
 
50
- # "Typename" => merged_directive
56
+ # "directive_name" => merged_directive
51
57
  @schema_directives = @candidate_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
52
58
  memo[directive_name] = build_directive(directive_name, directives_by_location)
53
59
  end
@@ -82,19 +88,20 @@ module GraphQL
82
88
 
83
89
  # "Typename" => merged_type
84
90
  schema_types = @candidate_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
85
- kinds = types_by_location.values.map { _1.kind.name }.uniq
91
+ kinds = types_by_location.values.map { _1.kind.name }.tap(&:uniq!)
86
92
 
87
93
  if kinds.length > 1
88
94
  raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
89
95
  end
90
96
 
97
+ extract_boundaries(type_name, types_by_location) if type_name == @query_name
98
+
91
99
  memo[type_name] = case kinds.first
92
100
  when "SCALAR"
93
101
  build_scalar_type(type_name, types_by_location)
94
102
  when "ENUM"
95
103
  build_enum_type(type_name, types_by_location, enum_usage)
96
104
  when "OBJECT"
97
- extract_boundaries(type_name, types_by_location) if type_name == @query_name
98
105
  build_object_type(type_name, types_by_location)
99
106
  when "INTERFACE"
100
107
  build_interface_type(type_name, types_by_location)
@@ -194,8 +201,8 @@ module GraphQL
194
201
  graphql_name(directive_name)
195
202
  description(builder.merge_descriptions(directive_name, directives_by_location))
196
203
  repeatable(directives_by_location.values.any?(&:repeatable?))
197
- locations(*directives_by_location.values.flat_map(&:locations).uniq)
198
- builder.build_merged_arguments(directive_name, directives_by_location, self)
204
+ locations(*directives_by_location.values.flat_map(&:locations).tap(&:uniq!))
205
+ builder.build_merged_arguments(directive_name, directives_by_location, self, directive_name: directive_name)
199
206
  end
200
207
  end
201
208
 
@@ -256,7 +263,7 @@ module GraphQL
256
263
  description(builder.merge_descriptions(type_name, types_by_location))
257
264
 
258
265
  interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
259
- interface_names.uniq.each do |interface_name|
266
+ interface_names.tap(&:uniq!).each do |interface_name|
260
267
  implements(builder.build_type_binding(interface_name))
261
268
  end
262
269
 
@@ -274,7 +281,7 @@ module GraphQL
274
281
  description(builder.merge_descriptions(type_name, types_by_location))
275
282
 
276
283
  interface_names = types_by_location.values.flat_map { _1.interfaces.map(&:graphql_name) }
277
- interface_names.uniq.each do |interface_name|
284
+ interface_names.tap(&:uniq!).each do |interface_name|
278
285
  implements(builder.build_type_binding(interface_name))
279
286
  end
280
287
 
@@ -290,7 +297,7 @@ module GraphQL
290
297
  graphql_name(type_name)
291
298
  description(builder.merge_descriptions(type_name, types_by_location))
292
299
 
293
- possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.uniq
300
+ possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.tap(&:uniq!)
294
301
  possible_types(*possible_names.map { builder.build_type_binding(_1) })
295
302
  builder.build_merged_directives(type_name, types_by_location, self)
296
303
  end
@@ -343,7 +350,7 @@ module GraphQL
343
350
  end
344
351
  end
345
352
 
346
- def build_merged_arguments(type_name, members_by_location, owner, field_name: nil)
353
+ def build_merged_arguments(type_name, members_by_location, owner, field_name: nil, directive_name: nil)
347
354
  # "argument_name" => "location" => argument
348
355
  args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
349
356
  member_candidate.arguments.each do |argument_name, argument|
@@ -369,7 +376,7 @@ module GraphQL
369
376
 
370
377
  kwargs = {}
371
378
  default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
372
- next if argument.default_value.class == Object # << pass on NOT_CONFIGURED (todo: improve this check)
379
+ next if argument.default_value == NO_DEFAULT_VALUE
373
380
  memo[location] = argument.default_value
374
381
  end
375
382
 
@@ -378,7 +385,8 @@ module GraphQL
378
385
  type_name: type_name,
379
386
  field_name: field_name,
380
387
  argument_name: argument_name,
381
- })
388
+ directive_name: directive_name,
389
+ }.tap(&:compact!))
382
390
  end
383
391
 
384
392
  type = merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name)
@@ -430,7 +438,7 @@ module GraphQL
430
438
  enum_value: enum_value,
431
439
  directive_name: directive_name,
432
440
  kwarg_name: kwarg_name,
433
- }.compact!)
441
+ }.tap(&:compact!))
434
442
  end
435
443
 
436
444
  owner.directive(directive_class, **kwargs)
@@ -438,7 +446,7 @@ module GraphQL
438
446
  end
439
447
 
440
448
  def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
441
- path = [type_name, field_name, argument_name].compact.join(".")
449
+ path = [type_name, field_name, argument_name].tap(&:compact!).join(".")
442
450
  alt_structures = type_candidates.map { Util.flatten_type_structure(_1) }
443
451
  basis_structure = alt_structures.shift
444
452
 
@@ -475,7 +483,7 @@ module GraphQL
475
483
  field_name: field_name,
476
484
  argument_name: argument_name,
477
485
  enum_value: enum_value,
478
- }.compact!)
486
+ }.tap(&:compact!))
479
487
  end
480
488
 
481
489
  def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
@@ -485,7 +493,7 @@ module GraphQL
485
493
  field_name: field_name,
486
494
  argument_name: argument_name,
487
495
  enum_value: enum_value,
488
- }.compact!)
496
+ }.tap(&:compact!))
489
497
  end
490
498
 
491
499
  def extract_boundaries(type_name, types_by_location)
@@ -16,7 +16,7 @@ module GraphQL
16
16
 
17
17
  if op.if_type
18
18
  # operations planned around unused fragment conditions should not trigger requests
19
- origin_set.select! { _1[SelectionHint.typename_node.alias] == op.if_type }
19
+ origin_set.select! { _1[ExportSelection.typename_node.alias] == op.if_type }
20
20
  end
21
21
 
22
22
  memo[op] = origin_set if origin_set.any?
@@ -94,9 +94,9 @@ module GraphQL
94
94
  end
95
95
 
96
96
  def build_key(key, origin_obj, federation: false)
97
- key_value = JSON.generate(origin_obj[SelectionHint.key(key)])
97
+ key_value = JSON.generate(origin_obj[ExportSelection.key(key)])
98
98
  if federation
99
- "{ __typename: \"#{origin_obj[SelectionHint.typename_node.alias]}\", #{key}: #{key_value} }"
99
+ "{ __typename: \"#{origin_obj[ExportSelection.typename_node.alias]}\", #{key}: #{key_value} }"
100
100
  else
101
101
  key_value
102
102
  end
@@ -160,7 +160,8 @@ module GraphQL
160
160
  errors_result.concat(pathed_errors_by_object_id.values)
161
161
  end
162
162
  end
163
- errors_result.flatten!
163
+
164
+ errors_result.tap(&:flatten!)
164
165
  end
165
166
 
166
167
  private
@@ -4,18 +4,18 @@ module GraphQL
4
4
  module Stitching
5
5
  # Builds hidden selection fields added by stitiching code,
6
6
  # used to request operational data about resolved objects.
7
- class SelectionHint
8
- HINT_PREFIX = "_STITCH_"
7
+ class ExportSelection
8
+ EXPORT_PREFIX = "_export_"
9
9
 
10
10
  class << self
11
11
  def key?(name)
12
12
  return false unless name
13
13
 
14
- name.start_with?(HINT_PREFIX)
14
+ name.start_with?(EXPORT_PREFIX)
15
15
  end
16
16
 
17
17
  def key(name)
18
- "#{HINT_PREFIX}#{name}"
18
+ "#{EXPORT_PREFIX}#{name}"
19
19
  end
20
20
 
21
21
  def key_node(field_name)
@@ -42,7 +42,7 @@ module GraphQL
42
42
  # Adjoining selections not available here get split off into new entrypoints (C).
43
43
  # B.3) Collect all variable definitions used within the filtered selection.
44
44
  # These specify which request variables to pass along with each step.
45
- # B.4) Add a `__typename` hint to abstracts and types that implement fragments.
45
+ # B.4) Add a `__typename` export to abstracts and types that implement fragments.
46
46
  # This provides resolved type information used during execution.
47
47
  #
48
48
  # C) Delegate adjoining selections to new entrypoint locations.
@@ -199,7 +199,9 @@ module GraphQL
199
199
  input_selections.each do |node|
200
200
  case node
201
201
  when GraphQL::Language::Nodes::Field
202
- if node.name == TYPENAME
202
+ if node.alias&.start_with?(ExportSelection::EXPORT_PREFIX)
203
+ raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{ExportSelection::EXPORT_PREFIX}" is a reserved prefix.)
204
+ elsif node.name == TYPENAME
203
205
  locale_selections << node
204
206
  next
205
207
  end
@@ -257,10 +259,10 @@ module GraphQL
257
259
  end
258
260
  end
259
261
 
260
- # B.4) Add a `__typename` hint to abstracts and types that implement
262
+ # B.4) Add a `__typename` export to abstracts and types that implement
261
263
  # fragments so that resolved type information is available during execution.
262
264
  if requires_typename
263
- locale_selections << SelectionHint.typename_node
265
+ locale_selections << ExportSelection.typename_node
264
266
  end
265
267
 
266
268
  if remote_selections
@@ -274,18 +276,18 @@ module GraphQL
274
276
  routes.each_value do |route|
275
277
  route.reduce(locale_selections) do |parent_selections, boundary|
276
278
  # E.1) Add the key of each boundary query into the prior location's selection set.
277
- foreign_key = SelectionHint.key(boundary.key)
279
+ foreign_key = ExportSelection.key(boundary.key)
278
280
  has_key = false
279
281
  has_typename = false
280
282
 
281
283
  parent_selections.each do |node|
282
284
  next unless node.is_a?(GraphQL::Language::Nodes::Field)
283
285
  has_key ||= node.alias == foreign_key
284
- has_typename ||= node.alias == SelectionHint.typename_node.alias
286
+ has_typename ||= node.alias == ExportSelection.typename_node.alias
285
287
  end
286
288
 
287
- parent_selections << SelectionHint.key_node(boundary.key) unless has_key
288
- parent_selections << SelectionHint.typename_node unless has_typename
289
+ parent_selections << ExportSelection.key_node(boundary.key) unless has_key
290
+ parent_selections << ExportSelection.typename_node unless has_typename
289
291
 
290
292
  # E.2) Add a planner step for each new entrypoint location.
291
293
  add_step(
@@ -4,14 +4,13 @@ module GraphQL
4
4
  module Stitching
5
5
  class Request
6
6
  SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
+ SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
7
8
 
8
9
  attr_reader :document, :variables, :operation_name, :context
9
10
 
10
11
  def initialize(document, operation_name: nil, variables: nil, context: nil)
11
- @may_contain_runtime_directives = true
12
-
13
12
  @document = if document.is_a?(String)
14
- @may_contain_runtime_directives = document.include?("@")
13
+ @string = document
15
14
  GraphQL.parse(document)
16
15
  else
17
16
  document
@@ -23,13 +22,21 @@ module GraphQL
23
22
  end
24
23
 
25
24
  def string
26
- @string ||= @document.to_query_string
25
+ @string || normalized_string
26
+ end
27
+
28
+ def normalized_string
29
+ @normalized_string ||= @document.to_query_string
27
30
  end
28
31
 
29
32
  def digest
30
33
  @digest ||= Digest::SHA2.hexdigest(string)
31
34
  end
32
35
 
36
+ def normalized_digest
37
+ @normalized_digest ||= Digest::SHA2.hexdigest(normalized_string)
38
+ end
39
+
33
40
  def operation
34
41
  @operation ||= begin
35
42
  operation_defs = @document.definitions.select do |d|
@@ -69,18 +76,15 @@ module GraphQL
69
76
 
70
77
  def prepare!
71
78
  operation.variables.each do |v|
72
- @variables[v.name] ||= v.default_value
79
+ @variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
73
80
  end
74
81
 
75
- if @may_contain_runtime_directives
76
- @document, modified = SkipInclude.render(@document, @variables)
77
-
78
- if modified
79
- @string = nil
80
- @digest = nil
81
- @operation = nil
82
- @variable_definitions = nil
83
- @fragment_definitions = nil
82
+ if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
83
+ SkipInclude.render(@document, @variables) do |modified_ast|
84
+ @document = modified_ast
85
+ @string = @normalized_string = nil
86
+ @digest = @normalized_digest = nil
87
+ @operation = @operation_directives = @variable_definitions = nil
84
88
  end
85
89
  end
86
90
 
@@ -4,7 +4,7 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  # Shapes the final results payload to the request selection and schema definition.
7
- # This eliminates unrequested selection hints and applies null bubbling.
7
+ # This eliminates unrequested export selections and applies null bubbling.
8
8
  class Shaper
9
9
  def initialize(supergraph:, request:)
10
10
  @supergraph = supergraph
@@ -21,8 +21,8 @@ module GraphQL
21
21
  def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
22
22
  return nil if raw_object.nil?
23
23
 
24
- typename ||= raw_object[SelectionHint.typename_node.alias]
25
- raw_object.reject! { |key, _v| SelectionHint.key?(key) }
24
+ typename ||= raw_object[ExportSelection.typename_node.alias]
25
+ raw_object.reject! { |key, _v| ExportSelection.key?(key) }
26
26
 
27
27
  selections.each do |node|
28
28
  case node
@@ -15,7 +15,11 @@ module GraphQL
15
15
  definition
16
16
  end
17
17
 
18
- return document.merge(definitions: definitions), changed
18
+ return document unless changed
19
+
20
+ document = document.merge(definitions: definitions)
21
+ yield(document) if block_given?
22
+ document
19
23
  end
20
24
 
21
25
  private
@@ -35,7 +39,7 @@ module GraphQL
35
39
  end
36
40
 
37
41
  if filtered_selections.none?
38
- filtered_selections << SelectionHint.typename_node
42
+ filtered_selections << ExportSelection.typename_node
39
43
  end
40
44
 
41
45
  if changed
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.0.4"
5
+ VERSION = "1.0.6"
6
6
  end
7
7
  end
@@ -28,12 +28,12 @@ require_relative "stitching/boundary"
28
28
  require_relative "stitching/client"
29
29
  require_relative "stitching/composer"
30
30
  require_relative "stitching/executor"
31
+ require_relative "stitching/export_selection"
31
32
  require_relative "stitching/http_executable"
32
33
  require_relative "stitching/plan"
33
34
  require_relative "stitching/planner_step"
34
35
  require_relative "stitching/planner"
35
36
  require_relative "stitching/request"
36
- require_relative "stitching/selection_hint"
37
37
  require_relative "stitching/shaper"
38
38
  require_relative "stitching/skip_include"
39
39
  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: 1.0.4
4
+ version: 1.0.6
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-11-03 00:00:00.000000000 Z
11
+ date: 2023-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -106,12 +106,12 @@ files:
106
106
  - lib/graphql/stitching/executor.rb
107
107
  - lib/graphql/stitching/executor/boundary_source.rb
108
108
  - lib/graphql/stitching/executor/root_source.rb
109
+ - lib/graphql/stitching/export_selection.rb
109
110
  - lib/graphql/stitching/http_executable.rb
110
111
  - lib/graphql/stitching/plan.rb
111
112
  - lib/graphql/stitching/planner.rb
112
113
  - lib/graphql/stitching/planner_step.rb
113
114
  - lib/graphql/stitching/request.rb
114
- - lib/graphql/stitching/selection_hint.rb
115
115
  - lib/graphql/stitching/shaper.rb
116
116
  - lib/graphql/stitching/skip_include.rb
117
117
  - lib/graphql/stitching/supergraph.rb