graphql-stitching 1.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ff6bbdb8e949da02a0ed58f6db5ab56a6e622d9befc436042dcaaed6556a0f0
4
- data.tar.gz: ecb682677bb576a3742e1a0ebf8eabe4d17e2dfbc214120aed4cbef09db881df
3
+ metadata.gz: 4ce8bb1075d536f01aa63487df3e1686bc0c2899b58ca14e95383caef38812e2
4
+ data.tar.gz: ae39e685bdb31cbf3962216cfce263b2970aa2aeac6be1901a7397cee6eb1656
5
5
  SHA512:
6
- metadata.gz: 506a02bced23940043c43bfb59b4a3cba9cf98c7177f0f0abe58b16b0543498cb7acdeecac4d44d22d867d3dd12f3be756236b36ac534e22eb3ed2f347f16984
7
- data.tar.gz: b409cbe17f3d4792bb8aa4ae0553d8d15dcb5a5316999248a66644fabae67133036804f9a34d805b3bed58e11e40158ca36444365f80773e945c2b586588fd39
6
+ metadata.gz: e718527102969773accf28b2323cbde1d2c467339c44bb876144ae5387a7aaec64143b35b6dfff21401fb087c64ed8742f79cda78e743518ba045871c320f51f
7
+ data.tar.gz: 74bc97f475b61ea51dd8ce9e42a1ff2783d0c1717a38ff812e256962d64b97ef0ccc092de11e529a22e274d8ad5cf77af9fd7ed2fd680e20e0c058534942a5d6
data/README.md CHANGED
@@ -9,7 +9,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
9
9
  - Multiple keys per merged type.
10
10
  - Shared objects, fields, enums, and inputs across locations.
11
11
  - Combining local and remote schemas.
12
- - Type merging via federation `_entities` protocol.
12
+ - Type merging via arbitrary queries or federation `_entities` protocol.
13
13
 
14
14
  **NOT Supported:**
15
15
  - Computed fields (ie: federation-style `@requires`).
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ Boundary = Struct.new(
6
+ :location,
7
+ :type_name,
8
+ :key,
9
+ :field,
10
+ :arg,
11
+ :list,
12
+ :federation,
13
+ keyword_init: true
14
+ ) do
15
+ def as_json
16
+ {
17
+ location: location,
18
+ type_name: type_name,
19
+ key: key,
20
+ field: field,
21
+ arg: arg,
22
+ list: list,
23
+ federation: federation,
24
+ }.tap(&:compact!)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -41,7 +41,7 @@ module GraphQL
41
41
  GraphQL::Stitching::Planner.new(
42
42
  supergraph: @supergraph,
43
43
  request: request,
44
- ).perform.to_h
44
+ ).perform
45
45
  end
46
46
 
47
47
  GraphQL::Stitching::Executor.new(
@@ -76,16 +76,16 @@ module GraphQL
76
76
  def fetch_plan(request)
77
77
  if @on_cache_read
78
78
  cached_plan = @on_cache_read.call(request.digest, request.context)
79
- return JSON.parse(cached_plan) if cached_plan
79
+ return GraphQL::Stitching::Plan.from_json(JSON.parse(cached_plan)) if cached_plan
80
80
  end
81
81
 
82
- plan_json = yield
82
+ plan = yield
83
83
 
84
84
  if @on_cache_write
85
- @on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
85
+ @on_cache_write.call(request.digest, JSON.generate(plan.as_json), request.context)
86
86
  end
87
87
 
88
- plan_json
88
+ plan
89
89
  end
90
90
 
91
91
  def error_result(errors)
@@ -32,16 +32,16 @@ module GraphQL
32
32
 
33
33
  # only one boundary allowed per type/location/key
34
34
  boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
35
- if memo.dig(boundary["location"], boundary["key"])
36
- raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["key"]}` "\
37
- "found in #{boundary["location"]}. Limit one boundary query per type and key in each location. "\
35
+ if memo.dig(boundary.location, boundary.key)
36
+ raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary.key}` "\
37
+ "found in #{boundary.location}. Limit one boundary query per type and key in each location. "\
38
38
  "Abstract boundaries provide all possible types."
39
39
  end
40
- memo[boundary["location"]] ||= {}
41
- memo[boundary["location"]][boundary["key"]] = boundary
40
+ memo[boundary.location] ||= {}
41
+ memo[boundary.location][boundary.key] = boundary
42
42
  end
43
43
 
44
- boundary_keys = boundaries.map { _1["key"] }.uniq
44
+ boundary_keys = boundaries.map { _1.key }.uniq
45
45
  key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
46
46
  subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
47
47
  end
@@ -32,12 +32,12 @@ module GraphQL
32
32
  interface_type_structure.each_with_index do |interface_struct, index|
33
33
  possible_struct = possible_type_structure[index]
34
34
 
35
- if possible_struct[:name] != interface_struct[:name]
35
+ if possible_struct.name != interface_struct.name
36
36
  raise Composer::ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
37
37
  "#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
38
38
  end
39
39
 
40
- if possible_struct[:null] && !interface_struct[:null]
40
+ if possible_struct.null? && interface_struct.non_null?
41
41
  raise Composer::ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
42
42
  "#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
43
43
  end
@@ -430,21 +430,21 @@ module GraphQL
430
430
  raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
431
431
  end
432
432
 
433
- if alt_structure.last[:name] != basis_structure.last[:name]
433
+ if alt_structure.last.name != basis_structure.last.name
434
434
  raise ComposerError, "Cannot compose mixed types at `#{path}`."
435
435
  end
436
436
  end
437
437
 
438
438
  type = GraphQL::Schema::BUILT_IN_TYPES.fetch(
439
- basis_structure.last[:name],
440
- build_type_binding(basis_structure.last[:name])
439
+ basis_structure.last.name,
440
+ build_type_binding(basis_structure.last.name)
441
441
  )
442
442
 
443
443
  basis_structure.reverse!.each_with_index do |basis, index|
444
444
  rev_index = basis_structure.length - index - 1
445
- non_null = alt_structures.each_with_object([!basis[:null]]) { |s, m| m << !s[rev_index][:null] }
445
+ non_null = alt_structures.each_with_object([basis.non_null?]) { |s, m| m << s[rev_index].non_null? }
446
446
 
447
- type = type.to_list_type if basis[:list]
447
+ type = type.to_list_type if basis.list?
448
448
  type = type.to_non_null_type if argument_name ? non_null.any? : non_null.all?
449
449
  end
450
450
 
@@ -509,15 +509,15 @@ module GraphQL
509
509
  end
510
510
 
511
511
  @boundary_map[impl_type_name] ||= []
512
- @boundary_map[impl_type_name] << {
513
- "location" => location,
514
- "type_name" => impl_type_name,
515
- "key" => key_selections[0].name,
516
- "field" => field_candidate.name,
517
- "arg" => argument_name,
518
- "list" => boundary_structure.first[:list],
519
- "federation" => kwargs[:federation],
520
- }.compact
512
+ @boundary_map[impl_type_name] << Boundary.new(
513
+ location: location,
514
+ type_name: impl_type_name,
515
+ key: key_selections[0].name,
516
+ field: field_candidate.name,
517
+ arg: argument_name,
518
+ list: boundary_structure.first.list?,
519
+ federation: kwargs[:federation],
520
+ )
521
521
  end
522
522
  end
523
523
  end
@@ -10,13 +10,13 @@ module GraphQL
10
10
 
11
11
  def fetch(ops)
12
12
  origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
13
- origin_set = op["path"].reduce([@executor.data]) do |set, path_segment|
13
+ origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
14
14
  set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
15
15
  end
16
16
 
17
- if op["if_type"]
17
+ if op.if_type
18
18
  # operations planned around unused fragment conditions should not trigger requests
19
- origin_set.select! { _1["_STITCH_typename"] == op["if_type"] }
19
+ origin_set.select! { _1[SelectionHint.typename_node.alias] == op.if_type }
20
20
  end
21
21
 
22
22
  memo[op] = origin_set if origin_set.any?
@@ -34,7 +34,7 @@ module GraphQL
34
34
  @executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
35
35
  end
36
36
 
37
- ops.map { origin_sets_by_operation[_1] ? _1["order"] : nil }
37
+ ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
38
38
  end
39
39
 
40
40
  # Builds batched boundary queries
@@ -47,21 +47,21 @@ module GraphQL
47
47
  def build_document(origin_sets_by_operation, operation_name = nil)
48
48
  variable_defs = {}
49
49
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
50
- variable_defs.merge!(op["variables"])
51
- boundary = op["boundary"]
50
+ variable_defs.merge!(op.variables)
51
+ boundary = op.boundary
52
52
 
53
- if boundary["list"]
53
+ if boundary.list
54
54
  input = origin_set.each_with_index.reduce(String.new) do |memo, (origin_obj, index)|
55
55
  memo << "," if index > 0
56
- memo << build_key(boundary["key"], origin_obj, federation: boundary["federation"])
56
+ memo << build_key(boundary.key, origin_obj, federation: boundary.federation)
57
57
  memo
58
58
  end
59
59
 
60
- "_#{batch_index}_result: #{boundary["field"]}(#{boundary["arg"]}:[#{input}]) #{op["selections"]}"
60
+ "_#{batch_index}_result: #{boundary.field}(#{boundary.arg}:[#{input}]) #{op.selections}"
61
61
  else
62
62
  origin_set.map.with_index do |origin_obj, index|
63
- input = build_key(boundary["key"], origin_obj, federation: boundary["federation"])
64
- "_#{batch_index}_#{index}_result: #{boundary["field"]}(#{boundary["arg"]}:#{input}) #{op["selections"]}"
63
+ input = build_key(boundary.key, origin_obj, federation: boundary.federation)
64
+ "_#{batch_index}_#{index}_result: #{boundary.field}(#{boundary.arg}:#{input}) #{op.selections}"
65
65
  end
66
66
  end
67
67
  end
@@ -71,7 +71,7 @@ module GraphQL
71
71
  if operation_name
72
72
  doc << " #{operation_name}"
73
73
  origin_sets_by_operation.each_key do |op|
74
- doc << "_#{op["order"]}"
74
+ doc << "_#{op.step}"
75
75
  end
76
76
  end
77
77
 
@@ -86,9 +86,9 @@ module GraphQL
86
86
  end
87
87
 
88
88
  def build_key(key, origin_obj, federation: false)
89
- key_value = JSON.generate(origin_obj["_STITCH_#{key}"])
89
+ key_value = JSON.generate(origin_obj[SelectionHint.key(key)])
90
90
  if federation
91
- "{ __typename: \"#{origin_obj["_STITCH_typename"]}\", #{key}: #{key_value} }"
91
+ "{ __typename: \"#{origin_obj[SelectionHint.typename_node.alias]}\", #{key}: #{key_value} }"
92
92
  else
93
93
  key_value
94
94
  end
@@ -12,8 +12,8 @@ module GraphQL
12
12
  op = ops.first # There should only ever be one per location at a time
13
13
 
14
14
  query_document = build_document(op, @executor.request.operation_name)
15
- query_variables = @executor.request.variables.slice(*op["variables"].keys)
16
- result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
15
+ query_variables = @executor.request.variables.slice(*op.variables.keys)
16
+ result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request.context)
17
17
  @executor.query_count += 1
18
18
 
19
19
  @executor.data.merge!(result["data"]) if result["data"]
@@ -22,25 +22,25 @@ module GraphQL
22
22
  @executor.errors.concat(result["errors"])
23
23
  end
24
24
 
25
- ops.map { op["order"] }
25
+ ops.map(&:step)
26
26
  end
27
27
 
28
28
  # Builds root source documents
29
29
  # "query MyOperation_1($var:VarType) { rootSelections ... }"
30
30
  def build_document(op, operation_name = nil)
31
31
  doc = String.new
32
- doc << op["operation_type"]
32
+ doc << op.operation_type
33
33
 
34
34
  if operation_name
35
- doc << " #{operation_name}_#{op["order"]}"
35
+ doc << " #{operation_name}_#{op.step}"
36
36
  end
37
37
 
38
- if op["variables"].any?
39
- variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
38
+ if op.variables.any?
39
+ variable_defs = op.variables.map { |k, v| "$#{k}:#{v}" }.join(",")
40
40
  doc << "(#{variable_defs})"
41
41
  end
42
42
 
43
- doc << op["selections"]
43
+ doc << op.selections
44
44
  doc
45
45
  end
46
46
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  def initialize(supergraph:, request:, plan:, nonblocking: false)
12
12
  @supergraph = supergraph
13
13
  @request = request
14
- @queue = plan["ops"]
14
+ @queue = plan.ops
15
15
  @data = {}
16
16
  @errors = []
17
17
  @query_count = 0
@@ -47,8 +47,8 @@ module GraphQL
47
47
 
48
48
  @dataloader.append_job do
49
49
  tasks = @queue
50
- .select { next_ordinals.include?(_1["after"]) }
51
- .group_by { [_1["location"], _1["boundary"].nil?] }
50
+ .select { next_ordinals.include?(_1.after) }
51
+ .group_by { [_1.location, _1.boundary.nil?] }
52
52
  .map do |(location, root_source), ops|
53
53
  if root_source
54
54
  @dataloader.with(RootSource, self, location).request_all(ops)
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ class Plan
6
+ Op = Struct.new(
7
+ :step,
8
+ :after,
9
+ :location,
10
+ :operation_type,
11
+ :selections,
12
+ :variables,
13
+ :path,
14
+ :if_type,
15
+ :boundary,
16
+ keyword_init: true
17
+ ) do
18
+ def as_json
19
+ {
20
+ step: step,
21
+ after: after,
22
+ location: location,
23
+ operation_type: operation_type,
24
+ selections: selections,
25
+ variables: variables,
26
+ path: path,
27
+ if_type: if_type,
28
+ boundary: boundary&.as_json
29
+ }.tap(&:compact!)
30
+ end
31
+ end
32
+
33
+ class << self
34
+ def from_json(json)
35
+ ops = json["ops"]
36
+ ops = ops.map do |op|
37
+ boundary = op["boundary"]
38
+ Op.new(
39
+ step: op["step"],
40
+ after: op["after"],
41
+ location: op["location"],
42
+ operation_type: op["operation_type"],
43
+ selections: op["selections"],
44
+ variables: op["variables"],
45
+ path: op["path"],
46
+ if_type: op["if_type"],
47
+ boundary: boundary ? GraphQL::Stitching::Boundary.new(**boundary) : nil,
48
+ )
49
+ end
50
+ new(ops: ops)
51
+ end
52
+ end
53
+
54
+ attr_reader :ops
55
+
56
+ def initialize(ops: [])
57
+ @ops = ops
58
+ end
59
+
60
+ def as_json
61
+ { ops: @ops.map(&:as_json) }
62
+ end
63
+ end
64
+ end
65
+ end