graph_weaver 0.6.1 → 0.7.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 +4 -4
- data/Gemfile +8 -0
- data/Gemfile.lock +153 -4
- data/README.md +45 -79
- data/docs/alternatives.md +195 -0
- data/docs/cassettes.md +61 -50
- data/docs/editors.md +32 -47
- data/docs/errors.md +360 -103
- data/docs/federation.md +692 -473
- data/docs/generated_modules.md +441 -314
- data/docs/getting_started.md +370 -194
- data/docs/i18n.md +171 -0
- data/docs/logging.md +197 -50
- data/docs/real_world.md +42 -27
- data/docs/scalars.md +307 -176
- data/docs/testing.md +473 -220
- data/docs/transports.md +224 -151
- data/docs/upgrading.md +258 -305
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +19 -3
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +69 -11
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +406 -195
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +284 -46
- data/lib/graph_weaver/federation.rb +129 -27
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +27 -15
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +80 -0
- data/lib/graph_weaver/internal/headers.rb +70 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +249 -14
- data/lib/graph_weaver/log_subscriber.rb +74 -0
- data/lib/graph_weaver/logging.rb +163 -19
- data/lib/graph_weaver/query_module.rb +44 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +45 -13
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +266 -56
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +34 -10
- data/lib/graph_weaver/testing/endpoint.rb +107 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +164 -45
- data/lib/graph_weaver/testing/router.rb +64 -13
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +48 -6
- data/lib/graph_weaver/transport.rb +134 -27
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +495 -106
- metadata +71 -3
- data/CHANGELOG.md +0 -2355
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time" # Time.httpdate, for Retry-After
|
|
5
|
+
|
|
6
|
+
module GraphWeaver
|
|
7
|
+
module Internal
|
|
8
|
+
# Response headers, as ServerError carries them. HTTP field names are
|
|
9
|
+
# case-insensitive (RFC 9110 §5.1) and the spelling a caller reaches for
|
|
10
|
+
# is the one the server sent ("Retry-After"), while transports store them
|
|
11
|
+
# downcased — so the name is folded on the way in and on the lookups that
|
|
12
|
+
# take one: #[], #fetch, #dig, #key?. Only lookup is forgiving: iteration,
|
|
13
|
+
# #keys and #to_h stay one spelling, which is what a log line and a doc
|
|
14
|
+
# can promise.
|
|
15
|
+
class Headers < Hash
|
|
16
|
+
# tells "no default given" from a default of nil
|
|
17
|
+
MISSING = Object.new
|
|
18
|
+
private_constant :MISSING
|
|
19
|
+
|
|
20
|
+
# A Hash of any casing as one of these; already one, untouched.
|
|
21
|
+
def self.wrap(headers)
|
|
22
|
+
return headers if headers.is_a?(self)
|
|
23
|
+
|
|
24
|
+
headers.each_with_object(new) { |(name, value), out| out.store(fold(name), value) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.fold(name) = name.to_s.downcase
|
|
28
|
+
|
|
29
|
+
def [](name) = super(Headers.fold(name))
|
|
30
|
+
|
|
31
|
+
# spelled out rather than forwarded: sorbet can't splat into Hash#fetch's
|
|
32
|
+
# overloads, and a block beats a default there (Ruby warns if given both)
|
|
33
|
+
def fetch(name, default = MISSING, &block)
|
|
34
|
+
folded = Headers.fold(name)
|
|
35
|
+
return super(folded, &block) if block || default.equal?(MISSING)
|
|
36
|
+
|
|
37
|
+
super(folded, default)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Hash#dig reads the slot directly rather than through #[], so the fold
|
|
41
|
+
# has to happen here; a header value is a String, so `rest` can only error
|
|
42
|
+
def dig(name, *rest)
|
|
43
|
+
value = self[name]
|
|
44
|
+
rest.empty? ? value : value&.dig(*rest)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Seconds to wait per the server's Retry-After, which is either a delay
|
|
48
|
+
# in seconds or an HTTP-date. nil when absent or unparseable; a date
|
|
49
|
+
# already past clamps to 0. See RFC 9110 §10.2.3.
|
|
50
|
+
#
|
|
51
|
+
# Here rather than on ServerError because a rate limit reaches a caller
|
|
52
|
+
# two ways — raised, and returned as the envelope a 4xx/5xx WITH a
|
|
53
|
+
# GraphQL errors body makes — and both have to read the one rule.
|
|
54
|
+
def retry_after
|
|
55
|
+
value = self["retry-after"]&.strip
|
|
56
|
+
return if value.nil? || value.empty?
|
|
57
|
+
return value.to_f if value.match?(/\A\d+(\.\d+)?\z/)
|
|
58
|
+
|
|
59
|
+
[Time.httpdate(value) - Time.now, 0.0].max
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def key?(name) = super(Headers.fold(name))
|
|
65
|
+
alias_method :has_key?, :key?
|
|
66
|
+
alias_method :include?, :key?
|
|
67
|
+
alias_method :member?, :key?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -10,6 +10,10 @@ module GraphWeaver
|
|
|
10
10
|
# suite-wide ones while the block that set them is still on the stack,
|
|
11
11
|
# and a fake built with its own checks them then.
|
|
12
12
|
module Overrides
|
|
13
|
+
# The key a Hash `list_size:` says its fallback under — every list it
|
|
14
|
+
# doesn't name.
|
|
15
|
+
LIST_SIZE_DEFAULT = "default"
|
|
16
|
+
|
|
13
17
|
class << self
|
|
14
18
|
# A pin key names something in the schema: a type ("Money",
|
|
15
19
|
# "Person"), a "Type.field" coordinate, or a bare field name
|
|
@@ -22,6 +26,44 @@ module GraphWeaver
|
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# Whether `key` reads as a reference into the schema rather than a
|
|
30
|
+
# plain word — what tells a pin from an option at a fake's door, where
|
|
31
|
+
# both arrive as the same keywords. A "Type.field" coordinate can only
|
|
32
|
+
# be a pin; a bare word is one when the schema knows it. Whether the
|
|
33
|
+
# reference RESOLVES is validate!'s question, so a coordinate naming
|
|
34
|
+
# no type is still a pin and gets that refusal rather than "unknown
|
|
35
|
+
# option".
|
|
36
|
+
#
|
|
37
|
+
# Casing can't decide it: Hasura's types are lowercase, and
|
|
38
|
+
# `pokemon_v2_pokemon` read as a misspelled option.
|
|
39
|
+
def schema_reference?(schema, key)
|
|
40
|
+
key = key.to_s
|
|
41
|
+
return true if key.include?(".") || key.start_with?("__")
|
|
42
|
+
|
|
43
|
+
!schema.get_type(key).nil? || field_names(schema).include?(key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Every name a pin may be keyed by — the dictionary a refusal guesses
|
|
47
|
+
# from when a key is neither a pin nor an option.
|
|
48
|
+
def pin_names(schema) = schema.types.keys + field_names(schema)
|
|
49
|
+
|
|
50
|
+
# A Hash `list_size:` sizes one list at a time, keyed the way a pin is
|
|
51
|
+
# minus the bare type name: a type says nothing about how long any one
|
|
52
|
+
# of its fields is.
|
|
53
|
+
def validate_list_size!(schema, list_size)
|
|
54
|
+
return unless list_size.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
list_size.each do |key, value|
|
|
57
|
+
unless value.is_a?(Integer) || value.is_a?(Range)
|
|
58
|
+
raise GraphWeaver::Error, "list_size: #{key.to_s.inspect} must be an Integer or a " \
|
|
59
|
+
"Range — how long an unbounded list is — got #{value.inspect}"
|
|
60
|
+
end
|
|
61
|
+
next if key.to_s == LIST_SIZE_DEFAULT
|
|
62
|
+
|
|
63
|
+
validate_field_key!(schema, key.to_s, "list_size: key")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
25
67
|
# A pin that's a proc is handed the seeded Random when it takes one
|
|
26
68
|
# and called bare when it doesn't, so a varying pin still reproduces
|
|
27
69
|
# under `rspec --seed`.
|
|
@@ -55,16 +97,36 @@ module GraphWeaver
|
|
|
55
97
|
|
|
56
98
|
# a leading capital names a type, as it does wherever a pin is written
|
|
57
99
|
dictionary = type_name.match?(/\A[A-Z]/) ? schema.types.keys : known
|
|
58
|
-
bad!(key, "matches no type or field in this schema", dictionary, type_name)
|
|
100
|
+
bad!("override key", key, "matches no type or field in this schema", dictionary, type_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
coordinate!(schema, "override key", key, type_name, field_name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A key naming one FIELD — a "Type.field" coordinate, or a bare field
|
|
107
|
+
# name matching that field on any type.
|
|
108
|
+
def validate_field_key!(schema, key, label)
|
|
109
|
+
type_name, field_name = key.split(".", 2)
|
|
110
|
+
return if (field_name || type_name).start_with?("__")
|
|
111
|
+
|
|
112
|
+
if field_name.nil?
|
|
113
|
+
known = field_names(schema)
|
|
114
|
+
return if known.include?(type_name)
|
|
115
|
+
|
|
116
|
+
bad!(label, key, "matches no field in this schema", known, type_name)
|
|
59
117
|
end
|
|
60
118
|
|
|
119
|
+
coordinate!(schema, label, key, type_name, field_name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def coordinate!(schema, label, key, type_name, field_name)
|
|
61
123
|
type = schema.get_type(type_name)
|
|
62
124
|
unless type.respond_to?(:fields)
|
|
63
|
-
bad!(key, "names no object type in this schema", schema.types.keys, type_name)
|
|
125
|
+
bad!(label, key, "names no object type in this schema", schema.types.keys, type_name)
|
|
64
126
|
end
|
|
65
127
|
return if type.fields.key?(field_name)
|
|
66
128
|
|
|
67
|
-
bad!(key, "is not a field of #{type_name}", type.fields.keys, field_name)
|
|
129
|
+
bad!(label, key, "is not a field of #{type_name}", type.fields.keys, field_name)
|
|
68
130
|
end
|
|
69
131
|
|
|
70
132
|
# A type pin says what every value of that type is, and the fake only
|
|
@@ -84,10 +146,10 @@ module GraphWeaver
|
|
|
84
146
|
"#{type.graphql_name}, and a pin fabricates a scalar, enum or object: #{advice}"
|
|
85
147
|
end
|
|
86
148
|
|
|
87
|
-
def bad!(key, problem, dictionary, term)
|
|
149
|
+
def bad!(label, key, problem, dictionary, term)
|
|
88
150
|
suggestion = Util.did_you_mean(dictionary, term)
|
|
89
151
|
hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
|
|
90
|
-
raise GraphWeaver::Error, "
|
|
152
|
+
raise GraphWeaver::Error, "#{label} #{key.inspect} #{problem}#{hint}"
|
|
91
153
|
end
|
|
92
154
|
|
|
93
155
|
# Every output field name in the schema — walked only when a bare key
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "graphql"
|
|
5
5
|
|
|
6
|
+
require_relative "selection"
|
|
6
7
|
require_relative "subgraphs"
|
|
7
8
|
|
|
8
9
|
module GraphWeaver
|
|
@@ -155,7 +156,20 @@ module GraphWeaver
|
|
|
155
156
|
.map { |error| Wire.graphql_error(error.message, "GRAPHQL_VALIDATION_FAILED") }
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
# @defer/@stream send the rest of the answer in later payloads over a
|
|
160
|
+
# multipart body; this router answers in one. Public and document-wide
|
|
161
|
+
# because the caller has to ask it BEFORE validation — the composed API
|
|
162
|
+
# schema usually doesn't declare the directives, so graphql-ruby's
|
|
163
|
+
# "Directive @defer is not defined" fires first and shadows this.
|
|
164
|
+
def refuse_incremental!(document)
|
|
165
|
+
node = Selection.incremental_directive(document) or return
|
|
166
|
+
|
|
167
|
+
refuse :incremental_delivery,
|
|
168
|
+
"this operation carries @#{node.name}, and the answer would arrive in more than one payload"
|
|
169
|
+
end
|
|
170
|
+
|
|
158
171
|
def plan(document, operation_name: nil)
|
|
172
|
+
refuse_incremental!(document)
|
|
159
173
|
operation = pick_operation(document, operation_name)
|
|
160
174
|
refuse(:operation_type, "this document is a subscription") if
|
|
161
175
|
operation.operation_type == "subscription"
|
|
@@ -178,7 +192,7 @@ module GraphWeaver
|
|
|
178
192
|
return plan
|
|
179
193
|
end
|
|
180
194
|
|
|
181
|
-
|
|
195
|
+
check_reachable!(root.graphql_name, selections, fragments)
|
|
182
196
|
|
|
183
197
|
entry = single_subgraph(root.graphql_name, selections, fragments)
|
|
184
198
|
if entry
|
|
@@ -223,13 +237,14 @@ module GraphWeaver
|
|
|
223
237
|
|
|
224
238
|
private
|
|
225
239
|
|
|
226
|
-
# Every
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
|
|
240
|
+
# Every position this operation reaches, refused if the supergraph says
|
|
241
|
+
# something about it no plan can honour — whatever plan it gets. It runs
|
|
242
|
+
# here, above the verbatim shortcut, because that is the only place
|
|
243
|
+
# every plan passes: a check that lives in {#plan_step} silently skips
|
|
244
|
+
# the query one subgraph answers whole. Asked per query rather than at
|
|
245
|
+
# construction — one such directive shouldn't cost you the queries that
|
|
246
|
+
# never touch it.
|
|
247
|
+
def check_reachable!(type_name, selections, fragments, depth = 0)
|
|
233
248
|
return if depth > MAX_DEPTH
|
|
234
249
|
|
|
235
250
|
selections.each do |node|
|
|
@@ -237,18 +252,20 @@ module GraphWeaver
|
|
|
237
252
|
when GraphQL::Language::Nodes::Field
|
|
238
253
|
next if node.name.start_with?("__")
|
|
239
254
|
|
|
255
|
+
contextual!(type_name, node)
|
|
256
|
+
progressive_override!(type_name, node)
|
|
240
257
|
child = raw_child_type(type_name, node.name) or next
|
|
241
258
|
interface_object!(child, "#{type_name}.#{node.name} returns #{child}")
|
|
242
|
-
|
|
259
|
+
check_reachable!(child, node.selections, fragments, depth + 1)
|
|
243
260
|
when GraphQL::Language::Nodes::InlineFragment
|
|
244
261
|
condition = node.type&.name || type_name
|
|
245
262
|
interface_object!(condition, "this operation selects ... on #{condition}")
|
|
246
|
-
|
|
263
|
+
check_reachable!(condition, node.selections, fragments, depth + 1)
|
|
247
264
|
when GraphQL::Language::Nodes::FragmentSpread
|
|
248
265
|
fragment = fragments[node.name] or next
|
|
249
266
|
condition = fragment.type.name
|
|
250
267
|
interface_object!(condition, "...#{node.name} is on #{condition}")
|
|
251
|
-
|
|
268
|
+
check_reachable!(condition, fragment.selections, fragments, depth + 1)
|
|
252
269
|
end
|
|
253
270
|
end
|
|
254
271
|
end
|
|
@@ -352,7 +369,6 @@ module GraphWeaver
|
|
|
352
369
|
|
|
353
370
|
here = step(subgraph, type_name)
|
|
354
371
|
selections.each do |node|
|
|
355
|
-
contextual!(type_name, node)
|
|
356
372
|
# a subtree that never leaves this subgraph goes over as written:
|
|
357
373
|
# the boundary rules govern stitching, so they have no business
|
|
358
374
|
# applying to a query that was never going to cross one
|
|
@@ -388,9 +404,9 @@ module GraphWeaver
|
|
|
388
404
|
|
|
389
405
|
# A @fromContext argument is filled by the GATEWAY, out of a selection on
|
|
390
406
|
# an ancestor — so a fetch this planner writes leaves it unset and the
|
|
391
|
-
# field resolves from nothing.
|
|
392
|
-
#
|
|
393
|
-
#
|
|
407
|
+
# field resolves from nothing. A subgraph's own resolver never fills one
|
|
408
|
+
# either, so handing it the subtree whole doesn't help: this is asked of
|
|
409
|
+
# every field the operation reaches, on every path.
|
|
394
410
|
def contextual!(type_name, node)
|
|
395
411
|
names = @table.field(type_name, node.name)&.contextual
|
|
396
412
|
return if names.nil? || names.empty?
|
|
@@ -400,6 +416,20 @@ module GraphWeaver
|
|
|
400
416
|
"on its own"
|
|
401
417
|
end
|
|
402
418
|
|
|
419
|
+
# A progressive @override(label:) leaves BOTH subgraphs resolving the
|
|
420
|
+
# field — the label is the rollout rule the gateway evaluates per
|
|
421
|
+
# request to pick between them. A plain @override drops the losing copy
|
|
422
|
+
# at composition, so there is nothing to decide and nothing to refuse;
|
|
423
|
+
# a labelled one is a coin only the gateway can toss.
|
|
424
|
+
def progressive_override!(type_name, node)
|
|
425
|
+
field = @table.field(type_name, node.name)
|
|
426
|
+
label = field&.override_label or return
|
|
427
|
+
|
|
428
|
+
refuse :progressive_override, "#{type_name}.#{node.name} is mid-rollout under " \
|
|
429
|
+
"@override(label: #{label.inspect}) — #{field.graphs.join(" and ")} both resolve it, " \
|
|
430
|
+
"and a local router can't evaluate a rollout percentage"
|
|
431
|
+
end
|
|
432
|
+
|
|
403
433
|
# The keys a fetch injects are stripped from the answer, so a caller's
|
|
404
434
|
# alias spelling one is stripped with it — silently, since the two are
|
|
405
435
|
# then indistinguishable. Asked of what this fetch actually injects
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module GraphWeaver
|
|
5
|
+
module Internal
|
|
6
|
+
# The verdict a coercion refusal already reached, riding on the plain
|
|
7
|
+
# ::TypeError / ::ArgumentError Coerce raises rather than on a class of
|
|
8
|
+
# its own — so `rescue ::TypeError` still catches it and Coerce keeps
|
|
9
|
+
# complaining the way Kernel#Integer does.
|
|
10
|
+
#
|
|
11
|
+
# The layers that brand a failure (InputStruct.field, Coerce.variable)
|
|
12
|
+
# read it off the exception instead of deriving a kind from the message.
|
|
13
|
+
# A message is prose; reading a verdict back out of one would be a guess,
|
|
14
|
+
# and a wrong `kind` is worse than none — the app will have translated it
|
|
15
|
+
# into a confident sentence.
|
|
16
|
+
module Refusal
|
|
17
|
+
attr_accessor :graph_weaver_kind, :graph_weaver_details
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Tag an exception with its verdict; returns it, ready to raise.
|
|
21
|
+
def brand(error, kind, **details)
|
|
22
|
+
error.extend(Refusal)
|
|
23
|
+
error.graph_weaver_kind = kind
|
|
24
|
+
error.graph_weaver_details = details
|
|
25
|
+
error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# What a raised exception says went wrong, as an InputError kind.
|
|
29
|
+
# Coerce's own refusals carry their verdict; an app's `cast:` or
|
|
30
|
+
# `serialize:` raises whatever it likes, and Ruby's own convention
|
|
31
|
+
# splits the two it raises for a value it can't use: TypeError means
|
|
32
|
+
# the CLASS was wrong, ArgumentError the CONTENT — which is the
|
|
33
|
+
# difference between "send something else" and "fix the text".
|
|
34
|
+
# Anything else is :refused rather than a guess at what it meant.
|
|
35
|
+
def kind_of(error)
|
|
36
|
+
return error.graph_weaver_kind if error.is_a?(Refusal)
|
|
37
|
+
|
|
38
|
+
case error
|
|
39
|
+
when ::TypeError then :type_mismatch
|
|
40
|
+
when ::ArgumentError then :unparseable
|
|
41
|
+
else :refused
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def details_of(error) = error.is_a?(Refusal) ? error.graph_weaver_details : {}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -27,20 +27,34 @@ module GraphWeaver
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Does this schema carry the coordinate — "Type", or "Type.field"?
|
|
30
|
-
def defines?(schema, coordinate)
|
|
31
|
-
type_name, field_name = coordinate.split(".", 2)
|
|
32
|
-
type = schema.get_type(type_name) or return false
|
|
33
|
-
return true unless field_name
|
|
34
|
-
|
|
35
|
-
return type.fields.key?(field_name) if type.respond_to?(:fields)
|
|
36
|
-
# an input object's members are arguments, not fields
|
|
37
|
-
return type.arguments.key?(field_name) if type.respond_to?(:arguments)
|
|
30
|
+
def defines?(schema, coordinate) = !member(schema, coordinate).nil?
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
# The type this schema gives "Type.field", printed the way SDL prints
|
|
33
|
+
# it — so it compares directly against a supergraph's own spelling.
|
|
34
|
+
# Presence is the cheaper question and answers a different one: a
|
|
35
|
+
# field that is still *there* can have been retyped underneath the
|
|
36
|
+
# composition, which reads as a match until the types are compared.
|
|
37
|
+
def signature(schema, coordinate)
|
|
38
|
+
member = member(schema, coordinate)
|
|
39
|
+
member.type.to_type_signature if member.respond_to?(:type)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
+
# The field (or, on an input object, the argument — its members are
|
|
45
|
+
# arguments, not fields) at "Type.field", or the type itself for a
|
|
46
|
+
# bare coordinate.
|
|
47
|
+
def member(schema, coordinate)
|
|
48
|
+
type_name, field_name = coordinate.split(".", 2)
|
|
49
|
+
type = schema.get_type(type_name) or return
|
|
50
|
+
return type unless field_name
|
|
51
|
+
|
|
52
|
+
return type.fields[field_name] if type.respond_to?(:fields)
|
|
53
|
+
return type.arguments[field_name] if type.respond_to?(:arguments)
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
44
58
|
def descendants(klass)
|
|
45
59
|
klass.subclasses.flat_map { |subclass| [subclass] + descendants(subclass) }
|
|
46
60
|
end
|
|
@@ -12,6 +12,40 @@ module GraphWeaver
|
|
|
12
12
|
module Selection
|
|
13
13
|
include Kernel # for sorbet: hosts are Objects
|
|
14
14
|
|
|
15
|
+
# The directives that ask for the answer in instalments. Neither the
|
|
16
|
+
# generated code nor the local router reads a multipart body, and both
|
|
17
|
+
# have to say so BY NAME: whether the schema in hand declares @defer is
|
|
18
|
+
# graphql-ruby's business, so "Directive @defer is not defined" is an
|
|
19
|
+
# accident that happens to refuse — and it stops happening the day a
|
|
20
|
+
# supergraph @links the defer spec.
|
|
21
|
+
INCREMENTAL = %w[defer stream].freeze
|
|
22
|
+
|
|
23
|
+
# The first @defer/@stream node anywhere in a parsed document, or nil.
|
|
24
|
+
# A module function rather than part of the walk below: the callers ask
|
|
25
|
+
# before they have a schema, an operation, or a host to walk with.
|
|
26
|
+
def self.incremental_directive(node)
|
|
27
|
+
if node.respond_to?(:directives)
|
|
28
|
+
# a spread carries directives and no selections (`...Frag @defer`),
|
|
29
|
+
# so this is asked of every node rather than only of the ones below
|
|
30
|
+
applied = node.directives.find { |d| INCREMENTAL.include?(d.name) }
|
|
31
|
+
return applied if applied
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
children = if node.is_a?(GraphQL::Language::Nodes::Document)
|
|
35
|
+
node.definitions
|
|
36
|
+
elsif node.respond_to?(:selections)
|
|
37
|
+
node.selections
|
|
38
|
+
else
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
children.each do |child|
|
|
43
|
+
nested = incremental_directive(child)
|
|
44
|
+
return nested if nested
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
15
49
|
# Every method here becomes an instance method of its host (Codegen,
|
|
16
50
|
# FakeClient, the cassette Anonymizer) — private so the walk stays the
|
|
17
51
|
# host's own business rather than part of its API.
|