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/CLAUDE.md
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## What this is
|
|
6
|
-
|
|
7
|
-
A typed GraphQL client for Ruby: generates `# typed: strict` Ruby (nested
|
|
8
|
-
`T::Struct`s + a typed `execute`) from your queries, checked against the schema
|
|
9
|
-
at generation time. Sorbet is core to the product.
|
|
10
|
-
|
|
11
|
-
## Design principle — correct and simple, in that order
|
|
12
|
-
|
|
13
|
-
Adoption follows delight, and delight follows from a tool that solves the real
|
|
14
|
-
problem without making you think. So the bar for any change is: is it correct,
|
|
15
|
-
and is it the *simplest* thing that is correct? Complexity is not neutral — it
|
|
16
|
-
is confusion, bugs, and frustration, paid for by every future reader and user.
|
|
17
|
-
|
|
18
|
-
What this means when choosing between designs:
|
|
19
|
-
|
|
20
|
-
- **One rule beats a rule with exceptions.** A behavior you can state in a
|
|
21
|
-
sentence, and predict without reading the source, is worth more than one that
|
|
22
|
-
is marginally more capable. Generated class names come from the response key —
|
|
23
|
-
full stop — rather than from the type name with disambiguation-on-collision,
|
|
24
|
-
because the second rule can't be stated without describing its own edge cases.
|
|
25
|
-
- **Prefer removing a decision to adding a knob.** The transport default got
|
|
26
|
-
*better* by deleting auto-detection: one less thing to know, one less way to be
|
|
27
|
-
surprised, and the fast path became the default. Reach for a config option only
|
|
28
|
-
after the simple default has actually failed someone.
|
|
29
|
-
- **A convention can beat a capability.** One file holds one operation. That's a
|
|
30
|
-
convention, and it makes module naming derivable from the filename; supporting
|
|
31
|
-
multiple operations per file would be more capable and worse.
|
|
32
|
-
- **No spooky action at a distance.** Behavior should follow from the code in
|
|
33
|
-
front of you, not from what else is in the Gemfile, what ran first, or which
|
|
34
|
-
selection the walk happened to reach earlier.
|
|
35
|
-
- **Match the ecosystem's conventions** where one exists — a familiar shape costs
|
|
36
|
-
the user zero learning, which is the cheapest simplicity available.
|
|
37
|
-
- **Errors are part of the interface.** A good message names what went wrong,
|
|
38
|
-
where, and what to do about it. `optional: true` in the message beats the same
|
|
39
|
-
advice buried in docs. The best bug fix often makes an error impossible; the
|
|
40
|
-
next best makes it self-explanatory.
|
|
41
|
-
- **Refuse rather than guess.** When intent is ambiguous, fail loudly at
|
|
42
|
-
generation time. A silent wrong answer is the most expensive outcome this
|
|
43
|
-
library can produce, because the generated code looks authoritative.
|
|
44
|
-
|
|
45
|
-
When simplicity and capability genuinely conflict, say so out loud and pick
|
|
46
|
-
deliberately — but the default is simple.
|
|
47
|
-
|
|
48
|
-
**Docs are a complexity detector.** A section that belabors a topic is usually
|
|
49
|
-
not a writing problem — it is the library pushing complexity onto the reader.
|
|
50
|
-
Before expanding an explanation, ask whether the thing being explained should
|
|
51
|
-
exist. If two paragraphs are needed to say which of two ways applies, delete one
|
|
52
|
-
of the ways; the docs then shrink on their own, which is the signal the cut was
|
|
53
|
-
right.
|
|
54
|
-
|
|
55
|
-
## Sorbet typing policy — type by value, not for coverage
|
|
56
|
-
|
|
57
|
-
Sorbet being core does **not** mean every file should be `# typed: strict`. Type
|
|
58
|
-
where it pays off in developer experience; leave the rest at `# typed: true`.
|
|
59
|
-
|
|
60
|
-
- **Strict (full sigs) — developer-facing contracts.** The types users touch:
|
|
61
|
-
`response.rb` (the envelope every `execute` returns), the error hierarchy
|
|
62
|
-
(`errors.rb`), and the **generated code** (emitted `# typed: strict`). Concrete
|
|
63
|
-
types here give downstream apps real call-site checking + autocomplete — that's
|
|
64
|
-
the product.
|
|
65
|
-
- **`# typed: true` (loose) — dynamic / boundary internals.** The codegen
|
|
66
|
-
(`codegen.rb`, `codegen/nodes.rb`, `codegen/emit.rb`, `codegen/scalar_type.rb`,
|
|
67
|
-
`codegen/enum_type.rb`, `codegen/type_helpers.rb`) walks graphql-ruby's
|
|
68
|
-
approximately-typed AST and builds modules/strings dynamically; `client.rb`
|
|
69
|
-
wraps a graphql-ruby schema and a duck-typed transport. Strict here is ~all
|
|
70
|
-
`T.untyped` — paperwork that documents shape without catching anything.
|
|
71
|
-
**Don't promote these to strict.**
|
|
72
|
-
- Rule of thumb: if a sig would be mostly `T.untyped`, it isn't worth writing.
|
|
73
|
-
Concrete types = value; `T.untyped` sigs = paperwork.
|
|
74
|
-
- `railtie.rb` / `tasks.rb` are `# typed: ignore` (Rails/Rake DSL).
|
|
75
|
-
|
|
76
|
-
## Design invariants (don't "fix" these)
|
|
77
|
-
|
|
78
|
-
- **The client slot is duck-typed.** A transport, `Retry`, a live graphql-ruby
|
|
79
|
-
schema class, or a test fake all satisfy one contract —
|
|
80
|
-
`execute(query, variables:) => {"data" => ..., "errors" => ...}` — with no
|
|
81
|
-
shared base class. **Don't formalize it as a strict Sorbet interface**: a
|
|
82
|
-
graphql-ruby `Schema` class fits the slot without inheriting anything, and a
|
|
83
|
-
strict interface would exclude it. This is why the transport/client seams stay
|
|
84
|
-
loosely typed.
|
|
85
|
-
- **Codegen is query-driven.** Structs are generated per selection set, only for
|
|
86
|
-
the types a query actually touches — not the whole schema (so extra schema
|
|
87
|
-
types, e.g. federation `join__*`, generate no code).
|
|
88
|
-
- **Leaf codecs vs composite decoration.** `register_scalar` / `register_enum`
|
|
89
|
-
*define/replace* how a leaf deserializes (its Ruby shape is fixed);
|
|
90
|
-
`extend_type` only *decorates* a generated composite struct with mixins — it
|
|
91
|
-
can't replace one, because a composite's shape varies per query. Don't add a
|
|
92
|
-
"replace a composite's deserializer" path.
|
|
93
|
-
|
|
94
|
-
## Green before commit
|
|
95
|
-
|
|
96
|
-
`bundle exec` needs the rvm ruby — the default PATH ruby is 2.6 and can't even
|
|
97
|
-
load bundler:
|
|
98
|
-
|
|
99
|
-
```sh
|
|
100
|
-
source ~/.rvm/scripts/rvm && rvm use 3.4.9
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Then:
|
|
104
|
-
|
|
105
|
-
```sh
|
|
106
|
-
bundle exec rspec # full suite
|
|
107
|
-
bundle exec srb tc # Sorbet typecheck (CI gates on this too)
|
|
108
|
-
bundle exec ruby bin/generate # regenerate fixtures — must leave the tree clean
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
All three must pass. Sorbet sigs are runtime-checked by sorbet-runtime, so a
|
|
112
|
-
wrong sig surfaces as an rspec failure, not only a `srb tc` error — a green suite
|
|
113
|
-
validates the sigs against real usage.
|
|
114
|
-
|
|
115
|
-
Two more when the change could reach them:
|
|
116
|
-
|
|
117
|
-
```sh
|
|
118
|
-
bundle exec rspec --order rand:1 # and a couple of other seeds
|
|
119
|
-
bundle exec ruby bin/federation-diff # the fixture supergraph still composes
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Order-independence is worth checking rather than assuming — four order-dependent
|
|
123
|
-
failures have hidden behind the default `:defined` order, and a *burst* of them
|
|
124
|
-
usually means one shared resource cascading rather than many bugs.
|
|
125
|
-
|
|
126
|
-
## Drive it from a throwaway app when the host seam changes
|
|
127
|
-
|
|
128
|
-
**The suite cannot test the gem's relationship with its host.** It is not a Rails
|
|
129
|
-
app, so anything that depends on Rails' boot order, Zeitwerk, or rake's task
|
|
130
|
-
graph is structurally invisible to it — and both bugs found that way were silent
|
|
131
|
-
in development and only appeared in production boot or when registrations
|
|
132
|
-
mattered:
|
|
133
|
-
|
|
134
|
-
- `rake graph_weaver:generate` never ran `:environment`, because the task asked
|
|
135
|
-
`Rake::Task.task_defined?("environment")` at *load* time and Rails defines it
|
|
136
|
-
*after* railties' `rake_tasks` blocks. Every `register_scalar`/`extend_type` in
|
|
137
|
-
an initializer was silently dropped at generation, and `verify` reported the
|
|
138
|
-
result up to date.
|
|
139
|
-
- `app/graphql/generated` sits under a Zeitwerk root while its files define
|
|
140
|
-
top-level constants, so eager loading raised `NameError`. Lazy dev boot was
|
|
141
|
-
fine; production was not.
|
|
142
|
-
|
|
143
|
-
So when the railtie, the rake tasks, the generator, or the documented install
|
|
144
|
-
path changes, spin up a scratch Rails app outside the repo, point its Gemfile at
|
|
145
|
-
your checkout, and actually use it. Worth exercising: **both** a remote endpoint
|
|
146
|
-
and the app's own graphql-ruby schema in-process; `RAILS_ENV=production` boot and
|
|
147
|
-
`rails zeitwerk:check`; a registration in `config/initializers` that must reach
|
|
148
|
-
generated output; the rake tasks end to end; and the testing harness from inside
|
|
149
|
-
the app's own specs. The app is disposable — rebuilding it is cheaper than the
|
|
150
|
-
bugs it catches.
|
|
151
|
-
|
|
152
|
-
## Version bumps
|
|
153
|
-
|
|
154
|
-
Bump `lib/graph_weaver/version.rb` and, in the **same commit**:
|
|
155
|
-
|
|
156
|
-
- update `Gemfile.lock` (the gem pins its own version there; CI runs a frozen
|
|
157
|
-
`bundle install`, which fails at the *setup* step with exit code 16 — before
|
|
158
|
-
tests — if the lock is stale), and
|
|
159
|
-
- add a `CHANGELOG.md` entry.
|
|
160
|
-
|
|
161
|
-
`gem push` (the actual RubyGems release) is a separate, manual step.
|
data/DECISIONS.md
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
# Decisions
|
|
2
|
-
|
|
3
|
-
Roads not taken, and why. The code shows what was chosen; it is silent about
|
|
4
|
-
what was considered and rejected — and those are the ones that get re-litigated,
|
|
5
|
-
usually by someone with the same good instinct that was already followed to its
|
|
6
|
-
end once.
|
|
7
|
-
|
|
8
|
-
Only entries where the rejected path is *tempting* belong here. An obvious call
|
|
9
|
-
needs no record. What changed and when lives in `CHANGELOG.md`; the invariants
|
|
10
|
-
and principles live in `CLAUDE.md`; stated non-goals (subscriptions, `@defer`,
|
|
11
|
-
uploads, normalized caching, fragment masking, batching) live in `REVIEW.md` §7.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## The client slot stays duck-typed
|
|
16
|
-
|
|
17
|
-
**Considered:** formalizing `execute(query, variables:, operation_name:)` as a
|
|
18
|
-
Sorbet interface or a base class, so the thing every client implements is
|
|
19
|
-
declared rather than implied.
|
|
20
|
-
|
|
21
|
-
**Rejected because** a live graphql-ruby `Schema` class satisfies the slot and
|
|
22
|
-
cannot inherit from us. An interface would exclude the one implementation we
|
|
23
|
-
neither wrote nor control — and that implementation is the whole in-process
|
|
24
|
-
story. The openness is the feature.
|
|
25
|
-
|
|
26
|
-
Downstream of this: `Parsing` requires `#schema` of its includers and can't
|
|
27
|
-
declare it, hence one `T.unsafe(self)` in that mixin.
|
|
28
|
-
|
|
29
|
-
## `InProcess` lives outside `Transport::`
|
|
30
|
-
|
|
31
|
-
**Considered:** moving it under `Transport::`, or restructuring so
|
|
32
|
-
`Transport::HTTP` parents a native and a Faraday implementation and `InProcess`
|
|
33
|
-
slots in beside them.
|
|
34
|
-
|
|
35
|
-
**Rejected because** `Transport` is a base class, not a namespace: it owns the
|
|
36
|
-
GraphQL-over-HTTP flow (encode the body, classify network failures, `ServerError`
|
|
37
|
-
on non-2xx, parse). `InProcess` implements none of that. And the restructure
|
|
38
|
-
wouldn't achieve its goal anyway — a bare `Schema` class is in the client slot
|
|
39
|
-
too, so `Transport::` still wouldn't mean "things you can pass as `transport:`".
|
|
40
|
-
The taxonomy can't be clean because the slot is deliberately open.
|
|
41
|
-
|
|
42
|
-
Renaming the abstract base (`Transport::Base`) to stop it sharing a name with its
|
|
43
|
-
namespace is the smaller, honest version if this ever itches again.
|
|
44
|
-
|
|
45
|
-
## `Transport::HTTP` is the default; Faraday is opt-in
|
|
46
|
-
|
|
47
|
-
**Considered:** keeping the previous `defined?(::Faraday)` auto-detection.
|
|
48
|
-
|
|
49
|
-
**Rejected because** Faraday arrives transitively through stripe, octokit and
|
|
50
|
-
friends, so adding an unrelated gem silently changed your transport, your
|
|
51
|
-
timeouts, and your connection reuse (Faraday's default adapter reconnects per
|
|
52
|
-
request; measured 10 connections for 10 requests versus 1). Behaviour that
|
|
53
|
-
depends on what else is in the Gemfile is unreasonable-about-able. Same code,
|
|
54
|
-
same transport.
|
|
55
|
-
|
|
56
|
-
## Cassettes live in `spec/cassettes`, not `spec/fixtures`
|
|
57
|
-
|
|
58
|
-
**Considered:** `spec/fixtures/`, or a namespaced `spec/fixtures/graph_weaver/`,
|
|
59
|
-
as the more conventional home for test data.
|
|
60
|
-
|
|
61
|
-
**Rejected because** Rails globs the fixture path for `{**,*}/*.yml`
|
|
62
|
-
(`active_record/test_fixtures.rb`) — recursively, so a subdirectory doesn't save
|
|
63
|
-
you. `fixtures :all` would try to load cassettes as ActiveRecord fixtures. The
|
|
64
|
-
conventional-looking choice is the broken one.
|
|
65
|
-
|
|
66
|
-
## Directories organize queries; they don't namespace modules
|
|
67
|
-
|
|
68
|
-
**Considered:** deriving module names from nested query directories, so
|
|
69
|
-
`admin/pets.graphql` becomes `AdminPetsQuery` instead of colliding with
|
|
70
|
-
`pets.graphql`.
|
|
71
|
-
|
|
72
|
-
**Rejected because** it *moves* collisions rather than removing them —
|
|
73
|
-
`admin/pets.graphql` and `admin_pets.graphql` would then collide — so the
|
|
74
|
-
refusal has to exist either way, and the naming rule stops being statable
|
|
75
|
-
without describing which path segments count. A duplicate base name refuses,
|
|
76
|
-
naming both files.
|
|
77
|
-
|
|
78
|
-
## `#parse` requires a schema, so `Retry` doesn't have it
|
|
79
|
-
|
|
80
|
-
**Considered:** delegating `#parse` through `Retry` to whatever it wraps, so
|
|
81
|
-
`Retry.new(client).parse(q)` reads naturally.
|
|
82
|
-
|
|
83
|
-
**Rejected because** `respond_to?(:parse)` would then be true half the time and
|
|
84
|
-
false the other half — a `Retry` over a bare HTTP transport has no schema to
|
|
85
|
-
reach through to. The rule is "anything holding a schema can parse against it",
|
|
86
|
-
which is a domain, not an exception. `Retry` doesn't meet the precondition.
|
|
87
|
-
|
|
88
|
-
## Mutations generate `…Mutation` modules
|
|
89
|
-
|
|
90
|
-
**Considered, and initially rejected:** keeping the uniform `…Query` suffix,
|
|
91
|
-
because renaming breaks call sites *and* because the generated filename changes,
|
|
92
|
-
leaving a stale `_query.rb` that `load_generated!` keeps requiring.
|
|
93
|
-
|
|
94
|
-
**Reversed once generated-file pruning landed** — the stale-file half of the
|
|
95
|
-
objection dissolved, and the remaining "one rule beats a conditional one"
|
|
96
|
-
argument lost to `SaveListEntryMutation.execute!` being what a user types every
|
|
97
|
-
day. The generated filename mirrors the constant, so `ls generated/` still
|
|
98
|
-
answers "what's the constant".
|
|
99
|
-
|
|
100
|
-
## One shared types module, not three
|
|
101
|
-
|
|
102
|
-
**Considered:** keeping `GraphQLInputs`, `GraphQLUnions` and `GraphQLEnums`
|
|
103
|
-
separate on the grounds that their file shapes genuinely differ (a manifest plus
|
|
104
|
-
per-type files versus a single file).
|
|
105
|
-
|
|
106
|
-
**Rejected because** file layout is an implementation detail and the constant a
|
|
107
|
-
user types is not. Merging also *deleted* a mechanism: the artifacts used to
|
|
108
|
-
alias each other's constants across files, which lexical scope now handles for
|
|
109
|
-
free. Per-type files were extended to all three rather than dropped — adding an
|
|
110
|
-
enum value diffs one file.
|
|
111
|
-
|
|
112
|
-
## Subgraph mapping is derived, then verified — never guessed
|
|
113
|
-
|
|
114
|
-
**Considered:** requiring an explicit `subgraphs:` map, on the grounds that
|
|
115
|
-
auto-detection is guessing and a wrong guess silently points a test suite at the
|
|
116
|
-
wrong resolvers.
|
|
117
|
-
|
|
118
|
-
**Rejected because** matching on *what a schema defines* against the routing
|
|
119
|
-
table is a derivation with evidence, not a guess — and the ambiguous cases
|
|
120
|
-
(two candidates, or none) refuse rather than pick. Detection and validation are
|
|
121
|
-
the same check run in two directions, so there is no second code path to
|
|
122
|
-
disagree.
|
|
123
|
-
|
|
124
|
-
## Only some `ArgumentError`s were branded
|
|
125
|
-
|
|
126
|
-
**Considered:** two uniform answers. Brand all ~50 `raise ArgumentError` sites
|
|
127
|
-
under `GraphWeaver::Error`, so "everything descends from `Error`" is literally
|
|
128
|
-
true; or leave subgraph detection's refusals as `ArgumentError` and qualify the
|
|
129
|
-
sentence in `docs/errors.md`.
|
|
130
|
-
|
|
131
|
-
**Rejected because** the first throws away the one thing `ArgumentError`
|
|
132
|
-
communicates — you passed something wrong at this call site, like any Ruby
|
|
133
|
-
method — and `pool_size: must be >= 1` is exactly that. The second leaves the
|
|
134
|
-
refusals a `Testing::Router` user actually meets outside the umbrella the docs
|
|
135
|
-
point them at, which is where a spec helper rescues.
|
|
136
|
-
|
|
137
|
-
What survives is a line that can be stated: **what the library concludes,
|
|
138
|
-
having read your schema, is a `GraphWeaver::Error`; an argument wrong on its
|
|
139
|
-
face is an `ArgumentError`.** Subgraph mapping (`ConfigurationError`) and a
|
|
140
|
-
query file whose name can't spell a constant are verdicts; `cast:` not being a
|
|
141
|
-
Symbol is not. A rule with a stated boundary beats a uniform one that lies
|
|
142
|
-
about half its cases.
|
|
143
|
-
|
|
144
|
-
## An abstract type is bucketed on `__typename`, not planned away
|
|
145
|
-
|
|
146
|
-
**Considered:** keeping the `abstract_boundary` refusal, on the reasoning that a
|
|
147
|
-
representation needs one concrete `__typename` and the planner — which takes no
|
|
148
|
-
variables and runs before any fetch — cannot know it.
|
|
149
|
-
|
|
150
|
-
**Rejected because** the planner doesn't have to know it. It only has to plan
|
|
151
|
-
*every* possibility: the supergraph says which concrete types a subgraph can
|
|
152
|
-
answer a union or interface with, so the plan carries a branch per type and
|
|
153
|
-
execution picks the one the data came back as. Deciding at execution is the
|
|
154
|
-
existing precedent — `@skip`/`@include` already filter deferrals against the
|
|
155
|
-
variables in hand for exactly the same reason.
|
|
156
|
-
|
|
157
|
-
The corollary is smaller and sharper than the rule it replaced. A fragment
|
|
158
|
-
whose condition can't hold at a position — `... on Note` where the answering
|
|
159
|
-
subgraph's union holds no Note — is **dropped**, not refused, even though
|
|
160
|
-
"refuse rather than guess" pulls the other way. It isn't a guess: the fragment
|
|
161
|
-
can never match, so `{}` is the only answer, and a real `@apollo/gateway`
|
|
162
|
-
returns exactly that. What still refuses is the case where the supergraph
|
|
163
|
-
genuinely doesn't say — no `@join__unionMember`/`@join__implements`, and the
|
|
164
|
-
type in more than one subgraph — because then the branch list itself would be
|
|
165
|
-
invented.
|
|
166
|
-
|
|
167
|
-
## A nested field set crosses whole, or not at all
|
|
168
|
-
|
|
169
|
-
**Considered:** assembling a nested `@key`/`@requires` object from more than
|
|
170
|
-
one fetch — `store { id }` from the subgraph in hand and `store { region
|
|
171
|
-
{ code } }` from a prefetch, deep-merged into one representation. It is what
|
|
172
|
-
`@apollo/gateway` does internally (`deepMerge(entity, dataReceivedFromService)`),
|
|
173
|
-
and it would close the last nested case rather than refusing it.
|
|
174
|
-
|
|
175
|
-
**Rejected because** the only shapes that produce the split are ones where the
|
|
176
|
-
gateway is no longer an oracle. A field set reaching *through* a key field is
|
|
177
|
-
the common one, and there the gateway doesn't split at all: composition drops
|
|
178
|
-
`@external` from key fields — an entity's key is answerable by any subgraph
|
|
179
|
-
declaring it — so the extending subgraph looks able to resolve the whole path,
|
|
180
|
-
the gateway satisfies the `@requires` locally, and gets back whatever that
|
|
181
|
-
subgraph happens to hold. Merging would mean answering *better* than the
|
|
182
|
-
gateway, which under `0 wrong` is the same failure as answering worse. So a
|
|
183
|
-
root fed by two fetches refuses, naming both halves and where each comes from.
|
|
184
|
-
|
|
185
|
-
That trap is also why the fixture graph's nested `@requires` walks a plain
|
|
186
|
-
external field (`dimensions`) and not the nested `@key`'s object (`store`):
|
|
187
|
-
the first is diffable against a real gateway, the second isn't.
|
|
188
|
-
|
|
189
|
-
## The in-process router refuses rather than approximates
|
|
190
|
-
|
|
191
|
-
**Considered:** planning every query shape, falling back to a best-effort answer
|
|
192
|
-
where the semantics are uncertain.
|
|
193
|
-
|
|
194
|
-
**Rejected because** a test double that answers 5% of queries differently from
|
|
195
|
-
production is worse than one that answers 80% and declines the rest loudly. The
|
|
196
|
-
refusal boundary *is* the product. Non-null propagation is the concrete reason:
|
|
197
|
-
before that pass existed, three queries returned silently wrong data where the
|
|
198
|
-
real router returned `data: null`.
|
|
199
|
-
|
|
200
|
-
A corollary: a `--strict` mode for the drift differ was built and then deleted,
|
|
201
|
-
because once a partly-local supergraph became a supported setup, failing on any
|
|
202
|
-
skipped subgraph was wrong for every graph except a fully-local one — and that
|
|
203
|
-
one's report already says "checked 3 of 3".
|
|
204
|
-
|
|
205
|
-
## `federation:diff` fails when it checked *nothing*
|
|
206
|
-
|
|
207
|
-
**Considered:** leaving zero-checked as a pass, on the `--strict` reasoning
|
|
208
|
-
directly above — absence is supported, and the headline already says "checked 0
|
|
209
|
-
of 4".
|
|
210
|
-
|
|
211
|
-
**Rejected because** zero is not a small number of subgraphs, it is a different
|
|
212
|
-
kind of answer: the gate would pass whatever the subgraphs said, so a green run
|
|
213
|
-
carries no information at all. "Checked 3 of 4" did real work. And the failure
|
|
214
|
-
that produced it was silent — Rails leaves `rake_eager_load` false, so a stock
|
|
215
|
-
app's CI gated on nothing while printing honest prose. There is no setup where
|
|
216
|
-
you'd deliberately run this task against a supergraph none of whose subgraphs
|
|
217
|
-
are here; the abort says to drop it from CI if that's really you.
|
|
218
|
-
|
|
219
|
-
The rule stays statable in one sentence: it fails when it found drift, and when
|
|
220
|
-
it had nothing to look at.
|
|
221
|
-
|
|
222
|
-
## Output structs allow Ruby-keyword prop names
|
|
223
|
-
|
|
224
|
-
**Considered:** narrowing the ban to types with a registered `alias:` whose path
|
|
225
|
-
starts at the prop.
|
|
226
|
-
|
|
227
|
-
**Rejected because** the ban turned out to be unnecessary, not merely too broad.
|
|
228
|
-
All 33 producible keywords construct, deserialize and typecheck as props; the
|
|
229
|
-
only bare read is an `alias:` delegator's first hop, which now spells
|
|
230
|
-
`self.next`. The proposed narrowing would also have made generation depend on
|
|
231
|
-
unrelated global registry state.
|
|
232
|
-
|
|
233
|
-
## Queries directories are a list again
|
|
234
|
-
|
|
235
|
-
**Considered:** leaving `queries_path` singular, as 0.4.x made it — one
|
|
236
|
-
`generate!` run reads one directory against one schema, and a second entry
|
|
237
|
-
would produce modules at runtime that `rake graph_weaver:generate` never
|
|
238
|
-
generated and `verify` never checked.
|
|
239
|
-
|
|
240
|
-
**Rejected because** that failure was the *divergence*, not the plurality:
|
|
241
|
-
back then `load_queries!` walked the list and `generate!` read only its first
|
|
242
|
-
entry. Every reader now goes through `GraphWeaver.query_files`, so a second
|
|
243
|
-
directory is generated, verified and loaded alike — and a duplicate module
|
|
244
|
-
name across two directories refuses, as it already did within one. What
|
|
245
|
-
survives is the honest half of the argument: one run reads one *schema*, so
|
|
246
|
-
`schema_path` stays singular.
|
|
247
|
-
|
|
248
|
-
## Deferred, deliberately
|
|
249
|
-
|
|
250
|
-
- **`write_timeout` on `Transport::HTTP`** — a real gap (nothing bounds sending),
|
|
251
|
-
but not yet worth a kwarg. Note `open_timeout` *is* the connect timeout and
|
|
252
|
-
covers the TLS handshake too (`net-http`'s `ssl_socket_connect(s, @open_timeout)`);
|
|
253
|
-
`ssl_timeout` is the OpenSSL session timeout and not a handshake deadline.
|
|
254
|
-
- **A `net_http:` passthrough hash** — the answer if the timeout/TLS kwarg list
|
|
255
|
-
keeps growing. Deliberately a hash and not a block: the pool creates
|
|
256
|
-
connections lazily and on failure, so a block would run an unpredictable number
|
|
257
|
-
of times. Configuration survives that; behaviour doesn't.
|
|
258
|
-
|
|
259
|
-
## Coercion says *whether*, never *how*
|
|
260
|
-
|
|
261
|
-
**Considered:** keeping `coerce: <Symbol>` (`register_scalar("ID", String, coerce: :to_s)`),
|
|
262
|
-
which let a registration name the conversion as well as opt into it.
|
|
263
|
-
|
|
264
|
-
**Rejected because** it asked the user to answer a question the library already
|
|
265
|
-
answers — the conversion for every scalar that has one is derived from the
|
|
266
|
-
scalar itself, and a custom scalar's conversion is its `cast:`/`serialize:`
|
|
267
|
-
pair. Its documented showcase existed only to re-enable something deliberately
|
|
268
|
-
removed from the auto path: the feature arguing for its own removal.
|
|
269
|
-
|
|
270
|
-
**Also considered:** dropping the `Int`/`Float` conversion entirely, leaving
|
|
271
|
-
parse as the single coercion mechanism. **Rejected because** `first: params[:page_size]`
|
|
272
|
-
arriving as a String is the most common real coercion in a Rails app, and
|
|
273
|
-
without it `auto_coerce` would loosen nothing among the built-ins but `Date` —
|
|
274
|
-
capability loss wearing simplicity's clothes.
|
|
275
|
-
|
|
276
|
-
`coerce:` and `auto_coerce` both survive because they are one question at two
|
|
277
|
-
scopes — a global default with a local override, the standard shape.
|
|
278
|
-
|
|
279
|
-
## `config.schema` refuses a subgraph rather than splitting in two
|
|
280
|
-
|
|
281
|
-
**Considered:** splitting the setting, since it serves two masters — the schema
|
|
282
|
-
fakes are fabricated against, and the live class `:in_process` runs. In a
|
|
283
|
-
federated app you can't have both, and setting one to make `:in_process` work
|
|
284
|
-
silently repointed `:fake` at a fraction of the graph.
|
|
285
|
-
|
|
286
|
-
**Rejected because** the split is a second knob plus a rule about which one
|
|
287
|
-
applies, and it buys a capability nothing lost: `:router` runs a subgraph's
|
|
288
|
-
real resolvers too, stitched. The two masters only want different objects in a
|
|
289
|
-
federated app, so refusing a subgraph class is the smaller change that makes
|
|
290
|
-
`config.schema` mean one thing again — and the refusal `:in_process` already
|
|
291
|
-
raises ("a federated graph has no one schema class — tag those examples
|
|
292
|
-
`graphql: :router`") becomes the whole story instead of half of it.
|
|
293
|
-
|
|
294
|
-
## The `graphql:` tag names a mode; a client is built in the example
|
|
295
|
-
|
|
296
|
-
**Considered:** letting the tag carry a client — `graphql: Failure.throttled`,
|
|
297
|
-
or `graphql: FakeClient.new(overrides: …)` — which reads well and would make
|
|
298
|
-
the tag's value a description rather than the cleanup marker it had become.
|
|
299
|
-
|
|
300
|
-
**Rejected because** metadata is evaluated when the file loads: one client
|
|
301
|
-
object would be shared by every example in the group, built before
|
|
302
|
-
`Testing.configure` had run. Spooky at a distance, and stateful — `#requests`
|
|
303
|
-
would accumulate across examples.
|
|
304
|
-
|
|
305
|
-
What the reading was right about was the leak underneath, and that is fixed
|
|
306
|
-
elsewhere: `GraphWeaver.client` is snapshotted and restored around *every*
|
|
307
|
-
example, so the tag no longer earns its keep as a cleanup marker, and building
|
|
308
|
-
a client is a plain assignment in a `before` block. `graphql_fake(**options)`
|
|
309
|
-
exists only because a fake needs the schema derivation the tag was doing.
|
data/Makefile
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
.PHONY: check generate test tc integration docs
|
|
2
|
-
|
|
3
|
-
# full verify loop: regenerate, test, typecheck
|
|
4
|
-
check: generate test tc
|
|
5
|
-
|
|
6
|
-
# manual/one-off checks against real GraphQL APIs (network; GitHub needs
|
|
7
|
-
# `gh auth login` or GITHUB_TOKEN; federation boots an Apollo gateway — node)
|
|
8
|
-
integration:
|
|
9
|
-
INTEGRATION=1 bundle exec rspec spec/integration
|
|
10
|
-
|
|
11
|
-
generate:
|
|
12
|
-
bundle exec ruby bin/generate
|
|
13
|
-
|
|
14
|
-
test:
|
|
15
|
-
bundle exec rspec
|
|
16
|
-
|
|
17
|
-
tc:
|
|
18
|
-
bundle exec srb tc
|
|
19
|
-
|
|
20
|
-
# preview what rubydoc.info will render
|
|
21
|
-
docs:
|
|
22
|
-
bundle exec yard doc
|
|
23
|
-
open doc/index.html
|
data/NOTES.md
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
Research notes — graphql-client exploration → GraphWeaver
|
|
2
|
-
======
|
|
3
|
-
|
|
4
|
-
**What this is now:** a working prototype of a standalone, Sorbet-typed
|
|
5
|
-
GraphQL client for Ruby — "graphql-codegen for Ruby". `.graphql` queries +
|
|
6
|
-
a schema (live class, introspection JSON, or SDL) generate `# typed: strict`
|
|
7
|
-
Ruby: nested `T::Struct`s, casting code, and a typed `execute`, so `srb tc`
|
|
8
|
-
sees the exact shape of every query result. It is **not** a graphql-client
|
|
9
|
-
extension: generated code depends only on `graphql` (generation time) and
|
|
10
|
-
`sorbet-runtime` (runtime); transport is a pluggable `executor:` (in-process
|
|
11
|
-
schema or the bundled `HttpExecutor`).
|
|
12
|
-
|
|
13
|
-
Start with `PLAN.md` for current state and next steps. Key files:
|
|
14
|
-
`lib/struct_codegen.rb` (the generator), `queries/` → `bin/generate` →
|
|
15
|
-
`lib/generated/` (the build loop), `StructCodegen.load` (build-free dynamic
|
|
16
|
-
mode for development).
|
|
17
|
-
|
|
18
|
-
**How it got here:** the repo began as an exploration of
|
|
19
|
-
[graphql-client](https://github.com/github-community-projects/graphql-client)
|
|
20
|
-
internals — could its class-generation layer be swapped to emit custom
|
|
21
|
-
classes? (Yes: the `StructTypes` spike below.) The per-query codegen
|
|
22
|
-
approach then outgrew graphql-client entirely, and everything below the
|
|
23
|
-
next heading is preserved as the lab notebook: findings in chronological
|
|
24
|
-
order, each backed by a spec.
|
|
25
|
-
|
|
26
|
-
The specs are the documentation — each one asserts an observed behavior:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
bundle exec rspec
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Findings, in exploration order
|
|
33
|
-
|
|
34
|
-
- The client runs fine against an in-process schema: `GraphQL::Client.new(schema: Schema, execute: Schema)` — no HTTP involved.
|
|
35
|
-
- Each query selection gets its own dynamically generated wrapper class (subclass of `GraphQL::Client::Schema::ObjectClass`); fields are snake_case readers, and unselected fields raise instead of returning nil.
|
|
36
|
-
- **Custom scalars are deserialized automatically** when the client is built from a live schema class: the reader casts wire values through the scalar's `coerce_input` (e.g. `"1990-06-15"` → `Date`). This is the built-in hook for producing rich Ruby values.
|
|
37
|
-
- Caveat: this only works with `schema:` as a live schema class. A schema loaded from an introspection JSON dump has no coercion logic, so scalars would stay raw.
|
|
38
|
-
- `to_h` returns the raw wire values (strings), not the casted ones — hydration code should read via the typed readers, not `to_h`.
|
|
39
|
-
- Hydrating into `T::Struct`s is straightforward manually; the interesting next step is generating the structs (or a generic hydrator) from the parsed query definition, since the client already knows each selection's shape and types.
|
|
40
|
-
|
|
41
|
-
## Swapping the class-generation layer (answered: yes)
|
|
42
|
-
|
|
43
|
-
`lib/struct_types.rb` + `spec/struct_types_spec.rb` prove the generation layer
|
|
44
|
-
can be replaced wholesale — the client deserializes straight into generated
|
|
45
|
-
`T::Struct`s, no `ObjectClass` involved.
|
|
46
|
-
|
|
47
|
-
How the pipeline hangs together (graphql-client 0.26.0):
|
|
48
|
-
|
|
49
|
-
- `Client#initialize` builds the types module: `@types = Schema.generate(schema)`
|
|
50
|
-
(`attr_reader :types`, no setter — swap via `instance_variable_set` or a subclass).
|
|
51
|
-
- `Client#parse` → `Definition#initialize` calls
|
|
52
|
-
`client.types.define_class(definition, ast_nodes, type)` and stores the result
|
|
53
|
-
as `definition.schema_class`. This is the ONLY thing the client asks of the
|
|
54
|
-
types module.
|
|
55
|
-
- `Client#query` → `definition.new(data, errors)` → `schema_class.new(data, errors)`.
|
|
56
|
-
- Everything below that is the `cast(value, errors)` protocol, composed
|
|
57
|
-
recursively per the query selection (NonNull/List wrappers, scalars, objects).
|
|
58
|
-
|
|
59
|
-
So the replacement contract is just:
|
|
60
|
-
- `define_class(definition, ast_nodes, type)` returning casters
|
|
61
|
-
- casters respond to `cast(value, errors)`
|
|
62
|
-
- the top-level caster must satisfy `Definition#new`'s case dispatch, which
|
|
63
|
-
tests `===` against the `GraphQL::Client::Schema::ObjectType` module —
|
|
64
|
-
including that module in your caster class is enough, plus a
|
|
65
|
-
`new(data, errors)` method
|
|
66
|
-
|
|
67
|
-
Gotchas found:
|
|
68
|
-
- the client injects `__typename` into every selection (`QueryTypename`), so a
|
|
69
|
-
custom generator must skip/handle `__`-prefixed fields
|
|
70
|
-
- scalar casting reuses the schema type's `coerce_isolated_input` — same hook
|
|
71
|
-
the stock `ScalarType` uses
|
|
72
|
-
- prop nullability comes for free from the type walk: everything is
|
|
73
|
-
`T.nilable` unless wrapped in NON_NULL
|
|
74
|
-
|
|
75
|
-
## Sorbet
|
|
76
|
-
|
|
77
|
-
- `sorbet` + `tapioca` are set up (`bundle exec srb tc` is green); rbis in `sorbet/rbi/gems`
|
|
78
|
-
- `struct_types.rb` typechecks at `# typed: true`
|
|
79
|
-
- generated structs are real `T::Struct`s: schema-derived prop types
|
|
80
|
-
(`T.nilable(Date)`, `T::Array[StructTypes::Pet]`) and runtime type
|
|
81
|
-
enforcement on bad wire data
|
|
82
|
-
|
|
83
|
-
## Codegen: srb tc sees query result types (answered: yes)
|
|
84
|
-
|
|
85
|
-
`lib/struct_codegen.rb` goes one step further than the runtime swap: it
|
|
86
|
-
emits plain `# typed: strict` Ruby source from a query + schema — nested
|
|
87
|
-
`T::Struct` classes, fully generated `from_h` casting code (no runtime
|
|
88
|
-
reflection), and a sig'd `execute`.
|
|
89
|
-
|
|
90
|
-
- source of truth: `queries/*.graphql`; regenerate with `bin/generate`
|
|
91
|
-
into `lib/generated/`; a spec asserts the checked-in output is current
|
|
92
|
-
- queries are validated against the schema at generation time
|
|
93
|
-
- `srb tc` statically checks result access end to end:
|
|
94
|
-
`result.person&.nmae` → `Method nmae does not exist on
|
|
95
|
-
PersonQuery::Result::Person`
|
|
96
|
-
- custom scalar deserialization is inlined by the generator
|
|
97
|
-
(`Date.iso8601(...)`) via a scalar registry; nullability and list
|
|
98
|
-
casting come from the NON_NULL/LIST walk
|
|
99
|
-
- note: generated `execute` runs against the schema directly, replacing
|
|
100
|
-
graphql-client at runtime entirely — the client's remaining value here
|
|
101
|
-
would be its HTTP adapter, which the generated code could target instead
|
|
102
|
-
|
|
103
|
-
## Fragments & unions (answered for codegen)
|
|
104
|
-
|
|
105
|
-
`queries/search.graphql` + `lib/generated/search_query.rb` exercise the
|
|
106
|
-
design:
|
|
107
|
-
|
|
108
|
-
- inline fragments and named fragment spreads are flattened into their
|
|
109
|
-
matching member's selection (exact type-name condition match; interface
|
|
110
|
-
conditions still open)
|
|
111
|
-
- unions emit a module per selection site: one `T::Struct` per possible
|
|
112
|
-
type, a `Type = T.type_alias { T.any(...) }`, and a `from_h` that
|
|
113
|
-
dispatches on `__typename` — codegen refuses union selections that
|
|
114
|
-
don't select `__typename`
|
|
115
|
-
- every possible type gets a member struct even without a fragment (it
|
|
116
|
-
still carries `__typename`), so dispatch is total
|
|
117
|
-
|
|
118
|
-
## Introspection / __type metadata
|
|
119
|
-
|
|
120
|
-
- `__type` / `__schema` queries work against the demo schema as expected
|
|
121
|
-
(see `spec/introspection_spec.rb` for the shapes)
|
|
122
|
-
- the key result: `GraphQL::Schema.from_introspection(Demo::Schema.as_json)`
|
|
123
|
-
produces a schema that codegen runs against **byte-identically** — so
|
|
124
|
-
generation works for remote APIs known only via an introspection dump.
|
|
125
|
-
Custom scalar handling survives because the codegen scalar registry is
|
|
126
|
-
keyed by type *name*, unlike runtime `coerce_input` which needs the live
|
|
127
|
-
schema class (the caveat that broke graphql-client's scalar casting)
|
|
128
|
-
|
|
129
|
-
## Federation / supergraph
|
|
130
|
-
|
|
131
|
-
- join__/link-annotated supergraph SDL parses via
|
|
132
|
-
`GraphQL::Schema.from_definition`, and codegen runs against it
|
|
133
|
-
unchanged — the directives are transparent to result typing
|
|
134
|
-
(`spec/federation_spec.rb` generates from a mini supergraph and casts a
|
|
135
|
-
response with no live subgraphs)
|
|
136
|
-
- gotcha: graphql-ruby's SDL builder does not apply directive-argument
|
|
137
|
-
defaults, so real Apollo `join v0.3` SDL (non-null defaulted args like
|
|
138
|
-
`extension: Boolean! = false`) fails to load unless those args are
|
|
139
|
-
provided or the directive defs are trimmed — a compatibility issue a
|
|
140
|
-
real tool would need to patch around
|
|
141
|
-
- client-side, federation needs nothing more: you query the router like
|
|
142
|
-
any schema. The *server-side* angle (emitting `@key`/`@external` via
|
|
143
|
-
apollo-federation) is a separate exploration — potentially relevant to
|
|
144
|
-
autographql
|
|
145
|
-
|
|
146
|
-
## Round 2: enums, interface conditions, loaders, dynamic mode, HTTP
|
|
147
|
-
|
|
148
|
-
- **enums** generate `T::Enum` classes (`Species::Dog`), deserialized via
|
|
149
|
-
`Species.deserialize(...)` in `from_h`; values sorted so output is
|
|
150
|
-
deterministic across schema sources
|
|
151
|
-
- **interface fragment conditions** (`... on Named { name }`) apply via
|
|
152
|
-
`schema.possible_types`, not just exact type-name match. Interface-typed
|
|
153
|
-
*fields* (a field returning `Named`) are still open — they'd emit like
|
|
154
|
-
unions with `__typename` dispatch
|
|
155
|
-
- **SchemaLoader** accepts both formats a remote service can hand you:
|
|
156
|
-
introspection dump (`.json`) or SDL (`.graphql`/`.gql`); both generate
|
|
157
|
-
byte-identically to the live schema class
|
|
158
|
-
- **dynamic mode**: `StructCodegen.load(...)` generates + evals in one
|
|
159
|
-
step — no build artifact, same runtime semantics, right for development
|
|
160
|
-
or one-off scripts. Tradeoff: the module is invisible to `srb tc`, so
|
|
161
|
-
static checking of result access needs the build step
|
|
162
|
-
- **HTTP transport**: generated `execute` takes `executor:` — anything
|
|
163
|
-
with `execute(query, variables:)` returning `{"data" => ...}`.
|
|
164
|
-
`HttpExecutor` (Net::HTTP POST) runs the same generated structs against
|
|
165
|
-
a live server (`spec/http_spec.rb` proves it against a local WEBrick
|
|
166
|
-
serving Demo::Schema)
|
|
167
|
-
- **directive defaults gap**: root cause found —
|
|
168
|
-
`BuildFromDefinition#prepare_directives` passes only usage-site args
|
|
169
|
-
while `Directive#initialize` validates all defined args without
|
|
170
|
-
applying `default_value`. `lib/directive_defaults_patch.rb` prepends
|
|
171
|
-
the fix; the federation spec now loads the *real* join v0.3 SDL.
|
|
172
|
-
Present in graphql 2.6.3 (latest) — worth an upstream issue/PR
|
|
173
|
-
|
|
174
|
-
## Open questions
|
|
175
|
-
|
|
176
|
-
- interface-typed fields (vs fragment conditions, which work)
|
|
177
|
-
- ~~name collisions~~ ANSWERED: path-based won. A generated type is named
|
|
178
|
-
for the response key that selects it, so the name is a function of the
|
|
179
|
-
field's own position — no walk order, no first-come-first-served, and an
|
|
180
|
-
unrelated selection can't move it. GraphQL aliases double as the explicit
|
|
181
|
-
naming escape hatch (`pet: pets` names the struct `Pet`)
|
|
182
|
-
- mutations/subscriptions (only query operations generate)
|