graph_weaver 0.4.6 → 0.5.0
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/CHANGELOG.md +1314 -0
- data/CLAUDE.md +100 -8
- data/DECISIONS.md +309 -0
- data/Gemfile.lock +23 -23
- data/NOTES.md +5 -5
- data/PLAN.md +106 -135
- data/README.md +115 -96
- data/REVIEW.md +946 -0
- data/docs/cassettes.md +75 -48
- data/docs/editors.md +82 -0
- data/docs/errors.md +32 -30
- data/docs/federation.md +520 -48
- data/docs/generated_modules.md +352 -137
- data/docs/getting_started.md +237 -67
- data/docs/logging.md +35 -6
- data/docs/real_world.md +21 -15
- data/docs/scalars.md +49 -154
- data/docs/testing.md +299 -52
- data/docs/transports.md +129 -30
- data/docs/upgrading.md +112 -0
- data/graph_weaver.gemspec +3 -1
- data/lib/generators/graph_weaver/install_generator.rb +259 -0
- data/lib/graph_weaver/client.rb +114 -111
- data/lib/graph_weaver/codegen/aliases.rb +217 -0
- data/lib/graph_weaver/codegen/emit.rb +272 -258
- data/lib/graph_weaver/codegen/enum_type.rb +27 -124
- data/lib/graph_weaver/codegen/nodes.rb +72 -13
- data/lib/graph_weaver/codegen/scalar_type.rb +68 -66
- data/lib/graph_weaver/codegen/type_helpers.rb +142 -0
- data/lib/graph_weaver/codegen.rb +593 -334
- data/lib/graph_weaver/errors.rb +127 -10
- data/lib/graph_weaver/federation.rb +272 -0
- data/lib/graph_weaver/hints.rb +9 -1
- data/lib/graph_weaver/in_process.rb +90 -0
- data/lib/graph_weaver/input_struct.rb +14 -2
- data/lib/graph_weaver/logging.rb +29 -0
- data/lib/graph_weaver/parsing.rb +67 -0
- data/lib/graph_weaver/query_module.rb +55 -0
- data/lib/graph_weaver/railtie.rb +23 -1
- data/lib/graph_weaver/representation.rb +74 -0
- data/lib/graph_weaver/response.rb +7 -0
- data/lib/graph_weaver/retry.rb +29 -8
- data/lib/graph_weaver/rspec.rb +214 -16
- data/lib/graph_weaver/schema_loader.rb +794 -59
- data/lib/graph_weaver/schemas.rb +46 -0
- data/lib/graph_weaver/selection.rb +43 -8
- data/lib/graph_weaver/tasks.rb +216 -21
- data/lib/graph_weaver/testing/cassette.rb +160 -61
- data/lib/graph_weaver/testing/coverage.rb +165 -0
- data/lib/graph_weaver/testing/failure.rb +10 -23
- data/lib/graph_weaver/testing/fake_client.rb +181 -21
- data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
- data/lib/graph_weaver/testing/router.rb +1431 -0
- data/lib/graph_weaver/testing/subgraphs.rb +130 -0
- data/lib/graph_weaver/testing.rb +204 -14
- data/lib/graph_weaver/transport/faraday.rb +28 -10
- data/lib/graph_weaver/transport/http.rb +99 -36
- data/lib/graph_weaver/transport.rb +67 -14
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +389 -170
- metadata +20 -3
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
require "graphql"
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
|
+
require_relative "../parsing"
|
|
8
|
+
|
|
7
9
|
# A fake client that fabricates schema-correct responses for whatever query
|
|
8
10
|
# arrives — the zero-setup way to test code built on generated modules:
|
|
9
11
|
#
|
|
10
|
-
# fake = GraphWeaver::Testing::FakeClient.new
|
|
11
|
-
# result = PersonQuery.execute!(id: "1"
|
|
12
|
+
# fake = GraphWeaver::Testing::FakeClient.new
|
|
13
|
+
# result = PersonQuery.execute!(client: fake, id: "1")
|
|
12
14
|
# result.person.name # => a plausible String, typed and castable
|
|
13
15
|
#
|
|
14
16
|
# Values are type-correct by construction (real enum values, valid
|
|
@@ -18,15 +20,33 @@ require "json"
|
|
|
18
20
|
#
|
|
19
21
|
# overrides: pin fields by GraphQL name — schema vocabulary, so keys
|
|
20
22
|
# survive query refactors. "Type.field" beats "field"; values are
|
|
21
|
-
# literals or zero-arg procs.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
23
|
+
# literals or zero-arg procs. Keys are checked against the schema, since
|
|
24
|
+
# a typo'd one would pin nothing and leave the test green. (An override
|
|
25
|
+
# with a wrong-typed value is also the way to simulate a corrupt
|
|
26
|
+
# payload — casting raises GraphWeaver::TypeError.)
|
|
24
27
|
#
|
|
25
28
|
# FakeClient.new(schema:, overrides: {
|
|
26
29
|
# "Person.name" => "Daniel",
|
|
27
30
|
# "email" => -> { "test@example.com" },
|
|
28
31
|
# })
|
|
29
32
|
#
|
|
33
|
+
# An override pins a whole subtree as readily as a leaf, and **merges**
|
|
34
|
+
# rather than replaces: name the fields the test is about and the rest of
|
|
35
|
+
# the selection is still fabricated. A list pins its own length, so "two
|
|
36
|
+
# orders, the first one paid" is the literal thing you write:
|
|
37
|
+
#
|
|
38
|
+
# FakeClient.new(schema:, overrides: {
|
|
39
|
+
# "Reader.name" => "Ada",
|
|
40
|
+
# "Reader.orders" => [{ "status" => "PAID" }, {}],
|
|
41
|
+
# })
|
|
42
|
+
#
|
|
43
|
+
# Keys inside a pinned subtree are response keys — what comes back on the
|
|
44
|
+
# wire, aliases included — and one the query doesn't select is refused,
|
|
45
|
+
# for the same reason a typo'd coordinate is.
|
|
46
|
+
#
|
|
47
|
+
# requests: every execute, in order ({ query:, variables:, operation_name: })
|
|
48
|
+
# — "did we send the right variables", and "did we call it at all".
|
|
49
|
+
#
|
|
30
50
|
# Partial failures: fail_at simulates a field-level error with
|
|
31
51
|
# spec-correct null propagation — the field's error lands in the errors
|
|
32
52
|
# array (with its concrete path), the field becomes null, and nulls
|
|
@@ -45,19 +65,39 @@ require "json"
|
|
|
45
65
|
#
|
|
46
66
|
# FakeClient.new(schema:, corrupt: "Person.birthday")
|
|
47
67
|
#
|
|
48
|
-
# seed: makes a run reproducible (also seeds faker).
|
|
49
|
-
#
|
|
68
|
+
# seed: makes a run reproducible (also seeds faker). Every option,
|
|
69
|
+
# schema: included, falls back to GraphWeaver::Testing.config — and the
|
|
70
|
+
# config's schema falls back to the committed dump.
|
|
50
71
|
class GraphWeaver::Testing::FakeClient
|
|
72
|
+
include GraphWeaver::Parsing
|
|
51
73
|
include GraphWeaver::Selection
|
|
52
74
|
|
|
53
75
|
# sentinel: a simulated failure bubbling up to the nearest nullable spot
|
|
54
76
|
NULL_BUBBLE = Object.new.freeze
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
# sentinel: no override here — distinct from an override OF nil, which
|
|
79
|
+
# pins the field null
|
|
80
|
+
UNPINNED = Object.new.freeze
|
|
81
|
+
private_constant :UNPINNED
|
|
82
|
+
|
|
83
|
+
# the schema responses are fabricated against — the way to reach it from
|
|
84
|
+
# a graphql: :fake spec, where GraphWeaver.client is one of these
|
|
85
|
+
attr_reader :schema
|
|
86
|
+
|
|
87
|
+
# Every execute, in order: { query:, variables:, operation_name: }.
|
|
88
|
+
#
|
|
89
|
+
# expect(fake.requests.size).to eq 1 # memoized, then
|
|
90
|
+
# expect(fake.requests.last[:variables]).to eq({ "id" => "1" })
|
|
91
|
+
attr_reader :requests
|
|
92
|
+
|
|
93
|
+
def initialize(schema: nil, overrides: {}, seed: nil, mode: nil, list_size: nil, null_chance: nil,
|
|
57
94
|
errors: nil, fail_at: nil, corrupt: nil)
|
|
58
95
|
config = GraphWeaver::Testing.config
|
|
59
|
-
@schema = schema
|
|
96
|
+
@schema = schema || config.schema || raise(GraphWeaver::Error,
|
|
97
|
+
"no schema to fake against — set GraphWeaver::Testing.config.schema, pass schema:, " \
|
|
98
|
+
"or commit a schema dump at #{GraphWeaver.schema_path}")
|
|
60
99
|
@overrides = config.overrides.merge(overrides)
|
|
100
|
+
GraphWeaver::Testing.validate_overrides!(@schema, @overrides)
|
|
61
101
|
@values = GraphWeaver::Testing::Values.new(seed:, mode:)
|
|
62
102
|
@list_size = list_size || config.list_size
|
|
63
103
|
@null_chance = null_chance || config.null_chance
|
|
@@ -65,9 +105,25 @@ class GraphWeaver::Testing::FakeClient
|
|
|
65
105
|
@extra_errors = wrap(errors).map { |error| normalize_error(error) }
|
|
66
106
|
@fail_at = wrap(fail_at).map { |spec| normalize_fail_spec(spec) }
|
|
67
107
|
@corrupt = wrap(corrupt)
|
|
108
|
+
@requests = []
|
|
68
109
|
end
|
|
69
110
|
|
|
70
|
-
|
|
111
|
+
# operation_name: is accepted for contract parity and ignored — one
|
|
112
|
+
# document holds one operation, so there is nothing to select between
|
|
113
|
+
# (see Selection#load_operation).
|
|
114
|
+
def execute(query, variables: {}, operation_name: nil)
|
|
115
|
+
# recorded before validation: "we never called it" and "we called it with
|
|
116
|
+
# a query that doesn't compile" are different failures
|
|
117
|
+
@requests << { query:, variables:, operation_name: }.freeze
|
|
118
|
+
|
|
119
|
+
# Validate first, as the other clients in the slot do: a field the schema
|
|
120
|
+
# doesn't have would otherwise walk into `get_field(...).type` on nil, and
|
|
121
|
+
# a NoMethodError from inside the fabricator is undiagnosable next to the
|
|
122
|
+
# "Did you mean" a real server gives. This is the commonest mistake there
|
|
123
|
+
# is — a query drifting ahead of the dump, or a typo in an ad-hoc one.
|
|
124
|
+
invalid = @schema.validate(GraphQL.parse(query))
|
|
125
|
+
return { "data" => nil, "errors" => invalid.map(&:to_h) } if invalid.any?
|
|
126
|
+
|
|
71
127
|
operation = load_operation(query)
|
|
72
128
|
root_type = operation_root_type(operation)
|
|
73
129
|
|
|
@@ -84,6 +140,27 @@ class GraphWeaver::Testing::FakeClient
|
|
|
84
140
|
response
|
|
85
141
|
end
|
|
86
142
|
|
|
143
|
+
# One fabricated object of `type_name` for these selections — the seam
|
|
144
|
+
# {FakeSubgraph} answers a federation `_entities` fetch through, where the
|
|
145
|
+
# representation names the type and the document only ever reached it
|
|
146
|
+
# through an inline fragment.
|
|
147
|
+
def object(type_name, selections, fragments: {})
|
|
148
|
+
type = @schema.get_type(type_name) or
|
|
149
|
+
raise GraphWeaver::Error, "#{type_name} is not a type of this schema"
|
|
150
|
+
|
|
151
|
+
@fragments = fragments
|
|
152
|
+
@path = []
|
|
153
|
+
@failures = []
|
|
154
|
+
value = object_value(type, selections)
|
|
155
|
+
value.equal?(NULL_BUBBLE) ? nil : value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The Selection mixin's walk is machinery, not interface: a developer who
|
|
159
|
+
# types `.methods` on a fake should find execute, schema, requests, object
|
|
160
|
+
# and parse, not the twelve steps behind them.
|
|
161
|
+
private :load_operation, :operation_root_type, :each_field, :gather,
|
|
162
|
+
:gather_conditional, :applies?, :conditional?
|
|
163
|
+
|
|
87
164
|
private
|
|
88
165
|
|
|
89
166
|
def rng = @values.rng
|
|
@@ -104,14 +181,24 @@ class GraphWeaver::Testing::FakeClient
|
|
|
104
181
|
spec.is_a?(String) ? { "path" => spec } : JSON.parse(JSON.generate(spec))
|
|
105
182
|
end
|
|
106
183
|
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
# pins: the response keys an override pinned at this object, merged in as
|
|
185
|
+
# the walk reaches them — everything it doesn't name is fabricated.
|
|
186
|
+
def object_value(type, selections, pins: nil, source: nil)
|
|
109
187
|
# gather (not each_field) so a field selected twice — `a { x } a { y }` —
|
|
110
188
|
# fabricates the MERGED shape codegen's struct expects, not last-writer-wins
|
|
111
|
-
gather(type, selections)
|
|
189
|
+
fields = gather(type, selections)
|
|
190
|
+
check_pins!(fields, pins, source) if pins
|
|
191
|
+
|
|
192
|
+
result = {}
|
|
193
|
+
fields.each do |key, nodes|
|
|
112
194
|
node = nodes.first
|
|
195
|
+
pin = (pins && pins.key?(key)) ? pins[key] : UNPINNED
|
|
113
196
|
@path.push(key)
|
|
114
|
-
value = node.name == "__typename"
|
|
197
|
+
value = if node.name == "__typename"
|
|
198
|
+
type.graphql_name
|
|
199
|
+
else
|
|
200
|
+
field_value(type, node, nodes.flat_map(&:selections), pin, source)
|
|
201
|
+
end
|
|
115
202
|
@path.pop
|
|
116
203
|
|
|
117
204
|
if value.equal?(NULL_BUBBLE)
|
|
@@ -132,7 +219,7 @@ class GraphWeaver::Testing::FakeClient
|
|
|
132
219
|
@schema.get_field(type.graphql_name, node.name).type.kind.name == "NON_NULL"
|
|
133
220
|
end
|
|
134
221
|
|
|
135
|
-
def field_value(parent_type, node, selections)
|
|
222
|
+
def field_value(parent_type, node, selections, pin = UNPINNED, source = nil)
|
|
136
223
|
if (spec = matching_failure)
|
|
137
224
|
@failures << {
|
|
138
225
|
"message" => spec["message"] || "simulated failure",
|
|
@@ -143,19 +230,92 @@ class GraphWeaver::Testing::FakeClient
|
|
|
143
230
|
return NULL_BUBBLE
|
|
144
231
|
end
|
|
145
232
|
|
|
146
|
-
|
|
147
|
-
|
|
233
|
+
coordinate = "#{parent_type.graphql_name}.#{node.name}"
|
|
234
|
+
if pin.equal?(UNPINNED)
|
|
235
|
+
# most specific key wins; #fetch (not #[]) so an override OF nil pins null
|
|
236
|
+
pin = @overrides.fetch(coordinate) { @overrides.fetch(node.name, UNPINNED) }
|
|
237
|
+
source = @overrides.key?(coordinate) ? coordinate : node.name unless pin.equal?(UNPINNED)
|
|
148
238
|
end
|
|
149
|
-
return override.is_a?(Proc) ? override.call : override unless override.nil?
|
|
150
239
|
|
|
151
240
|
field_type = @schema.get_field(parent_type.graphql_name, node.name).type
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
end
|
|
241
|
+
return pinned_value(field_type, node, selections, pin, source) unless pin.equal?(UNPINNED)
|
|
242
|
+
return corrupt_value(field_type) if @corrupt.include?(coordinate)
|
|
155
243
|
|
|
156
244
|
type_value(field_type, node, selections)
|
|
157
245
|
end
|
|
158
246
|
|
|
247
|
+
# What an override pins here. A leaf takes the value outright; a composite
|
|
248
|
+
# MERGES — the keys it names are pinned and the rest of the selection is
|
|
249
|
+
# fabricated, so pinning one nested field never means hand-writing the
|
|
250
|
+
# subtree around it. A pinned list is exactly as long as it is written.
|
|
251
|
+
def pinned_value(type, node, selections, value, source)
|
|
252
|
+
value = value.call if value.is_a?(Proc)
|
|
253
|
+
|
|
254
|
+
case type.kind.name
|
|
255
|
+
when "NON_NULL" then pinned_value(type.of_type, node, selections, value, source)
|
|
256
|
+
when "LIST"
|
|
257
|
+
return value unless value.is_a?(Array)
|
|
258
|
+
|
|
259
|
+
value.each_with_index.map do |element, index|
|
|
260
|
+
@path.push(index)
|
|
261
|
+
begin
|
|
262
|
+
pinned_value(type.of_type, node, selections, element, source)
|
|
263
|
+
ensure
|
|
264
|
+
@path.pop
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
when "OBJECT", "UNION", "INTERFACE"
|
|
268
|
+
return value unless value.is_a?(Hash)
|
|
269
|
+
|
|
270
|
+
object_value(pinned_type(type, value, source), selections, pins: value, source:)
|
|
271
|
+
else
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The concrete type a pinned object is fabricated as. At a union or
|
|
277
|
+
# interface the pin has to say: picking a member at random would fabricate
|
|
278
|
+
# a shape the pinned keys don't fit, in whichever fraction of runs the
|
|
279
|
+
# seed lands there.
|
|
280
|
+
def pinned_type(type, value, source)
|
|
281
|
+
named = value["__typename"]
|
|
282
|
+
return type if named == type.graphql_name
|
|
283
|
+
|
|
284
|
+
members = (type.kind.name == "OBJECT") ? [type] : @schema.possible_types(type)
|
|
285
|
+
if named.nil?
|
|
286
|
+
return type if members.one?
|
|
287
|
+
|
|
288
|
+
raise GraphWeaver::Error, "override #{source.inspect} pins an object at #{location}, where " \
|
|
289
|
+
"the query can return #{members.map(&:graphql_name).sort.join(" or ")} — name the one you " \
|
|
290
|
+
"mean with \"__typename\"."
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
found = members.find { |member| member.graphql_name == named }
|
|
294
|
+
return found if found
|
|
295
|
+
|
|
296
|
+
raise GraphWeaver::Error, "override #{source.inspect} pins __typename #{named.inspect} at " \
|
|
297
|
+
"#{location}, where the query can only return #{members.map(&:graphql_name).sort.join(" or ")}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# A pinned key the query doesn't select would fabricate the field anyway
|
|
301
|
+
# and quietly leave the pin unread — the same silent-green failure a typo'd
|
|
302
|
+
# coordinate is validated against, one level down.
|
|
303
|
+
def check_pins!(fields, pins, source)
|
|
304
|
+
# __typename in a pin names the type to fabricate (see #pinned_type); it
|
|
305
|
+
# is an instruction, not a field the query has to have selected
|
|
306
|
+
unknown = pins.keys.reject { |key| key == "__typename" || fields.key?(key) }
|
|
307
|
+
return if unknown.empty?
|
|
308
|
+
|
|
309
|
+
suggestion = GraphWeaver.did_you_mean(fields.keys, unknown.first.to_s)
|
|
310
|
+
hint = suggestion ? " — did you mean #{suggestion.inspect}?" : "."
|
|
311
|
+
raise GraphWeaver::Error, "override #{source.inspect} supplies #{unknown.first.inspect} at " \
|
|
312
|
+
"#{location}, which this query doesn't select#{hint} An override's keys are response keys, " \
|
|
313
|
+
"exactly as they arrive on the wire (#{fields.keys.join(", ")})."
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# where the walk is, for a message: "reader.orders.0"
|
|
317
|
+
def location = @path.empty? ? "the root" : @path.join(".")
|
|
318
|
+
|
|
159
319
|
# a value casting can't accept, derived from the field's own type — and
|
|
160
320
|
# wrapped per list layer so the corruption lands on the element cast
|
|
161
321
|
def corrupt_value(type)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "graphql"
|
|
5
|
+
|
|
6
|
+
require_relative "fake_client"
|
|
7
|
+
|
|
8
|
+
module GraphWeaver
|
|
9
|
+
module Testing
|
|
10
|
+
# A subgraph this process doesn't serve, answered with fabricated data.
|
|
11
|
+
#
|
|
12
|
+
# Composed into the supergraph but routed to another service, a subgraph
|
|
13
|
+
# has no schema class here, so by default {Router} refuses any query that
|
|
14
|
+
# reaches its fields. `subgraphs: { "reviews" => :fake }` swaps that
|
|
15
|
+
# refusal for schema-correct fabricated data — mid-migration that's often
|
|
16
|
+
# what you want: the fields exist, the shapes are right, and nothing has
|
|
17
|
+
# to be stood up to exercise the rest of the graph.
|
|
18
|
+
#
|
|
19
|
+
# It satisfies the same contract every other subgraph does, including the
|
|
20
|
+
# `_entities(representations:)` fetch a boundary crossing sends. Each
|
|
21
|
+
# representation names its own `__typename`, so the entity comes back as
|
|
22
|
+
# that type — a `_Entity` union faked the ordinary way would answer as a
|
|
23
|
+
# random member and match nothing the query asked for.
|
|
24
|
+
#
|
|
25
|
+
# Fabricated data is invented data, and a green test against invented
|
|
26
|
+
# data is worse than a red one — so {Router} marks every fetch that came
|
|
27
|
+
# from here (`faked: true` in #trace) and logs it at :warn.
|
|
28
|
+
class FakeSubgraph
|
|
29
|
+
# the subgraph being faked, as the supergraph names it
|
|
30
|
+
attr_reader :name
|
|
31
|
+
|
|
32
|
+
# the schema responses are fabricated against — the composed API
|
|
33
|
+
# schema, which carries every type this subgraph can be asked for
|
|
34
|
+
attr_reader :schema
|
|
35
|
+
|
|
36
|
+
def initialize(name, schema, **options)
|
|
37
|
+
@name = name
|
|
38
|
+
@schema = schema
|
|
39
|
+
@client = FakeClient.new(schema:, **options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspect = "#<#{self.class.name} #{@name.inspect}>"
|
|
43
|
+
alias to_s inspect
|
|
44
|
+
|
|
45
|
+
# context: is accepted for contract parity and ignored — there are no
|
|
46
|
+
# resolvers here to receive one.
|
|
47
|
+
def execute(query, variables: {}, operation_name: nil, context: nil)
|
|
48
|
+
document = GraphQL.parse(query)
|
|
49
|
+
entities = entities_field(document)
|
|
50
|
+
return @client.execute(query, variables:, operation_name:) unless entities
|
|
51
|
+
|
|
52
|
+
{ "data" => { "_entities" => entities_value(entities, document, variables) } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# the router sends _entities as the operation's only root field
|
|
58
|
+
def entities_field(document)
|
|
59
|
+
operation = document.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).first
|
|
60
|
+
operation&.selections&.find do |node|
|
|
61
|
+
node.is_a?(GraphQL::Language::Nodes::Field) && node.name == "_entities"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# one object per representation, in order and as the type it names —
|
|
66
|
+
# which is the contract _entities answers on
|
|
67
|
+
def entities_value(field, document, variables)
|
|
68
|
+
fragments = document.definitions
|
|
69
|
+
.grep(GraphQL::Language::Nodes::FragmentDefinition).to_h { |node| [node.name, node] }
|
|
70
|
+
|
|
71
|
+
representations(variables).map do |representation|
|
|
72
|
+
type_name = representation["__typename"] or raise GraphWeaver::Error,
|
|
73
|
+
"a representation sent to #{@name} carries no __typename: #{representation.inspect}"
|
|
74
|
+
|
|
75
|
+
@client.object(type_name, field.selections, fragments:)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def representations(variables)
|
|
80
|
+
found = variables.to_h.transform_keys(&:to_s)["representations"]
|
|
81
|
+
(found || []).map { |representation| representation.to_h.transform_keys(&:to_s) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|