graph_weaver 0.7.4 → 0.7.6
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 +2 -2
- data/README.md +1 -0
- data/docs/errors.md +12 -5
- data/docs/generated_modules.md +134 -17
- data/docs/getting_started.md +182 -20
- data/docs/logging.md +79 -35
- data/docs/migrating.md +126 -0
- data/docs/scalars.md +50 -6
- data/docs/testing.md +78 -14
- data/docs/upgrading.md +44 -2
- 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 +49 -2
- data/lib/graph_weaver/client.rb +0 -23
- data/lib/graph_weaver/codegen/aliases.rb +36 -3
- data/lib/graph_weaver/codegen/emit.rb +20 -15
- data/lib/graph_weaver/codegen/enum_type.rb +52 -11
- data/lib/graph_weaver/codegen/nodes.rb +75 -32
- data/lib/graph_weaver/codegen.rb +260 -100
- data/lib/graph_weaver/coerce.rb +1 -1
- data/lib/graph_weaver/federation.rb +1 -6
- data/lib/graph_weaver/graph.rb +55 -5
- data/lib/graph_weaver/hints.rb +20 -5
- data/lib/graph_weaver/in_process.rb +2 -4
- data/lib/graph_weaver/input_struct.rb +50 -10
- data/lib/graph_weaver/internal/overrides.rb +126 -14
- data/lib/graph_weaver/internal/subgraphs.rb +1 -10
- data/lib/graph_weaver/internal/test_clients.rb +29 -7
- data/lib/graph_weaver/internal/unused.rb +62 -18
- data/lib/graph_weaver/internal/values.rb +24 -7
- data/lib/graph_weaver/internal.rb +23 -6
- data/lib/graph_weaver/log_subscriber.rb +27 -17
- data/lib/graph_weaver/logging.rb +115 -82
- data/lib/graph_weaver/parsing.rb +32 -3
- data/lib/graph_weaver/query_module.rb +67 -12
- data/lib/graph_weaver/railtie.rb +7 -2
- data/lib/graph_weaver/rspec.rb +41 -18
- data/lib/graph_weaver/schema_diff.rb +24 -5
- data/lib/graph_weaver/schema_loader.rb +29 -17
- data/lib/graph_weaver/tasks.rb +98 -16
- data/lib/graph_weaver/testing/fake_client.rb +28 -31
- data/lib/graph_weaver/testing/router.rb +26 -25
- data/lib/graph_weaver/testing.rb +27 -8
- data/lib/graph_weaver/transport.rb +1 -1
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +72 -42
- metadata +3 -2
|
@@ -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,
|
|
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
|
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
|
|
|
@@ -352,7 +356,6 @@ module GraphWeaver
|
|
|
352
356
|
def runnable(schema)
|
|
353
357
|
schema if schema.is_a?(Class) && schema <= GraphQL::Schema
|
|
354
358
|
end
|
|
355
|
-
|
|
356
359
|
end
|
|
357
360
|
|
|
358
361
|
class << self
|
|
@@ -412,7 +415,7 @@ module GraphWeaver
|
|
|
412
415
|
# the precision case gets exercised at all.
|
|
413
416
|
def check_scalars!(schema)
|
|
414
417
|
registry = Internal::Util.registry_for(schema)
|
|
415
|
-
values = Internal::Values.new(seed: 0, schema:, registry:)
|
|
418
|
+
values = Internal::Values.new(seed: 0, schema:, registry:, pin_advice: method(:pin_for_check))
|
|
416
419
|
context = GraphQL::Query.new(schema, "{ __typename }").context
|
|
417
420
|
|
|
418
421
|
disagreed = schema.types.values.sort_by(&:graphql_name).filter_map do |type|
|
|
@@ -432,6 +435,14 @@ module GraphWeaver
|
|
|
432
435
|
|
|
433
436
|
private
|
|
434
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
|
+
|
|
435
446
|
# One scalar's verdict, or nil when the two halves agree. Each step is
|
|
436
447
|
# a different mistake, so each says which.
|
|
437
448
|
def disagreement(scalar, type, values, context)
|
|
@@ -449,8 +460,8 @@ module GraphWeaver
|
|
|
449
460
|
begin
|
|
450
461
|
sample = cast.call(wire)
|
|
451
462
|
rescue StandardError => e
|
|
452
|
-
return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it
|
|
453
|
-
"
|
|
463
|
+
return "#{name}: cast: can't read #{wire.inspect}, the value fabricated for it " \
|
|
464
|
+
"(#{e.message}). #{pin_for_check(name)}"
|
|
454
465
|
end
|
|
455
466
|
|
|
456
467
|
if scalar.serialize? && !scalar.serialize_value?
|
|
@@ -476,7 +487,15 @@ module GraphWeaver
|
|
|
476
487
|
end
|
|
477
488
|
return if back == sample
|
|
478
489
|
|
|
479
|
-
|
|
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"
|
|
480
499
|
end
|
|
481
500
|
|
|
482
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
|
@@ -214,7 +214,7 @@ module GraphWeaver
|
|
|
214
214
|
attr_accessor :skip_generated_load
|
|
215
215
|
|
|
216
216
|
# The name of the shared module — the types that live once per schema
|
|
217
|
-
# (input types, enums,
|
|
217
|
+
# (input types, enums, the types hoisted from shared fragments) and are
|
|
218
218
|
# aliased into every query module that touches them. Constant, not derived
|
|
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
|
|
@@ -614,8 +614,8 @@ module GraphWeaver
|
|
|
614
614
|
# # "line" => 4, "column" => 5 }] }
|
|
615
615
|
#
|
|
616
616
|
# Empty means every query validates. schema: defaults to the server as
|
|
617
|
-
# it is now — a FRESH introspection of the
|
|
618
|
-
# live schema class
|
|
617
|
+
# it is now — a FRESH introspection of the source behind each graph's
|
|
618
|
+
# dump, or the live schema class where that source IS one — and the dump
|
|
619
619
|
# is left alone; pass schema: and nothing touches the network.
|
|
620
620
|
#
|
|
621
621
|
# When that dump is a composed supergraph, an error naming a type is
|
|
@@ -623,17 +623,25 @@ module GraphWeaver
|
|
|
623
623
|
# (products, reviews)", plus a "subgraphs" key — since knowing whose
|
|
624
624
|
# code to look at is half the answer. A plain schema is unaffected.
|
|
625
625
|
#
|
|
626
|
-
# One query you have as a *string* is
|
|
627
|
-
# entries, against that
|
|
626
|
+
# One query you have as a *string* is #check_query, which every client
|
|
627
|
+
# holding a schema answers — the same entries, against that schema.
|
|
628
628
|
#
|
|
629
629
|
# A different question from verify_generated!, which asks whether the
|
|
630
630
|
# committed Ruby matches the committed schema. `rake
|
|
631
631
|
# graph_weaver:queries:check` prints this and exits non-zero.
|
|
632
632
|
def check_queries(schema: nil, queries: nil, fragments: fragments_paths)
|
|
633
633
|
shared = Codegen.load_fragments(fragments)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
634
|
+
# one graph's server being down is that graph's verdict: every graph is
|
|
635
|
+
# attempted, so a run names every server it couldn't reach rather than
|
|
636
|
+
# ending at the first
|
|
637
|
+
unreachable = []
|
|
638
|
+
|
|
639
|
+
failures = graphs_for(schema:, queries:).each_with_object({}) do |graph, failures|
|
|
640
|
+
begin
|
|
641
|
+
checked = checked_schema(graph)
|
|
642
|
+
rescue Error => e
|
|
643
|
+
next unreachable << unreachable_source(graph, e)
|
|
644
|
+
end
|
|
637
645
|
table = checked_routing_table(graph)
|
|
638
646
|
Internal::Util.query_files(graph.queries).each do |path|
|
|
639
647
|
errors = Internal::QueryCheck.errors(checked, File.read(path), shared, table)
|
|
@@ -646,13 +654,39 @@ module GraphWeaver
|
|
|
646
654
|
failures[key] = failures.key?(key) ? failures[key] | errors : errors
|
|
647
655
|
end
|
|
648
656
|
end
|
|
657
|
+
|
|
658
|
+
raise Error, unreachable.join("\n") unless unreachable.empty?
|
|
659
|
+
|
|
660
|
+
failures
|
|
649
661
|
end
|
|
650
662
|
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
|
|
655
|
-
|
|
663
|
+
# Which graph couldn't be checked, and against what. The bare socket error
|
|
664
|
+
# named neither — in a multi-graph app it was the entire output, and the
|
|
665
|
+
# reader could not tell which of their servers was down.
|
|
666
|
+
def unreachable_source(graph, error)
|
|
667
|
+
whose = graph.name ? "graph #{graph.name.inspect}" : "this app"
|
|
668
|
+
dump = graph.dump_path || graph.named_dump_path
|
|
669
|
+
subject = dump ? Internal::Util.relative(dump) : "these queries"
|
|
670
|
+
"#{whose}: couldn't reach the schema behind #{subject} — #{error.message}"
|
|
671
|
+
end
|
|
672
|
+
private :unreachable_source
|
|
673
|
+
|
|
674
|
+
# What this graph is checked against: Graph#source, the rule `schema:refresh`
|
|
675
|
+
# and `schema:diff` follow too. A live class is asked directly — for an app
|
|
676
|
+
# that IS the server, a dump is a snapshot of its own code, and checking
|
|
677
|
+
# against it reports phantom errors about a field just added. A url is
|
|
678
|
+
# re-introspected, so no refresh step (and no rewritten dump) is needed
|
|
679
|
+
# first. A graph with neither — hand-written SDL nothing serves, a composed
|
|
680
|
+
# supergraph — has nothing to re-read, so it is checked as committed.
|
|
681
|
+
def checked_schema(graph)
|
|
682
|
+
source = graph.source
|
|
683
|
+
return source if source.is_a?(Module)
|
|
684
|
+
# Internal::Util.locate_schema! raises the conventional "no schema dump"
|
|
685
|
+
# message when the default graph has no dump either
|
|
686
|
+
return graph.schema unless source
|
|
687
|
+
|
|
688
|
+
SchemaLoader.introspect(graph.source_transport)
|
|
689
|
+
end
|
|
656
690
|
private :checked_schema
|
|
657
691
|
|
|
658
692
|
# The routing table behind the schema check_queries is about to use, when
|
|
@@ -663,28 +697,6 @@ module GraphWeaver
|
|
|
663
697
|
end
|
|
664
698
|
private :checked_routing_table
|
|
665
699
|
|
|
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
700
|
# Load the generated modules — one line in an initializer or spec
|
|
689
701
|
# helper (loading happens only when you call this; skip it and
|
|
690
702
|
# require files yourself if you'd rather):
|
|
@@ -795,7 +807,7 @@ module GraphWeaver
|
|
|
795
807
|
|
|
796
808
|
# (filename, source) per artifact. Types a schema shares across queries —
|
|
797
809
|
# input types, schema enums, and each named shared fragment spread as a
|
|
798
|
-
# whole
|
|
810
|
+
# whole field — are emitted once into the shared module, with query
|
|
799
811
|
# modules aliasing what they use. That's the difference between hundreds of
|
|
800
812
|
# duplicated bool_exp structs (or one Ruby class per query for the same
|
|
801
813
|
# schema enum) and one copy per schema. (Single-query parse inlines
|
|
@@ -814,7 +826,7 @@ module GraphWeaver
|
|
|
814
826
|
@unmatched_registrations |= registry.unmatched_registrations(schema)
|
|
815
827
|
|
|
816
828
|
used = { inputs: [], enums: [], mapped: [] }
|
|
817
|
-
|
|
829
|
+
hoisted = []
|
|
818
830
|
helpers = []
|
|
819
831
|
shared = Codegen.load_fragments(fragments)
|
|
820
832
|
|
|
@@ -831,30 +843,30 @@ module GraphWeaver
|
|
|
831
843
|
name:,
|
|
832
844
|
graph_name: graph.name,
|
|
833
845
|
types_namespace: graph.types_module,
|
|
834
|
-
|
|
846
|
+
hoistable_fragments: Codegen.shared_fragment_spreads(source, shared, path),
|
|
835
847
|
path:,
|
|
836
848
|
registry:,
|
|
837
849
|
)
|
|
838
850
|
out = codegen.generate
|
|
839
851
|
codegen.variable_type_names.each { |kind, names| used[kind] |= names }
|
|
840
852
|
found.concat(codegen.untyped_scalars).uniq!
|
|
841
|
-
|
|
853
|
+
hoisted |= codegen.used_fragment_names
|
|
842
854
|
helpers |= codegen.block_helpers
|
|
843
855
|
[filename, out]
|
|
844
856
|
rescue GraphWeaver::Error => e
|
|
845
857
|
# collected, not raised: nothing is written either way, and an adopter
|
|
846
858
|
# aiming generate! at an existing query directory wants the list
|
|
847
|
-
refusals << e
|
|
859
|
+
refusals << name_query_file(e, path)
|
|
848
860
|
nil
|
|
849
861
|
end
|
|
850
862
|
refuse_all!(refusals, paths.size)
|
|
851
863
|
|
|
852
|
-
if
|
|
864
|
+
if hoisted.any? || used.values.any?(&:any?)
|
|
853
865
|
refuse_duplicate_types!(seen, graph)
|
|
854
866
|
codegen = Codegen.new(schema:, query: "", name: graph.types_module, registry:)
|
|
855
867
|
types = codegen.generate_types(
|
|
856
868
|
inputs: used[:inputs], enums: used[:enums] + used[:mapped],
|
|
857
|
-
|
|
869
|
+
hoisted:, fragments: shared,
|
|
858
870
|
)
|
|
859
871
|
found.concat(codegen.untyped_scalars).uniq!
|
|
860
872
|
helpers |= codegen.block_helpers
|
|
@@ -907,6 +919,18 @@ module GraphWeaver
|
|
|
907
919
|
# itself, so a single bad file reads exactly as it always has — class,
|
|
908
920
|
# message and all; several become one list, because clearing them a file
|
|
909
921
|
# per run is the slowest way there is to adopt this.
|
|
922
|
+
# Almost every codegen refusal is about the query in front of it and never
|
|
923
|
+
# says which file that was — so a list of them named nothing, which is the
|
|
924
|
+
# one thing a list is for. This rescue is the only place holding both.
|
|
925
|
+
# #exception keeps the class and its fields and swaps only the message.
|
|
926
|
+
def name_query_file(error, path)
|
|
927
|
+
reported = Internal::Util.relative(path)
|
|
928
|
+
return error if error.message.include?(reported)
|
|
929
|
+
|
|
930
|
+
error.exception("#{reported}: #{error.message}")
|
|
931
|
+
end
|
|
932
|
+
private :name_query_file
|
|
933
|
+
|
|
910
934
|
def refuse_all!(refusals, considered)
|
|
911
935
|
return if refusals.empty?
|
|
912
936
|
raise refusals.first if refusals.one?
|
|
@@ -1051,6 +1075,12 @@ module GraphWeaver
|
|
|
1051
1075
|
# mid-rename needs:
|
|
1052
1076
|
#
|
|
1053
1077
|
# GraphWeaver.register_enum("Status", alias: { "legacy_mode" => "LEGACY_MODE" })
|
|
1078
|
+
#
|
|
1079
|
+
# fallback: true is the type-less form of forward-compat: the generated
|
|
1080
|
+
# enum gains an Other member and casts every value the schema doesn't
|
|
1081
|
+
# declare to it, so a server adding one doesn't take the client down.
|
|
1082
|
+
#
|
|
1083
|
+
# GraphWeaver.register_enum("Species", fallback: true)
|
|
1054
1084
|
def register_enum(graphql_name, type = nil, positional_map = nil, map: nil, fallback: nil, requires: nil,
|
|
1055
1085
|
alias: nil)
|
|
1056
1086
|
# `alias` is a Ruby keyword, so the parameter is only readable through binding
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -281,6 +281,7 @@ files:
|
|
|
281
281
|
- docs/getting_started.md
|
|
282
282
|
- docs/i18n.md
|
|
283
283
|
- docs/logging.md
|
|
284
|
+
- docs/migrating.md
|
|
284
285
|
- docs/real_world.md
|
|
285
286
|
- docs/scalars.md
|
|
286
287
|
- docs/testing.md
|
|
@@ -363,7 +364,7 @@ licenses:
|
|
|
363
364
|
- MIT
|
|
364
365
|
metadata:
|
|
365
366
|
bug_tracker_uri: https://github.com/dpep/graph_weaver/issues
|
|
366
|
-
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.
|
|
367
|
+
changelog_uri: https://github.com/dpep/graph_weaver/blob/v0.7.6/CHANGELOG.md
|
|
367
368
|
documentation_uri: https://github.com/dpep/graph_weaver/tree/main/docs
|
|
368
369
|
rubygems_mfa_required: 'true'
|
|
369
370
|
source_code_uri: https://github.com/dpep/graph_weaver
|