graph_weaver 0.7.5 → 0.7.7
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.lock +43 -43
- data/docs/errors.md +7 -3
- data/docs/generated_modules.md +66 -16
- data/docs/getting_started.md +39 -19
- data/docs/logging.md +79 -35
- data/docs/migrating.md +11 -4
- data/docs/scalars.md +12 -3
- data/docs/testing.md +75 -13
- data/docs/upgrading.md +43 -3
- data/examples/README.md +4 -2
- data/examples/github/generate.rb +22 -8
- data/examples/github/generated/star_mutation.rb +2 -2
- data/examples/github/generated/stargazers_query.rb +2 -2
- data/examples/github/generated/starred_query.rb +2 -2
- data/examples/github/run.rb +1 -0
- data/examples/github/setup.rb +16 -8
- data/graph_weaver.gemspec +15 -6
- data/lib/generators/graph_weaver/install_generator.rb +20 -2
- data/lib/graph_weaver/client.rb +0 -23
- data/lib/graph_weaver/codegen/aliases.rb +15 -3
- data/lib/graph_weaver/codegen/emit.rb +7 -6
- data/lib/graph_weaver/codegen/enum_type.rb +26 -2
- data/lib/graph_weaver/codegen/nodes.rb +21 -2
- data/lib/graph_weaver/codegen.rb +106 -54
- data/lib/graph_weaver/graph.rb +67 -6
- data/lib/graph_weaver/in_process.rb +1 -1
- data/lib/graph_weaver/input_struct.rb +29 -4
- data/lib/graph_weaver/internal/overrides.rb +126 -14
- data/lib/graph_weaver/internal/test_clients.rb +29 -7
- data/lib/graph_weaver/internal/unused.rb +30 -11
- data/lib/graph_weaver/internal/values.rb +17 -5
- data/lib/graph_weaver/internal.rb +23 -6
- data/lib/graph_weaver/log_subscriber.rb +27 -17
- data/lib/graph_weaver/logging.rb +89 -53
- data/lib/graph_weaver/parsing.rb +32 -3
- data/lib/graph_weaver/query_module.rb +48 -8
- data/lib/graph_weaver/railtie.rb +250 -11
- data/lib/graph_weaver/rspec.rb +41 -17
- data/lib/graph_weaver/schema_diff.rb +24 -5
- data/lib/graph_weaver/schema_loader.rb +22 -9
- data/lib/graph_weaver/tasks.rb +71 -44
- data/lib/graph_weaver/testing/fake_client.rb +24 -21
- data/lib/graph_weaver/testing.rb +27 -7
- data/lib/graph_weaver/transport.rb +1 -1
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +76 -43
- metadata +2 -2
data/lib/graph_weaver/tasks.rb
CHANGED
|
@@ -170,9 +170,9 @@ 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
|
|
174
|
-
#
|
|
175
|
-
# stands on disk, which is `verify`'s question — and both exited 0
|
|
173
|
+
# What `queries:check` just answered. It re-introspects the source behind
|
|
174
|
+
# a dump and asks a live class directly; a graph with neither it checks
|
|
175
|
+
# as it stands on disk, which is `verify`'s question — and both exited 0
|
|
176
176
|
# saying "against the schema".
|
|
177
177
|
def self.validated
|
|
178
178
|
dumps = GraphWeaver.graphs.filter_map { |graph| as_committed(graph) }
|
|
@@ -183,14 +183,11 @@ module GraphWeaver
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
# A graph whose queries were checked against the file rather than the
|
|
186
|
-
# server:
|
|
187
|
-
#
|
|
186
|
+
# server: nothing behind the dump to re-read, which is what a nil
|
|
187
|
+
# Graph#source says.
|
|
188
188
|
def self.as_committed(graph)
|
|
189
189
|
path = graph.dump_path
|
|
190
|
-
|
|
191
|
-
return if !graph.named_schema? && GraphWeaver::SchemaLoader.provenance(path)&.key?("url")
|
|
192
|
-
|
|
193
|
-
GraphWeaver::Internal::Util.relative(path)
|
|
190
|
+
GraphWeaver::Internal::Util.relative(path) if path && graph.source.nil?
|
|
194
191
|
end
|
|
195
192
|
private_class_method :as_committed
|
|
196
193
|
|
|
@@ -214,16 +211,18 @@ module GraphWeaver
|
|
|
214
211
|
# schema kept by hand, still has a server behind it, and refusing one
|
|
215
212
|
# took every other graph's refresh down with it.
|
|
216
213
|
def self.dumps
|
|
217
|
-
GraphWeaver.graphs.map { |graph| [graph, graph.dump_path, graph.
|
|
214
|
+
GraphWeaver.graphs.map { |graph| [graph, graph.dump_path, graph.source] }
|
|
218
215
|
end
|
|
219
216
|
|
|
220
|
-
# A dump with no recorded url whose graph names no
|
|
221
|
-
# behind the file to re-read, so the file is the schema.
|
|
222
|
-
#
|
|
223
|
-
#
|
|
217
|
+
# A dump with no recorded url whose graph names no server: nothing
|
|
218
|
+
# behind the file to re-read, so the file is the schema. One shape, one
|
|
219
|
+
# diagnosis — :refresh steps over it and :diff can't assert anything
|
|
220
|
+
# about it, and they used to describe it in two different sentences and
|
|
221
|
+
# disagree about whose whole run it ended.
|
|
224
222
|
def self.no_source(path)
|
|
225
223
|
"#{GraphWeaver::Internal::Util.relative(path)} records no source url and the graph names " \
|
|
226
|
-
"no
|
|
224
|
+
"no server behind it — no client posting to one, and no graphql-ruby schema class in " \
|
|
225
|
+
"this process — so the file is the schema and nothing here can re-read it"
|
|
227
226
|
end
|
|
228
227
|
|
|
229
228
|
# A graph that generates straight from a schema class has no dump
|
|
@@ -242,19 +241,6 @@ module GraphWeaver
|
|
|
242
241
|
"#{whose} generates from #{source_name(source)} directly — no dump to keep in step"
|
|
243
242
|
end
|
|
244
243
|
|
|
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
|
-
|
|
258
244
|
# How a dump's source reads in a report: a url as itself, a schema
|
|
259
245
|
# class by name.
|
|
260
246
|
def self.source_name(source) = source.is_a?(Module) ? GraphWeaver::SchemaLoader.endpoint(source) : source
|
|
@@ -393,18 +379,31 @@ namespace :graph_weaver do
|
|
|
393
379
|
|
|
394
380
|
desc "Fail when the schema behind the dump has drifted from it"
|
|
395
381
|
task diff: :own_schema do
|
|
396
|
-
|
|
397
|
-
abort
|
|
398
|
-
|
|
399
|
-
|
|
382
|
+
# One rule per graph, and no graph's verdict depends on what the others
|
|
383
|
+
# have: the "no dump anywhere" abort used to run before the loop, so a
|
|
384
|
+
# lone live-class graph was refused naming a path it never mentions
|
|
385
|
+
# while the same graph passed as soon as a sibling had a dump.
|
|
386
|
+
ungated = []
|
|
387
|
+
stale = GraphWeaver::Internal::Tasks.dumps.filter_map do |graph, path, source|
|
|
400
388
|
heading = GraphWeaver::Internal::Tasks.heading(graph)
|
|
401
389
|
puts heading if heading
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
390
|
+
unless path
|
|
391
|
+
# a graph that names a live class generates straight from it: no
|
|
392
|
+
# dump between the code and the output, so nothing can be stale.
|
|
393
|
+
# One whose dump is merely missing has something this gate can't see.
|
|
394
|
+
puts GraphWeaver::Internal::Tasks.no_dump_needed(graph, source)
|
|
395
|
+
ungated << graph if graph.named_dump_path || !source
|
|
396
|
+
next
|
|
397
|
+
end
|
|
398
|
+
unless source
|
|
399
|
+
# the shape :refresh steps over, in :refresh's own words — this task
|
|
400
|
+
# still fails on it, because a gate cannot assert nothing
|
|
401
|
+
puts GraphWeaver::Internal::Tasks.no_source(path)
|
|
402
|
+
ungated << graph
|
|
403
|
+
next
|
|
404
|
+
end
|
|
405
405
|
|
|
406
|
-
diff = GraphWeaver::SchemaLoader.diff(path,
|
|
407
|
-
transport: GraphWeaver::Internal::Tasks.diff_transport(graph, source))
|
|
406
|
+
diff = GraphWeaver::SchemaLoader.diff(path, transport: graph.source_transport)
|
|
408
407
|
dump = GraphWeaver::Internal::Util.relative(path)
|
|
409
408
|
next puts "#{dump} matches #{GraphWeaver::Internal::Tasks.source_name(source)}" if diff.empty?
|
|
410
409
|
|
|
@@ -419,6 +418,12 @@ namespace :graph_weaver do
|
|
|
419
418
|
abort "#{stale.join(", ")} is stale — the schema behind it has drifted " \
|
|
420
419
|
"(rake graph_weaver:schema:refresh)"
|
|
421
420
|
end
|
|
421
|
+
# a gate that passes on having compared nothing is worse than one that
|
|
422
|
+
# admits it: the sentence above says which graph and why
|
|
423
|
+
unless ungated.empty?
|
|
424
|
+
abort "#{GraphWeaver::Internal::Tasks.whose(ungated)}nothing here could be compared — " \
|
|
425
|
+
"this run gated nothing"
|
|
426
|
+
end
|
|
422
427
|
rescue GraphWeaver::Error => e
|
|
423
428
|
# e.g. a dump that is its own source — same clean exit as :refresh
|
|
424
429
|
abort e.message
|
|
@@ -439,7 +444,10 @@ namespace :graph_weaver do
|
|
|
439
444
|
next puts "refreshed #{GraphWeaver::Internal::Util.relative(path)} from #{source}"
|
|
440
445
|
end
|
|
441
446
|
|
|
442
|
-
|
|
447
|
+
# Every graph is its own job: one unreachable server used to end the run
|
|
448
|
+
# with the graphs after it never attempted, so an app learned about them
|
|
449
|
+
# one per run. The exit code is the only thing they share.
|
|
450
|
+
failed = GraphWeaver::Internal::Tasks.dumps.filter_map do |graph, path, source|
|
|
443
451
|
heading = GraphWeaver::Internal::Tasks.heading(graph)
|
|
444
452
|
puts heading if heading
|
|
445
453
|
# a graph that names a live class generates straight from it — no
|
|
@@ -450,13 +458,28 @@ namespace :graph_weaver do
|
|
|
450
458
|
# below and refresh! bootstraps its first dump (or says how).
|
|
451
459
|
path ||= graph.named_dump_path
|
|
452
460
|
next puts GraphWeaver::Internal::Tasks.no_dump_needed(graph, source) if !path && graph.named_schema?
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
|
|
461
|
+
# composition is the only thing that rebuilds a supergraph, so this
|
|
462
|
+
# graph is one to step over however good its client is — introspecting
|
|
463
|
+
# the router answers with the API schema, which routes nothing
|
|
464
|
+
next puts GraphWeaver::SchemaLoader.recompose_hint(path) if graph.supergraph
|
|
465
|
+
next puts GraphWeaver::Internal::Tasks.no_source(path) if path && !source
|
|
466
|
+
|
|
467
|
+
begin
|
|
468
|
+
written, from = GraphWeaver::SchemaLoader.refresh!(url: (source unless source.is_a?(Module)),
|
|
469
|
+
schema: (source if source.is_a?(Module)), path:)
|
|
470
|
+
puts "refreshed #{GraphWeaver::Internal::Util.relative(written)} from #{from}"
|
|
471
|
+
nil
|
|
472
|
+
rescue GraphWeaver::Error => e
|
|
473
|
+
puts e.message
|
|
474
|
+
graph
|
|
475
|
+
end
|
|
476
|
+
end
|
|
456
477
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
478
|
+
# abort writes to unbuffered stderr; the reports above went to
|
|
479
|
+
# block-buffered stdout, so a piped CI log shows them first
|
|
480
|
+
$stdout.flush
|
|
481
|
+
unless failed.empty?
|
|
482
|
+
abort "#{GraphWeaver::Internal::Tasks.whose(failed)}not refreshed — the dump on disk is unchanged"
|
|
460
483
|
end
|
|
461
484
|
rescue GraphWeaver::Error => e
|
|
462
485
|
abort e.message
|
|
@@ -481,6 +504,10 @@ namespace :graph_weaver do
|
|
|
481
504
|
$stdout.flush
|
|
482
505
|
abort "#{failures.size} invalid #{(failures.size == 1) ? "query" : "queries"}" if failures.any?
|
|
483
506
|
puts GraphWeaver::Internal::Tasks.validated
|
|
507
|
+
rescue GraphWeaver::Error => e
|
|
508
|
+
# this task reaches a server now, so an unreachable one is a condition to
|
|
509
|
+
# report — the same clean exit :diff and :refresh give
|
|
510
|
+
abort e.message
|
|
484
511
|
end
|
|
485
512
|
end
|
|
486
513
|
|
|
@@ -86,12 +86,14 @@ require_relative "../parsing"
|
|
|
86
86
|
#
|
|
87
87
|
# FakeClient.new(schema:, corrupt: "Person.birthday")
|
|
88
88
|
#
|
|
89
|
-
# null_chance: how often a nullable field comes back null —
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# example
|
|
89
|
+
# null_chance: how often a nullable field comes back null — a number from 0
|
|
90
|
+
# to 1 for all of them, or a Hash per field keyed the way list_size: is. 0
|
|
91
|
+
# by default, and per fake only: "does this render with no email" is one
|
|
92
|
+
# example's question, and a suite-wide answer would sprinkle nils through
|
|
93
|
+
# every other example instead.
|
|
93
94
|
#
|
|
94
95
|
# FakeClient.new(schema:, null_chance: 1.0) # everything nullable, null
|
|
96
|
+
# FakeClient.new(schema:, null_chance: { "Person.nickname" => 1.0, default: 0 })
|
|
95
97
|
#
|
|
96
98
|
# list_size: how long an unbounded list is — an Integer exactly, a Range
|
|
97
99
|
# randomized within it, and a Hash per list, keyed the way a pin is (a
|
|
@@ -167,10 +169,10 @@ class GraphWeaver::Testing::FakeClient
|
|
|
167
169
|
@registry = options[:registry] || GraphWeaver::Internal::Util.registry_for(@schema)
|
|
168
170
|
@values = GraphWeaver::Internal::Values.new(seed: options[:seed], values: options[:values],
|
|
169
171
|
pins: @overrides, schema: @schema, registry: @registry)
|
|
170
|
-
@list_size = options[:list_size] || config.list_size
|
|
171
|
-
@list_size = @list_size.transform_keys(&:to_s) if @list_size.is_a?(Hash)
|
|
172
|
+
@list_size = stringify(options[:list_size] || config.list_size)
|
|
172
173
|
GraphWeaver::Internal::Overrides.validate_list_size!(@schema, @list_size)
|
|
173
|
-
@null_chance = options[:null_chance] || 0.0
|
|
174
|
+
@null_chance = stringify(options[:null_chance] || 0.0)
|
|
175
|
+
GraphWeaver::Internal::Overrides.validate_null_chance!(@schema, @null_chance)
|
|
174
176
|
# NOT Array(): it would explode a bare Hash into key/value pairs
|
|
175
177
|
@extra_errors = wrap(options[:errors]).map { |error| normalize_error(error) }
|
|
176
178
|
@fail_at = wrap(options[:fail_at]).map { |spec| normalize_fail_spec(spec) }
|
|
@@ -612,7 +614,7 @@ class GraphWeaver::Testing::FakeClient
|
|
|
612
614
|
return [capped, 0].max if capped.is_a?(Integer)
|
|
613
615
|
return 0 if errors_list?(node.name)
|
|
614
616
|
|
|
615
|
-
size =
|
|
617
|
+
size = per_field(@list_size, coordinate, node.name, GraphWeaver::Testing::Config::DEFAULT_LIST_SIZE)
|
|
616
618
|
# an Integer list_size means exactly that many; a Range randomizes within it
|
|
617
619
|
size.is_a?(Range) ? rng.rand(size) : size
|
|
618
620
|
end
|
|
@@ -624,28 +626,29 @@ class GraphWeaver::Testing::FakeClient
|
|
|
624
626
|
# until it is pinned. Pin it to fabricate the failure path.
|
|
625
627
|
def errors_list?(name) = name.downcase.end_with?("errors")
|
|
626
628
|
|
|
627
|
-
#
|
|
628
|
-
# specific first like a pin — which is what keeps
|
|
629
|
-
# multiplying: every list the walk reaches re-reads
|
|
630
|
-
# for all of them is n rows x n tags.
|
|
631
|
-
def
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
@list_size.fetch(GraphWeaver::Internal::Overrides::LIST_SIZE_DEFAULT,
|
|
637
|
-
GraphWeaver::Testing::Config::DEFAULT_LIST_SIZE)
|
|
638
|
-
end
|
|
629
|
+
# What a per-field option (list_size:, null_chance:) says here. A Hash says
|
|
630
|
+
# it per field, read most specific first like a pin — which is what keeps
|
|
631
|
+
# nested lists from multiplying: every list the walk reaches re-reads
|
|
632
|
+
# list_size, so one number for all of them is n rows x n tags.
|
|
633
|
+
def per_field(option, coordinate, name, fallback)
|
|
634
|
+
return option unless option.is_a?(Hash)
|
|
635
|
+
|
|
636
|
+
option.fetch(coordinate) do
|
|
637
|
+
option.fetch(name) { option.fetch(GraphWeaver::Internal::Overrides::DEFAULT_KEY, fallback) }
|
|
639
638
|
end
|
|
640
639
|
end
|
|
641
640
|
|
|
641
|
+
# A per-field option's keys are GraphQL names, written as either a String
|
|
642
|
+
# or a Symbol (`default:` most of all).
|
|
643
|
+
def stringify(option) = option.is_a?(Hash) ? option.transform_keys(&:to_s) : option
|
|
644
|
+
|
|
642
645
|
def type_value(type, node, selections, coordinate: nil, non_null: false)
|
|
643
646
|
if type.kind.name == "NON_NULL"
|
|
644
647
|
return type_value(type.of_type, node, selections, coordinate:, non_null: true)
|
|
645
648
|
end
|
|
646
649
|
# every nullable position, a list included — null_chance is about the
|
|
647
650
|
# nilable props codegen emitted, and it emits one for `[Thing!]` too
|
|
648
|
-
return if !non_null && rng.rand < @null_chance
|
|
651
|
+
return if !non_null && rng.rand < per_field(@null_chance, coordinate, node.name, 0.0)
|
|
649
652
|
|
|
650
653
|
case type.kind.name
|
|
651
654
|
when "LIST"
|
data/lib/graph_weaver/testing.rb
CHANGED
|
@@ -340,10 +340,14 @@ module GraphWeaver
|
|
|
340
340
|
"Declare it with the class: GraphWeaver.graph(#{graph.name.inspect}) " \
|
|
341
341
|
"{ schema -> { MySchema } }."
|
|
342
342
|
else
|
|
343
|
+
# the helper needs no tag, which is what makes it reachable from
|
|
344
|
+
# BOTH paths into here: a tagged example is already past its own
|
|
345
|
+
# before hook, so "name it in the example" only works if the tag goes
|
|
343
346
|
"GraphWeaver.client isn't running one in-process to borrow. Name it in the example — " \
|
|
344
|
-
"graphql_in_process(MySchema) —
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
+
"graphql_in_process(MySchema) — instead of the graphql: :in_process tag, which " \
|
|
348
|
+
"builds this client in a `before` hook of its own and so is already past. Or set " \
|
|
349
|
+
"GraphWeaver::Testing.config.schema = MySchema for the whole suite. A federated app " \
|
|
350
|
+
"names the subgraph it means, per example; graphql: :router runs the graph stitched."
|
|
347
351
|
end
|
|
348
352
|
end
|
|
349
353
|
|
|
@@ -411,7 +415,7 @@ module GraphWeaver
|
|
|
411
415
|
# the precision case gets exercised at all.
|
|
412
416
|
def check_scalars!(schema)
|
|
413
417
|
registry = Internal::Util.registry_for(schema)
|
|
414
|
-
values = Internal::Values.new(seed: 0, schema:, registry:)
|
|
418
|
+
values = Internal::Values.new(seed: 0, schema:, registry:, pin_advice: method(:pin_for_check))
|
|
415
419
|
context = GraphQL::Query.new(schema, "{ __typename }").context
|
|
416
420
|
|
|
417
421
|
disagreed = schema.types.values.sort_by(&:graphql_name).filter_map do |type|
|
|
@@ -431,6 +435,14 @@ module GraphWeaver
|
|
|
431
435
|
|
|
432
436
|
private
|
|
433
437
|
|
|
438
|
+
# check_scalars! takes the schema and nothing else, and runs outside
|
|
439
|
+
# every fake — so of the doors onto a pin only the suite-wide one is a
|
|
440
|
+
# door here. graphql_fake is a no-op, and there is no second argument.
|
|
441
|
+
def pin_for_check(name)
|
|
442
|
+
"Pin the form this server sends: GraphWeaver::Testing.config.overrides = " \
|
|
443
|
+
"{ #{name.inspect} => ... }."
|
|
444
|
+
end
|
|
445
|
+
|
|
434
446
|
# One scalar's verdict, or nil when the two halves agree. Each step is
|
|
435
447
|
# a different mistake, so each says which.
|
|
436
448
|
def disagreement(scalar, type, values, context)
|
|
@@ -448,8 +460,8 @@ module GraphWeaver
|
|
|
448
460
|
begin
|
|
449
461
|
sample = cast.call(wire)
|
|
450
462
|
rescue StandardError => e
|
|
451
|
-
return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it
|
|
452
|
-
"
|
|
463
|
+
return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it " \
|
|
464
|
+
"(#{e.message}). #{pin_for_check(name)}"
|
|
453
465
|
end
|
|
454
466
|
|
|
455
467
|
if scalar.serialize? && !scalar.serialize_value?
|
|
@@ -475,7 +487,15 @@ module GraphWeaver
|
|
|
475
487
|
end
|
|
476
488
|
return if back == sample
|
|
477
489
|
|
|
478
|
-
|
|
490
|
+
sent = Internal::Redact.spell(sample)
|
|
491
|
+
got = Internal::Redact.spell(back)
|
|
492
|
+
return "#{name}: round-trips lossily — sent #{sent}, got back #{got}" unless sent == got
|
|
493
|
+
|
|
494
|
+
# two identical spellings for two unequal values: the class inherits
|
|
495
|
+
# Object#==, which is identity, so this can't tell a lossy round trip
|
|
496
|
+
# from a faithful one and must not claim either
|
|
497
|
+
"#{name}: #{sample.class} defines no ==, so a round trip can't be checked — sent " \
|
|
498
|
+
"#{sent} and got back #{got}, and nothing here can tell those apart"
|
|
479
499
|
end
|
|
480
500
|
|
|
481
501
|
# The registration's `cast:`, RUN rather than emitted. A cast builds
|
|
@@ -99,7 +99,7 @@ class GraphWeaver::Transport
|
|
|
99
99
|
payload = { url: safe_url, operation: operation_name, client: self.class,
|
|
100
100
|
kind: GraphWeaver::Internal::Wire.kind(query) }
|
|
101
101
|
|
|
102
|
-
GraphWeaver::Internal::Log.
|
|
102
|
+
GraphWeaver::Internal::Log.instrument_request(payload) do
|
|
103
103
|
perform(query, variables, operation_name, payload)
|
|
104
104
|
end
|
|
105
105
|
end
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -219,9 +219,15 @@ module GraphWeaver
|
|
|
219
219
|
# from where you put the files: set it globally, or pass types_module: per
|
|
220
220
|
# generate!. A multi-schema layout names it in the same initializer that
|
|
221
221
|
# sets its paths.
|
|
222
|
+
#
|
|
223
|
+
# Unset, the default is whichever name is free where the constants land:
|
|
224
|
+
# GraphQLTypes at the top level, where a bare ::Types would collide with
|
|
225
|
+
# graphql-ruby's own Types:: convention, and plain Types inside a graph's
|
|
226
|
+
# namespace, where it can't — which is what leaves types.rb a file Zeitwerk
|
|
227
|
+
# names correctly (see Graph#types_module).
|
|
222
228
|
attr_writer :types_module
|
|
223
229
|
|
|
224
|
-
def types_module = @types_module ||
|
|
230
|
+
def types_module(default = "GraphQLTypes") = @types_module || default
|
|
225
231
|
|
|
226
232
|
# Declare a second schema — and a third, and the rest. Everything a graph
|
|
227
233
|
# knows is said inside its block, in call style:
|
|
@@ -614,8 +620,8 @@ module GraphWeaver
|
|
|
614
620
|
# # "line" => 4, "column" => 5 }] }
|
|
615
621
|
#
|
|
616
622
|
# Empty means every query validates. schema: defaults to the server as
|
|
617
|
-
# it is now — a FRESH introspection of the
|
|
618
|
-
# live schema class
|
|
623
|
+
# it is now — a FRESH introspection of the source behind each graph's
|
|
624
|
+
# dump, or the live schema class where that source IS one — and the dump
|
|
619
625
|
# is left alone; pass schema: and nothing touches the network.
|
|
620
626
|
#
|
|
621
627
|
# When that dump is a composed supergraph, an error naming a type is
|
|
@@ -623,17 +629,25 @@ module GraphWeaver
|
|
|
623
629
|
# (products, reviews)", plus a "subgraphs" key — since knowing whose
|
|
624
630
|
# code to look at is half the answer. A plain schema is unaffected.
|
|
625
631
|
#
|
|
626
|
-
# One query you have as a *string* is
|
|
627
|
-
# entries, against that
|
|
632
|
+
# One query you have as a *string* is #check_query, which every client
|
|
633
|
+
# holding a schema answers — the same entries, against that schema.
|
|
628
634
|
#
|
|
629
635
|
# A different question from verify_generated!, which asks whether the
|
|
630
636
|
# committed Ruby matches the committed schema. `rake
|
|
631
637
|
# graph_weaver:queries:check` prints this and exits non-zero.
|
|
632
638
|
def check_queries(schema: nil, queries: nil, fragments: fragments_paths)
|
|
633
639
|
shared = Codegen.load_fragments(fragments)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
640
|
+
# one graph's server being down is that graph's verdict: every graph is
|
|
641
|
+
# attempted, so a run names every server it couldn't reach rather than
|
|
642
|
+
# ending at the first
|
|
643
|
+
unreachable = []
|
|
644
|
+
|
|
645
|
+
failures = graphs_for(schema:, queries:).each_with_object({}) do |graph, failures|
|
|
646
|
+
begin
|
|
647
|
+
checked = checked_schema(graph)
|
|
648
|
+
rescue Error => e
|
|
649
|
+
next unreachable << unreachable_source(graph, e)
|
|
650
|
+
end
|
|
637
651
|
table = checked_routing_table(graph)
|
|
638
652
|
Internal::Util.query_files(graph.queries).each do |path|
|
|
639
653
|
errors = Internal::QueryCheck.errors(checked, File.read(path), shared, table)
|
|
@@ -646,13 +660,39 @@ module GraphWeaver
|
|
|
646
660
|
failures[key] = failures.key?(key) ? failures[key] | errors : errors
|
|
647
661
|
end
|
|
648
662
|
end
|
|
663
|
+
|
|
664
|
+
raise Error, unreachable.join("\n") unless unreachable.empty?
|
|
665
|
+
|
|
666
|
+
failures
|
|
649
667
|
end
|
|
650
668
|
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
|
|
655
|
-
|
|
669
|
+
# Which graph couldn't be checked, and against what. The bare socket error
|
|
670
|
+
# named neither — in a multi-graph app it was the entire output, and the
|
|
671
|
+
# reader could not tell which of their servers was down.
|
|
672
|
+
def unreachable_source(graph, error)
|
|
673
|
+
whose = graph.name ? "graph #{graph.name.inspect}" : "this app"
|
|
674
|
+
dump = graph.dump_path || graph.named_dump_path
|
|
675
|
+
subject = dump ? Internal::Util.relative(dump) : "these queries"
|
|
676
|
+
"#{whose}: couldn't reach the schema behind #{subject} — #{error.message}"
|
|
677
|
+
end
|
|
678
|
+
private :unreachable_source
|
|
679
|
+
|
|
680
|
+
# What this graph is checked against: Graph#source, the rule `schema:refresh`
|
|
681
|
+
# and `schema:diff` follow too. A live class is asked directly — for an app
|
|
682
|
+
# that IS the server, a dump is a snapshot of its own code, and checking
|
|
683
|
+
# against it reports phantom errors about a field just added. A url is
|
|
684
|
+
# re-introspected, so no refresh step (and no rewritten dump) is needed
|
|
685
|
+
# first. A graph with neither — hand-written SDL nothing serves, a composed
|
|
686
|
+
# supergraph — has nothing to re-read, so it is checked as committed.
|
|
687
|
+
def checked_schema(graph)
|
|
688
|
+
source = graph.source
|
|
689
|
+
return source if source.is_a?(Module)
|
|
690
|
+
# Internal::Util.locate_schema! raises the conventional "no schema dump"
|
|
691
|
+
# message when the default graph has no dump either
|
|
692
|
+
return graph.schema unless source
|
|
693
|
+
|
|
694
|
+
SchemaLoader.introspect(graph.source_transport)
|
|
695
|
+
end
|
|
656
696
|
private :checked_schema
|
|
657
697
|
|
|
658
698
|
# The routing table behind the schema check_queries is about to use, when
|
|
@@ -663,40 +703,19 @@ module GraphWeaver
|
|
|
663
703
|
end
|
|
664
704
|
private :checked_routing_table
|
|
665
705
|
|
|
666
|
-
# The schema check_queries defaults to: the server as it is now. Over a
|
|
667
|
-
# socket that's a fresh introspection of the url the local dump recorded,
|
|
668
|
-
# so no refresh step (and no rewritten dump) is needed first. In-process
|
|
669
|
-
# it's the live schema class — for an app that IS the server, a dump is a
|
|
670
|
-
# snapshot of its own code, and checking against it reports phantom
|
|
671
|
-
# errors about a field you just added. Dumps with no url and no live
|
|
672
|
-
# class — hand-written SDL, a composed supergraph — have nothing to
|
|
673
|
-
# re-read, so they're checked as they are.
|
|
674
|
-
def refreshed_schema
|
|
675
|
-
live = Internal::Util.live_schema
|
|
676
|
-
return live if live
|
|
677
|
-
|
|
678
|
-
# Internal::Util.locate_schema! raises the conventional "no schema dump" message
|
|
679
|
-
path = SchemaLoader.locate_path or Internal::Util.locate_schema!
|
|
680
|
-
return SchemaLoader.load(path) unless SchemaLoader.provenance(path)&.key?("url")
|
|
681
|
-
|
|
682
|
-
# source_transport rather than one built here: it reads the auth ENV var
|
|
683
|
-
# the dump named, so `--auth MY_TOKEN` reaches this path too
|
|
684
|
-
SchemaLoader.introspect(SchemaLoader.source_transport(path))
|
|
685
|
-
end
|
|
686
|
-
private :refreshed_schema
|
|
687
|
-
|
|
688
706
|
# Load the generated modules — one line in an initializer or spec
|
|
689
707
|
# helper (loading happens only when you call this; skip it and
|
|
690
708
|
# require files yourself if you'd rather):
|
|
691
709
|
#
|
|
692
710
|
# GraphWeaver.load_generated!
|
|
693
711
|
#
|
|
694
|
-
# In Rails,
|
|
695
|
-
#
|
|
696
|
-
#
|
|
697
|
-
#
|
|
712
|
+
# In Rails this is automatic, and a tree Zeitwerk already names correctly
|
|
713
|
+
# is skipped: it autoloads on first reference (see the Railtie). Everything
|
|
714
|
+
# else is required — Zeitwerk would expect Generated::PersonQuery from
|
|
715
|
+
# generated/person_query.rb, and generated code only changes on
|
|
716
|
+
# regeneration anyway (restart, like a schema migration).
|
|
698
717
|
def load_generated!(path = nil)
|
|
699
|
-
paths = path ? [path] : Internal::Util.
|
|
718
|
+
paths = path ? [path] : Internal::Util.required_dirs
|
|
700
719
|
files = paths.flat_map { |dir| Dir[File.join(Internal::Util.resolve(dir), "**/*.rb")].sort }.uniq
|
|
701
720
|
files.each do |file|
|
|
702
721
|
require file
|
|
@@ -749,7 +768,9 @@ module GraphWeaver
|
|
|
749
768
|
# whose query was just deleted keeps its old constant until restart —
|
|
750
769
|
# nothing on disk says what it was called any more.
|
|
751
770
|
def reload_generated!
|
|
752
|
-
|
|
771
|
+
# a graph Zeitwerk owns unloads and reloads itself; undefining its
|
|
772
|
+
# constants here would only take them out from under it
|
|
773
|
+
names = graphs.reject { |graph| Internal::Util.left_to_zeitwerk?(graph.output) }.flat_map do |graph|
|
|
753
774
|
modules = Internal::Util.query_files(graph.queries).map do |path|
|
|
754
775
|
graph.generated_names(path, File.read(path)).first
|
|
755
776
|
end
|
|
@@ -757,7 +778,7 @@ module GraphWeaver
|
|
|
757
778
|
end
|
|
758
779
|
names.each { |name| undefine(name) }
|
|
759
780
|
|
|
760
|
-
Internal::Util.
|
|
781
|
+
Internal::Util.required_dirs.each do |dir|
|
|
761
782
|
Dir[File.join(Internal::Util.resolve(dir), "**/*.rb")].each do |file|
|
|
762
783
|
# require stores the realpath; the path load_generated! passed is
|
|
763
784
|
# the other one under a symlinked checkout
|
|
@@ -844,7 +865,7 @@ module GraphWeaver
|
|
|
844
865
|
rescue GraphWeaver::Error => e
|
|
845
866
|
# collected, not raised: nothing is written either way, and an adopter
|
|
846
867
|
# aiming generate! at an existing query directory wants the list
|
|
847
|
-
refusals << e
|
|
868
|
+
refusals << name_query_file(e, path)
|
|
848
869
|
nil
|
|
849
870
|
end
|
|
850
871
|
refuse_all!(refusals, paths.size)
|
|
@@ -907,6 +928,18 @@ module GraphWeaver
|
|
|
907
928
|
# itself, so a single bad file reads exactly as it always has — class,
|
|
908
929
|
# message and all; several become one list, because clearing them a file
|
|
909
930
|
# per run is the slowest way there is to adopt this.
|
|
931
|
+
# Almost every codegen refusal is about the query in front of it and never
|
|
932
|
+
# says which file that was — so a list of them named nothing, which is the
|
|
933
|
+
# one thing a list is for. This rescue is the only place holding both.
|
|
934
|
+
# #exception keeps the class and its fields and swaps only the message.
|
|
935
|
+
def name_query_file(error, path)
|
|
936
|
+
reported = Internal::Util.relative(path)
|
|
937
|
+
return error if error.message.include?(reported)
|
|
938
|
+
|
|
939
|
+
error.exception("#{reported}: #{error.message}")
|
|
940
|
+
end
|
|
941
|
+
private :name_query_file
|
|
942
|
+
|
|
910
943
|
def refuse_all!(refusals, considered)
|
|
911
944
|
return if refusals.empty?
|
|
912
945
|
raise refusals.first if refusals.one?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graph_weaver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -364,7 +364,7 @@ licenses:
|
|
|
364
364
|
- MIT
|
|
365
365
|
metadata:
|
|
366
366
|
bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
|
|
367
|
-
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.
|
|
367
|
+
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.7/CHANGELOG.md
|
|
368
368
|
documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
|
|
369
369
|
rubygems_mfa_required: 'true'
|
|
370
370
|
source_code_uri: https://github.com/dpep/graph_weaver
|