graph_weaver 0.5.0 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +537 -0
  3. data/Gemfile.lock +19 -19
  4. data/README.md +74 -53
  5. data/docs/cassettes.md +29 -4
  6. data/docs/editors.md +3 -1
  7. data/docs/errors.md +75 -16
  8. data/docs/federation.md +206 -155
  9. data/docs/generated_modules.md +223 -166
  10. data/docs/getting_started.md +106 -82
  11. data/docs/logging.md +35 -5
  12. data/docs/scalars.md +119 -24
  13. data/docs/testing.md +196 -155
  14. data/docs/transports.md +47 -19
  15. data/docs/upgrading.md +243 -22
  16. data/graph_weaver.gemspec +16 -2
  17. data/lib/generators/graph_weaver/install_generator.rb +31 -16
  18. data/lib/graph_weaver/client.rb +52 -15
  19. data/lib/graph_weaver/codegen/aliases.rb +15 -8
  20. data/lib/graph_weaver/codegen/emit.rb +107 -42
  21. data/lib/graph_weaver/codegen/enum_type.rb +4 -3
  22. data/lib/graph_weaver/codegen/nodes.rb +42 -21
  23. data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
  24. data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
  25. data/lib/graph_weaver/codegen.rb +382 -105
  26. data/lib/graph_weaver/coerce.rb +113 -0
  27. data/lib/graph_weaver/errors.rb +57 -13
  28. data/lib/graph_weaver/federation.rb +10 -22
  29. data/lib/graph_weaver/hints.rb +76 -2
  30. data/lib/graph_weaver/in_process.rb +11 -8
  31. data/lib/graph_weaver/inflect.rb +2 -0
  32. data/lib/graph_weaver/input_struct.rb +115 -12
  33. data/lib/graph_weaver/internal/overrides.rb +101 -0
  34. data/lib/graph_weaver/internal/planner.rb +868 -0
  35. data/lib/graph_weaver/internal/schemas.rb +50 -0
  36. data/lib/graph_weaver/internal/selection.rb +127 -0
  37. data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
  38. data/lib/graph_weaver/internal/values.rb +181 -0
  39. data/lib/graph_weaver/internal.rb +206 -0
  40. data/lib/graph_weaver/logging.rb +108 -20
  41. data/lib/graph_weaver/parsing.rb +6 -13
  42. data/lib/graph_weaver/query_module.rb +2 -0
  43. data/lib/graph_weaver/railtie.rb +113 -14
  44. data/lib/graph_weaver/representation.rb +30 -2
  45. data/lib/graph_weaver/response.rb +15 -0
  46. data/lib/graph_weaver/retry.rb +54 -22
  47. data/lib/graph_weaver/rspec.rb +63 -18
  48. data/lib/graph_weaver/schema_diff.rb +293 -0
  49. data/lib/graph_weaver/schema_loader.rb +126 -35
  50. data/lib/graph_weaver/tasks.rb +88 -36
  51. data/lib/graph_weaver/testing/cassette.rb +131 -78
  52. data/lib/graph_weaver/testing/coverage.rb +11 -15
  53. data/lib/graph_weaver/testing/failure.rb +14 -8
  54. data/lib/graph_weaver/testing/fake_client.rb +253 -60
  55. data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
  56. data/lib/graph_weaver/testing/router.rb +147 -840
  57. data/lib/graph_weaver/testing.rb +40 -83
  58. data/lib/graph_weaver/transport/faraday.rb +1 -1
  59. data/lib/graph_weaver/transport/http.rb +29 -12
  60. data/lib/graph_weaver/transport.rb +11 -34
  61. data/lib/graph_weaver/version.rb +1 -1
  62. data/lib/graph_weaver.rb +221 -118
  63. metadata +17 -13
  64. data/CLAUDE.md +0 -161
  65. data/DECISIONS.md +0 -309
  66. data/Makefile +0 -23
  67. data/NOTES.md +0 -182
  68. data/PLAN.md +0 -115
  69. data/REVIEW.md +0 -946
  70. data/lib/graph_weaver/schemas.rb +0 -46
  71. data/lib/graph_weaver/selection.rb +0 -120
  72. data/lib/graph_weaver/testing/values.rb +0 -98
data/docs/federation.md CHANGED
@@ -1,75 +1,105 @@
1
1
  # Federation
2
2
 
3
- Feed weaver any federation artifact: a supergraph, an API schema, a subgraph
4
- SDL, or the live router. It recognizes which it got — `SchemaLoader.load` (and
5
- `Client.new(path_or_sdl)`) take each as an SDL file or an introspection dump.
3
+ For an app that is a client of a federated graph, a subgraph in one, or both.
4
+ Two halves: **generating** against a composed supergraph (which mostly means
5
+ "point it at the file and forget"), and the **local router**, which runs a
6
+ stitched query against your own resolvers in-process so specs need no gateway.
7
+
8
+ GraphWeaver takes any federation artifact — a supergraph, an API schema, a
9
+ subgraph SDL, or a live router — and recognizes which it got.
10
+ `SchemaLoader.load` (and `Client.new(path_or_sdl)`) accept each as an SDL file
11
+ or an introspection dump.
12
+
13
+ ## Generating for a federated graph
14
+
15
+ **Queries go through the gateway?** Generate against the supergraph. It is the
16
+ whole graph in one schema, so every registration matches and there is nothing
17
+ else to decide.
18
+
19
+ **Calling subgraphs directly?** One client per subgraph, one `generate!` each.
20
+ Registrations stay in one global registry, because names compose by identity
21
+ across a graph — `Money` is one Ruby type wherever it appears, and `Person` is
22
+ one entity even though a single subgraph owns `birthday`. So a registration a
23
+ given subgraph doesn't declare is not an error; generation warns and carries on.
24
+ `rake graph_weaver:generate` and `verify` print the list once per run, after the
25
+ files:
6
26
 
7
- ## Pointing weaver at a supergraph
27
+ ```
28
+ register_scalar("Money") matches no scalar in Billing::Schema — a typo, or a registration for another schema
29
+ ```
30
+
31
+ `GraphWeaver.unmatched_registrations` is that same list as data, for a Rakefile
32
+ or a spec that would rather gate on it than read it.
33
+
34
+ Register everything once and read those lines, or scope each generation to what
35
+ it needs and get a silent build:
36
+
37
+ ```ruby
38
+ GraphWeaver.register_scalar("Money", Money)
39
+ GraphWeaver.generate!(schema: "billing.graphql",
40
+ queries: "app/graphql/billing", output: "app/graphql/generated/billing")
41
+
42
+ GraphWeaver.reset_registrations!
43
+
44
+ GraphWeaver.register_scalar("Person.birthday", Date)
45
+ GraphWeaver.generate!(schema: "directory.graphql",
46
+ queries: "app/graphql/directory", output: "app/graphql/generated/directory")
47
+ ```
8
48
 
9
- A supergraph SDL works as-is. When `SchemaLoader` recognizes a composed graph it
10
- strips the composition machinery before building the schema the synthetic
11
- `join__*`/`link__*` types and directive definitions, and every `@join__*`/`@link`
12
- application on the real typesso what codegen sees is the merged graph's
13
- ordinary type shapes, with no federation plumbing leaking into `schema.types`.
14
- (A pure AST rewrite of the SDL; plain schemas pass through untouched.) Field
15
- shapes nullability, args, enums, inputs are identical to the API schema, so
16
- your generated structs are correct.
49
+ This holds for entity fields too, which is the case that would otherwise bite:
50
+ every subgraph referencing an entity declares it, so a subgraph carrying
51
+ `Person` for its `@key` alone sees `register_scalar("Person.birthday", Date)` as
52
+ a field it doesn't owna warning, not a failure.
53
+
54
+ What a subgraph *can* disprove still fails generation: a name it declares as
55
+ something else (`register_scalar("Species")` where `Species` is an enum), and a
56
+ coordinate whose field it declares as a composite. Neither is redeemable by any
57
+ schema in the graph.
58
+
59
+ ## Generating against a supergraph
60
+
61
+ A supergraph SDL works as-is. On load, GraphWeaver strips the composition
62
+ machinery — the synthetic `join__*`/`link__*` types and directive definitions,
63
+ and every `@join__*`/`@link` application on the real types — so codegen sees the
64
+ merged graph's ordinary type shapes with no federation plumbing in
65
+ `schema.types`. Field shapes (nullability, args, enums, inputs) are identical to
66
+ the API schema, so your generated structs are correct.
17
67
 
18
68
  **Which names count as machinery is read off the schema**, not a fixed list.
19
69
  Federation namespaces itself through [`@link`](https://specs.apollo.dev/link/v1.0/)
20
- (v2) or [`@core`](https://specs.apollo.dev/core/v0.2/) (v1), and weaver applies
21
- those declarations as written: a spec URL's name segment gives the namespace
22
- (`https://specs.apollo.dev/join/v0.3` → `join__`), `as:` renames it, and
23
- `import:` binds names into the root namespace, `{name: "@key", as: "@myKey"}`
24
- renames included. So a graph on fed 2.5+ auth strips its `@requiresScopes` /
25
- `@policy` / `@context` machinery (`federation__Scope`, `context__ContextFieldValue`,
26
- …) the same way `join__` goes, and a renamed `@inaccessible` still hides what it
27
- marks. A schema that declares nothing still gets the `join__`/`link__`/`core__`
28
- floor.
29
-
30
- Federation **v1** supergraphs (`@core` + `@join__owner`/`@join__type`) load the
31
- same way — the older spelling of the same machinery is stripped too.
32
-
33
- A supergraph is a **superset** of the API schema — it carries elements the
34
- public API hides, marked `@inaccessible`. Weaver removes those on load (below),
35
- so the schema it generates against is the API schema, not the superset.
70
+ (v2) or [`@core`](https://specs.apollo.dev/core/v0.2/) (v1), and those
71
+ declarations are applied as written: the spec URL's name segment gives the
72
+ namespace (`https://specs.apollo.dev/join/v0.3` → `join__`), `as:` renames it,
73
+ `import:` binds names into the root namespace. So a fed 2.5+ graph's
74
+ `@requiresScopes` / `@policy` / `@context` machinery strips the same way
75
+ `join__` does, a renamed `@inaccessible` still hides what it marks, and v1
76
+ supergraphs (`@core` + `@join__owner`) load identically. A schema that declares
77
+ nothing still gets the `join__`/`link__`/`core__` floor.
36
78
 
37
79
  ### `@inaccessible`
38
80
 
39
- A federation-v2 directive marking an element as *present in the federated graph
40
- but removed from the public API schema*. You'll meet it rolling out a change to
41
- a **shared type**: add the field to one subgraph marked `@inaccessible` (so
42
- composition doesn't require every subgraph to have it yet), roll it out to the
43
- rest, then drop the directive to publish it. (Apollo contracts also pair `@tag`
44
- + `@inaccessible` to build filtered API variants.)
45
-
46
- Weaver derives the API schema from the supergraph for you: loading strips every
47
- `@inaccessible` element and cascades — a field/argument/union-member/interface
48
- referencing a removed type goes too, and a type left empty is removed in turn.
49
- So codegen validates against what clients can query, with no need for Apollo's
50
- JS tooling (`@apollo/federation-internals`) to subtract the API schema first;
51
- feed weaver the raw supergraph and you get the router's contract. (A pure SDL
52
- rewrite at load time — see
53
- [`SchemaLoader`](../lib/graph_weaver/schema_loader.rb).)
54
-
55
- The directive is matched by the **local name it was linked under**, so
56
- `@link(url: "…/federation/v2.5", import: [{name: "@inaccessible", as: "@private"}])`
57
- subtracts what `@private` marks.
58
-
59
- **Only on the supergraph path.** The subtraction runs when weaver recognizes a
60
- composed supergraph. Plain SDL and subgraph SDL are taken at face value:
61
- `@inaccessible` there is left as a directive and its fields stay queryable.
62
-
63
- Other federation directives hide nothing from the schema, so weaver keeps the
64
- field and ignores the directive: `@requiresScopes` / `@policy` / `@authenticated`
65
- enforce access at runtime; `@tag` / `@requires` / `@provides` / `@external` are
66
- metadata.
67
-
68
- The derivation is diffed against Apollo's own `composeServices` +
69
- `toAPISchema()` in
70
- [`spec/integration/api_schema_spec.rb`](../spec/integration/api_schema_spec.rb),
71
- over composed supergraphs carrying `@interfaceObject`, `@join__unionMember`,
72
- `@join__enumValue` and an aliased `@inaccessible` — identical in each.
81
+ A supergraph is a **superset** of the API schema: it carries elements the public
82
+ API hides, marked `@inaccessible`. You'll meet the directive rolling out a change
83
+ to a **shared type** add the field to one subgraph marked `@inaccessible` so
84
+ composition doesn't require every subgraph to have it yet, roll it out, then drop
85
+ the directive to publish it. (Apollo contracts also pair `@tag` + `@inaccessible`
86
+ to build filtered API variants.)
87
+
88
+ Loading strips every `@inaccessible` element and cascades: a
89
+ field/argument/union-member/interface referencing a removed type goes too, and a
90
+ type left empty is removed in turn. So codegen validates against what clients can
91
+ actually query, with no need for Apollo's JS tooling to subtract the API schema
92
+ first feed it the raw supergraph and you get the router's contract. The
93
+ derivation is diffed against Apollo's own `composeServices` + `toAPISchema()` in
94
+ [`spec/integration/api_schema_spec.rb`](https://github.com/dpep/graph_weaver/blob/main/spec/integration/api_schema_spec.rb).
95
+
96
+ Two bounds. The directive is matched by the **local name it was linked under**, so
97
+ an `import:` alias subtracts what that alias marks. And the subtraction runs
98
+ **only on the supergraph path** plain and subgraph SDL are taken at face value,
99
+ where `@inaccessible` stays a directive and its fields stay queryable. Directives
100
+ that hide nothing keep their field and are ignored: `@requiresScopes` / `@policy`
101
+ / `@authenticated` enforce at runtime, `@tag` / `@requires` / `@provides` /
102
+ `@external` are metadata.
73
103
 
74
104
  ### The routing table
75
105
 
@@ -99,21 +129,17 @@ from `@join__unionMember`/`@join__implements` — and `nil` where the supergraph
99
129
  doesn't say, which is a different fact from "none".
100
130
 
101
131
  A `@join__` directive the table hasn't been taught lands in `#unsupported`
102
- rather than being skipped a table that silently ignores half a spec version
103
- answers confidently and wrongly. Callers refuse on a non-empty list; that is
104
- what bounds the maintenance tail across federation spec versions.
132
+ rather than being skipped, and callers refuse on a non-empty list: a table that
133
+ silently ignores half a spec version answers confidently and wrongly.
105
134
  `#interface_objects` is the one construct kept out of that list
106
135
  (`{"Media" => ["catalog"]}`), because it's a fact about one *type* rather than
107
- about the table: the router refuses the queries that reach it and plans the
108
- rest.
109
-
110
- The table is what [`Testing::Router`](#the-local-router)
111
- plans against, and it's a reasonable read on its own — "which subgraph owns
112
- this field" is the sentence a good error message wants.
136
+ about the table the router refuses the queries that reach it and plans the rest.
113
137
 
114
- Weaver says it where it has the coordinate to say it about. When the schema
115
- dump is a composed supergraph, `rake graph_weaver:queries:check` brands each
116
- validation error with the subgraphs behind the type it names:
138
+ The table is what [`Testing::Router`](#the-local-router) plans against, and it's
139
+ a reasonable read on its own "which subgraph owns this field" is the sentence a
140
+ good error message wants. So when the schema dump is a composed supergraph,
141
+ `rake graph_weaver:queries:check` brands each validation error with the subgraphs
142
+ behind the type it names:
117
143
 
118
144
  ```
119
145
  app/graphql/queries/product.graphql
@@ -160,34 +186,25 @@ not checked — answered with fabricated data:
160
186
 
161
187
  Both directions, because they mean opposite things: **stale** is "recompose",
162
188
  **not composed in** is "publish the subgraph". The stale side names the
163
- subgraph the supergraph blames, which is the sentence you want whose code to
164
- look at, whose team to talk to.
165
-
166
- "Defines" is deliberately looser than field-set equality: a subgraph carries
167
- federation plumbing (`_entities`, `_service`) no supergraph has, and a field can
189
+ subgraph the supergraph blames whose code to look at, whose team to talk to.
190
+ "Defines" is deliberately looser than field-set equality, since a subgraph
191
+ carries plumbing (`_entities`, `_service`) no supergraph has and a field can
168
192
  legitimately sit in more than one subgraph (`@external` copies, `@shareable`).
169
- So a coordinate is compared only against the schemas that could *be* the
170
- subgraph the supergraph attributes it to, the uncomposed side reports only a
171
- field the supergraph's type doesn't carry **at all**, and underscore-prefixed
172
- fields never count.
173
193
 
174
194
  **A supergraph is routinely only partly local**, so the report names three
175
195
  states rather than two: checked, not here (running elsewhere — or the type is
176
196
  gone), and [faked](#the-local-router). A clean report that quietly checked one
177
197
  subgraph of three would be actively misleading, so the headline counts them and
178
198
  the sections name them. Only drift fails the task; absence is a supported
179
- setup, not a failure.
180
-
181
- Checking **none** of them is a failure, though — "checked 0 of 4" attached to
182
- exit 0 is a gate that passes whatever the subgraphs say, so the task exits
183
- non-zero and says so. Under Rails it won't come up: the `federation:*` tasks
184
- eager-load the app, because `config.rake_eager_load` defaults to false and
185
- detection only sees loaded classes.
199
+ setup. Checking **none** of them fails too — "checked 0 of 4" attached to exit 0
200
+ is a gate that passes whatever the subgraphs say. (Under Rails it won't come up:
201
+ the `federation:*` tasks eager-load the app, because `config.rake_eager_load`
202
+ defaults to false and detection only sees loaded classes.)
186
203
 
187
- Detection is what drift breaks — a schema is recognized by what it defines,
188
- and a subgraph whose *types* are gone stops being recognizable — so the same
189
- `subgraphs:` map [`Testing::Router`](#the-local-router)
190
- takes is accepted here, and a named schema skips detection:
204
+ Detection is what drift breaks — a schema is recognized by what it defines, and
205
+ a subgraph whose *types* are gone stops being recognizable — so the same
206
+ `subgraphs:` map [`Testing::Router`](#the-local-router) takes is accepted here,
207
+ and a named schema skips detection:
191
208
 
192
209
  ```ruby
193
210
  GraphWeaver::Federation::Drift.new(
@@ -224,10 +241,10 @@ is which subgraphs a code path touched and a service object rarely runs one
224
241
  query. The rspec tag resets it before each example; outside rspec call
225
242
  `router.reset_trace` around the code path you're measuring.
226
243
 
227
- **[`examples/federation.rb`](../examples/federation.rb)** is the whole shape
244
+ **[`examples/federation.rb`](https://github.com/dpep/graph_weaver/blob/main/examples/federation.rb)** is the whole shape
228
245
  in one runnable file, and the only example that needs no network: three real
229
246
  subgraphs, a boundary-crossing query through a generated module, the trace,
230
- and a refusal. [`spec/router_spec.rb`](../spec/router_spec.rb) is the
247
+ and a refusal. [`spec/router_spec.rb`](https://github.com/dpep/graph_weaver/blob/main/spec/router_spec.rb) is the
231
248
  exhaustive reference — every plan shape, every refusal, the partly-local
232
249
  graph and the `:fake` opt-in, each as a named example.
233
250
 
@@ -237,8 +254,13 @@ graph and the `:fake` opt-in, each as a named example.
237
254
  schemas define**: a schema serves subgraph `s` when it defines every type and
238
255
  field the routing table says `s` resolves. That's evidence rather than a guess,
239
256
  and a wrong guess would point a suite at the wrong resolvers and still pass — so
240
- exactly one match is used, and **two** matches refuse, naming both. **No** match
241
- isn't a refusal: that subgraph is simply served somewhere else (next section).
257
+ exactly one match is used. Neither other outcome refuses at construction, since
258
+ which classes are loaded is not a fact about the query you're running: **no**
259
+ match means the subgraph is served somewhere else (next section), and **two**
260
+ means detection can't say which loaded schema class serves it. Both are refused
261
+ by the query that reaches the subgraph's fields, each naming its own fix — for
262
+ two, `subgraphs: { "reviews" => App::Reviews::Schema }` pins it, and
263
+ `router.ambiguous` lists them.
242
264
 
243
265
  Name them yourself when you'd rather have the wiring committed, or when
244
266
  detection can't settle it — including partially, with the rest derived:
@@ -316,6 +338,26 @@ absent. Values come from the same engine as
316
338
  [`graphql: :fake`](testing.md#fabricated-data--graphql-fake), so `config.seed`,
317
339
  `config.overrides` and the rest apply.
318
340
 
341
+ `fake:` says how they fabricate — the [pins](testing.md#pins) and options a
342
+ fake takes, in one hash. It goes on `Router.new` outside rspec, on
343
+ `Testing.config.router` for the suite, and on `graphql_router` for the one
344
+ example that cares:
345
+
346
+ ```ruby
347
+ Testing.config.router = { subgraphs: { "shipping" => :fake },
348
+ fake: { list_size: 2 } }
349
+
350
+ it "shows the carrier" do
351
+ graphql_router(fake: { "Shipment.carrier" => "UPS" })
352
+ ...
353
+ end
354
+ ```
355
+
356
+ One `fake:` covers every faked subgraph, because a pin's key
357
+ (`"Shipment.carrier"`) already says which type it means. And `graphql_router` is
358
+ `graphql: :router` with somewhere to put arguments — the router itself is still
359
+ built once for the suite, and the options last one example.
360
+
319
361
  ### What it plans
320
362
 
321
363
  An operation that resolves in **one subgraph** goes over verbatim. One that
@@ -340,7 +382,10 @@ router.trace.map { _1[:subgraph] } # => ["reviews", "products", "reviews"]
340
382
  `products` owns — so the plan fetches those into hidden keys, hands them back
341
383
  in the representation, and only then asks for the estimate. One hop only: the
342
384
  key for the first fetch has to come from the subgraph already in hand, so a
343
- chain can't grow a chain.
385
+ chain can't grow a chain. And two `@requires` field sets crossing into the same
386
+ subgraph on the same `@key` ride **one** prefetch, as Apollo's do — the
387
+ representations would be identical, so a second call would only re-run the
388
+ resolvers.
344
389
 
345
390
  A **nested field set** — `@key(fields: "id organization { id }")`,
346
391
  `@requires(fields: "origin { lat lon }")` — is a selection set like any other,
@@ -392,19 +437,19 @@ answer production disagrees with, which is the most expensive thing this library
392
437
  can produce. Each refusal names the coordinate that stopped it and what to do —
393
438
  `examples/federation.rb` prints one.
394
439
 
395
- What it refuses, and why:
396
-
397
- Every category, spelled as `Unplannable#category` reports it:
440
+ Every category, in the words `Unplannable#label` uses (`#category` is the
441
+ matching symbol):
398
442
 
399
443
  | Refusal | Why |
400
444
  |---|---|
401
445
  | no `@key` to cross the boundary on | an entity fetch sends a representation built from a `@key`; with none there is nothing to send |
402
446
  | an abstract type the supergraph doesn't break down | bucketing needs the concrete types a subgraph answers a union or interface with, and `@join__unionMember`/`@join__implements` is where a supergraph records that. A composition old enough to carry neither leaves nothing but a guess |
403
447
  | an `@interfaceObject` the routing table can't attribute | one subgraph resolves a whole interface's implementations, so the supergraph never says which subgraph answers each of its fields. Per query, not per graph: a query that doesn't reach the type plans as if the directive weren't there |
448
+ | a `@fromContext` argument no fetch here can supply | federation 2.8's `@context`/`@fromContext` fills a field's argument from a selection on an ancestor, and only the gateway that planned the fetch knows what to put there. Per query, like `@interfaceObject`: a subtree one subgraph answers whole sets its own context and plans normally |
404
449
  | a `@requires` whose field set names another `@requires` field | the router satisfies a `@requires` with one fetch, so it can't first satisfy that field's own requirement |
405
450
  | a nested field set no one fetch can build | a nested field set crosses as one object, so one fetch has to answer the whole of it. Nesting itself is fine — this is the set whose fields are split across subgraphs, so the object would arrive half-built from each |
406
451
  | `@skip`/`@include` on both a fragment and its field | one selection can't carry two conditions of the same name. Spell the condition once |
407
- | an alias shadowing an injected `@key` | Apollo's router lets its injected key win over your alias and a spec-conformant server doesn't there is no one answer to agree with |
452
+ | an alias shadowing an injected `@key` | a fetch carries the `@key` it crosses on under a response key — Apollo under the field's own name, the local router under a reserved one and an alias spelling either claims a key the fetch needs |
408
453
  | a mutation's root fields span subgraphs | root mutation fields run in series, and splitting them across subgraphs would run them in whatever order the plan happened to. Sharing one subgraph they're fine, stitching below them and all — that's an ordinary read afterwards. Query roots are independent, so those are always fine |
409
454
  | the routing table names no subgraph | nothing can route a field the supergraph doesn't place |
410
455
  | a subgraph nothing here serves | it's served by another process, so there is nothing here to ask — unless you fake it (above) |
@@ -415,9 +460,23 @@ Every category, spelled as `Unplannable#category` reports it:
415
460
  | a federation construct the routing table doesn't read | an incomplete table makes every answer about this supergraph a guess. The one refusal raised **at construction**, before a single query |
416
461
  | nested deeper than the router walks | past the walk's depth limit, which validation would have rejected first |
417
462
 
418
- A subgraph two loaded schemas both fit refuses at construction too, as a
419
- `ConfigurationError` rather than an `Unplannable` (above) — it's a wiring
420
- mistake, not a query the router declines.
463
+ A subgraph two loaded schemas both fit raises a `ConfigurationError` rather than
464
+ an `Unplannable` — it's a wiring mistake, not a query the router declines — but
465
+ it raises where every other one does, on the query that reaches the subgraph.
466
+ Which classes happen to be loaded is not a fact about the query under test.
467
+ Naming a class in `subgraphs:` *is* a claim, so a wrong one still fails at
468
+ construction.
469
+
470
+ A double that quietly answered *differently* from the router would be worse than
471
+ no double at all, so
472
+ [`spec/integration/router_parity_spec.rb`](https://github.com/dpep/graph_weaver/blob/main/spec/integration/router_parity_spec.rb)
473
+ serves the demo subgraphs over HTTP, boots a real `@apollo/gateway` on the same
474
+ supergraph, and runs the whole corpus through both — plus boundary probes and
475
+ queries where a subgraph deliberately **fails**, which are the cases where a
476
+ merge that doesn't re-propagate hands back a populated tree while the real router
477
+ answers `data: null`. Three outcomes, one of them a defect: match, refuse, or
478
+ answer differently, and the spec fails on the third. `make integration` runs it
479
+ (node required).
421
480
 
422
481
  ### Is it worth wiring up? Measure.
423
482
 
@@ -435,7 +494,8 @@ $ rake graph_weaver:federation:coverage SUPERGRAPH=supergraph.graphql
435
494
  the graph — could the router split this query faithfully at all. *Servable
436
495
  here* is what your suite actually gets: every subgraph that plan reaches is one
437
496
  this process serves. In a [partly-local supergraph](#a-supergraph-only-partly-local)
438
- they differ, and the plannable number alone reads optimistically:
497
+ they differ, and the plannable number alone reads optimistically — sketched
498
+ here on a graph whose `billing` and `shipping` run elsewhere:
439
499
 
440
500
  ```
441
501
  5/5 queries plannable locally (100%), 2 servable here
@@ -448,44 +508,22 @@ plannable, but nothing here serves what they reach (3) — name a schema for tho
448
508
  ```
449
509
 
450
510
  `QUERIES=` picks the directory (default `GraphWeaver.queries_paths`). Planning
451
- needs the supergraph and nothing else, so this still runs in CI with the SDL
452
- alone no subgraph has to be loadable, and with none loaded the report drops
453
- the second number and says it counted planning only. The subgraph line says
454
- which subgraphs each query touches, so a graph whose queries all sit in one is
455
- visibly a different situation from one that stitches everywhere. Anything
456
- refused is listed after it, grouped by category, so one glance says whether the
457
- gap is one construct or many. The runs above are against the demo graph in
458
- `spec/support/federation`, not a real app's mix.
459
-
460
- ### How the refusals are kept honest
461
-
462
- A double that quietly answered *differently* from the router would be worse
463
- than no double at all, so
464
- [`spec/integration/router_parity_spec.rb`](../spec/integration/router_parity_spec.rb)
465
- serves the demo subgraphs over HTTP, boots a real `@apollo/gateway` on the same
466
- supergraph, and runs the whole corpus through both. Three outcomes, one of them
467
- a defect: match, refuse, or answer differently — and the spec fails on the
468
- third. It also checks that the gateway answers every refusal cleanly, so each
469
- refusal is a capability gap rather than a broken query. `make integration` runs
470
- it (node required).
471
-
472
- Alongside the corpus it runs boundary probes and a handful of queries where a
473
- subgraph deliberately **fails** — a resolver erroring under a stitched fetch, an
474
- entity nothing can resolve, a `@requires` fetch that comes back empty. Those are
475
- the ones that matter most: each is a case where a merge that doesn't
476
- re-propagate hands back a populated tree while the real router answers
477
- `data: null`, so it's checked against the real thing rather than against an
478
- expectation someone wrote down.
479
-
480
- ## Pointing weaver at a subgraph
511
+ needs the supergraph and nothing else, so this runs in CI with the SDL alone —
512
+ with no subgraph loaded the report drops the second number and says it counted
513
+ planning only. The subgraph line says which subgraphs each query touches, and
514
+ anything refused is listed after it grouped by category, so one glance says
515
+ whether the gap is one construct or many. (The first run above is the demo graph
516
+ in `spec/support/federation`, not a real app's mix.)
517
+
518
+ ## Generating against a subgraph
481
519
 
482
520
  A raw subgraph SDL — `rover subgraph fetch`, `_service { sdl }`, or the
483
521
  `.graphql` in a service repo — loads too. It applies `@key`/`@external`/
484
522
  `@shareable`/… without declaring them (federation v1 leaves them implicit, v2
485
523
  imports them via `@link`, including under a namespace as
486
- `@federation__key`), so weaver supplies the missing definitions on load;
487
- anything the file declares itself wins. The federation directives themselves
488
- generate no code — codegen is query-driven.
524
+ `@federation__key`), so the missing definitions are supplied on load; anything
525
+ the file declares itself wins. The federation directives themselves generate no
526
+ code — codegen is query-driven.
489
527
 
490
528
  Reach for this when the subgraph is what you have, or to type an `_entities`
491
529
  query (below). But a subgraph is one service's slice of the graph, and its
@@ -498,10 +536,10 @@ graph, feed the composed artifact.
498
536
  Every subgraph serves the entity resolver
499
537
  `_entities(representations: [_Any!]!): [_Entity]!`, and **no subgraph SDL
500
538
  contains it**: `_service { sdl }` and `rover subgraph fetch` print the
501
- *published* schema, where the plumbing is implicit. So weaver supplies it on the
539
+ *published* schema, where the plumbing is implicit. So it's supplied on the
502
540
  subgraph path — `_Any`, `_Service`, and an `_Entity` union over the file's own
503
- `@key`'d types — the same way it supplies the `@key`/`@external` definitions. A
504
- file that declares its own keeps it.
541
+ `@key`'d types — the same way the `@key`/`@external` definitions are. A file
542
+ that declares its own keeps it.
505
543
 
506
544
  The read side is a normal union selection; `alias:` turns the
507
545
  single-entity case into a clean accessor (see
@@ -536,15 +574,28 @@ Key field sets are selection sets, so they're parsed as such:
536
574
  A type with one `@key` types its fields as **required kwargs**, so an
537
575
  incomplete representation is an `srb tc` error rather than a round trip. What a
538
576
  sig can't say is checked at runtime and raises `GraphWeaver::InputError` naming
539
- the type and the field: which of two alternative keys you meant to supply, and
540
- whether a nested sub-hash carries the fields the key set declares. Only the
541
- declared key fields reach the wire — an extra key in a nested hash is dropped.
542
-
543
- Two bounds worth knowing. Builders are emitted **only for the entities a
544
- query's `_entities` selection reaches** — codegen is query-driven, so a
545
- subgraph with fifty entities emits nothing for the forty-nine you didn't name.
546
- And a `@key(..., resolvable: false)` declares a key this subgraph does *not*
547
- answer for, so it builds nothing. Key fields typed as scalars get their
548
- registered Ruby type; anything else (a nested selection) is an open `Hash` the
549
- runtime narrows. Every shape above is a named example in
550
- [`spec/federation_spec.rb`](../spec/federation_spec.rb).
577
+ the type and the field:
578
+
579
+ ```
580
+ Variant representation satisfies none of its @keys — supply "id", or "serial"
581
+ Listing representation is missing @key "organization.id"
582
+ ```
583
+
584
+ Key fields take the same loose input an `execute` kwarg does — a `params[:sku]`
585
+ String converts to the `Int` the `@key` declares and a value that converts to
586
+ nothing raises `GraphWeaver::InputError` naming the representation and the
587
+ field:
588
+
589
+ ```
590
+ Product representation sku: expected an Int, got "forty-two"
591
+ ```
592
+
593
+ Only the declared key fields reach the wire — an extra key in a nested hash is
594
+ dropped. Builders are emitted **only for the entities a query's `_entities`
595
+ selection reaches** (codegen is query-driven, so a subgraph with fifty entities
596
+ emits nothing for the forty-nine you didn't name), and a
597
+ `@key(..., resolvable: false)` declares a key this subgraph does *not* answer
598
+ for, so it builds nothing. Key fields typed as scalars get their registered Ruby
599
+ type; anything else (a nested selection) is an open `Hash` the runtime narrows.
600
+ Every shape above is a named example in
601
+ [`spec/federation_spec.rb`](https://github.com/dpep/graph_weaver/blob/main/spec/federation_spec.rb).