graph_weaver 0.7.4 → 0.7.5

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.
@@ -57,6 +57,11 @@ class GraphWeaver::Internal::Values
57
57
  # registration says the server writes "12.5".
58
58
  WIRE = [NilClass, TrueClass, FalseClass, Integer, Float, String, Symbol, Array, Hash].freeze
59
59
 
60
+ # Whether a value is already one of those. At a leaf it means the registry's
61
+ # serializer has nothing to do; at a composite position (FakeClient) it means
62
+ # the pin stands as written.
63
+ def self.wire?(value) = WIRE.any? { |klass| value.is_a?(klass) }
64
+
60
65
  # The fallback, for a scalar nobody registered: its prop is T.untyped, so
61
66
  # anything holds and a plausible shape beats a placeholder.
62
67
  NAMED_SHAPES = {
@@ -146,10 +151,10 @@ class GraphWeaver::Internal::Values
146
151
  # as written. Shared with the object-pin door, so both read a pin the same
147
152
  # way.
148
153
  def wire(type_name, value, coordinate = nil)
149
- return value if WIRE.any? { |klass| value.is_a?(klass) }
154
+ return value if self.class.wire?(value)
150
155
 
151
156
  serialized = @registry.scalar(type_name, coordinate).serialize_value(value)
152
- return serialized if WIRE.any? { |klass| serialized.is_a?(klass) }
157
+ return serialized if self.class.wire?(serialized)
153
158
 
154
159
  article = GraphWeaver::Internal::Util.article(value.class.to_s)
155
160
  raise GraphWeaver::Error, "the pin for #{type_name.inspect} is #{article} #{value.class}, and a pin " \
@@ -18,6 +18,11 @@ module GraphWeaver
18
18
  # lexical scope, so a private constant would be unreachable from exactly
19
19
  # the files that need it. The name and the surface lock carry the rule.
20
20
  module Internal
21
+ # The wire value the member register_enum fallback: true adds to a
22
+ # generated enum serializes to. The GraphQL spec reserves a leading `__`,
23
+ # so no schema can declare a value that collides with it.
24
+ ENUM_FALLBACK_WIRE = "__other__"
25
+
21
26
  # Odds and ends several files share. Each is here because more than one
22
27
  # caller needs it, not because it belongs together with the others.
23
28
  module Util
@@ -50,6 +55,18 @@ module GraphWeaver
50
55
  # "a" or "an" for a word an error message is about to name.
51
56
  def article(word) = word.downcase.start_with?(/[aeiou]/) ? "an" : "a"
52
57
 
58
+ # how many entries a message names before it says "and N more"
59
+ SAMPLE = 5
60
+ private_constant :SAMPLE
61
+
62
+ # A list a message names inline, held to a readable length — a wall
63
+ # of schema coordinates says less than the first few and a count.
64
+ def sample(list)
65
+ return list.join(", ") if list.size <= SAMPLE
66
+
67
+ "#{list.first(SAMPLE).join(", ")} and #{list.size - SAMPLE} more"
68
+ end
69
+
53
70
  # The module a .graphql file generates, and the basename of the file
54
71
  # it generates into: the camelized file name plus the operation's own
55
72
  # word. Every run of non-alphanumerics in the name is a word boundary,
@@ -101,7 +101,11 @@ module GraphWeaver
101
101
  # about the key the value arrived under, so it reads a filtered key one
102
102
  # level in as safe; this scrubs at every depth, like #value. The key is
103
103
  # optional because a coercer refusing a value hasn't been told one.
104
- def shown(raw, key = nil) = filtered?(key) ? FILTERED : cap(value(key, raw).inspect)
104
+ def shown(raw, key = nil) = filtered?(key) ? FILTERED : cap(spell(value(key, raw)))
105
+
106
+ # How a value reads inside a sentence. inspect, except that
107
+ # BigDecimal#inspect is scientific ("0.25e1" for the 2.5 a caller wrote).
108
+ def spell(value) = defined?(BigDecimal) && value.is_a?(BigDecimal) ? value.to_s("F") : value.inspect
105
109
 
106
110
  # A short server-chosen string the library republishes inside its own
107
111
  # text — the APM's :code, the [CODE] in the one line info writes, a
@@ -215,36 +219,13 @@ module GraphWeaver
215
219
  end
216
220
 
217
221
  # What a Retry has already spent, read by the attempt it is about
218
- # to make. A dynamic extent rather than a global: the count is only
219
- # visible while the call it describes is on the stack, so a client
220
- # that never reaches instrument can't leave a stale one behind.
221
- def with_retries(count)
222
- return yield unless GraphWeaver.instrumenter
223
-
224
- previous = Thread.current[RETRIES]
225
- Thread.current[RETRIES] = count
226
- begin
227
- yield
228
- ensure
229
- Thread.current[RETRIES] = previous
230
- end
231
- end
222
+ # to make.
223
+ def with_retries(count, &block) = during(RETRIES, count, &block)
232
224
 
233
225
  # The graph a generated module is dispatching, read by the request it
234
- # is about to make. Same dynamic extent as with_retries, for the same
235
- # reason and instrument clears it for the duration of the request it
236
- # labels, so exactly one request wears the label.
237
- def with_graph(name)
238
- return yield unless GraphWeaver.instrumenter
239
-
240
- previous = Thread.current[GRAPH]
241
- Thread.current[GRAPH] = name
242
- begin
243
- yield
244
- ensure
245
- Thread.current[GRAPH] = previous
246
- end
247
- end
226
+ # is about to make and instrument clears it for the duration of the
227
+ # request it labels, so exactly one request wears the label.
228
+ def with_graph(name, &block) = during(GRAPH, name, &block)
248
229
 
249
230
  # The variables as one JSON line for a log: filtered, and unable to
250
231
  # raise. A value with no JSON form (NaN, binary) is the caller's bug
@@ -278,6 +259,22 @@ module GraphWeaver
278
259
 
279
260
  private
280
261
 
262
+ # One fiber-local, set for the length of one call. A dynamic extent
263
+ # rather than a global: the value is only visible while the call it
264
+ # describes is on the stack, so a client that never reaches instrument
265
+ # can't leave a stale one behind.
266
+ def during(key, value)
267
+ return yield unless GraphWeaver.instrumenter
268
+
269
+ previous = Thread.current[key]
270
+ Thread.current[key] = value
271
+ begin
272
+ yield
273
+ ensure
274
+ Thread.current[key] = previous
275
+ end
276
+ end
277
+
281
278
  # The GraphQL errors a response carries, whatever answered it — a
282
279
  # Hash from a transport, a graphql-ruby Result in-process, a fake.
283
280
  # Never raises: an instrumenter that decides which exception a
@@ -7,8 +7,6 @@ require_relative "internal"
7
7
  require_relative "internal/test_clients"
8
8
 
9
9
  module GraphWeaver
10
- # Called by generated code — not semver'd for direct use.
11
- #
12
10
  # Runtime for generated query modules: the client plumbing, which is the
13
11
  # one part of a generated module that carries no per-query type
14
12
  # information — every module's copy was identical. `extend
@@ -16,6 +14,13 @@ module GraphWeaver
16
14
  # stay generated, since their sigs are the query's types and those are the
17
15
  # point.
18
16
  #
17
+ # It is also the type every generated module satisfies, so code that takes
18
+ # any of them says `GraphWeaver::QueryModule` and reads `query_string` /
19
+ # `operation_name` with a sig behind each — rather than `const_get(:QUERY)`
20
+ # on a Module, which is what rubocop-sorbet forbids (ConstantsFromStrings,
21
+ # and ForbidTUnsafe for the T.unsafe that gets around it). Those readers
22
+ # and `client` are the supported surface; the rest is generated code's.
23
+ #
19
24
  # Resolution order, per the docs: per call → a test mode's stand-in
20
25
  # (Internal::TestClients) → the client the module's graph names →
21
26
  # `GraphWeaver.client`. A module has no fifth slot you can set: a parsed
@@ -32,6 +37,18 @@ module GraphWeaver
32
37
  @client || default_client
33
38
  end
34
39
 
40
+ # The operation, verbatim — what goes on the wire as `query`.
41
+ sig { returns(String) }
42
+ def query_string
43
+ T.unsafe(self).const_get(:QUERY)
44
+ end
45
+
46
+ # What goes on the wire as `operationName`; nil for an anonymous operation.
47
+ sig { returns(T.nilable(String)) }
48
+ def operation_name
49
+ T.unsafe(self).const_get(:OPERATION_NAME)
50
+ end
51
+
35
52
  private
36
53
 
37
54
  # Bound by GraphWeaver.parse, which is the only caller: a parsed module
@@ -59,12 +76,10 @@ module GraphWeaver
59
76
  # comes through here.)
60
77
  GraphWeaver::Internal::Wire.check_variables!(variables)
61
78
 
62
- mod = T.unsafe(self)
63
79
  # the graph codegen baked in, never one inferred from the client — a
64
80
  # wrong label on a request is worse than no label
65
81
  GraphWeaver::Internal::Log.with_graph(graph_name) do
66
- client_for(client).execute(mod.const_get(:QUERY), variables:,
67
- operation_name: mod.const_get(:OPERATION_NAME))
82
+ client_for(client).execute(query_string, variables:, operation_name:)
68
83
  end
69
84
  end
70
85
 
@@ -35,7 +35,7 @@ class GraphWeaver::Railtie < Rails::Railtie
35
35
  KEYS = %i[watch].freeze
36
36
 
37
37
  def method_missing(name, *args)
38
- key = name.to_s.delete_suffix("=").delete_suffix("?").delete_suffix("!").to_sym
38
+ key = setting(name)
39
39
  return super if KEYS.include?(key)
40
40
 
41
41
  raise ArgumentError, refusal(key)
@@ -53,11 +53,16 @@ class GraphWeaver::Railtie < Rails::Railtie
53
53
  alias_method :store, :[]=
54
54
 
55
55
  def respond_to_missing?(name, _private = false)
56
- KEYS.include?(name.to_s.delete_suffix("=").delete_suffix("?").delete_suffix("!").to_sym)
56
+ KEYS.include?(setting(name))
57
57
  end
58
58
 
59
59
  private
60
60
 
61
+ # the setting a reader, writer or predicate is about
62
+ def setting(name)
63
+ name.to_s.delete_suffix("=").delete_suffix("?").delete_suffix("!").to_sym
64
+ end
65
+
61
66
  def refusal(key)
62
67
  near = GraphWeaver::Internal::Util.did_you_mean(KEYS.map(&:to_s), key.to_s)
63
68
  fix =
@@ -592,7 +592,6 @@ module GraphWeaver
592
592
  "may itself be a proc, so it can vary per request."
593
593
  end
594
594
  private_class_method :wire_context!
595
-
596
595
  end
597
596
  end
598
597
  end
@@ -887,9 +887,10 @@ module GraphWeaver::SchemaLoader
887
887
  return recompose_hint(path) if composed_dump?(path)
888
888
 
889
889
  missing = path ? "#{path} records no source url" : "no schema dump at #{GraphWeaver.schema_path}"
890
- "#{missing} — pass one: rake graph_weaver:schema:refresh URL=https://api.example.com/graphql " \
891
- "(if this app serves the schema itself, point GraphWeaver.client at the class and the dump is " \
892
- "rebuilt from it see docs/getting_started.md#your-apps-own-schema-in-process)"
890
+ "#{missing} — pass one: rake graph_weaver:schema:refresh " \
891
+ "URL=https://api.example.com/graphql, or point the graph's client at the server an app " \
892
+ "that serves the schema itself points GraphWeaver.client at the class and the dump is built " \
893
+ "from that (docs/getting_started.md#your-apps-own-schema-in-process)"
893
894
  end
894
895
  private_class_method :refresh_hint
895
896
 
@@ -922,11 +923,9 @@ module GraphWeaver::SchemaLoader
922
923
  # ends up honoured in some places and not others.
923
924
  def self.source_transport(path)
924
925
  meta = provenance(path)
925
- unless meta&.key?("url")
926
- raise GraphWeaver::Error,
927
- "#{path} records no source url — it wasn't introspected from one. Pass transport:, " \
928
- "or rebuild it from the schema class that produced it."
929
- end
926
+ # the same sentence refresh! gives: this is reached by typing a rake task
927
+ # (schema:diff), and `transport:` is not something a rake user can pass
928
+ raise GraphWeaver::Error, refresh_hint(path) unless meta&.key?("url")
930
929
 
931
930
  GraphWeaver.new(meta["url"], auth: ENV[auth_env(path)]).transport
932
931
  end
@@ -170,6 +170,30 @@ module GraphWeaver
170
170
  }.filter_map { |kind, names| " #{kind}: #{names.sort.join(", ")}" if names.any? }
171
171
  end
172
172
 
173
+ # What `queries:check` just answered. It re-introspects the url a dump
174
+ # records and asks a live class directly; anything else it checks as it
175
+ # stands on disk, which is `verify`'s question — and both exited 0
176
+ # saying "against the schema".
177
+ def self.validated
178
+ dumps = GraphWeaver.graphs.filter_map { |graph| as_committed(graph) }
179
+ return "every query validates against the schema" if dumps.empty?
180
+
181
+ "every query validates against #{dumps.join(", ")} as committed — not the server " \
182
+ "(rake graph_weaver:schema:diff asks whether the server moved)"
183
+ end
184
+
185
+ # A graph whose queries were checked against the file rather than the
186
+ # server: a graph that names its own schema is checked against exactly
187
+ # that, and a dump with no recorded url has nothing to re-read.
188
+ def self.as_committed(graph)
189
+ path = graph.dump_path
190
+ return unless path && graph.live_schema.nil?
191
+ return if !graph.named_schema? && GraphWeaver::SchemaLoader.provenance(path)&.key?("url")
192
+
193
+ GraphWeaver::Internal::Util.relative(path)
194
+ end
195
+ private_class_method :as_committed
196
+
173
197
  # Neither task that needs the committed dump can take one itself, so both
174
198
  # say which task can — the same sentence SchemaLoader gives on refresh.
175
199
  def self.no_dump
@@ -184,7 +208,23 @@ module GraphWeaver
184
208
  # rewrites it from the graph's source, `diff` says how far that source
185
209
  # has drifted from it, whichever the source is. A graph whose schema IS
186
210
  # a live class reads no dump at all, so it has neither.
187
- def self.dumps = GraphWeaver.graphs.map { |graph| [graph, graph.dump_path, graph.dump_source] }
211
+ #
212
+ # The dump's own record of where it came from first, then the client the
213
+ # graph's modules already call: a dump inherited with no provenance, or a
214
+ # schema kept by hand, still has a server behind it, and refusing one
215
+ # took every other graph's refresh down with it.
216
+ def self.dumps
217
+ GraphWeaver.graphs.map { |graph| [graph, graph.dump_path, graph.dump_source || graph.client_url] }
218
+ end
219
+
220
+ # A dump with no recorded url whose graph names no client: nothing
221
+ # behind the file to re-read, so the file is the schema. Said and
222
+ # stepped over rather than refused — the task's job is the graphs it
223
+ # CAN refresh.
224
+ def self.no_source(path)
225
+ "#{GraphWeaver::Internal::Util.relative(path)} records no source url and the graph names " \
226
+ "no client — left as checked in"
227
+ end
188
228
 
189
229
  # A graph that generates straight from a schema class has no dump
190
230
  # between the code and the output — so there is nothing here to
@@ -202,6 +242,19 @@ module GraphWeaver
202
242
  "#{whose} generates from #{source_name(source)} directly — no dump to keep in step"
203
243
  end
204
244
 
245
+ # What `diff` re-introspects for one graph. A schema class answers
246
+ # introspection itself. A url the dump recorded is left to
247
+ # SchemaLoader.diff, which builds the transport and so honours the
248
+ # auth_env the dump named. A source that came from the graph's client
249
+ # instead is that client's own transport — headers, auth and all.
250
+ def self.diff_transport(graph, source)
251
+ return source if source.is_a?(Module)
252
+ return if graph.dump_source
253
+
254
+ client = graph.client || GraphWeaver.client
255
+ client.respond_to?(:transport) ? client.transport : client
256
+ end
257
+
205
258
  # How a dump's source reads in a report: a url as itself, a schema
206
259
  # class by name.
207
260
  def self.source_name(source) = source.is_a?(Module) ? GraphWeaver::SchemaLoader.endpoint(source) : source
@@ -350,9 +403,8 @@ namespace :graph_weaver do
350
403
  # dump between the code and the output, so nothing can be stale
351
404
  next puts GraphWeaver::Internal::Tasks.no_dump_needed(graph, source) unless path
352
405
 
353
- # a schema class answers introspection itself; left nil, diff builds
354
- # the dump's own transport, auth and all
355
- diff = GraphWeaver::SchemaLoader.diff(path, transport: (source if source.is_a?(Module)))
406
+ diff = GraphWeaver::SchemaLoader.diff(path,
407
+ transport: GraphWeaver::Internal::Tasks.diff_transport(graph, source))
356
408
  dump = GraphWeaver::Internal::Util.relative(path)
357
409
  next puts "#{dump} matches #{GraphWeaver::Internal::Tasks.source_name(source)}" if diff.empty?
358
410
 
@@ -398,6 +450,9 @@ namespace :graph_weaver do
398
450
  # below and refresh! bootstraps its first dump (or says how).
399
451
  path ||= graph.named_dump_path
400
452
  next puts GraphWeaver::Internal::Tasks.no_dump_needed(graph, source) if !path && graph.named_schema?
453
+ # a supergraph has no source by construction — nothing serves one —
454
+ # and refresh! answers that with "recompose", which is not a skip
455
+ next puts GraphWeaver::Internal::Tasks.no_source(path) if path && !source && !graph.supergraph
401
456
 
402
457
  written, from = GraphWeaver::SchemaLoader.refresh!(url: (source unless source.is_a?(Module)),
403
458
  schema: (source if source.is_a?(Module)), path:)
@@ -425,7 +480,7 @@ namespace :graph_weaver do
425
480
  # block-buffered stdout, so a piped CI log shows the verdict first
426
481
  $stdout.flush
427
482
  abort "#{failures.size} invalid #{(failures.size == 1) ? "query" : "queries"}" if failures.any?
428
- puts "every query validates against the schema"
483
+ puts GraphWeaver::Internal::Tasks.validated
429
484
  end
430
485
  end
431
486
 
@@ -137,12 +137,6 @@ class GraphWeaver::Testing::FakeClient
137
137
  null_chance: nil, errors: nil, fail_at: nil, corrupt: nil,
138
138
  }.freeze
139
139
 
140
- # JSON's own types are already on the wire: at a leaf they skip the
141
- # registry's serializer (Values#wire), and at a composite position (a Hash
142
- # aside, which is response keys) they pin the field as written — nil is
143
- # null, the rest is the corrupt payload the example asked for.
144
- WIRE = GraphWeaver::Internal::Values::WIRE
145
-
146
140
  # Methods every Ruby object answers aren't fields: a schema does have a
147
141
  # `hash` or a `count`, and a Struct answers both with plausible nonsense
148
142
  # where fabricating is right.
@@ -151,7 +145,7 @@ class GraphWeaver::Testing::FakeClient
151
145
  # The scalars the GraphQL spec serializes as JSON strings, whatever Ruby
152
146
  # holds them.
153
147
  STRING_SCALARS = %w[ID String].freeze
154
- private_constant :OPTIONS, :WIRE, :RUBY_OWN, :STRING_SCALARS
148
+ private_constant :OPTIONS, :RUBY_OWN, :STRING_SCALARS
155
149
 
156
150
  def initialize(pins = {}, **options)
157
151
  config = GraphWeaver::Testing.config
@@ -467,7 +461,9 @@ class GraphWeaver::Testing::FakeClient
467
461
  # reads them off — a FactoryBot build, a model, a Struct. Either way it
468
462
  # MERGES: what it doesn't answer is fabricated.
469
463
  def pinned_object(type, selections, value, source)
470
- return value if !value.is_a?(Hash) && wire?(value)
464
+ # what JSON already holds is the pin as written — nil is null, and the rest
465
+ # is the corrupt payload the example asked for (a Hash is response keys)
466
+ return value if !value.is_a?(Hash) && GraphWeaver::Internal::Values.wire?(value)
471
467
 
472
468
  concrete = pinned_type(type, value, source)
473
469
  pins = value.is_a?(Hash) ? value : read_fields(concrete, selections, value)
@@ -497,8 +493,6 @@ class GraphWeaver::Testing::FakeClient
497
493
  object.respond_to?(name) && !RUBY_OWN.include?(object.method(name).owner)
498
494
  end
499
495
 
500
- def wire?(value) = WIRE.any? { |klass| value.is_a?(klass) }
501
-
502
496
  # An object pin holds Ruby values — a Time, a Money, a T::Enum — where the
503
497
  # wire holds what the registration says they serialize to. A value that
504
498
  # is already wire-shaped is taken as written.
@@ -326,11 +326,14 @@ module GraphWeaver
326
326
 
327
327
  plan = @planner.plan(document, operation_name:)
328
328
  return introspect(query, variables, plan.operation_name) if plan.introspection
329
+ # read once, so every hop runs as the identity the query started
330
+ # with, whatever writes #context= while it is in flight
331
+ context = Internal::Util.context!(@context)
329
332
  # one subgraph answers the whole thing: hand it the document as
330
333
  # written, so nothing is rewritten that didn't have to be
331
- return fetch(plan.entry, query, variables, plan.operation_name) if plan.verbatim
334
+ return fetch(plan.entry, query, variables, plan.operation_name, context) if plan.verbatim
332
335
 
333
- run(plan, variables)
336
+ run(plan, variables, context)
334
337
  end
335
338
 
336
339
  # never leak the context (tokens, current_user) through logs or errors
@@ -374,7 +377,7 @@ module GraphWeaver
374
377
 
375
378
  # ---- execution ----------------------------------------------------
376
379
 
377
- def run(plan, variables)
380
+ def run(plan, variables, context)
378
381
  errors = []
379
382
  data = {}
380
383
  # An operation's declared defaults are part of the variables, and
@@ -384,7 +387,7 @@ module GraphWeaver
384
387
  .merge(variables.to_h { |name, value| [name.to_s, value] })
385
388
 
386
389
  plan.steps.each do |step|
387
- result = fetch_step(step, plan.operation, given)
390
+ result = fetch_step(step, plan.operation, given, context)
388
391
  Array(result["errors"]).each { |error| errors << rewrite(error, step.subgraph) }
389
392
  payload = result["data"]
390
393
  if payload.nil?
@@ -397,7 +400,7 @@ module GraphWeaver
397
400
  end
398
401
  end
399
402
 
400
- plan.steps.each { |step| stitch(step, [[data, []]], plan.operation, given, errors) }
403
+ plan.steps.each { |step| stitch(step, [[data, []]], plan.operation, given, context, errors) }
401
404
 
402
405
  # A stitched fetch can leave a null where the composed schema says
403
406
  # non-null, and nothing re-applies GraphQL's propagation rules over a
@@ -443,7 +446,7 @@ module GraphWeaver
443
446
  # Everything the plan applies at this level: one _entities fetch per
444
447
  # subgraph the level defers to (all nodes at once — _entities answers
445
448
  # in representation order), then the same again one level down.
446
- def stitch(step, nodes, operation, variables, errors)
449
+ def stitch(step, nodes, operation, variables, context, errors)
447
450
  return if nodes.empty?
448
451
 
449
452
  # An abstract position: the plan holds one branch per concrete type
@@ -454,12 +457,12 @@ module GraphWeaver
454
457
  if step.is_a?(Internal::Planner::Branches)
455
458
  step.steps.each do |type_name, branch|
456
459
  stitch(branch, nodes.select { |(node, _)| node[TYPENAME] == type_name },
457
- operation, variables, errors)
460
+ operation, variables, context, errors)
458
461
  end
459
462
  return
460
463
  end
461
464
 
462
- blocked = prefetch(step, nodes, operation, variables, errors)
465
+ blocked = prefetch(step, nodes, operation, variables, context, errors)
463
466
 
464
467
  # A fetch for a selection the operation excluded is a fetch a real
465
468
  # router never makes, and `trace` is something specs assert on. The
@@ -478,7 +481,8 @@ module GraphWeaver
478
481
 
479
482
  entities = []
480
483
  if fetched.any?
481
- result = entities_fetch(target, step.type_name, deferrals.map(&:node), representations, operation, variables)
484
+ result = entities_fetch(target, step.type_name, deferrals.map(&:node), representations, operation,
485
+ variables, context)
482
486
  entities = result.dig("data", "_entities") || []
483
487
  Array(result["errors"]).each { |error| errors << rewrite(error, target, fetched) }
484
488
  end
@@ -502,12 +506,12 @@ module GraphWeaver
502
506
  deferrals.each do |deferral|
503
507
  next unless deferral.step
504
508
 
505
- stitch(deferral.step, descend(nodes, deferral.response_key), operation, variables, errors)
509
+ stitch(deferral.step, descend(nodes, deferral.response_key), operation, variables, context, errors)
506
510
  end
507
511
  end
508
512
 
509
513
  step.children.each do |key, child|
510
- stitch(child, descend(nodes, key), operation, variables, errors)
514
+ stitch(child, descend(nodes, key), operation, variables, context, errors)
511
515
  end
512
516
 
513
517
  nodes.each { |(node, _)| strip!(node, step) }
@@ -517,7 +521,7 @@ module GraphWeaver
517
521
  # keys before the fetch whose representation carries them. Returns the
518
522
  # nodes the holding subgraph didn't recognize: their required fields
519
523
  # don't exist, so nothing depending on them can resolve.
520
- def prefetch(step, nodes, operation, variables, errors)
524
+ def prefetch(step, nodes, operation, variables, context, errors)
521
525
  blocked = []
522
526
  # the field it feeds was excluded, so this is a fetch a real router
523
527
  # never makes — and a test double that runs a resolver production
@@ -533,7 +537,7 @@ module GraphWeaver
533
537
  selections = Internal::Planner.injected_selections(group.flat_map(&:paths).uniq)
534
538
  roots = selections.map(&:alias)
535
539
 
536
- result = entities_fetch(subgraph, step.type_name, selections, representations, operation, variables)
540
+ result = entities_fetch(subgraph, step.type_name, selections, representations, operation, variables, context)
537
541
  entities = result.dig("data", "_entities") || []
538
542
  Array(result["errors"]).each { |error| errors << rewrite(error, subgraph, nodes) }
539
543
 
@@ -621,16 +625,16 @@ module GraphWeaver
621
625
  Array(path).map { |segment| segment.is_a?(String) ? segment.delete_prefix(PREFIX) : segment }
622
626
  end
623
627
 
624
- def fetch_step(step, operation, variables)
628
+ def fetch_step(step, operation, variables, context)
625
629
  document = GraphQL::Language::Nodes::OperationDefinition.new(
626
630
  operation_type: operation.operation_type || "query",
627
631
  variables: used_variables(step.selections, operation),
628
632
  selections: step.selections,
629
633
  )
630
- run_subgraph(step.subgraph, document, variables)
634
+ run_subgraph(step.subgraph, document, variables, context)
631
635
  end
632
636
 
633
- def entities_fetch(subgraph, type_name, nodes, representations, operation, variables)
637
+ def entities_fetch(subgraph, type_name, nodes, representations, operation, variables, context)
634
638
  entities = GraphQL::Language::Nodes::Field.new(
635
639
  name: "_entities",
636
640
  arguments: [GraphQL::Language::Nodes::Argument.new(
@@ -647,7 +651,7 @@ module GraphWeaver
647
651
  variables: [REPRESENTATIONS_DEFINITION] + used_variables(nodes, operation),
648
652
  selections: [entities],
649
653
  )
650
- run_subgraph(subgraph, document, variables.merge(REPRESENTATIONS => representations))
654
+ run_subgraph(subgraph, document, variables.merge(REPRESENTATIONS => representations), context)
651
655
  end
652
656
 
653
657
  REPRESENTATIONS = "representations"
@@ -682,12 +686,12 @@ module GraphWeaver
682
686
  end
683
687
  end
684
688
 
685
- def run_subgraph(subgraph, document, variables)
689
+ def run_subgraph(subgraph, document, variables, context)
686
690
  declared = document.variables.map(&:name)
687
- fetch(subgraph, document.to_query_string, variables.slice(*declared), nil)
691
+ fetch(subgraph, document.to_query_string, variables.slice(*declared), nil, context)
688
692
  end
689
693
 
690
- def fetch(name, query, variables, operation_name)
694
+ def fetch(name, query, variables, operation_name, context)
691
695
  faked = @faked.include?(name)
692
696
  entry = { subgraph: name, query:, variables: variables.to_h }
693
697
  entry[:faked] = true if faked
@@ -701,14 +705,12 @@ module GraphWeaver
701
705
  end
702
706
 
703
707
  GraphWeaver::Internal::Log.log(:debug) do
704
- "router -> #{name} #{tag} variables=#{JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))}
705
- " \
708
+ "router -> #{name} #{tag} variables=#{JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))}\n" \
706
709
  "#{GraphWeaver::Internal::Wire.truncate_for_log(query)}"
707
710
  end
708
711
 
709
712
  GraphWeaver::Internal::Log.log_timed(:debug, "router -> #{name} #{tag} completed") do
710
- @subgraphs.fetch(name).execute(query, variables:, operation_name:,
711
- context: Internal::Util.context!(@context)).to_h
713
+ @subgraphs.fetch(name).execute(query, variables:, operation_name:, context:).to_h
712
714
  end
713
715
  end
714
716
 
@@ -783,7 +785,6 @@ module GraphWeaver
783
785
  end
784
786
  ordered
785
787
  end
786
-
787
788
  end
788
789
  end
789
790
  end
@@ -352,7 +352,6 @@ module GraphWeaver
352
352
  def runnable(schema)
353
353
  schema if schema.is_a?(Class) && schema <= GraphQL::Schema
354
354
  end
355
-
356
355
  end
357
356
 
358
357
  class << self
@@ -476,7 +475,7 @@ module GraphWeaver
476
475
  end
477
476
  return if back == sample
478
477
 
479
- "#{name}: round-trips lossily — sent #{sample.inspect}, got back #{back.inspect}"
478
+ "#{name}: round-trips lossily — sent #{Internal::Redact.spell(sample)}, got back #{Internal::Redact.spell(back)}"
480
479
  end
481
480
 
482
481
  # The registration's `cast:`, RUN rather than emitted. A cast builds
@@ -1,3 +1,3 @@
1
1
  module GraphWeaver
2
- VERSION = "0.7.4"
2
+ VERSION = "0.7.5"
3
3
  end