graph_weaver 0.5.1 → 0.6.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -0
  3. data/Gemfile.lock +19 -19
  4. data/README.md +74 -53
  5. data/docs/cassettes.md +6 -1
  6. data/docs/editors.md +3 -1
  7. data/docs/errors.md +73 -16
  8. data/docs/federation.md +201 -151
  9. data/docs/generated_modules.md +222 -165
  10. data/docs/getting_started.md +105 -81
  11. data/docs/logging.md +34 -4
  12. data/docs/scalars.md +119 -24
  13. data/docs/testing.md +191 -151
  14. data/docs/transports.md +47 -19
  15. data/docs/upgrading.md +210 -11
  16. data/lib/generators/graph_weaver/install_generator.rb +16 -1
  17. data/lib/graph_weaver/client.rb +46 -13
  18. data/lib/graph_weaver/codegen/aliases.rb +5 -4
  19. data/lib/graph_weaver/codegen/emit.rb +96 -39
  20. data/lib/graph_weaver/codegen/enum_type.rb +3 -0
  21. data/lib/graph_weaver/codegen/nodes.rb +42 -21
  22. data/lib/graph_weaver/codegen/scalar_type.rb +82 -79
  23. data/lib/graph_weaver/codegen/type_helpers.rb +1 -0
  24. data/lib/graph_weaver/codegen.rb +284 -84
  25. data/lib/graph_weaver/coerce.rb +113 -0
  26. data/lib/graph_weaver/errors.rb +30 -7
  27. data/lib/graph_weaver/federation.rb +6 -5
  28. data/lib/graph_weaver/hints.rb +76 -2
  29. data/lib/graph_weaver/in_process.rb +11 -8
  30. data/lib/graph_weaver/inflect.rb +2 -0
  31. data/lib/graph_weaver/input_struct.rb +115 -12
  32. data/lib/graph_weaver/internal/overrides.rb +101 -0
  33. data/lib/graph_weaver/internal/planner.rb +868 -0
  34. data/lib/graph_weaver/internal/schemas.rb +50 -0
  35. data/lib/graph_weaver/internal/selection.rb +127 -0
  36. data/lib/graph_weaver/{testing → internal}/subgraphs.rb +39 -41
  37. data/lib/graph_weaver/internal/values.rb +181 -0
  38. data/lib/graph_weaver/internal.rb +206 -0
  39. data/lib/graph_weaver/logging.rb +108 -20
  40. data/lib/graph_weaver/parsing.rb +5 -4
  41. data/lib/graph_weaver/query_module.rb +2 -0
  42. data/lib/graph_weaver/railtie.rb +113 -14
  43. data/lib/graph_weaver/representation.rb +30 -2
  44. data/lib/graph_weaver/response.rb +15 -0
  45. data/lib/graph_weaver/retry.rb +54 -22
  46. data/lib/graph_weaver/rspec.rb +50 -11
  47. data/lib/graph_weaver/schema_diff.rb +293 -0
  48. data/lib/graph_weaver/schema_loader.rb +96 -29
  49. data/lib/graph_weaver/tasks.rb +78 -29
  50. data/lib/graph_weaver/testing/cassette.rb +49 -65
  51. data/lib/graph_weaver/testing/coverage.rb +5 -4
  52. data/lib/graph_weaver/testing/failure.rb +10 -6
  53. data/lib/graph_weaver/testing/fake_client.rb +253 -60
  54. data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
  55. data/lib/graph_weaver/testing/router.rb +94 -808
  56. data/lib/graph_weaver/testing.rb +35 -84
  57. data/lib/graph_weaver/transport/faraday.rb +1 -1
  58. data/lib/graph_weaver/transport/http.rb +29 -12
  59. data/lib/graph_weaver/transport.rb +11 -34
  60. data/lib/graph_weaver/version.rb +1 -1
  61. data/lib/graph_weaver.rb +188 -110
  62. metadata +10 -5
  63. data/lib/graph_weaver/schemas.rb +0 -48
  64. data/lib/graph_weaver/selection.rb +0 -120
  65. data/lib/graph_weaver/testing/values.rb +0 -98
@@ -6,9 +6,11 @@ require "json"
6
6
 
7
7
  require_relative "../parsing"
8
8
  require_relative "../schema_loader"
9
- require_relative "../selection"
9
+ require_relative "../internal/selection"
10
+ require_relative "../internal"
10
11
  require_relative "../transport"
11
- require_relative "subgraphs"
12
+ require_relative "../internal/planner"
13
+ require_relative "../internal/subgraphs"
12
14
 
13
15
  module GraphWeaver
14
16
  module Testing
@@ -42,6 +44,12 @@ module GraphWeaver
42
44
  "doesn't say which subgraph answers each of its fields. Run this one against a real " \
43
45
  "router.",
44
46
  ],
47
+ context_argument: [
48
+ "a @fromContext argument no fetch here can supply",
49
+ "federation 2.8's @context/@fromContext fills the argument from a selection on an " \
50
+ "ancestor, and only the gateway that planned the fetch knows what to put there. Run " \
51
+ "this one against a real router.",
52
+ ],
45
53
  chained_requires: [
46
54
  "a @requires whose field set names another @requires field",
47
55
  "the router satisfies a @requires with one fetch, so it can't first satisfy that " \
@@ -60,8 +68,9 @@ module GraphWeaver
60
68
  ],
61
69
  shadowed_key: [
62
70
  "an alias shadowing an injected @key",
63
- "Apollo's router resolves that collision in favour of its own injected key and a " \
64
- "spec-conformant server doesn't, so there is no one answer to agree with. Rename the alias.",
71
+ "the local router injects the @key it crosses on under a reserved response key and " \
72
+ "Apollo injects it under the field's own name, so either way this alias claims a key " \
73
+ "the fetch needs. Rename the alias.",
65
74
  ],
66
75
  root_fields_span: [
67
76
  "a mutation's root fields span subgraphs",
@@ -144,7 +153,8 @@ module GraphWeaver
144
153
  # context: { current_user: user },
145
154
  # )
146
155
  #
147
- # (`subgraphs:` is optional — see {Subgraphs}.)
156
+ # (`subgraphs:` is optional — they're derived from what each loaded
157
+ # schema defines.)
148
158
  #
149
159
  # A supergraph only **partly** local — the rest of it served by other
150
160
  # processes — needs nothing extra: the subgraphs nobody here defines are
@@ -196,6 +206,11 @@ module GraphWeaver
196
206
  # refused at plan time, everything else runs
197
207
  attr_reader :absent
198
208
 
209
+ # subgraphs several loaded schema classes fit equally, so detection
210
+ # can't say which serves them — refused per query like an absent one,
211
+ # and the refusal lists the candidates
212
+ def ambiguous = @ambiguous.keys
213
+
199
214
  # subgraphs answered with fabricated data instead of that refusal
200
215
  attr_reader :faked
201
216
 
@@ -203,55 +218,20 @@ module GraphWeaver
203
218
  # run as a different user without rebuilding the router
204
219
  attr_accessor :context
205
220
 
206
- # response keys the planner injects to carry a @key across a boundary,
207
- # stripped before the caller sees the tree
208
- PREFIX = "_gw_"
209
-
210
- # Where the injected __typename lands. Which concrete type an abstract
211
- # position holds is a fact only the data carries, so every abstract
212
- # fetch asks for it — under this key whether or not the caller did.
213
- TYPENAME = "#{PREFIX}__typename"
214
-
215
- # A field set as dotted paths, back into the selection set it was parsed
216
- # from ({"origin" => {"lat" => {}, "lon" => {}}}). Both sides of a
217
- # crossing need it: one to ask for the fields, the other to read them
218
- # back in the shape the SDL spells.
219
- def self.field_tree(paths)
220
- paths.each_with_object({}) do |path, tree|
221
- path.split(".").reduce(tree) { |node, segment| node[segment] ||= {} }
222
- end
223
- end
224
-
225
- # What a fetch adds to carry a field set across a boundary: one field
226
- # per root, aliased under PREFIX so the caller's answer never gains a
227
- # field it didn't ask for, and nested exactly as the field set is —
228
- # `origin { lat lon }` comes back whole, under one response key.
229
- def self.injected_selections(paths)
230
- field_tree(paths).map do |root, children|
231
- GraphQL::Language::Nodes::Field.new(
232
- name: root, field_alias: PREFIX + root, selections: field_selections(children),
233
- )
234
- end
235
- end
236
-
237
- def self.field_selections(tree)
238
- tree.map do |name, children|
239
- GraphQL::Language::Nodes::Field.new(name:, selections: field_selections(children))
240
- end
241
- end
242
-
243
- # one error in the shape a GraphQL response carries them — a class
244
- # method because the Planner refuses documents before a Router exists
245
- def self.graphql_error(message, code)
246
- { "message" => message, "extensions" => { "code" => code } }
247
- end
221
+ # The planner injects key fields under this prefix, and the concrete
222
+ # __typename under that key; reading an answer back means stripping
223
+ # both, so the two sides share one definition.
224
+ PREFIX = Internal::Planner::PREFIX
225
+ TYPENAME = Internal::Planner::TYPENAME
226
+ private_constant :PREFIX, :TYPENAME
248
227
 
249
228
  # subgraphs: names the Ruby schema serving each subgraph. Omit it (or
250
229
  # any of its entries) and the rest are derived from what each loaded
251
- # schema defines — see {Subgraphs}, which also checks the ones you name.
230
+ # schema defines — see {Internal::Subgraphs}, which also checks the ones you name.
252
231
  # A subgraph nothing serves is absent (refused per query, not here);
253
232
  # `"reviews" => :fake` fabricates its answers instead.
254
- def initialize(supergraph:, subgraphs: nil, context: {})
233
+ # fake: how those fabricate — see {#fake=}.
234
+ def initialize(supergraph:, subgraphs: nil, context: {}, fake: {})
255
235
  source = supergraph.to_s # a path, or the SDL itself — Pathname included
256
236
  @schema = GraphWeaver::SchemaLoader.load(source)
257
237
  @table = GraphWeaver::SchemaLoader.routing_table(source)
@@ -260,13 +240,16 @@ module GraphWeaver
260
240
 
261
241
  Unplannable.unsupported!(@table)
262
242
 
263
- served = Subgraphs.resolve(@table, subgraphs)
264
- @faked = served.select { |_name, schema| schema == Subgraphs::FAKE }.keys.freeze
265
- @absent = (@table.subgraphs - served.keys).freeze
266
- @subgraphs = served.to_h do |name, schema|
267
- [name, (schema == Subgraphs::FAKE) ? FakeSubgraph.new(name, @schema) : schema]
268
- end
269
- @planner = Planner.new(table: @table, schema: @schema, absent: @absent)
243
+ resolution = Internal::Subgraphs.resolve(@table, subgraphs)
244
+ served = resolution.served
245
+ @ambiguous = resolution.ambiguous.freeze
246
+ @faked = served.select { |_name, schema| schema == Internal::Subgraphs::FAKE }.keys.freeze
247
+ @absent = (@table.subgraphs - served.keys - @ambiguous.keys).freeze
248
+ @subgraphs = served.reject { |_name, schema| schema == Internal::Subgraphs::FAKE }
249
+ @built_fake = check_fake!(fake)
250
+ @fake = @built_fake
251
+ build_fakes
252
+ @planner = Internal::Planner.new(table: @table, schema: @schema, absent: @absent, ambiguous: @ambiguous)
270
253
  end
271
254
 
272
255
  # Drop the fetches recorded so far, so #trace answers about what runs
@@ -284,15 +267,29 @@ module GraphWeaver
284
267
  # which is exactly what `rspec --seed` promises it won't.
285
268
  def reset!
286
269
  reset_trace
287
- @faked.each { |name| @subgraphs[name] = FakeSubgraph.new(name, @schema) }
270
+ @fake = @built_fake
271
+ build_fakes
288
272
  self
289
273
  end
290
274
 
275
+ # How faked subgraphs fabricate, for the example in hand: the pins and
276
+ # options {FakeClient} takes, in one hash (`"Shipment.carrier" => "UPS",
277
+ # list_size: 2`), merged onto the ones the router was built with. A router is
278
+ # built once for the suite, so this is how one example pins the data a
279
+ # faked subgraph answers with; {#reset!} puts it back.
280
+ def fake=(options)
281
+ @fake = check_fake!(@built_fake.merge(options.to_h))
282
+ build_fakes
283
+ end
284
+
285
+ # the options every faked subgraph is currently fabricating with
286
+ attr_reader :fake
287
+
291
288
  def execute(query, variables: {}, operation_name: nil)
292
289
  document = begin
293
290
  GraphQL.parse(query)
294
291
  rescue GraphQL::ParseError => e
295
- return { "data" => nil, "errors" => [Router.graphql_error(e.message, "GRAPHQL_PARSE_FAILED")] }
292
+ return { "data" => nil, "errors" => [Internal::Wire.graphql_error(e.message, "GRAPHQL_PARSE_FAILED")] }
296
293
  end
297
294
 
298
295
  # validate the way a router does, so a stale query fails as it fails
@@ -314,12 +311,28 @@ module GraphWeaver
314
311
  parts = ["subgraphs=#{(@subgraphs.keys - @faked).inspect}"]
315
312
  parts << "faked=#{@faked.inspect}" if @faked.any?
316
313
  parts << "absent=#{@absent.inspect}" if @absent.any?
314
+ parts << "ambiguous=#{ambiguous.inspect}" if @ambiguous.any?
317
315
  "#<#{self.class.name} #{parts.join(" ")}>"
318
316
  end
319
317
  alias to_s inspect
320
318
 
321
319
  private
322
320
 
321
+ def build_fakes
322
+ @faked.each { |name| @subgraphs[name] = FakeSubgraph.new(name, @schema, **@fake) }
323
+ end
324
+
325
+ # Options nothing fabricates with pin nothing and leave the example
326
+ # green — the same silent pass a typo'd override key is refused for.
327
+ def check_fake!(options)
328
+ options = options.to_h
329
+ return options.freeze if options.empty? || @faked.any?
330
+
331
+ raise GraphWeaver::ConfigurationError, "fake: says how faked subgraphs fabricate, and this " \
332
+ "router fakes none — ask for one with subgraphs: { \"#{@table.subgraphs.first}\" => " \
333
+ "#{Internal::Subgraphs::FAKE.inspect} }"
334
+ end
335
+
323
336
  # __schema / __type describe the COMPOSED graph; a subgraph would
324
337
  # answer with its own slice
325
338
  def introspect(query, variables, operation_name)
@@ -384,7 +397,7 @@ module GraphWeaver
384
397
  # and includes under @skip — the same way graphql-ruby resolves it.
385
398
  def included?(node, variables)
386
399
  node.directives.all? do |directive|
387
- next true unless GraphWeaver::Selection::CONDITIONAL_DIRECTIVES.include?(directive.name)
400
+ next true unless GraphWeaver::Internal::Selection::CONDITIONAL_DIRECTIVES.include?(directive.name)
388
401
 
389
402
  argument = directive.arguments.find { |arg| arg.name == "if" } or next true
390
403
  value = argument.value
@@ -405,7 +418,7 @@ module GraphWeaver
405
418
  # So bucket on the __typename that came back — each bucket then
406
419
  # crosses on its own type's @key, which is what a representation
407
420
  # needs and what the planner could not have known.
408
- if step.is_a?(Planner::Branches)
421
+ if step.is_a?(Internal::Planner::Branches)
409
422
  step.steps.each do |type_name, branch|
410
423
  stitch(branch, nodes.select { |(node, _)| node[TYPENAME] == type_name },
411
424
  operation, variables, errors)
@@ -425,7 +438,7 @@ module GraphWeaver
425
438
  # a real router makes too
426
439
  wanted.group_by { |d| [d.subgraph, d.requires.any?] }.each do |(target, chained), deferrals|
427
440
  fetched = chained ? nodes.reject { |(node, _)| blocked.include?(node.object_id) } : nodes
428
- tree = Router.field_tree(deferrals.flat_map(&:representation).uniq)
441
+ tree = Internal::Planner.field_tree(deferrals.flat_map(&:representation).uniq)
429
442
  representations = fetched.map { |(node, _)| representation(node, tree, step.type_name) }
430
443
 
431
444
  entities = []
@@ -471,13 +484,21 @@ module GraphWeaver
471
484
  # don't exist, so nothing depending on them can resolve.
472
485
  def prefetch(step, nodes, operation, variables, errors)
473
486
  blocked = []
474
- step.prefetches.each do |prefetch|
475
- key = Router.field_tree(prefetch.key)
487
+ # the field it feeds was excluded, so this is a fetch a real router
488
+ # never makes — and a test double that runs a resolver production
489
+ # wouldn't is answering a different question
490
+ wanted = step.prefetches.select { |prefetch| included?(prefetch.node, variables) }
491
+
492
+ # two @requires field sets that cross into the same subgraph on the
493
+ # same @key ride one entity fetch, as Apollo's do — the representations
494
+ # are identical, so a second call would only re-run resolvers
495
+ wanted.group_by { |prefetch| [prefetch.subgraph, prefetch.key] }.each do |(subgraph, keys), group|
496
+ key = Internal::Planner.field_tree(keys)
476
497
  representations = nodes.map { |(node, _)| representation(node, key, step.type_name) }
477
- selections = Router.injected_selections(prefetch.paths)
498
+ selections = Internal::Planner.injected_selections(group.flat_map(&:paths).uniq)
478
499
  roots = selections.map(&:alias)
479
500
 
480
- result = entities_fetch(prefetch.subgraph, step.type_name, selections, representations, operation, variables)
501
+ result = entities_fetch(subgraph, step.type_name, selections, representations, operation, variables)
481
502
  entities = result.dig("data", "_entities") || []
482
503
  Array(result["errors"]).each { |error| errors << rewrite(error, nodes) }
483
504
 
@@ -590,6 +611,7 @@ module GraphWeaver
590
611
  ),
591
612
  ),
592
613
  )
614
+ private_constant :REPRESENTATIONS, :REPRESENTATIONS_DEFINITION
593
615
 
594
616
  # A subgraph query may only declare the variables it uses, so each
595
617
  # fetch carries the slice of the operation's definitions it reached.
@@ -620,20 +642,21 @@ module GraphWeaver
620
642
  entry = { subgraph: name, query:, variables: variables.to_h }
621
643
  entry[:faked] = true if faked
622
644
  @trace << entry
623
- tag = GraphWeaver.logger && GraphWeaver::Transport.log_tag(operation_name)
645
+ tag = GraphWeaver.logger && GraphWeaver::Internal::Wire.log_tag(operation_name)
624
646
 
625
647
  # a fabricated answer that passes silently is worse than a failing
626
648
  # one, so it says so every fetch rather than once at construction
627
649
  if faked
628
- GraphWeaver.log(:warn) { "router -> #{name} #{tag} FAKED: fabricated data, not #{name}'s" }
650
+ GraphWeaver::Internal::Log.log(:warn) { "router -> #{name} #{tag} FAKED: fabricated data, not #{name}'s" }
629
651
  end
630
652
 
631
- GraphWeaver.log(:debug) do
632
- "router -> #{name} #{tag} variables=#{JSON.generate(variables)}\n" \
633
- "#{GraphWeaver::Transport.truncate_for_log(query)}"
653
+ GraphWeaver::Internal::Log.log(:debug) do
654
+ "router -> #{name} #{tag} variables=#{JSON.generate(GraphWeaver::Internal::Log.filter_variables(variables))}
655
+ " \
656
+ "#{GraphWeaver::Internal::Wire.truncate_for_log(query)}"
634
657
  end
635
658
 
636
- GraphWeaver.log_timed(:debug, "router -> #{name} #{tag} completed") do
659
+ GraphWeaver::Internal::Log.log_timed(:debug, "router -> #{name} #{tag} completed") do
637
660
  @subgraphs.fetch(name).execute(query, variables:, operation_name:, context: @context).to_h
638
661
  end
639
662
  end
@@ -710,743 +733,6 @@ module GraphWeaver
710
733
  ordered
711
734
  end
712
735
 
713
- # Decides which subgraph answers what — and, where an operation crosses
714
- # a boundary, the tree of fetches that answers it. Separate from the
715
- # Router because deciding needs only the supergraph: `rake
716
- # graph_weaver:federation:coverage` measures how much of a query set is
717
- # plannable without any subgraph being runnable.
718
- class Planner
719
- # One subgraph fetch. `selections` go over as written; `keys` names the
720
- # @key/@requires paths this fetch also asks for, to carry entities
721
- # across a boundary, and `injected` the response keys those land under
722
- # (Router::PREFIX + the path's first segment — a nested field set
723
- # arrives as one object), which the answer is stripped of. `children`
724
- # and `deferrals` are what happens to the objects it answers with — a
725
- # child stays in this subgraph and only carries deferrals deeper, a
726
- # deferral is refetched elsewhere. Both are lists: two selections can
727
- # share a response key, and each brings its own subtree.
728
- Step = Struct.new(:subgraph, :type_name, :selections, :keys, :injected, :prefetches,
729
- :children, :deferrals, keyword_init: true) do
730
- def subgraphs
731
- [subgraph] + prefetches.map(&:subgraph) +
732
- children.flat_map { |_key, child| child.subgraphs } + deferrals.flat_map(&:subgraphs)
733
- end
734
- end
735
-
736
- # the __typename every abstract fetch asks for, under the router's own
737
- # response key so the caller's answer never gains one it didn't ask for
738
- TYPENAME_FIELD = GraphQL::Language::Nodes::Field.new(
739
- name: "__typename", field_alias: Router::TYPENAME,
740
- )
741
-
742
- # What a field returning an abstract type defers to: one plan per
743
- # concrete type the subgraph can answer with. Which of them applies is
744
- # a fact about the data, and the planner runs before any fetch — so it
745
- # plans them all and {Router#stitch} picks by __typename.
746
- Branches = Struct.new(:steps, keyword_init: true) do
747
- def subgraphs = steps.each_value.flat_map(&:subgraphs)
748
-
749
- # what the parent's fetch asks for: each branch under its own type
750
- # condition, and the __typename that says which one answered
751
- def selections
752
- [TYPENAME_FIELD] + steps.filter_map do |type_name, step|
753
- next if step.selections.empty?
754
-
755
- GraphQL::Language::Nodes::InlineFragment.new(
756
- type: GraphQL::Language::Nodes::TypeName.new(name: type_name),
757
- selections: step.selections,
758
- )
759
- end
760
- end
761
- end
762
-
763
- # A @requires field set the router has to supply: fetch those fields
764
- # from the subgraph that holds them, into hidden keys on the object,
765
- # before the fetch whose representation carries them.
766
- Prefetch = Struct.new(:subgraph, :key, :paths, keyword_init: true)
767
-
768
- # A field this subgraph can't resolve: refetch the parent entity from
769
- # `subgraph` and read it there.
770
- Deferral = Struct.new(:node, :response_key, :subgraph, :step, :key, :requires,
771
- keyword_init: true) do
772
- def subgraphs = [subgraph] + (step ? step.subgraphs : [])
773
-
774
- # the paths a representation for this deferral has to carry
775
- def representation = (key + requires).uniq
776
- end
777
-
778
- # What one operation costs. `verbatim` is the shape the whole thing
779
- # resolves in one subgraph, where the document goes over untouched.
780
- Plan = Struct.new(:steps, :operation, :selections, :fragments, :root_type, :introspection,
781
- :verbatim, keyword_init: true) do
782
- def operation_name = operation&.name
783
-
784
- def entry = steps.first&.subgraph
785
-
786
- # every subgraph the plan fetches from, in plan order
787
- def subgraphs = steps.flat_map(&:subgraphs).uniq
788
-
789
- # which subgraphs it touches, for a report — "accounts+reviews"
790
- # when it stitches (sorted: fetch order is what #trace is for)
791
- def where = introspection ? "(introspection)" : subgraphs.sort.join("+")
792
- end
793
-
794
- # the fields a router answers itself rather than routing
795
- INTROSPECTION = %w[__schema __type].freeze
796
-
797
- # a fragment spread can't cycle (validation rejects that), so this is
798
- # only ever reached by a document validation didn't see
799
- MAX_DEPTH = 32
800
-
801
- # absent: subgraphs no schema serves here. Planning is otherwise
802
- # unchanged — coverage plans with none of them loaded, which is why
803
- # absence is a fact about this process rather than about the graph.
804
- def initialize(table:, schema:, absent: [])
805
- @table = table
806
- @schema = schema
807
- @absent = absent
808
- @interface_objects = table.interface_objects
809
- end
810
-
811
- # the operation's validation errors, GraphQL-wire shaped
812
- def validate(document)
813
- @schema.validate(document)
814
- .map { |error| Router.graphql_error(error.message, "GRAPHQL_VALIDATION_FAILED") }
815
- end
816
-
817
- def plan(document, operation_name: nil)
818
- operation = pick_operation(document, operation_name)
819
- refuse(:operation_type, "this document is a subscription") if
820
- operation.operation_type == "subscription"
821
-
822
- fragments = document.definitions
823
- .grep(GraphQL::Language::Nodes::FragmentDefinition).to_h { |f| [f.name, f] }
824
- root = root_type(operation)
825
- selections = narrow(root.graphql_name, operation.selections, fragments)
826
- plan = Plan.new(operation:, selections:, fragments:, root_type: root, steps: [])
827
-
828
- introspection, data = selections.partition { |node| INTROSPECTION.include?(node.name) }
829
- if introspection.any?
830
- if data.any? { |node| node.name != "__typename" }
831
- refuse :mixed_introspection,
832
- "this operation selects #{introspection.map(&:name).uniq.join(" and ")} " \
833
- "alongside data fields"
834
- end
835
-
836
- plan.introspection = true
837
- return plan
838
- end
839
-
840
- check_interface_objects!(root.graphql_name, selections, fragments) if @interface_objects.any?
841
-
842
- entry = single_subgraph(root.graphql_name, selections, fragments)
843
- if entry
844
- plan.verbatim = true
845
- plan.steps = [step(entry, root.graphql_name)]
846
- return plan
847
- end
848
-
849
- plan.steps = root_steps(root.graphql_name, selections, operation, fragments)
850
- plan
851
- end
852
-
853
- # The selections that apply to ONE concrete type, as plain fields a
854
- # step can route one at a time: fields written at this position, plus
855
- # every fragment whose condition that type satisfies, folded in. A
856
- # fragment it can't be never matches, so it is dropped rather than
857
- # travelling as written — every position a step plans is concrete, so
858
- # a condition either holds for all of its objects or for none.
859
- #
860
- # Public because {Router#propagate} asks the same question of the
861
- # merged tree: which selections describe the object in hand.
862
- def narrow(concrete, selections, fragments, depth = 0)
863
- return [] if depth > MAX_DEPTH
864
-
865
- selections.flat_map do |node|
866
- case node
867
- when GraphQL::Language::Nodes::Field then [node]
868
- when GraphQL::Language::Nodes::InlineFragment
869
- next [] unless applies?(node.type&.name, concrete)
870
-
871
- carry(node, narrow(concrete, node.selections, fragments, depth + 1))
872
- when GraphQL::Language::Nodes::FragmentSpread
873
- fragment = fragments[node.name] or
874
- refuse(:undefined_fragment, "the document spreads ...#{node.name}, which it never defines")
875
- next [] unless applies?(fragment.type.name, concrete)
876
-
877
- carry(node, narrow(concrete, fragment.selections, fragments, depth + 1))
878
- else []
879
- end
880
- end
881
- end
882
-
883
- private
884
-
885
- # Every type these selections reach, refused if one of them is an
886
- # @interfaceObject: a subgraph resolves the whole interface there, so
887
- # the supergraph records no per-field routing for it and every fetch
888
- # planned against it would be a guess. Asked per query rather than at
889
- # construction — one such directive shouldn't cost you the queries
890
- # that never touch the type.
891
- def check_interface_objects!(type_name, selections, fragments, depth = 0)
892
- return if depth > MAX_DEPTH
893
-
894
- selections.each do |node|
895
- case node
896
- when GraphQL::Language::Nodes::Field
897
- next if node.name.start_with?("__")
898
-
899
- child = raw_child_type(type_name, node.name) or next
900
- interface_object!(child, "#{type_name}.#{node.name} returns #{child}")
901
- check_interface_objects!(child, node.selections, fragments, depth + 1)
902
- when GraphQL::Language::Nodes::InlineFragment
903
- condition = node.type&.name || type_name
904
- interface_object!(condition, "this operation selects ... on #{condition}")
905
- check_interface_objects!(condition, node.selections, fragments, depth + 1)
906
- when GraphQL::Language::Nodes::FragmentSpread
907
- fragment = fragments[node.name] or next
908
- condition = fragment.type.name
909
- interface_object!(condition, "...#{node.name} is on #{condition}")
910
- check_interface_objects!(condition, fragment.selections, fragments, depth + 1)
911
- end
912
- end
913
- end
914
-
915
- def interface_object!(type_name, where)
916
- graphs = @interface_objects[type_name] or return
917
-
918
- refuse :interface_object,
919
- "#{where}, which #{graphs.join(" and ")} resolves as an @interfaceObject"
920
- end
921
-
922
- # Whether a fragment's condition holds for every object of `concrete`
923
- # — the type itself, or an abstract type it satisfies.
924
- def applies?(condition, concrete)
925
- return true if condition.nil? || condition == concrete
926
-
927
- type = @schema.get_type(condition)
928
- !!type&.kind&.abstract? && @schema.possible_types(type).any? { |t| t.graphql_name == concrete }
929
- end
930
-
931
- def step(subgraph, type_name)
932
- Step.new(subgraph:, type_name:, selections: [], keys: [], injected: [], prefetches: [],
933
- children: [], deferrals: [])
934
- end
935
-
936
- def pick_operation(document, name)
937
- operations = document.definitions.grep(GraphQL::Language::Nodes::OperationDefinition)
938
- named = operations.map { |op| op.name || "anonymous" }
939
- if name
940
- return operations.find { |op| op.name == name } || refuse(:ambiguous_operation,
941
- "the document defines no operation named #{name.inspect} (it has #{named.join(", ")})")
942
- end
943
- return operations.first if operations.one?
944
-
945
- refuse :ambiguous_operation, "the document holds #{operations.size} operations (#{named.join(", ")})"
946
- end
947
-
948
- def root_type(operation)
949
- root = (operation.operation_type == "mutation") ? @schema.mutation : @schema.query
950
- root || refuse(:operation_type,
951
- "the composed schema has no #{operation.operation_type || "query"} root type")
952
- end
953
-
954
- # The one subgraph that answers the whole operation, if there is one.
955
- # Root fields fix the candidates: they're independent, so the ones
956
- # they share are the only subgraphs that could answer everything.
957
- def single_subgraph(root, selections, fragments)
958
- fields = selections.reject { |node| node.name.start_with?("__") }
959
- shared = fields.map { |node| owners!(root, node.name) }.reduce(:&) || @table.subgraphs
960
- # no refusal here: an absent candidate just isn't one, and the
961
- # per-field walk below names it if that's what stops the query
962
- (shared - @absent).find { |subgraph| local?(root, selections, subgraph, fragments, []) }
963
- end
964
-
965
- # Root fields resolve independently, so each picks its own subgraph
966
- # and one fetch goes to each — preferring a subgraph already in the
967
- # plan, so a query that could run in fewer doesn't run in more.
968
- def root_steps(root, selections, operation, fragments)
969
- if operation.operation_type == "mutation"
970
- owners = selections.reject { |node| node.name.start_with?("__") }
971
- .to_h { |node| [node.name, owners!(root, node.name)] }
972
- # Root mutation fields run in series. Sharing a subgraph, they go
973
- # over as one document and it serializes them; only roots in
974
- # *different* subgraphs would run in whatever order the plan
975
- # happens to. Whatever stitches below a root is an ordinary read
976
- # afterwards, so it doesn't bear on the ordering.
977
- shared = owners.values.reduce(:&) || @table.subgraphs
978
- if shared.empty?
979
- refuse :root_fields_span, "this mutation's root fields span subgraphs: " \
980
- "#{owners.map { |name, graphs| "#{root}.#{name} (#{graphs.join(" or ")})" }.join(", ")}"
981
- end
982
-
983
- # one root field: shared IS its owners, so an absence names it
984
- subject = owners.one? ? "#{root}.#{owners.keys.first}" : root
985
- return [plan_step(root, selections, available!(shared, subject).first, fragments, [], 0)]
986
- end
987
-
988
- groups = {}
989
- loose = []
990
- selections.each do |node|
991
- if node.name.start_with?("__")
992
- loose << node
993
- next
994
- end
995
-
996
- graphs = available!(owners!(root, node.name), "#{root}.#{node.name}")
997
- (groups[(graphs & groups.keys).first || graphs.first] ||= []) << node
998
- end
999
- groups[available!(@table.subgraphs, root).first] ||= [] if groups.empty?
1000
- # __typename doesn't route; any subgraph answers it
1001
- groups[groups.keys.first].concat(loose)
1002
-
1003
- groups.map { |subgraph, nodes| plan_step(root, nodes, subgraph, fragments, [], 0) }
1004
- end
1005
-
1006
- # Build the fetch for `selections` on `type_name` in `subgraph`.
1007
- # `provided` names fields a @provides copy makes answerable here even
1008
- # though the routing table places them elsewhere.
1009
- def plan_step(type_name, selections, subgraph, fragments, provided, depth)
1010
- refuse(:too_deep, "this operation nests deeper than #{MAX_DEPTH} levels") if depth > MAX_DEPTH
1011
-
1012
- here = step(subgraph, type_name)
1013
- selections.each do |node|
1014
- # a subtree that never leaves this subgraph goes over as written:
1015
- # the boundary rules govern stitching, so they have no business
1016
- # applying to a query that was never going to cross one
1017
- if node.name.start_with?("__") || local?(type_name, [node], subgraph, fragments, provided)
1018
- here.selections << node
1019
- next
1020
- end
1021
-
1022
- owners = owners!(type_name, node.name)
1023
- field = @table.field(type_name, node.name)
1024
- resolves_here = owners.include?(subgraph) || provided.include?(node.name)
1025
- if resolves_here && held?(type_name, field, subgraph)
1026
- descend(here, type_name, node, subgraph, field, fragments, depth)
1027
- else
1028
- # a field whose @requires this subgraph can't supply is refetched
1029
- # even when it resolves here — the fields have to arrive in a
1030
- # representation, and only an entity fetch carries one
1031
- target = resolves_here ? subgraph : available!(owners, "#{type_name}.#{node.name}").first
1032
- defer(here, type_name, node, subgraph, target, field, fragments, selections, depth)
1033
- end
1034
- end
1035
-
1036
- check_one_source!(type_name, here, subgraph)
1037
- # every crossing this fetch feeds, asked for once and together: a
1038
- # field set shared by two deferrals is one selection, and a nested
1039
- # one is nested rather than a dotted alias no schema has
1040
- here.selections.concat(Router.injected_selections(here.keys))
1041
- here.injected = (here.keys + here.prefetches.flat_map(&:paths))
1042
- .map { |path| Router::PREFIX + path.split(".").first }.uniq
1043
- here
1044
- end
1045
-
1046
- # The field resolves in this subgraph but something under it doesn't.
1047
- def descend(step, type_name, node, subgraph, field, fragments, depth)
1048
- child = plan_child(type_name, node, subgraph, field, fragments, depth)
1049
-
1050
- step.selections << node.merge(selections: child.selections)
1051
- step.children << [node.alias || node.name, child]
1052
- end
1053
-
1054
- # The plan for what this field returns, run in `subgraph`.
1055
- def plan_child(type_name, node, subgraph, field, fragments, depth)
1056
- child_type = child_type_name(type_name, node.name)
1057
- return plan_branches(type_name, node, child_type, subgraph, field, fragments, depth) if
1058
- @schema.get_type(child_type)&.kind&.abstract?
1059
-
1060
- plan_step(child_type, narrow(child_type, node.selections, fragments), subgraph, fragments,
1061
- provides(field), depth + 1)
1062
- end
1063
-
1064
- # One plan per concrete type `subgraph` can answer this abstract type
1065
- # with — the supergraph says which those are, and a fetch may only name
1066
- # those: a subgraph rejects an `... on T` its own schema doesn't place
1067
- # in the abstract type.
1068
- def plan_branches(type_name, node, abstract_name, subgraph, field, fragments, depth)
1069
- possible = @table.possible_types(abstract_name, subgraph)
1070
- if possible.nil?
1071
- refuse :abstract_boundary, "#{type_name}.#{node.name} returns #{abstract_name}, and " \
1072
- "the supergraph doesn't record which concrete types #{subgraph} answers it with " \
1073
- "(no @join__unionMember or @join__implements, and #{abstract_name} is in more than " \
1074
- "one subgraph)"
1075
- end
1076
- if possible.empty?
1077
- refuse :abstract_boundary, "#{type_name}.#{node.name} returns #{abstract_name}, and " \
1078
- "the supergraph places none of its concrete types in #{subgraph}"
1079
- end
1080
-
1081
- Branches.new(steps: possible.sort.to_h do |concrete|
1082
- [concrete, plan_step(concrete, narrow(concrete, node.selections, fragments), subgraph,
1083
- fragments, provides(field), depth + 1)]
1084
- end)
1085
- end
1086
-
1087
- # Refetch this object from its @key in the subgraph that resolves the
1088
- # field, and read the field off the entity that comes back. `target`
1089
- # is this subgraph when the field lives here but @requires fields it
1090
- # doesn't hold — the router refetches for those too.
1091
- def defer(step, type_name, node, subgraph, target, field, fragments, siblings, depth)
1092
- key = usable_key(type_name, node, subgraph, target)
1093
- requires = requires_paths(field)
1094
- check_shadowing!(type_name, node, siblings, (key + requires).uniq)
1095
-
1096
- # a @requires field this subgraph doesn't hold is fetched from the
1097
- # one that does and handed back in the representation — a fetch
1098
- # before the fetch, which is what makes this a chain
1099
- elsewhere = requires.reject { |path| path_owners(type_name, path).include?(subgraph) }
1100
- prefetch(step, type_name, node, subgraph, elsewhere)
1101
-
1102
- ((key + requires).uniq - elsewhere).each { |path| inject(step, path) }
1103
-
1104
- child = plan_child(type_name, node, target, field, fragments, depth) if node.selections.any?
1105
-
1106
- step.deferrals << Deferral.new(
1107
- node: child ? node.merge(selections: child.selections) : node,
1108
- response_key: node.alias || node.name,
1109
- subgraph: target,
1110
- step: child || nil,
1111
- key:, requires:,
1112
- )
1113
- end
1114
-
1115
- # One fetch per subgraph holding a @requires field this one doesn't,
1116
- # ahead of the fetch that needs them. Only one hop: the key for each
1117
- # has to come from `subgraph` itself, so a chain can't grow a chain.
1118
- def prefetch(step, type_name, node, subgraph, paths)
1119
- paths.each { |path| check_chain!(type_name, node, path) }
1120
-
1121
- paths.group_by { |path| requires_holder(type_name, node, path) }.each do |holder, held|
1122
- key = usable_key(type_name, node, subgraph, holder)
1123
- key.each { |path| inject(step, path) }
1124
- step.prefetches << Prefetch.new(subgraph: holder, key:, paths: held)
1125
- end
1126
- end
1127
-
1128
- # A prefetch sends the entity's own @key and nothing else, so a required
1129
- # field that is itself @requires-ed gets computed from a representation
1130
- # missing its input — silently, and the same field then holds two
1131
- # different values in one response. Asked of every field a path walks
1132
- # through, not only its first: nesting doesn't make a chain shallower.
1133
- def check_chain!(type_name, node, path)
1134
- walk(type_name, path).each do |owner, name|
1135
- inner = @table.field(owner, name)&.requires or next
1136
-
1137
- refuse :chained_requires,
1138
- "#{type_name}.#{node.name} @requires #{path.inspect}, and #{owner}.#{name} " \
1139
- "itself @requires #{inner.inspect}"
1140
- end
1141
- end
1142
-
1143
- def requires_holder(type_name, node, path)
1144
- owners = path_owners(type_name, path)
1145
- return available!(owners, "#{type_name}.#{path}").first if owners.any?
1146
-
1147
- # two different facts, and only the second is about nesting: a field
1148
- # the supergraph places nowhere, or one whose path it places in
1149
- # subgraphs that don't overlap
1150
- pairs = walk(type_name, path)
1151
- orphan = pairs.find { |owner, name| @table.owners(owner, name).empty? }
1152
- missing = pairs.empty? ? "#{type_name}.#{path}" : orphan&.join(".")
1153
- refuse(:no_owner, "#{type_name}.#{node.name} @requires #{path.inspect}, and the " \
1154
- "supergraph places #{missing} in no subgraph") if missing
1155
-
1156
- refuse :nested_field_set, "#{type_name}.#{node.name} @requires a nested field set " \
1157
- "(#{field_set([path]).inspect}) no one subgraph holds whole (" +
1158
- pairs.map { |owner, name| "#{owner}.#{name} in #{@table.owners(owner, name).join(" or ")}" }
1159
- .join(", ") + ")"
1160
- end
1161
-
1162
- def inject(step, path)
1163
- step.keys << path unless step.keys.include?(path)
1164
- end
1165
-
1166
- # A nested field set arrives as ONE object under one response key, so
1167
- # every path sharing a root has to come from the same fetch: half of
1168
- # `origin` from here and half from a prefetch leaves the object
1169
- # half-built, and two prefetches overwrite each other's half.
1170
- def check_one_source!(type_name, step, subgraph)
1171
- sources = Hash.new { |roots, root| roots[root] = {} }
1172
- step.keys.each { |path| sources[path.split(".").first][path] = subgraph }
1173
- step.prefetches.each do |prefetch|
1174
- prefetch.paths.each { |path| sources[path.split(".").first][path] = prefetch.subgraph }
1175
- end
1176
-
1177
- sources.each do |root, from|
1178
- next if from.values.uniq.one?
1179
-
1180
- refuse :nested_field_set, "#{type_name}'s #{root.inspect} is part of a field set this " \
1181
- "fetch would have to build from more than one subgraph " \
1182
- "(#{from.map { |path, graph| "#{path} from #{graph}" }.join(", ")})"
1183
- end
1184
- end
1185
-
1186
- # Apollo's router injects the @key under its own name and lets it win,
1187
- # so `{ id: username }` next to a stitched field comes back as the
1188
- # user's id. That is an Apollo bug and a spec-conformant server
1189
- # disagrees — and since we can't match both, refuse rather than hand
1190
- # back an answer one of them contradicts.
1191
- def check_shadowing!(type_name, node, siblings, paths)
1192
- # Apollo injects a field set under its own names, so what an alias
1193
- # can collide with is each path's first segment — the field a flat
1194
- # path is, or the object a nested one arrives in
1195
- roots = paths.map { |path| path.split(".").first }.uniq
1196
- shadowed = siblings.select do |sibling|
1197
- sibling.alias && sibling.alias != sibling.name && roots.include?(sibling.alias)
1198
- end
1199
- return if shadowed.empty?
1200
-
1201
- refuse :shadowed_key,
1202
- "#{type_name}.#{node.name} is fetched on #{type_name}'s #{roots.map(&:inspect).join(", ")}, " \
1203
- "and this selection aliases " \
1204
- "#{shadowed.map { |s| "#{s.name} as #{s.alias.inspect}" }.join(", ")} over it"
1205
- end
1206
-
1207
- # A @key field set the source subgraph can build a representation
1208
- # from — the first the supergraph declares that it can, nested or
1209
- # flat. An @external copy counts: it exists precisely so this
1210
- # subgraph can name the field in its @key.
1211
- def usable_key(type_name, node, from, to)
1212
- candidates = @table.keys(type_name, to)
1213
- if candidates.empty?
1214
- refuse :no_key,
1215
- "#{type_name}.#{node.name} resolves in #{to}, and #{type_name} has no resolvable " \
1216
- "@key there"
1217
- end
1218
-
1219
- usable = candidates.find { |paths| paths.all? { |path| declares?(type_name, path, from) } }
1220
- return usable if usable
1221
-
1222
- refuse :no_key, "#{type_name}.#{node.name} needs a fetch into #{to}, and #{from} can't " \
1223
- "supply any of #{type_name}'s @keys there " \
1224
- "(#{candidates.map { |paths| field_set(paths).inspect }.join(", ")})"
1225
- end
1226
-
1227
- def requires_paths(field)
1228
- return [] unless field&.requires
1229
-
1230
- GraphWeaver::SchemaLoader::RoutingTable.parse_field_set(field.requires)
1231
- end
1232
-
1233
- # Dotted paths back to the selection set they were parsed from — the
1234
- # inverse of RoutingTable.parse_field_set, so a refusal spells the
1235
- # field set the way the schema does and is greppable against it.
1236
- def field_set(paths) = render_field_set(Router.field_tree(paths))
1237
-
1238
- def render_field_set(tree)
1239
- tree.map { |name, children|
1240
- children.empty? ? name : "#{name} { #{render_field_set(children)} }"
1241
- }.join(" ")
1242
- end
1243
-
1244
- # A @requires field set is supplied by the ROUTER: it fetches those
1245
- # fields elsewhere and hands them back in the representation. So a
1246
- # field is only answerable in place when its own subgraph already
1247
- # holds every one of them — which, since @requires fields are
1248
- # @external there, it essentially never does. When it doesn't, the
1249
- # field is planned as a fetch chain instead (see prefetch).
1250
- def held?(type_name, field, subgraph)
1251
- return true unless field&.requires
1252
-
1253
- GraphWeaver::SchemaLoader::RoutingTable.parse_field_set(field.requires)
1254
- .all? { |path| path_owners(type_name, path).include?(subgraph) }
1255
- end
1256
-
1257
- # The subgraphs that can answer a field set path in ONE fetch: the
1258
- # owners of every field it walks through, intersected. A representation
1259
- # carries the nested object whole, so a path answerable only a level at
1260
- # a time is answerable by nobody.
1261
- def path_owners(type_name, path)
1262
- pairs = walk(type_name, path)
1263
- return [] if pairs.empty?
1264
-
1265
- pairs.map { |owner, name| @table.owners(owner, name) }.reduce(:&)
1266
- end
1267
-
1268
- # Every [type, field] a dotted path names, from `type_name` down —
1269
- # empty when the composed schema doesn't carry the whole walk.
1270
- def walk(type_name, path)
1271
- pairs = []
1272
- path.split(".").each do |segment|
1273
- return [] if type_name.nil?
1274
-
1275
- pairs << [type_name, segment]
1276
- type_name = raw_child_type(type_name, segment)
1277
- end
1278
- pairs
1279
- end
1280
-
1281
- # Every field these selections reach is answerable by `subgraph`, so
1282
- # the whole subtree can go over untouched.
1283
- def local?(type_name, selections, subgraph, fragments, provided, depth = 0)
1284
- return false if depth > MAX_DEPTH
1285
-
1286
- selections.all? do |node|
1287
- case node
1288
- when GraphQL::Language::Nodes::Field
1289
- next true if node.name.start_with?("__")
1290
-
1291
- local_field?(type_name, node, subgraph, fragments, provided, depth)
1292
- when GraphQL::Language::Nodes::InlineFragment
1293
- condition = node.type&.name || type_name
1294
- declared_in?(condition, subgraph) &&
1295
- local?(condition, node.selections, subgraph, fragments, provided, depth + 1)
1296
- when GraphQL::Language::Nodes::FragmentSpread
1297
- fragment = fragments[node.name] or
1298
- refuse(:undefined_fragment, "the document spreads ...#{node.name}, which it never defines")
1299
- declared_in?(fragment.type.name, subgraph) &&
1300
- local?(fragment.type.name, fragment.selections, subgraph, fragments, provided, depth + 1)
1301
- else false
1302
- end
1303
- end
1304
- end
1305
-
1306
- def local_field?(type_name, node, subgraph, fragments, provided, depth)
1307
- owners = @table.owners(type_name, node.name)
1308
- return false unless owners.include?(subgraph) || provided.include?(node.name)
1309
-
1310
- field = @table.field(type_name, node.name)
1311
- return false unless held?(type_name, field, subgraph)
1312
- return true if node.selections.empty?
1313
-
1314
- child = raw_child_type(type_name, node.name) or return false
1315
- local?(child, node.selections, subgraph, fragments, provides(field), depth + 1)
1316
- end
1317
-
1318
- # Folding a same-type fragment into its parent drops the fragment node,
1319
- # so whatever @skip/@include it carried has to move onto the selections
1320
- # it guarded — otherwise a stitched plan answers a selection the
1321
- # operation excluded, and fetches a subgraph to do it.
1322
- def carry(node, expanded)
1323
- return expanded if node.directives.empty?
1324
-
1325
- expanded.map do |field|
1326
- clash = field.directives.map(&:name) & node.directives.map(&:name)
1327
- if clash.any?
1328
- # one selection can't hold two conditions of the same name
1329
- refuse :conditional_fragment,
1330
- "#{field.alias || field.name} carries @#{clash.first}, and so does the fragment " \
1331
- "spread around it"
1332
- end
1333
-
1334
- field.merge(directives: node.directives + field.directives)
1335
- end
1336
- end
1337
-
1338
- # A fragment's type condition has to exist in the subgraph running
1339
- # it; a type only another subgraph declares can't be matched there.
1340
- # Types the routing table says nothing about (scalars, enums) are
1341
- # nobody's.
1342
- def declared_in?(type_name, subgraph)
1343
- declared = @table.declared_in(type_name)
1344
- declared.empty? || declared.include?(subgraph)
1345
- end
1346
-
1347
- # Whether `subgraph` can hand back this field set path as part of a
1348
- # representation — every field it walks through, since a nested path
1349
- # is selected there in one go. An @external copy counts, which is the
1350
- # whole reason one is declared.
1351
- def declares?(type_name, path, subgraph)
1352
- pairs = walk(type_name, path)
1353
- pairs.any? && pairs.all? { |owner, name| declares_field?(owner, name, subgraph) }
1354
- end
1355
-
1356
- def declares_field?(type_name, field_name, subgraph)
1357
- field = @table.field(type_name, field_name)
1358
- return @table.declared_in(type_name).include?(subgraph) if field.nil?
1359
-
1360
- field.graphs.include?(subgraph) || field.external.include?(subgraph)
1361
- end
1362
-
1363
- # @provides says this subgraph carries its own copy of fields it
1364
- # doesn't own, and the router reads that copy rather than routing to
1365
- # the owner — so a query reaching only provided fields never leaves.
1366
- # Flat sets only; a nested one widens nothing and its fields fall back
1367
- # to the ordinary owner check.
1368
- def provides(field)
1369
- return [] unless field&.provides
1370
-
1371
- GraphWeaver::SchemaLoader::RoutingTable.parse_field_set(field.provides)
1372
- .reject { |path| path.include?(".") }
1373
- end
1374
-
1375
- def owners!(type_name, field_name)
1376
- owners = @table.owners(type_name, field_name)
1377
- return owners if owners.any?
1378
-
1379
- refuse :no_owner, "the supergraph places #{type_name}.#{field_name} in no subgraph"
1380
- end
1381
-
1382
- # The subgraphs among `owners` this process actually serves. A
1383
- # supergraph is routinely only partly local, so absence is refused
1384
- # here — where the field that reached for it is still in hand —
1385
- # rather than at construction, which would refuse the whole suite
1386
- # over fields it may never touch.
1387
- def available!(owners, coordinate)
1388
- here = owners - @absent
1389
- return here if here.any?
1390
-
1391
- absent = owners.map(&:inspect)
1392
- refuse :absent_subgraph, "#{coordinate} resolves in #{absent.join(" or ")}, which no " \
1393
- "schema here serves — nothing loaded defines what the supergraph says " \
1394
- "#{absent.first} resolves. #{advice(absent.first)}"
1395
- end
1396
-
1397
- # Two causes, and only one of them applies at a time. A class Rails
1398
- # hasn't autoloaded yet is the usual one — but not when eager loading
1399
- # is already on, and *that* the library can just ask, rather than
1400
- # leading with a guess it can see is wrong. The other cause is a
1401
- # subgraph that genuinely runs in another service, and its fix has to
1402
- # come first for the reader it applies to. Either way the surface
1403
- # named is the one an rspec example can reach: there is no Router.new
1404
- # in sight from inside one.
1405
- def advice(name)
1406
- fake = "subgraphs: { #{name} => #{Subgraphs::FAKE.inspect} } " \
1407
- "(GraphWeaver::Testing.config.router = { subgraphs: … } under the rspec tag, or " \
1408
- "subgraphs: on Router.new) — or a schema class in place of #{Subgraphs::FAKE.inspect}"
1409
- if eager_loaded?
1410
- "Eager loading is on, so it isn't a class waiting to be autoloaded — it runs " \
1411
- "elsewhere. Fabricate its answers: #{fake}."
1412
- else
1413
- "Rails autoloads, so the class is probably just not loaded yet: eager-load it " \
1414
- "(config.eager_load, or config.rake_eager_load under rake). If it runs elsewhere, " \
1415
- "fabricate its answers instead — #{fake}."
1416
- end
1417
- end
1418
-
1419
- # Whether the "not autoloaded yet" half of the advice is already ruled
1420
- # out. Outside Rails there is no autoloading to blame either.
1421
- # const_get rather than a bare Rails: sorbet can't resolve a constant
1422
- # the gem doesn't depend on.
1423
- def eager_loaded?
1424
- return false unless Object.const_defined?(:Rails)
1425
-
1426
- config = Object.const_get(:Rails).application&.config or return false
1427
- !!(config.eager_load ||
1428
- (Object.const_defined?(:Rake) && config.respond_to?(:rake_eager_load) && config.rake_eager_load))
1429
- rescue NoMethodError
1430
- false # something else named Rails
1431
- end
1432
-
1433
- def child_type_name(type_name, field_name)
1434
- raw_child_type(type_name, field_name) ||
1435
- refuse(:no_owner, "#{type_name}.#{field_name} is not a field of the composed schema")
1436
- end
1437
-
1438
- def raw_child_type(type_name, field_name)
1439
- type = @schema.get_type(type_name)
1440
- return unless type.respond_to?(:fields)
1441
-
1442
- field = type.fields[field_name] or return
1443
- field.type.unwrap.graphql_name
1444
- end
1445
-
1446
- def refuse(category, message)
1447
- raise Unplannable.new(message, category:)
1448
- end
1449
- end
1450
736
  end
1451
737
  end
1452
738
  end