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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +537 -0
- data/Gemfile.lock +19 -19
- data/README.md +74 -53
- data/docs/cassettes.md +29 -4
- data/docs/editors.md +3 -1
- data/docs/errors.md +75 -16
- data/docs/federation.md +206 -155
- data/docs/generated_modules.md +223 -166
- data/docs/getting_started.md +106 -82
- data/docs/logging.md +35 -5
- data/docs/scalars.md +119 -24
- data/docs/testing.md +196 -155
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +243 -22
- data/graph_weaver.gemspec +16 -2
- data/lib/generators/graph_weaver/install_generator.rb +31 -16
- data/lib/graph_weaver/client.rb +52 -15
- data/lib/graph_weaver/codegen/aliases.rb +15 -8
- data/lib/graph_weaver/codegen/emit.rb +107 -42
- data/lib/graph_weaver/codegen/enum_type.rb +4 -3
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
- data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
- data/lib/graph_weaver/codegen.rb +382 -105
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +57 -13
- data/lib/graph_weaver/federation.rb +10 -22
- data/lib/graph_weaver/hints.rb +76 -2
- data/lib/graph_weaver/in_process.rb +11 -8
- data/lib/graph_weaver/inflect.rb +2 -0
- data/lib/graph_weaver/input_struct.rb +115 -12
- data/lib/graph_weaver/internal/overrides.rb +101 -0
- data/lib/graph_weaver/internal/planner.rb +868 -0
- data/lib/graph_weaver/internal/schemas.rb +50 -0
- data/lib/graph_weaver/internal/selection.rb +127 -0
- data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
- data/lib/graph_weaver/internal/values.rb +181 -0
- data/lib/graph_weaver/internal.rb +206 -0
- data/lib/graph_weaver/logging.rb +108 -20
- data/lib/graph_weaver/parsing.rb +6 -13
- data/lib/graph_weaver/query_module.rb +2 -0
- data/lib/graph_weaver/railtie.rb +113 -14
- data/lib/graph_weaver/representation.rb +30 -2
- data/lib/graph_weaver/response.rb +15 -0
- data/lib/graph_weaver/retry.rb +54 -22
- data/lib/graph_weaver/rspec.rb +63 -18
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +126 -35
- data/lib/graph_weaver/tasks.rb +88 -36
- data/lib/graph_weaver/testing/cassette.rb +131 -78
- data/lib/graph_weaver/testing/coverage.rb +11 -15
- data/lib/graph_weaver/testing/failure.rb +14 -8
- data/lib/graph_weaver/testing/fake_client.rb +253 -60
- data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
- data/lib/graph_weaver/testing/router.rb +147 -840
- data/lib/graph_weaver/testing.rb +40 -83
- data/lib/graph_weaver/transport/faraday.rb +1 -1
- data/lib/graph_weaver/transport/http.rb +29 -12
- data/lib/graph_weaver/transport.rb +11 -34
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +221 -118
- metadata +17 -13
- data/CLAUDE.md +0 -161
- data/DECISIONS.md +0 -309
- data/Makefile +0 -23
- data/NOTES.md +0 -182
- data/PLAN.md +0 -115
- data/REVIEW.md +0 -946
- data/lib/graph_weaver/schemas.rb +0 -46
- data/lib/graph_weaver/selection.rb +0 -120
- data/lib/graph_weaver/testing/values.rb +0 -98
data/docs/federation.md
CHANGED
|
@@ -1,75 +1,105 @@
|
|
|
1
1
|
# Federation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 own — a 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
|
|
21
|
-
|
|
22
|
-
(`https://specs.apollo.dev/join/v0.3` → `join__`), `as:` renames it,
|
|
23
|
-
`import:` binds names into the root namespace
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
a **shared type
|
|
42
|
-
composition doesn't require every subgraph to have it yet
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
164
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
189
|
-
`subgraphs:` map [`Testing::Router`](#the-local-router)
|
|
190
|
-
|
|
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`](
|
|
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`](
|
|
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
|
|
241
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
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
|
|
504
|
-
|
|
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:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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).
|