graphql-stitching 1.0.0 → 1.0.1

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: 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