graph_weaver 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +8 -0
- data/Gemfile.lock +153 -4
- data/README.md +45 -79
- data/docs/alternatives.md +195 -0
- data/docs/cassettes.md +61 -50
- data/docs/editors.md +32 -47
- data/docs/errors.md +360 -103
- data/docs/federation.md +692 -473
- data/docs/generated_modules.md +441 -314
- data/docs/getting_started.md +370 -194
- data/docs/i18n.md +171 -0
- data/docs/logging.md +197 -50
- data/docs/real_world.md +42 -27
- data/docs/scalars.md +307 -176
- data/docs/testing.md +473 -220
- data/docs/transports.md +224 -151
- data/docs/upgrading.md +258 -305
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +19 -3
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +69 -11
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +406 -195
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +284 -46
- data/lib/graph_weaver/federation.rb +129 -27
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +27 -15
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +80 -0
- data/lib/graph_weaver/internal/headers.rb +70 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +249 -14
- data/lib/graph_weaver/log_subscriber.rb +74 -0
- data/lib/graph_weaver/logging.rb +163 -19
- data/lib/graph_weaver/query_module.rb +44 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +45 -13
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +266 -56
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +34 -10
- data/lib/graph_weaver/testing/endpoint.rb +107 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +164 -45
- data/lib/graph_weaver/testing/router.rb +64 -13
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +48 -6
- data/lib/graph_weaver/transport.rb +134 -27
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +495 -106
- metadata +71 -3
- data/CHANGELOG.md +0 -2355
data/CHANGELOG.md
DELETED
|
@@ -1,2355 +0,0 @@
|
|
|
1
|
-
### v0.6.1 (2026-09-10)
|
|
2
|
-
- **A stdlib scalar registers with nothing but its class.**
|
|
3
|
-
`GraphWeaver.register_scalar("Decimal", BigDecimal)` now emits the whole
|
|
4
|
-
codec: `BigDecimal(...)` reads the wire value, `to_s("F")` writes it back,
|
|
5
|
-
and the generated file requires `bigdecimal`. The careful spelling people
|
|
6
|
-
reached for was wrong — `serialize: :to_s` puts `"0.125e2"` on the wire —
|
|
7
|
-
so the library owns it. Cast inference gained Kernel's conversion functions
|
|
8
|
-
(`BigDecimal()`, and any `Kernel#Type` your app defines for a type the wire
|
|
9
|
-
can't already be), and `serialize:` accepts `[:method, *args]` for a
|
|
10
|
-
serializer that takes arguments. `cast:`/`serialize:`/`requires:` are
|
|
11
|
-
unchanged, for a class of your own.
|
|
12
|
-
- **Scalar names that are conventions are registered already**: graphql-ruby's
|
|
13
|
-
`ISO8601Date`, `ISO8601DateTime`, `BigInt` and `JSON`, plus `DateTime` —
|
|
14
|
-
what GitHub, Shopify and most hand-written schemas call an ISO 8601
|
|
15
|
-
timestamp. A date deserializes into `Date` and a timestamp into `Time`, so
|
|
16
|
-
nothing invents a midnight; `BigInt` reads the decimal string graphql-ruby
|
|
17
|
-
writes as well as a JSON number; `JSON` is registered as `T.untyped` on
|
|
18
|
-
purpose, which also drops it from the "unregistered custom scalars" report.
|
|
19
|
-
A schema that means something else by one of these names fails loudly — the
|
|
20
|
-
cast raises, naming the field — and one `register_scalar` overrides it, as
|
|
21
|
-
it always could. **Regenerate** if a query of yours reads a scalar with one
|
|
22
|
-
of those names: its prop was `T.untyped` and now has a type.
|
|
23
|
-
|
|
24
|
-
### v0.6.0 (2026-09-09)
|
|
25
|
-
- **One rule for relative path settings: they resolve against
|
|
26
|
-
`GraphWeaver.root`** — `Rails.root` in a Rails app, the working directory
|
|
27
|
-
otherwise. There were two: cassettes resolved against `Rails.root` while
|
|
28
|
-
the schema dump, queries and generated directories resolved against the
|
|
29
|
-
process's working directory, and the railtie patched three of the four
|
|
30
|
-
places that mattered. A Rails app started anywhere but its own root — a
|
|
31
|
-
`bin/rails server` or an `rspec` run from a subdirectory — loaded no
|
|
32
|
-
generated modules at all. An absolute setting is untouched, and the settings
|
|
33
|
-
keep returning what you configured, so `graphql.config.yml` stays portable.
|
|
34
|
-
Set `GraphWeaver.root` to override. The rule runs both ways: a path is
|
|
35
|
-
resolved against the root on access and reported relative to it on output,
|
|
36
|
-
so `generate!`'s return value, `changed_files`, `check_queries`' keys, the
|
|
37
|
-
rake tasks' `wrote`/`pruned` lines and the file an error names all come back
|
|
38
|
-
in the short form you configured. `cache:` also accepts a `Pathname` now,
|
|
39
|
-
as `schema:` and `query:` always have.
|
|
40
|
-
- **One spelling for the generated module's name: `name:`.** `GraphWeaver.parse`
|
|
41
|
-
took `name:` while `Codegen.generate` took `module_name:`. **Breaking:**
|
|
42
|
-
`module_name:` now raises naming `name:`. A parsed module also names itself
|
|
43
|
-
in errors — `GraphWeaver.parse::PersonQuery::Result::Person` instead of a
|
|
44
|
-
hex address — and assigning it to a constant upgrades every nested struct
|
|
45
|
-
to the real path.
|
|
46
|
-
- **`generate!` leaves a file alone when its content hasn't changed**, so the
|
|
47
|
-
rake task prints `wrote` only for files it wrote and `N already up to date`
|
|
48
|
-
for the rest, and a watching dev server reloads only what moved.
|
|
49
|
-
`GraphWeaver.changed_files` says which. A generated file now says which
|
|
50
|
-
release wrote it (`# Generated by GraphWeaver 0.6.0 — do not edit.`), so the
|
|
51
|
-
first `verify` after an upgrade reports the tree as stale. **Regenerate.**
|
|
52
|
-
- **A fake refuses an option it doesn't take**, naming the accepted ones and a
|
|
53
|
-
did-you-mean, at every door (`FakeClient.new`, `graphql_fake`,
|
|
54
|
-
`Router.new(fake:)`, `graphql_router(fake:)`) — it was a raw
|
|
55
|
-
`unknown keyword` from inside the fabricator. A scalar the fake can't
|
|
56
|
-
fabricate is now reported at its path (`at reader.orders.0.total`) with the
|
|
57
|
-
coordinate to override (`"Order.total"`).
|
|
58
|
-
- **A supergraph using federation 2.8's `@context`/`@fromContext` is refused
|
|
59
|
-
per query rather than routed with the argument unset.** `@join__field` was
|
|
60
|
-
on the routing table's known list, so its `contextArguments:` was read and
|
|
61
|
-
dropped: `Testing::Router` planned a fetch for the contextual field with the
|
|
62
|
-
argument empty. Apollo composes such a graph even when the context-setting
|
|
63
|
-
type and the contextual field sit in different subgraphs, so nothing
|
|
64
|
-
upstream caught it. The refusal fires only where the router would plan that
|
|
65
|
-
fetch itself; a subtree one subgraph answers whole still runs. New
|
|
66
|
-
`Unplannable` category `:context_argument`.
|
|
67
|
-
- **The local router makes one entity fetch where it made two** when an entity
|
|
68
|
-
has two `@requires` fields crossing into the same subgraph on the same
|
|
69
|
-
`@key`. Both sent the identical representation; Apollo makes one. Specs
|
|
70
|
-
asserting on `#trace` for such a query see one fewer entry. A missing `@key`
|
|
71
|
-
on a representation now names the entity type in `InputError#struct`, as a
|
|
72
|
-
coercion failure already did.
|
|
73
|
-
- **Three rake and generator polish items.** `rake -T` no longer prints a
|
|
74
|
-
queries path it can't know — a task description is baked before
|
|
75
|
-
`:environment`, so it presented the default as your setting; it now names
|
|
76
|
-
the defaults as defaults. `cassettes:anonymize` prints `no recordings in
|
|
77
|
-
<dir>` instead of nothing. `rails g graph_weaver:install` scaffolds
|
|
78
|
-
`app/graphql/fragments/.keep`, the directory its editor config already
|
|
79
|
-
globbed; the getting-started note names `rails g graphql:install
|
|
80
|
-
--skip-query-logs` for an app without Active Record.
|
|
81
|
-
- **`filter_parameters` scrubs error messages, not just the debug log.** A
|
|
82
|
-
rejected value reached the log twice: on the `variables=` line at debug, and
|
|
83
|
-
in the message every error writes at `warn` — above the gate, as free text.
|
|
84
|
-
One rule now covers both: a message the library composes about a value you
|
|
85
|
-
supplied names it only when the key it arrived under isn't filtered. So
|
|
86
|
-
`$password of Login: [FILTERED]`, while `$count of Compute: expected an Int,
|
|
87
|
-
got "lots"` is unchanged — the value is usually the whole diagnosis. Applies
|
|
88
|
-
to execute kwargs, input fields at any depth, federation `@key` fields, and
|
|
89
|
-
the missing-cassette report. A wrong-typed input field with no coercer now
|
|
90
|
-
says which field (`cents: expected BigDecimal, got 5`) instead of sorbet's
|
|
91
|
-
sentence, and the local federation router no longer logs variables
|
|
92
|
-
unscrubbed.
|
|
93
|
-
- **The public surface is what the docs name, what generated code calls, and
|
|
94
|
-
the `execute` slot — 421 names, down from about 700.** Everything else moved
|
|
95
|
-
under `GraphWeaver::Internal`, whose one rule is that nothing in it is API,
|
|
96
|
-
or went `private`. `spec/public_surface_spec.rb` diffs what's reachable from
|
|
97
|
-
`GraphWeaver` against a checked-in list, so the next accidental promotion
|
|
98
|
-
fails CI instead of shipping. **Breaking** only for code reaching past the
|
|
99
|
-
documented door: the federation query planner and its IR are
|
|
100
|
-
`Internal::Planner`; the fake-value engine is `Internal::Values`; the
|
|
101
|
-
selection walk is `Internal::Selection` (so `FakeClient` no longer answers
|
|
102
|
-
to `each_field` or `gather`); the cassette matching rules are
|
|
103
|
-
`Internal::RequestKey`; `GraphWeaver.log`/`.instrument`/`.filter_variables`
|
|
104
|
-
are `Internal::Log` (`logger=`, `instrumenter=`, `filter_parameters=` are
|
|
105
|
-
unchanged); `Transport.operation_name`/`.mutation?`/`.log_tag` left the
|
|
106
|
-
class you subclass for `Internal::Wire`. `SchemaDiff::Change`,
|
|
107
|
-
`Cassette::Check`, `Coverage::Result` and `InputStruct::Field` are `Data`,
|
|
108
|
-
not `Struct`, so they no longer hand out writers. `Codegen::ScalarType` is
|
|
109
|
-
private from load rather than from the first `reset_scalars!` — the
|
|
110
|
-
`private_constant` had landed inside the method body. Generated modules also
|
|
111
|
-
stop exposing their plumbing: `DEFAULT_CLIENT`, `FIELDS` and `ONE_OF` are
|
|
112
|
-
emitted as `private_constant`. **Regenerate**; nothing you call changes.
|
|
113
|
-
- **The retry options are flat.** `retries:` is how many attempts follow the
|
|
114
|
-
first; every other `Retry` option now sits beside it on the client —
|
|
115
|
-
`GraphWeaver.new(url, retries: 5, backoff: :linear, retry_codes:
|
|
116
|
-
["THROTTLED"])`. **Breaking:** `retries:` no longer takes a Hash (`retries:
|
|
117
|
-
{ retries: 5 }` read as a key nested in itself), and three options are
|
|
118
|
-
spelled out now that they sit next to a count: `on:` is `retry_on:`,
|
|
119
|
-
`base:`/`max:` are `base_delay:`/`max_delay:` — `retries: 5, max: 30`
|
|
120
|
-
invited reading `max:` as a second attempt count. The Hash form names its
|
|
121
|
-
flat replacement, and a retry option passed without `retries:` says so.
|
|
122
|
-
- **A representation's key fields are coerced, like an `execute` kwarg.**
|
|
123
|
-
`Representations.user(id: params[:id])` typechecked statically and then
|
|
124
|
-
raised sorbet-runtime's unbranded `TypeError` at runtime. The generated
|
|
125
|
-
builders now carry `.checked(:never)` and run each key field through the
|
|
126
|
-
registered scalar's own conversion; a value that converts to nothing raises
|
|
127
|
-
`GraphWeaver::InputError` naming the representation and the field.
|
|
128
|
-
**Regenerate.**
|
|
129
|
-
- **The library's internals are private.** Everything not documented and not
|
|
130
|
-
named by generated code is now `private` / `private_class_method` /
|
|
131
|
-
`private_constant`: codegen's IR and walk, the schema loader's detection
|
|
132
|
-
tables, the transport's log patterns, `Retry`'s default predicate, the
|
|
133
|
-
file-naming helpers. **Breaking:** `GraphWeaver::Selection`'s methods are
|
|
134
|
-
private in every class that mixes it in, so `Testing::FakeClient` no longer
|
|
135
|
-
answers to `each_field`, `gather` or `load_operation` — never a supported
|
|
136
|
-
door. The runtime that emitted source calls into (`cast_data`,
|
|
137
|
-
`check_envelope!`, `Coerce`, `Hints`, `InputStruct`, `QueryModule`,
|
|
138
|
-
`Representation`) stays public and says in its comments that it is
|
|
139
|
-
generated code's, not yours.
|
|
140
|
-
- **A registration another schema owns warns instead of failing generation.**
|
|
141
|
-
One registry serves a whole graph — federation composes scalars and types
|
|
142
|
-
by name — but validation ran against the single schema in hand, so the
|
|
143
|
-
natural multi-subgraph pattern (register once, generate each query against
|
|
144
|
-
its own subgraph) hard-failed. Generation now fails only on what the schema
|
|
145
|
-
can *disprove*: a name it declares as something else, or a coordinate whose
|
|
146
|
-
field it declares as a composite. Everything else warns, naming the schema
|
|
147
|
-
and suggesting a corrected name. `rake graph_weaver:generate` and `verify`
|
|
148
|
-
print that list once per run after the files;
|
|
149
|
-
`GraphWeaver.unmatched_registrations` is the same list for a Rakefile.
|
|
150
|
-
**Breaking**: a typo'd registration that used to stop generation is now a
|
|
151
|
-
printed warning. `docs/federation.md` gains "Generating for a federated
|
|
152
|
-
graph": through the gateway, generate against the supergraph; calling
|
|
153
|
-
subgraphs directly, one client each with `reset_registrations!` between.
|
|
154
|
-
- **Variables coerce inside `execute` now, and the sig stays as narrow as the
|
|
155
|
-
schema.** `execute(first: params[:first])` works — the String becomes an
|
|
156
|
-
Integer, an ISO-8601 string a `Date`, a model's Integer primary key an `ID` —
|
|
157
|
-
while `execute(first: "20")` written literally in a typed file is still an
|
|
158
|
-
`srb tc` error. The only way to accept loose input used to be `coerce: true`
|
|
159
|
-
/ `GraphWeaver.auto_coerce`, which bought it by *widening* the emitted kwarg
|
|
160
|
-
and so switched off the static check at every call site. The generated sigs
|
|
161
|
-
are now `.checked(:never)` so an untyped value reaches the body; coercion is
|
|
162
|
-
the check that replaces sorbet-runtime's, and it refuses more than sorbet
|
|
163
|
-
did. Bad input raises `GraphWeaver::InputError` naming the variable, the
|
|
164
|
-
operation and the value — the same 422 rescue point as an input-object
|
|
165
|
-
field. **Breaking:** `GraphWeaver.auto_coerce` and `register_scalar(coerce:)`
|
|
166
|
-
are gone; a scalar that had `coerce: true` keeps its behaviour but its kwarg
|
|
167
|
-
narrows, so a call site passing a literal String is now an `srb tc` error.
|
|
168
|
-
`cast:` is what a loose value converts through. **Regenerate.**
|
|
169
|
-
- **Numeric strings are read as a wire format, not as Ruby source.** `"010"`
|
|
170
|
-
is ten, not eight, and `"0x1f"` and `"1_0"` are refused, in variables and
|
|
171
|
-
responses alike — `Kernel#Integer` accepted all three, which made a
|
|
172
|
-
zero-padded form field silently mean something else. An `Int` refuses a
|
|
173
|
-
`Float` it can't hold losslessly (`2.5`), and a `Boolean` refuses a String:
|
|
174
|
-
Ruby has no `Kernel#Boolean`, so every rule for `"0"`/`"off"` is a
|
|
175
|
-
convention the library declines to pick — convert at the call site.
|
|
176
|
-
- **A faked subgraph takes the options every other fake takes, and
|
|
177
|
-
`graphql_router` says them per example.** `subgraphs: { "reviews" => :fake }`
|
|
178
|
-
built its fake with nothing, so `overrides:`, `list_size:` and `null_chance:`
|
|
179
|
-
had nowhere to go. `fake:` carries them, on `Router.new`, on
|
|
180
|
-
`Testing.config.router`, and per example through `graphql_router(fake:)`,
|
|
181
|
-
which mirrors `graphql_fake`. One `fake:` covers every faked subgraph —
|
|
182
|
-
coordinate-keyed overrides already say which type they mean. A `fail_at:`
|
|
183
|
-
inside an `_entities` fetch used to null the field and report no error; it
|
|
184
|
-
now comes back as an error on the caller's path.
|
|
185
|
-
- **A subgraph two loaded schema classes fit is refused by the query that
|
|
186
|
-
reaches it, not by `Router.new`.** In an app where several loaded schemas
|
|
187
|
-
each satisfy a foundational subgraph, building a router raised for
|
|
188
|
-
subgraphs the query under test never touched — and only when eager loading
|
|
189
|
-
happened to be on. The router now builds, every query that avoids the
|
|
190
|
-
subgraph runs, and one that reaches it raises naming the candidates, saying
|
|
191
|
-
they came from loaded schema classes, and showing the `subgraphs:` form that
|
|
192
|
-
pins one. A class named explicitly still fails at construction.
|
|
193
|
-
`router.ambiguous` lists them. `Testing::Subgraphs.resolve` now returns a
|
|
194
|
-
`Resolution` (`#served`, `#ambiguous`).
|
|
195
|
-
- **A `.graphql` edit reaches the next request in development.** The query
|
|
196
|
-
directories and the schema dump join Rails' own reloaders, and the
|
|
197
|
-
`to_prepare` that loads the generated modules now regenerates first — after
|
|
198
|
-
your registrations, so an `extend_type` in a `to_prepare` is in place before
|
|
199
|
-
codegen reads it. A query that doesn't compile is logged at `error` with its
|
|
200
|
-
file and position while the modules already loaded keep serving. Development
|
|
201
|
-
only, while the server is running; `config.graph_weaver.watch = false` turns
|
|
202
|
-
it off, and `rake graph_weaver:verify` still gates CI on the committed files.
|
|
203
|
-
`GraphWeaver.reload_generated!` does the same by hand after regenerating in
|
|
204
|
-
another terminal, and generated files are now written to a temp file and
|
|
205
|
-
renamed, so a `rake graph_weaver:generate` beside a watching dev server can't
|
|
206
|
-
hand the running app a prefix that no longer parses.
|
|
207
|
-
- **A variable passed `nil` now sends `null`; one left out is still left
|
|
208
|
-
out.** GraphQL tells an absent variable from an explicit null — `bio: null`
|
|
209
|
-
clears a bio, omitting it does nothing — and a Ruby kwarg with a nil default
|
|
210
|
-
cannot, so both spellings omitted the key and no generated mutation could
|
|
211
|
-
clear a field. An optional nullable variable now records whether the keyword
|
|
212
|
-
was supplied. Input objects get the same distinction where a Hash can express
|
|
213
|
-
it: `coerce({nickname: nil})` sends null, `coerce({})` omits; a struct built
|
|
214
|
-
with `.new` can't tell, so nil there still means omit. A non-null variable
|
|
215
|
-
can't carry null, so nil there also still omits and its schema default
|
|
216
|
-
applies. **Breaking**: `bio: params[:bio]` with a missing param now sends
|
|
217
|
-
`null` where it used to omit — pass the keyword only when you mean to.
|
|
218
|
-
**Regenerate.**
|
|
219
|
-
- **A response that won't cast says which field, and whose bug it is.** A
|
|
220
|
-
leaf's cast raises about the value alone — "invalid date" on a struct holding
|
|
221
|
-
four dates located nothing — so each casting leaf now carries its response
|
|
222
|
-
key. An `ID` the server sent unquoted is out of spec but read like a
|
|
223
|
-
graph_weaver bug in sorbet's words; the message now says which it is and how
|
|
224
|
-
to take it anyway. An enum value the generated enum doesn't hold now names
|
|
225
|
-
the legal values and says the likely cause is drift, matching the input
|
|
226
|
-
side. **Regenerate.**
|
|
227
|
-
- **`register_scalar` with a type the wire can't build is refused at
|
|
228
|
-
generation.** `register_scalar("Money", BigDecimal)` produced a `BigDecimal`
|
|
229
|
-
prop and no cast, so every response failed its prop check at runtime, far
|
|
230
|
-
from the initializer that caused it. Generation now refuses it where a query
|
|
231
|
-
reads that scalar back, naming the field and how to give it a `cast:`.
|
|
232
|
-
`docs/scalars.md` now tabulates what the wire carries in both directions.
|
|
233
|
-
- **A fake takes pins, keyed by a scalar type, an object type, or a field.**
|
|
234
|
-
A pin says what the fake uses instead of inventing a value: a wire value,
|
|
235
|
-
an object it reads the selected fields off — a FactoryBot build, a model, a
|
|
236
|
-
`Struct`; a field the object doesn't answer is still fabricated, and a Ruby
|
|
237
|
-
value goes on the wire the way its scalar registration serializes it — or a
|
|
238
|
-
proc handed the seeded `Random`. Pins lead and options follow, in one call;
|
|
239
|
-
suite-wide, the same hash is `Testing.config.overrides`, and a router's
|
|
240
|
-
`fake:` takes it for the subgraphs it fakes.
|
|
241
|
-
|
|
242
|
-
graphql_fake("Money" => "12.00", "Person" => build(:person), "Order.total" => "999.00")
|
|
243
|
-
|
|
244
|
-
A scalar registered as your own class — `register_scalar("Money", Money,
|
|
245
|
-
cast: :parse)` — told codegen how to read the wire value but left the
|
|
246
|
-
testing harness guessing what to write, so every fake response touching it
|
|
247
|
-
died inside `from_h` blaming `Money.parse` for a `"Money-1"` placeholder.
|
|
248
|
-
The type pin is the answer, from every door including cassette
|
|
249
|
-
anonymization; without one, fabrication refuses, naming the scalar, the
|
|
250
|
-
field, and the pin to add. Scalars registered as `Time`, `Date`, `Integer`,
|
|
251
|
-
`Float`, `String` or `T::Boolean` need nothing. Fakes also now honor a
|
|
252
|
-
per-field `register_scalar("User.birthday", Date)`, matching codegen's
|
|
253
|
-
resolution. `graphql_fake` and `graphql_router(fake:)` refuse a per-example
|
|
254
|
-
`seed:` — `rspec --seed` is the mechanism, and `config.seed` the suite-wide
|
|
255
|
-
one for a harness that isn't rspec.
|
|
256
|
-
- **`Testing.config.null_chance` and `Testing.config.mode` are gone; both
|
|
257
|
-
are per-fake now.** A suite-wide answer to a per-example question nils an
|
|
258
|
-
unrelated field one run in ten. **Move them:** `graphql_fake(null_chance:
|
|
259
|
-
0.3)` and `graphql_fake(values: :literal)` (the per-fake `mode:` is renamed
|
|
260
|
-
`values:` so it can't be confused with `graphql: :fake` /
|
|
261
|
-
`config.default_mode`, which stay). The auto-detect — faker when the gem is
|
|
262
|
-
loaded, else literal — is the only suite-wide value style.
|
|
263
|
-
`Testing::MODES` is now `VALUE_STYLES`.
|
|
264
|
-
- **`retries:` takes the count you'd write, and means the same thing
|
|
265
|
-
everywhere.** `GraphWeaver.new(url, retries: 3)` raised and pointed at
|
|
266
|
-
`retries: { tries: 3 }` — a second word for the same number that disagreed
|
|
267
|
-
about whether it counted the first attempt. One word now: `retries:` is how
|
|
268
|
-
many attempts follow the first, on the client and on `Retry` alike, so
|
|
269
|
-
`retries: 0` is one attempt. **Rename** `Retry.new(tries: n)` to
|
|
270
|
-
`Retry.new(retries: n - 1)`; the default is unchanged in effect.
|
|
271
|
-
- **`Response#to_h`.** Every error class answered `#to_h`; the envelope didn't.
|
|
272
|
-
It returns `{"data" =>, "errors" =>, "extensions" =>}` with each error as its
|
|
273
|
-
JSON-ready hash. `data` stays the typed struct rather than re-serialized:
|
|
274
|
-
`T::Struct#serialize` gives snake_case keys, drops nulls, and leaves a
|
|
275
|
-
registered scalar as its Ruby object — output that would pass for the
|
|
276
|
-
server's response without being one.
|
|
277
|
-
- **Sensitive variables are scrubbed from the debug log.** A `login(password:)`
|
|
278
|
-
mutation's variables were one log-level switch away from the log. Matching
|
|
279
|
-
values are replaced with `[FILTERED]` at any depth, on the wire and
|
|
280
|
-
in-process paths. Rails apps configure nothing: the railtie adopts the app's
|
|
281
|
-
own `config.filter_parameters`. Elsewhere,
|
|
282
|
-
`GraphWeaver.filter_parameters = [:password, /token/]`; the default list is
|
|
283
|
-
`[:password, :token, :secret, :authorization]`, matched as case-insensitive
|
|
284
|
-
substrings, and `[]` turns filtering off.
|
|
285
|
-
- **`schema:diff` names what changed, not just that something did.** It
|
|
286
|
-
reported `schema.json is stale` and stopped, so learning what moved meant
|
|
287
|
-
refreshing and reading a `git diff` of a 3 MB dump. It now prints one line
|
|
288
|
-
per change at its schema coordinate, breaking ones first — types, fields,
|
|
289
|
-
arguments, nullability, enum values, union members, interfaces and
|
|
290
|
-
deprecations — and still exits non-zero on any drift. Breaking is judged from
|
|
291
|
-
the client's side, so nullability is directional: an output losing `!` breaks
|
|
292
|
-
a generated struct, an input gaining one breaks a query that omits it.
|
|
293
|
-
`SchemaLoader.stale?` is now `SchemaLoader.diff`, returning the summary;
|
|
294
|
-
`diff(path).empty?` is what `stale?` answered.
|
|
295
|
-
- **A Rails app whose generated code includes an autoloaded helper now
|
|
296
|
-
boots.** `extend_type("Pet", PetHelpers)` and `register_enum("Species",
|
|
297
|
-
PetKind)` name constants your app autoloads, so the library tells you to
|
|
298
|
-
register them from a `to_prepare` block — but the railtie required the
|
|
299
|
-
generated files before Zeitwerk was set up and before `to_prepare` ran, so
|
|
300
|
-
the `include PetHelpers` those registrations emit raised `NameError` at every
|
|
301
|
-
boot, in every environment. They now load from a `to_prepare` block of their
|
|
302
|
-
own: after the autoloader, after your registrations, before eager loading.
|
|
303
|
-
- **A fragment on the abstract type no longer narrows the field away.**
|
|
304
|
-
`named { __typename ... on Named { name } ... on Pet { species } }` read the
|
|
305
|
-
`Named` fragment as a type condition and narrowed to Pet, so a Person came
|
|
306
|
-
back as `nil` and the `name` the server sent went on the floor — while the
|
|
307
|
-
same selection spelled bare dispatched correctly. Two ways of writing one
|
|
308
|
-
query disagreed, and the fragment is how anyone shares interface fields.
|
|
309
|
-
**Regenerate** to pick it up.
|
|
310
|
-
- **A `Float` field reads the whole number a JSON encoder writes.** JSON has
|
|
311
|
-
one number type, so `1.0` reaches Ruby as an `Integer` from graphql-js and
|
|
312
|
-
Go's `encoding/json` alike, and `const :amount, Float` refused a response the
|
|
313
|
-
spec calls legal. Widened with `Kernel#Float`, which still refuses what
|
|
314
|
-
`.to_f` would quietly turn into `0.0`. **Regenerate** to pick it up.
|
|
315
|
-
- **A guarded `__typename` no longer makes a narrowing miss look like a
|
|
316
|
-
match.** Narrowing without a readable tag decides on emptiness, but a
|
|
317
|
-
`__typename` behind `@skip`/`@include` still arrives for the member narrowing
|
|
318
|
-
means to filter, so the wrong member was cast. That query now refuses at
|
|
319
|
-
generation, with the message that already names the fix.
|
|
320
|
-
- **Generation refuses what Ruby would misread.** A result key that would
|
|
321
|
-
shadow a constant the file uses (`date` beside a `Date` scalar nested a
|
|
322
|
-
`class Date < T::Struct` and typed the scalar prop as that struct) is refused
|
|
323
|
-
naming both keys; so is an enum value that camelizes to nothing (`_`), which
|
|
324
|
-
emitted a file that died at load with a syntax error.
|
|
325
|
-
- **A bad enum value in a variable says what the enum accepts**, as an
|
|
326
|
-
`InputError` — it raised a bare `KeyError` naming an anonymous module as a
|
|
327
|
-
top-level variable while the same mistake inside an input object was already
|
|
328
|
-
branded. A missing required input field now lists every missing key instead
|
|
329
|
-
of sorbet's symptom for the first prop it reached. **Regenerate** to pick up
|
|
330
|
-
the new emitted coercion.
|
|
331
|
-
- **A named fragment now crosses a subgraph boundary.** A stitched fetch is
|
|
332
|
-
built from selections alone, so a spread that survived into one named a
|
|
333
|
-
fragment the subgraph had never seen and the whole subtree came back
|
|
334
|
-
`Fragment X was used, but not defined`. Every spread a fetch carries is now
|
|
335
|
-
spelled as the inline fragment it is.
|
|
336
|
-
- **A fake's value for a custom scalar comes from what you registered it
|
|
337
|
-
as.** It was picked off the schema's *name* for the scalar, so a `Timestamp`
|
|
338
|
-
registered as `Time` got `"Timestamp-1"` and every fake response touching it
|
|
339
|
-
died inside the codec codegen had emitted for it. A scalar registered as
|
|
340
|
-
`Time` now gets iso8601, one registered as `Integer` an Integer; an
|
|
341
|
-
unregistered one keeps its placeholder. Cassette anonymization runs through
|
|
342
|
-
the same engine and had the same hole, which made `cassettes:check` fail on
|
|
343
|
-
a cassette the anonymizer had just written. **Re-run
|
|
344
|
-
`rake graph_weaver:cassettes:anonymize`** on a cassette holding a registered
|
|
345
|
-
custom scalar.
|
|
346
|
-
- **A mutation is no longer retried.** A read timeout, a 502 or a reset socket
|
|
347
|
-
does not say whether the server applied the request, and a second `charge`
|
|
348
|
-
is worse than a failed one — so `Retry` gives a mutation one attempt and says
|
|
349
|
-
so on the logger. Pass `retry_mutations: true` for an API whose mutations are
|
|
350
|
-
idempotent. Each retry now logs the wait and the attempt number.
|
|
351
|
-
- **A connection that dies mid-body is a dropped connection.** net/http's
|
|
352
|
-
`ignore_eof` default handed back the bytes that arrived when a socket closed
|
|
353
|
-
short of its `Content-Length`, so a half-sent response reached you as a 200
|
|
354
|
-
with a truncated body — a permanent `ServerError` that `Retry` would not
|
|
355
|
-
retry. It now raises the retriable `TransportError` it is.
|
|
356
|
-
- **An in-process query gets its own context.** graphql-ruby writes a
|
|
357
|
-
resolver's `context[...] =` into the hash it is handed, and one `InProcess`
|
|
358
|
-
is normally the whole app's client — so a flag set by one request was still
|
|
359
|
-
there for the next, and racing between them under Puma.
|
|
360
|
-
- **A response that carries neither `data` nor `errors` is refused.** A client
|
|
361
|
-
returning `nil`, one keying the envelope by symbol, one that typo'd `"dat"`:
|
|
362
|
-
each produced a `Response` reporting `success?` with `data` nil.
|
|
363
|
-
`from_response` is documented public API and symbolized keys are the
|
|
364
|
-
likeliest mistake at that seam, so it now brands, naming the keys it found;
|
|
365
|
-
a client answering with a String brands too, instead of dying on `#to_h`.
|
|
366
|
-
**Regenerate** to pick that up.
|
|
367
|
-
- **`@skip`/`@include` are evaluated by the fake against the variables you
|
|
368
|
-
passed**, declared defaults included — the way a server and
|
|
369
|
-
`Testing::Router` already did, so one query no longer carries a key under
|
|
370
|
-
`graphql: :fake` and not under `graphql: :router`. A faked subgraph does the
|
|
371
|
-
same for directives inside its `_entities` selection. A `first:`/`last:`
|
|
372
|
-
arriving as a variable caps the fabricated list like a literal, a cap below
|
|
373
|
-
zero reads as a page of none, and `null_chance` reaches list positions.
|
|
374
|
-
- **An alias spelling a response key the router carries a `@key` under is
|
|
375
|
-
refused.** The router injects the `@key` it crosses on under a reserved key
|
|
376
|
-
and strips it from the answer, so `_gw_weight: weight` came back without
|
|
377
|
-
`_gw_weight` at all. It joins the `shadowed_key` refusal, which already
|
|
378
|
-
covered Apollo's half of the same collision.
|
|
379
|
-
- **A `@requires` field set excluded by `@skip`/`@include` no longer
|
|
380
|
-
prefetches.** The router ran a subgraph fetch a real router never makes —
|
|
381
|
-
which matters for a test double, where a resolver that runs is one your
|
|
382
|
-
example can observe.
|
|
383
|
-
- **`verify_generated!` fails over zero query documents**, and `generate!`
|
|
384
|
-
warns. A mistyped `queries_paths` left `verify` returning true having
|
|
385
|
-
compared nothing, so a CI gate stayed green forever.
|
|
386
|
-
- **`rake graph_weaver:generate` says what it pruned, and where it looked when
|
|
387
|
-
there was nothing to generate.** Deleting a `.graphql` deletes the checked-in
|
|
388
|
-
file it produced, and the task printed nothing and exited 0; so did a run
|
|
389
|
-
with no queries, the state every install starts in.
|
|
390
|
-
- **`Failure.server` takes the headers a backoff branches on.**
|
|
391
|
-
`ServerError#retry_after` and `#throttled?` read `Retry-After`, and there was
|
|
392
|
-
no way to set one: `Failure.server(status: 429, headers: { "retry-after" =>
|
|
393
|
-
"2" })`. `Failure.transport`'s message now names the class it caught, the
|
|
394
|
-
way the bundled transports do.
|
|
395
|
-
- **Cassette recording is safe across threads.** `record` was
|
|
396
|
-
read-modify-write; two threads recording through one cassette each saved a
|
|
397
|
-
snapshot missing the other's entry.
|
|
398
|
-
- **A `.json` schema dump that isn't JSON says which file and what it holds.**
|
|
399
|
-
A truncated download or a login page saved over the dump raised a bare
|
|
400
|
-
`JSON::ParserError`, outside `GraphWeaver::Error` and naming neither.
|
|
401
|
-
`rake graph_weaver:schema:refresh` — the fix — no longer trips over it.
|
|
402
|
-
- **Setup mistakes say what to do.** A schema source that looks like a url
|
|
403
|
-
(`localhost:4000/graphql` included) gets the url advice first; a non-token
|
|
404
|
-
`auth:`, a `retries:` that is neither true nor a Hash, a url
|
|
405
|
-
`Transport::HTTP` cannot POST to, a `parse` path without a `.graphql`
|
|
406
|
-
extension, a cache directory that can't be written, `schema:refresh URL=`
|
|
407
|
-
with a file path, and `schema:diff` with no dump all name the fix. A
|
|
408
|
-
`ServerError` carries the advice its status implies: a 3xx names where the
|
|
409
|
-
server pointed (redirects are not followed) and a 401/403 names `auth:`. A
|
|
410
|
-
spec with a `graphql:` tag but no `graph_weaver/rspec` is told to require
|
|
411
|
-
it, a misspelled tag reports one failure instead of two, `InputError` no
|
|
412
|
-
longer prints sorbet's `Caller:` frame, a generated file whose registered
|
|
413
|
-
constant is gone names the registration, and `rails g graph_weaver:install`
|
|
414
|
-
names a retry that works when introspection fails.
|
|
415
|
-
- **The GitHub example's checked-in modules were regenerated**, and a spec now
|
|
416
|
-
keeps them in step with the emitter.
|
|
417
|
-
- **A cold process no longer introspects once per in-flight thread.** A
|
|
418
|
-
url-built client fetches its schema lazily, and Puma serves its first
|
|
419
|
-
requests concurrently — so eight threads arriving together meant eight full
|
|
420
|
-
introspection round trips, each of them also writing the schema cache. One
|
|
421
|
-
now, whoever asks first.
|
|
422
|
-
- **The schema cache and cassettes are written atomically.** `File.write`
|
|
423
|
-
truncates before it writes, so an interrupted run — or a second writer, a
|
|
424
|
-
rake task beside a running app — could leave a half-written file where a
|
|
425
|
-
committed artifact used to be. Both now write beside the target and rename,
|
|
426
|
-
which is atomic: a reader sees the old file or the new one.
|
|
427
|
-
- **The connection pool keeps its ceiling under an interrupt.** An async raise
|
|
428
|
-
landing between taking a permit and the `ensure` that returns it would have
|
|
429
|
-
leaked one for the life of the process, and `Rack::Timeout` raises exactly
|
|
430
|
-
that way; the gap is closed. Its saturation warning is also once now rather
|
|
431
|
-
than once per racing thread.
|
|
432
|
-
|
|
433
|
-
### v0.5.1 (2026-09-07)
|
|
434
|
-
- **A union's catch-all struct keeps the fields an interface fragment asked
|
|
435
|
-
for.** `... on Named { name }` under a union types `name` on every member the
|
|
436
|
-
query named, but the catch-all — the member it didn't name, including one the
|
|
437
|
-
schema grows later — carried only `__typename`, so a `name` the server
|
|
438
|
-
actually sent was dropped on the floor. Those fields are `T.nilable`: the
|
|
439
|
-
member that arrives needn't implement the interface. **Regenerate** to pick
|
|
440
|
-
it up.
|
|
441
|
-
- **`schema_stale?` now recognises the drift a graphql-ruby server reports.**
|
|
442
|
-
It matched Apollo's one flat code plus a message pattern, but graphql-ruby —
|
|
443
|
-
which this library ships as its in-process client — names the rule that fired
|
|
444
|
-
in `extensions.code`. So a renamed type, a removed argument, an argument that
|
|
445
|
-
became required and four other drift shapes all raised without the one
|
|
446
|
-
sentence telling you to refresh the dump and regenerate, and `to_h` reported
|
|
447
|
-
`"schema_stale": false`.
|
|
448
|
-
- **A cast failure keeps the server's own explanation.** A field that came back
|
|
449
|
-
null *with a reason* — a permission rule, a partial outage — raised only
|
|
450
|
-
Sorbet's nil complaint, because the generated envelope built `data` before it
|
|
451
|
-
read `errors`. The reason is now in the message. **Regenerate** to pick it up.
|
|
452
|
-
- **Cassette anonymization missed `errors` and `extensions`.** It walked
|
|
453
|
-
`data` and nothing else, so a rejected request's error message — which
|
|
454
|
-
routinely quotes the input that caused it — and whatever the server hangs
|
|
455
|
-
off `extensions` went to disk verbatim, under a flag that says real data
|
|
456
|
-
never reaches it. **Re-run `rake graph_weaver:cassettes:anonymize` on any
|
|
457
|
-
cassette you've already committed**, or re-record it. Neither key has a
|
|
458
|
-
schema behind it, so both are now walked by shape: keys, nesting, nulls and
|
|
459
|
-
booleans survive and every string and number is replaced, with `path`,
|
|
460
|
-
`locations` and an error's `extensions.code` kept because they describe the
|
|
461
|
-
request rather than the data.
|
|
462
|
-
- **Recording flags a credential that lands in the file.** A cassette gets
|
|
463
|
-
committed as written, and its query and variables can't be anonymized —
|
|
464
|
-
they're the key replay matches on. So the bytes headed for disk are checked
|
|
465
|
-
for the shapes that are unmistakable (a JWT, `AKIA…`, `ghp_…`, `xox…`,
|
|
466
|
-
`sk_live_…`, a PEM block, a `Bearer` header) and named on stderr once per
|
|
467
|
-
cassette. Nothing is rewritten; a password like `hunter2` has no shape, so a
|
|
468
|
-
quiet run still isn't a clean bill of health.
|
|
469
|
-
- **An anonymized cassette no longer drops a key selected twice.** The
|
|
470
|
-
anonymizer kept the last occurrence's sub-selection where codegen merges
|
|
471
|
-
them, so `a { x } a { y }` came back holding only `y` — a recording that
|
|
472
|
-
then failed its own `cassettes:check`.
|
|
473
|
-
- **Three generated types disagreed with the schema.** `srb tc` proves the
|
|
474
|
-
generated code is self-consistent, not that it is true, so each of these was a
|
|
475
|
-
lie the typechecker endorsed. **Regenerate** to pick them up:
|
|
476
|
-
- A field reached only through a `@skip`/`@include`-guarded occurrence, where
|
|
477
|
-
the same key is also selected unguarded, was typed as guaranteed. The server
|
|
478
|
-
legitimately omits it, so `from_h` raised `key not found` on a valid
|
|
479
|
-
response — and a union whose `__typename` arrived that way crashed its own
|
|
480
|
-
dispatch. Those children are nilable now, and the `__typename` case refuses
|
|
481
|
-
at generation as it already did for the plainer spelling.
|
|
482
|
-
- A narrowed abstract inside a non-null list made the *list* nilable
|
|
483
|
-
(`[Thing!]!` with `... on Widget` → `T.nilable(T::Array[…])`). Only the
|
|
484
|
-
elements can be nil; the array is always there.
|
|
485
|
-
- An input field the schema gives a default is optional, but a non-null one
|
|
486
|
-
emitted `const :x, String, default: nil` — so `x.upcase` typechecked and
|
|
487
|
-
was a `NoMethodError`.
|
|
488
|
-
- **`rake graph_weaver:federation:coverage` no longer counts a subgraph as
|
|
489
|
-
served here when two loaded schemas fit it.** `Router.new` refuses that case
|
|
490
|
-
— picking either would be a coin flip — so the report was promising a run
|
|
491
|
-
the suite can't have. It now lands in the "nothing here serves what they
|
|
492
|
-
reach" list, whose advice (name a schema for those subgraphs) is the fix.
|
|
493
|
-
- **A supergraph that renamed the join spec is now refused instead of read as
|
|
494
|
-
empty.** `@link(url: ".../join/v0.3", as: "j")` spells every marker `j__`,
|
|
495
|
-
which the routing table doesn't follow — so it reported a composed graph with
|
|
496
|
-
*no subgraphs* and nothing unsupported, and `Testing::Router` built happily on
|
|
497
|
-
top of that. It now lands in `unsupported`, so the router and
|
|
498
|
-
`federation:coverage` refuse at construction and say why.
|
|
499
|
-
- **`rake graph_weaver:cassettes:anonymize` now finds an SDL schema dump.** It
|
|
500
|
-
opened `GraphWeaver.schema_path` directly instead of locating the dump the
|
|
501
|
-
way every sibling task does, so an app whose committed dump is
|
|
502
|
-
`schema.graphql` (what the generator writes for a supergraph) got a raw
|
|
503
|
-
`Errno::ENOENT`. A missing dump now says so.
|
|
504
|
-
- **`rake graph_weaver:queries:check` now authenticates with the token your
|
|
505
|
-
app uses.** It re-introspects the url the dump records, but read
|
|
506
|
-
`GRAPHWEAVER_AUTH` rather than the ENV var the dump named — so an app
|
|
507
|
-
installed with `--auth MY_TOKEN` reached its private API while the check
|
|
508
|
-
401'd. Every re-introspection now goes through `SchemaLoader.source_transport`,
|
|
509
|
-
which is also now public.
|
|
510
|
-
- **A `graphql: :router` example now starts from the same fabricated data
|
|
511
|
-
whether it runs alone or after two hundred others.** The router is built once
|
|
512
|
-
for the suite, and a faked subgraph kept fabricating from wherever the last
|
|
513
|
-
example left its sequence — so `rspec spec/foo_spec.rb:12` disagreed with the
|
|
514
|
-
same example in a full run, which is the opposite of what `--seed` promises.
|
|
515
|
-
`Router#reset!` is the example boundary (`reset_trace` still clears only the
|
|
516
|
-
trace); the rspec tag calls it for you.
|
|
517
|
-
- **An override key given as a Symbol now pins.** `graphql_fake(overrides: { name: "Ada" })`
|
|
518
|
-
validated clean and silently fabricated a random value, because lookup is by
|
|
519
|
-
String — the exact silent-green failure the override validation exists to
|
|
520
|
-
prevent.
|
|
521
|
-
- **A helper no longer contradicts `config.default_mode`.** With
|
|
522
|
-
`default_mode = :fake`, an untagged example calling `graphql_in_process`
|
|
523
|
-
raised *"this example is tagged graphql: :fake"* — naming a tag that wasn't
|
|
524
|
-
there, and blocking the untagged form the docs recommend. Only an explicit
|
|
525
|
-
tag can disagree with a helper now.
|
|
526
|
-
- **A variable default that isn't a Boolean no longer reaches a subgraph.** An
|
|
527
|
-
enum or input-object default is a parser AST node; sending one put a
|
|
528
|
-
back-pointer to the lexer on the wire, and an input object raised
|
|
529
|
-
`NoMethodError` outside any rescue. Only `@skip`/`@include` read these, and
|
|
530
|
-
they read Booleans; a subgraph applies its own defaults from the variable
|
|
531
|
-
declaration it already receives.
|
|
532
|
-
- The local router asks the schema for one type by name rather than rebuilding
|
|
533
|
-
the whole type map, at four sites — one of them per response row. On a
|
|
534
|
-
1200-type supergraph a stitched query over 200 rows was **5x** slower than it
|
|
535
|
-
needed to be; the cost is now flat in schema size.
|
|
536
|
-
- **`graphql: :in_process` ran the committed schema dump instead of your live
|
|
537
|
-
schema class.** A dump loads as an anonymous `GraphQL::Schema` subclass, which
|
|
538
|
-
looks like a runnable class and has no resolvers — so every app that followed
|
|
539
|
-
`getting_started.md` (the generator always commits a dump) got a graphql-ruby
|
|
540
|
-
500 blaming its own resolver, and the error that was written to explain this
|
|
541
|
-
was unreachable. Nothing to do; if you worked around it with
|
|
542
|
-
`Testing.config.schema = MySchema`, that still works and still wins.
|
|
543
|
-
- **`GraphWeaver.parse(query:)` accepts a `Pathname`.** `schema:` already did,
|
|
544
|
-
so `GraphWeaver.parse(schema: Rails.root.join("schema.graphql"), query:
|
|
545
|
-
Rails.root.join("app/graphql/queries/person.graphql"))` died on the query
|
|
546
|
-
argument with `undefined method 'end_with?'`. Same for `client.parse(path)`.
|
|
547
|
-
- **`rake graph_weaver:cassettes:check` and `:anonymize` now find cassettes
|
|
548
|
-
from any working directory.** Both read `config.cassette_dir` raw, while
|
|
549
|
-
`Cassette.new` resolves it against `Rails.root` — so run from anywhere but
|
|
550
|
-
the app root, `check` aborted with "this checked nothing, so it proved
|
|
551
|
-
nothing" and `anonymize` silently did nothing, both while a spec run found
|
|
552
|
-
the same files fine. They now use the same resolution the recordings do.
|
|
553
|
-
- **An `extend_type(alias:)` name a struct instance already answers to now
|
|
554
|
-
refuses** instead of silently overriding it. `alias: { hash: "…" }` emitted
|
|
555
|
-
`def hash`, which breaks every `Hash` and `Set` holding that struct;
|
|
556
|
-
`inspect`, `to_s`, `method` and `class` were the same story. A *wire field*
|
|
557
|
-
by any of those names was already refused, so this is the same rule reaching
|
|
558
|
-
the same case from the other side. **If generation now refuses an alias you
|
|
559
|
-
had**, rename it — the accessor it generated was overriding a Ruby method.
|
|
560
|
-
|
|
561
|
-
### v0.5.0 (2026-09-07)
|
|
562
|
-
- **`graphql_in_process(SomeSchema)`** runs one example against that schema
|
|
563
|
-
class's real resolvers — the sibling of `graphql_fake`, and how a federated
|
|
564
|
-
app tests a single subgraph directly rather than through the stitched graph.
|
|
565
|
-
`graphql: :in_process` is that call with no argument, which runs
|
|
566
|
-
`config.schema` when it is a live class, so a non-federated app needs
|
|
567
|
-
nothing. A suite testing several subgraphs names one per example, which no
|
|
568
|
-
suite-wide setting could express.
|
|
569
|
-
- **`Testing::Router` crosses a boundary on a nested `@key` or `@requires`.**
|
|
570
|
-
`@key(fields: "id organization { id }")` and
|
|
571
|
-
`@requires(fields: "origin { lat lon }")` used to refuse; they now plan, to
|
|
572
|
-
any depth, with the object carried in the representation the way the SDL
|
|
573
|
-
spells it (a null inner object included). **Nothing to do** — queries your
|
|
574
|
-
suite marked "run this one against a real router" may now simply run. Where
|
|
575
|
-
a type declares more than one `@key`, the plan takes the first one the
|
|
576
|
-
fetching subgraph can supply.
|
|
577
|
-
- **`:nested_field_set` narrowed rather than disappeared.** It now names only
|
|
578
|
-
a nested field set no one *fetch* can build — `origin` resolving in one
|
|
579
|
-
subgraph and `origin.lat` in another, or a nested `@key` whose object a
|
|
580
|
-
`@requires` would half-fill from somewhere else. Either way the object
|
|
581
|
-
would arrive in pieces, and a representation comes from one fetch. If you
|
|
582
|
-
group refusals by category, that bucket shrinks; its message and
|
|
583
|
-
`docs/federation.md`'s table say what is left. `:chained_requires` is
|
|
584
|
-
unchanged and still a different refusal.
|
|
585
|
-
- **`Testing.config.router` takes `subgraphs:` without `supergraph:`.** It
|
|
586
|
-
raised — "must be the arguments to build one, e.g. `{ supergraph: … }`" —
|
|
587
|
-
even where the committed dump already is the supergraph, which is the case
|
|
588
|
-
the docs call "no config at all" and the single most likely config a
|
|
589
|
-
federated app writes (marking a remote subgraph `:fake`). Either key alone
|
|
590
|
-
is enough now; a missing `supergraph:` derives exactly as it does with no
|
|
591
|
-
`config.router` at all.
|
|
592
|
-
- **`graphql_fake(**options)`** builds the example's fake where the example
|
|
593
|
-
can say what it needs — `graphql_fake(overrides: { "Reader.orders" => [{}, {}] })`
|
|
594
|
-
— and returns it, so `#requests` is in reach. `graphql: :fake` is this call
|
|
595
|
-
with no options. Options had nowhere to go before: the tag builds its client
|
|
596
|
-
in a `config.before(:each)`, which rspec runs ahead of every group hook, so
|
|
597
|
-
`Testing.config.overrides` set in a `before` block was always too late and
|
|
598
|
-
failed silently, as wrong data.
|
|
599
|
-
- **`GraphWeaver.client` is snapshotted and restored around *every* example**,
|
|
600
|
-
not only a tagged one. `graphql: false` used not to restore while
|
|
601
|
-
`graphql: :fake` did, which made "tag `:fake`, then throw the client away"
|
|
602
|
-
the idiom for cleanup. Building your own client is now a plain assignment in
|
|
603
|
-
a `before` block. An example that deliberately leaked a client into later
|
|
604
|
-
examples no longer can.
|
|
605
|
-
- **`graphql: :none` is gone** — a second spelling of `graphql: false`, which
|
|
606
|
-
stays. Change any `:none` tag to `false`.
|
|
607
|
-
- **`GraphWeaver.client!` names the tag** when `graph_weaver/rspec` is loaded:
|
|
608
|
-
`no client configured — tag the example graphql: :fake (or :in_process /
|
|
609
|
-
:router), or build one with graphql_fake`. "Set `GraphWeaver.client=`" was
|
|
610
|
-
advice for the wrong file.
|
|
611
|
-
- **An override pins a subtree by naming only the fields the test is about.**
|
|
612
|
-
`overrides: { "Reader.orders" => [{ "status" => "PAID" }, {}] }` pins the
|
|
613
|
-
list's length and merges each element onto fabricated data — the rest of the
|
|
614
|
-
selection is still generated. It used to *replace*, so pinning one nested
|
|
615
|
-
field meant hand-writing the whole selection set in wire casing, and
|
|
616
|
-
under-supplying died as `key not found: "book"` at cast time. A pinned key
|
|
617
|
-
the query doesn't select is now refused (spellchecked, and naming the
|
|
618
|
-
response keys it could have been), for the same reason a typo'd coordinate
|
|
619
|
-
is. At a union or interface, a pinned object names its `"__typename"` and
|
|
620
|
-
gets that member rather than a random one.
|
|
621
|
-
- **An override of `nil` pins the field null.** It used to read as "no
|
|
622
|
-
override" and fabricate a value.
|
|
623
|
-
- **`Testing::FakeClient#requests`** records every `execute` in order
|
|
624
|
-
(`{ query:, variables:, operation_name: }`) — "did we send the right
|
|
625
|
-
variables", and "did we call it at all", without a hand-rolled spy.
|
|
626
|
-
- **`FakeClient`'s selection-walking internals are private** (`each_field`,
|
|
627
|
-
`gather`, `load_operation`, …). Nothing documented called them; if you did,
|
|
628
|
-
`Object.new.extend(GraphWeaver::Selection)` is the supported host.
|
|
629
|
-
- **The local router refuses a `@requires` whose field set names another
|
|
630
|
-
`@requires` field** (`chained_requires`). It used to answer: a prefetch sends
|
|
631
|
-
the entity's own `@key` and nothing else, so the inner requirement never
|
|
632
|
-
arrived and the field was computed from a representation missing its input —
|
|
633
|
-
the same field then holding two different values in one response. The
|
|
634
|
-
one-hop limit was documented but not enforced.
|
|
635
|
-
- **An error path no longer names an injected `@key`/`@requires` alias.** A
|
|
636
|
-
stitched error came back as `["thing", "_gw_w"]`, a field no schema contains;
|
|
637
|
-
it is `["thing", "w"]` now, as a real router reports it.
|
|
638
|
-
- **`@skip`/`@include` driven by a variable's declared default was ignored on a
|
|
639
|
-
boundary-crossing field**, so `query($show: Boolean = true) { … @include(if: $show) }`
|
|
640
|
-
called with no variables silently dropped the field. graphql-ruby applies an
|
|
641
|
-
operation's defaults; the local router read only what the caller passed.
|
|
642
|
-
- **`Testing::FakeClient` raises a GraphQL validation error for an unknown
|
|
643
|
-
field**, as every other client in the slot does. It used to die with
|
|
644
|
-
`NoMethodError: undefined method 'type' for nil` from inside the fabricator —
|
|
645
|
-
undiagnosable, and the commonest mistake there is: a query drifting ahead of
|
|
646
|
-
the schema dump, or a typo in one written inside an example.
|
|
647
|
-
- **The local test router plans a union or interface at a subgraph boundary.**
|
|
648
|
-
`search { ... on Track { playCount } ... on Artist { upcomingEvents { … } } }`
|
|
649
|
-
— a feed, a search page, any polymorphic list — used to be refused
|
|
650
|
-
(`abstract_boundary`), because a representation names one concrete
|
|
651
|
-
`__typename` and the planner runs before any data exists. It now plans one
|
|
652
|
-
branch per concrete type the supergraph says the subgraph can answer with,
|
|
653
|
-
asks the fetch for `__typename`, and buckets the returned objects by it at
|
|
654
|
-
execution time — one `_entities` fetch per concrete type, which is what a
|
|
655
|
-
real router does. Nothing to change; queries that were refused now run.
|
|
656
|
-
- The `crosses_subgraph` refusal category is **gone**, and
|
|
657
|
-
`abstract_boundary` now means only one thing: the supergraph doesn't record
|
|
658
|
-
which concrete types a subgraph answers an abstract type with (no
|
|
659
|
-
`@join__unionMember`/`@join__implements`, and the type in more than one
|
|
660
|
-
subgraph). Match on `Unplannable#category` for either of those and you must
|
|
661
|
-
change it.
|
|
662
|
-
- A fragment whose type condition can't hold at a position is now dropped
|
|
663
|
-
rather than refused — `... on Note` under a field whose subgraph has no
|
|
664
|
-
Note in that union never matches, and a real router answers `{}` there too.
|
|
665
|
-
- **A query file whose name can't spell a constant now names the file.**
|
|
666
|
-
`01_home_featured.graphql` reported `module_name: must be a constant name,
|
|
667
|
-
got "01HomeFeaturedQuery"` and left you to find which of thirty files it
|
|
668
|
-
meant; it now names the path and says the fix is a rename. It's a
|
|
669
|
-
`GraphWeaver::Error` too, so `rake graph_weaver:generate` aborts on it
|
|
670
|
-
instead of burying it under a backtrace — rescue `ArgumentError` for this
|
|
671
|
-
and you must change it. An explicit `module_name:` argument still raises
|
|
672
|
-
`ArgumentError`.
|
|
673
|
-
- **`rails g graph_weaver:install <supergraph>` recognises a composed
|
|
674
|
-
supergraph** and says what changes because of it: the `federation:*` tasks,
|
|
675
|
-
and that `graphql: :router` runs specs against your real subgraph resolvers.
|
|
676
|
-
The initializer it writes says so too.
|
|
677
|
-
- **Every `rake graph_weaver:federation:*` task was a silent no-op in a stock
|
|
678
|
-
Rails app.** Rails defaults `config.rake_eager_load` to false, and subgraph
|
|
679
|
-
detection only sees *loaded* schema classes — so `federation:subgraphs`
|
|
680
|
-
reported `nil` for every subgraph and `federation:diff` printed "checked 0 of
|
|
681
|
-
4 subgraphs" and exited **0**, a CI gate permanently green while checking
|
|
682
|
-
nothing. The tasks now eager-load the app first, and detection resolves.
|
|
683
|
-
- **`federation:diff` fails when it compared against nothing** (exit 1, naming
|
|
684
|
-
what to do). Comparing against *some* subgraphs is still a pass — a
|
|
685
|
-
partly-local supergraph is a supported setup — but a comparison against none
|
|
686
|
-
of them proved nothing. If your subgraphs all run elsewhere, drop the task
|
|
687
|
-
from CI; it has nothing there to gate.
|
|
688
|
-
- **The absent-subgraph refusal names the cause.** It advised `subgraphs: { … }`
|
|
689
|
-
on a `Router.new` an rspec `graphql: :router` example never calls. It now
|
|
690
|
-
leads with the usual cause — the schema class isn't loaded yet, so
|
|
691
|
-
eager-load it — and names `GraphWeaver::Testing.config.router = { subgraphs:
|
|
692
|
-
… }` as the way to name one under the tag.
|
|
693
|
-
- **Subgraph-mapping refusals are `GraphWeaver::ConfigurationError` (was
|
|
694
|
-
`ArgumentError`)** — `rescue GraphWeaver::Error` around `Testing::Router.new`
|
|
695
|
-
now catches them, as `docs/errors.md` said it would. Rescue `ArgumentError`
|
|
696
|
-
for these and you must change it. The rule the docs now state: what the
|
|
697
|
-
library *concludes*, having read your schema, is a `GraphWeaver::Error`; an
|
|
698
|
-
argument wrong on its face (`pool_size: must be >= 1`) stays an
|
|
699
|
-
`ArgumentError`. An ambiguous-detection refusal also names each candidate
|
|
700
|
-
schema once, instead of repeating a reloaded constant.
|
|
701
|
-
- **The local test router refused every mutation that stitched below its root**,
|
|
702
|
-
with a message claiming its root fields "span subgraphs" when there was one
|
|
703
|
-
root field in one subgraph — and advice (split it per subgraph) that couldn't
|
|
704
|
-
be followed. It now plans any mutation whose root fields share a subgraph:
|
|
705
|
-
they go over as one document and that subgraph serializes them, and what
|
|
706
|
-
hangs below a root is an ordinary read afterwards. The refusal is left for
|
|
707
|
-
root fields that genuinely span subgraphs, where the message is true.
|
|
708
|
-
- **The local router honoured `@skip`/`@include` on a field but ignored it on a
|
|
709
|
-
fragment spread or inline fragment that crossed a subgraph boundary** — it
|
|
710
|
-
answered a selection the operation had excluded, and ran an extra subgraph
|
|
711
|
-
fetch to do it. Folding a same-type fragment into its parent dropped the
|
|
712
|
-
fragment node, and its directives with it. They now move onto the selections
|
|
713
|
-
they guarded, and a fetch is skipped entirely when everything it was for is
|
|
714
|
-
excluded (so `trace` matches what a real router does). A field and the
|
|
715
|
-
fragment around it both carrying the same directive refuses, as
|
|
716
|
-
`conditional_fragment` — one selection can't hold two conditions of one name.
|
|
717
|
-
- **`rake graph_weaver:federation:diff` reported false drift for every input
|
|
718
|
-
object**, so a correct supergraph failed the CI gate with advice to recompose
|
|
719
|
-
something that wasn't broken. An input object exposes its members as
|
|
720
|
-
`arguments`, not `fields`, and the check only asked for fields.
|
|
721
|
-
- **A `Pathname` works anywhere a schema path does** — `GraphWeaver.new`,
|
|
722
|
-
`generate!(schema:)`, `SchemaLoader.load`. `Rails.root.join("schema.graphql")`
|
|
723
|
-
previously failed as `undefined method 'lstrip' for an instance of Pathname`.
|
|
724
|
-
- **`Transport::HTTP`'s `pool_size:` defaults to `RAILS_MAX_THREADS`** (else 5,
|
|
725
|
-
as before) — the variable Rails already sizes its own connection pool from,
|
|
726
|
-
because it is the same question. A threaded app that raised its thread count
|
|
727
|
-
no longer silently queues behind five sockets.
|
|
728
|
-
- **A saturated pool says so.** The first request that has to wait for a
|
|
729
|
-
connection logs a warning naming the wait and the ceiling; later ones log at
|
|
730
|
-
debug. Queueing was previously indistinguishable from a slow server, which
|
|
731
|
-
mattered most under a fiber scheduler — `async`/Falcon multiplexes fine, but
|
|
732
|
-
nothing sets `RAILS_MAX_THREADS` there, so the default capped it at 5 with no
|
|
733
|
-
signal. See `docs/transports.md`.
|
|
734
|
-
|
|
735
|
-
#### One `execute`, one way to pass a client (**breaking**)
|
|
736
|
-
|
|
737
|
-
Every client answers the same call — `execute(query, variables:,
|
|
738
|
-
operation_name:)`, returning the raw response hash. Three objects a user holds
|
|
739
|
-
used to disagree with that, and each disagreement was a bug waiting.
|
|
740
|
-
|
|
741
|
-
- **`Client#execute` is that contract now**, so `GraphWeaver::Retry.new(client)`,
|
|
742
|
-
`Testing::Sequence.new(client, fake)` and a cassette recorder over a client
|
|
743
|
-
all work. Its one-shot parse-and-run sugar moved to **`Client#run` /
|
|
744
|
-
`#run!`** (and `GraphWeaver.run` / `.run!` for the throwaway-client form):
|
|
745
|
-
**rename `client.execute!(query, **vars)` to `client.run!(query, **vars)`.**
|
|
746
|
-
- **A generated module takes the per-call client as a kwarg**: rename
|
|
747
|
-
`PersonQuery.execute(some_client, id: "1")` to
|
|
748
|
-
`PersonQuery.execute(client: some_client, id: "1")`. Variables are
|
|
749
|
-
unaffected — `$client` was already refused at generation. It is also what
|
|
750
|
-
makes a mistyped variable name itself: `PersonQuery.execute(id: "1")` on a
|
|
751
|
-
query that declares no variables now raises `unknown keyword: :id` instead of
|
|
752
|
-
blaming the client. **Regenerate** (`rake graph_weaver:generate`);
|
|
753
|
-
`rake graph_weaver:verify` fails until you do.
|
|
754
|
-
- **`GraphWeaver.resolve_transport` is gone.** Nothing needs unwrapping any
|
|
755
|
-
more. A client that can't execute is still refused, by the module it was
|
|
756
|
-
passed to — whose name is now in the message.
|
|
757
|
-
- A module parsed from a `Client` bakes that client rather than its transport.
|
|
758
|
-
For a client built from a schema *dump* (no transport), `execute` now says
|
|
759
|
-
`this client has no transport (built from a schema dump)` instead of quietly
|
|
760
|
-
running on `GraphWeaver.client`.
|
|
761
|
-
- `$transport` is a usable GraphQL variable name again — the generated body has
|
|
762
|
-
no such local.
|
|
763
|
-
|
|
764
|
-
#### One reset, one plurality rule (**breaking**)
|
|
765
|
-
|
|
766
|
-
- **`GraphWeaver.reset_scalars!`, `clear_scalars!`, `reset_enums!` and
|
|
767
|
-
`reset_type_helpers!` are gone.** `GraphWeaver.reset_registrations!` is the
|
|
768
|
-
clean slate between tests; to reset one registry, call the same name on
|
|
769
|
-
`GraphWeaver::Codegen`.
|
|
770
|
-
- **Every directory setting is a list — `queries_paths`, `generated_paths`,
|
|
771
|
-
`fragments_paths`** — and *every entry is read*, by `generate!`,
|
|
772
|
-
`verify_generated!`, `check_queries` and `load_queries!` alike. (0.4.x
|
|
773
|
-
dropped a plural `queries_paths` because only `load_queries!` walked it;
|
|
774
|
-
that divergence is what the singular was protecting against, and it is gone.)
|
|
775
|
-
**Rename any `queries_path` / `generated_path` / `fragments_path` you set or
|
|
776
|
-
read** — assigning a String still works (`GraphWeaver.queries_paths =
|
|
777
|
-
"app/graphql/queries"`), and `generated_paths.first` is the one directory
|
|
778
|
-
`generate!` writes into. `schema_path` stays singular: a run reads one
|
|
779
|
-
schema, so a second entry would name a dump nothing opens.
|
|
780
|
-
|
|
781
|
-
- `rake graph_weaver:queries:check` prints an unparseable query's position once
|
|
782
|
-
rather than twice, and `GraphWeaver.check_queries` returns the documented
|
|
783
|
-
`"message"` / `"line"` / `"column"` shape for parse errors too — the position
|
|
784
|
-
is no longer folded into the message.
|
|
785
|
-
- Docs: a spec-local `generated_paths` entry needs an explicit
|
|
786
|
-
`GraphWeaver.load_generated!` (in Rails the Railtie has already run by then),
|
|
787
|
-
and belongs outside `spec/support/`, whose files rspec-rails requires itself
|
|
788
|
-
in sorted order. A registration naming one of your own constants goes in a
|
|
789
|
-
`to_prepare` block — the same rule the in-process client already follows —
|
|
790
|
-
rather than the `require Rails.root.join(...)` dance.
|
|
791
|
-
- **Removing an `extend_type` registration no longer bricks the app.**
|
|
792
|
-
Generated files carry `include GraphWeaver::TypeHelpers::Foo`, so dropping
|
|
793
|
-
the registration made boot fail — and because `rake graph_weaver:generate`
|
|
794
|
-
depends on `:environment`, the regeneration that would repair it failed the
|
|
795
|
-
same way. The graph_weaver tasks now skip loading generated modules (none of
|
|
796
|
-
them reads one), so `rake graph_weaver:generate` repairs the tree. Outside a
|
|
797
|
-
task, the dangling include now raises a `GraphWeaver::Error` naming the
|
|
798
|
-
registration that went missing and how to recover, instead of a bare
|
|
799
|
-
`NameError` pointing into generated code.
|
|
800
|
-
- **A cancelled request no longer leaks its socket.** `Transport::HTTP`
|
|
801
|
-
closed a connection of unknown state with a bare `rescue`, which catches
|
|
802
|
-
only `StandardError`. A fiber scheduler cancels with `Async::Stop`, which
|
|
803
|
-
descends from `Exception`, so cancelling an in-flight request walked past
|
|
804
|
-
the cleanup and left the socket open until GC. Affects any app under
|
|
805
|
-
`async`/Falcon with per-request timeouts. Nothing to do — the fix is
|
|
806
|
-
internal.
|
|
807
|
-
- `rake graph_weaver:queries:check` and `federation:diff` flush stdout before
|
|
808
|
-
aborting, so a piped CI log shows the details before the verdict rather
|
|
809
|
-
than after it.
|
|
810
|
-
- `generate!`, `verify_generated!` and `check_queries` accept a **path or SDL
|
|
811
|
-
string** for `schema:`, like every other schema slot in the library. A String
|
|
812
|
-
used to reach `schema.validate` as itself and die with `undefined method
|
|
813
|
-
'validate' for an instance of String`.
|
|
814
|
-
|
|
815
|
-
- **`#parse` on anything that holds a schema**, not just `Client`:
|
|
816
|
-
`GraphWeaver::InProcess`, `Testing::FakeClient` and `Testing::Router` gain it
|
|
817
|
-
(and `#load_queries!`) from the new `GraphWeaver::Parsing` mixin. Replace
|
|
818
|
-
`GraphWeaver.parse(schema: router.schema, client: router, query: q)` with
|
|
819
|
-
`router.parse(q)`. `Retry` holds no schema and has no `#parse` — parse from
|
|
820
|
-
what it wraps. Nothing changes for `Client`, and the client contract is
|
|
821
|
-
untouched: a bare `GraphQL::Schema` class still fills the client slot.
|
|
822
|
-
- **`graphql: false` (or `graphql: :none`) opts an example out of
|
|
823
|
-
`config.default_mode`** — no client is installed, so the example can wire its
|
|
824
|
-
own. Previously a default swept up every untagged example with no way out,
|
|
825
|
-
and both spellings raised "is not a mode"; that message now names the opt-out
|
|
826
|
-
alongside the modes.
|
|
827
|
-
- Docs: `graphql_context` is setup, so `docs/testing.md` now shows it in a
|
|
828
|
-
`before` block for a group sharing one identity, with the inline form kept
|
|
829
|
-
for one-offs. The per-example reset runs ahead of group hooks, so a
|
|
830
|
-
group-level `before` re-applies from the same baseline every time.
|
|
831
|
-
|
|
832
|
-
- `examples/federation.rb` — a runnable federated-testing example, and the first
|
|
833
|
-
one that needs no network: three real subgraphs, a boundary-crossing query
|
|
834
|
-
through a generated module, `router.trace`, and a plan-time refusal. Guarded
|
|
835
|
-
by `spec/examples_spec.rb`, which runs it.
|
|
836
|
-
- Docs: the local router moved from `docs/testing.md` to `docs/federation.md`.
|
|
837
|
-
`testing.md` now covers which client an example runs against; `federation.md`
|
|
838
|
-
covers what a federated graph does. No behaviour change — update any bookmark
|
|
839
|
-
to `docs/testing.md#the-in-process-router--graphql-router`, now
|
|
840
|
-
`docs/federation.md#the-local-router`.
|
|
841
|
-
- **`graphql: :in_process` no longer hunts for the live schema class.** It runs
|
|
842
|
-
against `GraphWeaver::Testing.config.schema`, or the schema class your client
|
|
843
|
-
already runs in-process — one sentence, no heuristic. The third fallback
|
|
844
|
-
(`Testing::LiveSchema`, which searched every loaded `GraphQL::Schema` for one
|
|
845
|
-
defining everything the reference schema declares) is **deleted**. It only
|
|
846
|
-
ever applied to an app whose client points at a *different* API, and under
|
|
847
|
-
Zeitwerk it failed on the first `:in_process` example anyway, since an
|
|
848
|
-
autoloaded schema isn't loaded until something names it. Such an app now sets
|
|
849
|
-
`config.schema = MySchema`; when neither source is there, the error says so.
|
|
850
|
-
- **One positive predicate on `Response`, not two.** `#ok?` is gone; `#success?`
|
|
851
|
-
is the survivor, so the pair is `errors?` / `success?`. `success?` is what
|
|
852
|
-
`Process::Status` and `Faraday::Response` call it, and `ok?` reads as HTTP
|
|
853
|
-
200 — which a GraphQL response carrying errors also is. Semantics unchanged:
|
|
854
|
-
partial data alongside top-level errors is **not** a success.
|
|
855
|
-
- **`Testing::Config#auto_fake` is gone** — it was the pre-tag spelling of
|
|
856
|
-
`config.default_mode = :fake`. Use that.
|
|
857
|
-
- **`rake graph_weaver:federation:diff` no longer loads the test harness.**
|
|
858
|
-
`Federation::Drift` needed one thing from it — the list of loaded schema
|
|
859
|
-
classes — and did `require "graph_weaver/testing"` from inside itself to get
|
|
860
|
-
it, pulling faker into a task that fabricates nothing. That question, and
|
|
861
|
-
"does this schema define this coordinate", now live in `GraphWeaver::Schemas`,
|
|
862
|
-
shared with `Testing::Subgraphs`. Measured over the fixture supergraph, the
|
|
863
|
-
task loads 15 files instead of 323 (253 of them faker's).
|
|
864
|
-
`Testing::Subgraphs.loaded` moved with it: call `GraphWeaver::Schemas.loaded`.
|
|
865
|
-
- Internal: `codegen/enum_type.rb` held `extend_type`, the type-helper and alias
|
|
866
|
-
registries and `GraphWeaver::TypeHelpers` — none of them enums, so a search
|
|
867
|
-
for `extend_type` landed in a file named for something else. Those moved to
|
|
868
|
-
`codegen/type_helpers.rb`; `enum_type.rb` now holds `EnumType` and the enum
|
|
869
|
-
registry, mirroring `scalar_type.rb`. No API change.
|
|
870
|
-
- **`register_enum` and `extend_type` say where to register** when handed a
|
|
871
|
-
constant's *name* instead of the constant. Passing a String is the natural
|
|
872
|
-
workaround for "`uninitialized constant PetKind` in my initializer", and the
|
|
873
|
-
answer is Rails' own: autoloading is set up after `config/initializers` run,
|
|
874
|
-
so register from a `Rails.application.config.to_prepare` block — which
|
|
875
|
-
`rake graph_weaver:generate` also runs before generating. Both registries
|
|
876
|
-
still take the constant itself; a name would be a second spelling that
|
|
877
|
-
couldn't reach `fallback:` or `map:` anyway, since those name enum *members*.
|
|
878
|
-
|
|
879
|
-
#### Scalar coercion is one switch (**breaking**)
|
|
880
|
-
|
|
881
|
-
`coerce:` takes `true`/`false` only — the Symbol form is gone. It was a third
|
|
882
|
-
way to ask one question (*may a variable of this scalar accept loose input?*)
|
|
883
|
-
and the only one that also made you answer *how*, which the scalar already
|
|
884
|
-
knows: `Int`/`Float` convert, anything with a `cast:`/`serialize:` pair parses,
|
|
885
|
-
and a pass-through scalar can't.
|
|
886
|
-
|
|
887
|
-
**What to do:**
|
|
888
|
-
|
|
889
|
-
- `coerce: :to_i` / `coerce: :to_f` — write `coerce: true`. Generated output is
|
|
890
|
-
unchanged.
|
|
891
|
-
- `coerce: :to_s` on `String`/`ID` — drop it and call `.to_s` at the call site.
|
|
892
|
-
Those have nothing to convert from, so `coerce: true` on one raises now
|
|
893
|
-
instead of emitting a no-op.
|
|
894
|
-
- any other Symbol on a custom scalar — give the scalar a `cast:`/`serialize:`
|
|
895
|
-
pair and `coerce: true`.
|
|
896
|
-
|
|
897
|
-
`GraphWeaver.auto_coerce` is unaffected, and `coerce: true` is now exactly what
|
|
898
|
-
it turns on for one scalar instead of all of them.
|
|
899
|
-
|
|
900
|
-
#### One shared module, not three (**breaking** — regenerate)
|
|
901
|
-
|
|
902
|
-
`GraphQLInputs`, `GraphQLUnions` and `GraphQLEnums` are now one `GraphQLTypes`.
|
|
903
|
-
They were three constants, three config knobs and three file shapes for one
|
|
904
|
-
idea — a type shared across query modules — and the rule now fits in a
|
|
905
|
-
sentence: **a type shared across query modules lives in the shared module and is
|
|
906
|
-
aliased in.**
|
|
907
|
-
|
|
908
|
-
**What to do:** run `rake graph_weaver:generate` (`verify` fails until you do,
|
|
909
|
-
naming the stale files). Every generated file changes: a query module opens with
|
|
910
|
-
one `require_relative "types"` instead of up to three, and its aliases read
|
|
911
|
-
`AdoptionInput = GraphQLTypes::AdoptionInput`. On disk, `enums.rb`, `inputs.rb`,
|
|
912
|
-
`inputs/` and `unions.rb` become `types.rb` (the manifest) plus one file per
|
|
913
|
-
type under `types/` — the old files are pruned for you, since pruning keys off
|
|
914
|
-
the generated header. If you referenced `GraphQLInputs::PetFilter` (or the other
|
|
915
|
-
two) by hand, spell it `GraphQLTypes::PetFilter`.
|
|
916
|
-
|
|
917
|
-
`GraphWeaver.inputs_module=` / `unions_module=` / `enums_module=` are now
|
|
918
|
-
`GraphWeaver.types_module=`, and `generate!`/`verify_generated!` take one
|
|
919
|
-
`types_module:` in place of three.
|
|
920
|
-
|
|
921
|
-
One namespace also removes the aliasing *between* the shared artifacts: an input
|
|
922
|
-
struct's props and a union member's selections spell their enums bare now, being
|
|
923
|
-
lexically inside the same module. The manifest requires the enum files first for
|
|
924
|
-
that reason.
|
|
925
|
-
|
|
926
|
-
New: a shared fragment whose name is already a schema type in that module is
|
|
927
|
-
refused at generation, naming both — a fragment is named by you, a type by the
|
|
928
|
-
schema, and one module is one namespace. Previously they lived apart and could
|
|
929
|
-
never meet.
|
|
930
|
-
|
|
931
|
-
#### Has anyone changed a subgraph without recomposing?
|
|
932
|
-
|
|
933
|
-
rake graph_weaver:federation:diff SUPERGRAPH=supergraph.graphql
|
|
934
|
-
|
|
935
|
-
A committed supergraph is a snapshot of a composition, and nothing checked that
|
|
936
|
-
it still described your subgraphs — so it could quietly promise a graph that no
|
|
937
|
-
longer exists. This reads the routing table against the subgraph schemas loaded
|
|
938
|
-
in this process, needs **no network**, and exits non-zero on drift, so it gates
|
|
939
|
-
a PR alongside `graph_weaver:verify`.
|
|
940
|
-
|
|
941
|
-
Both directions, because they mean opposite things: **stale** (the supergraph
|
|
942
|
-
carries `Product.weight`, nothing here defines it — recompose) and **not
|
|
943
|
-
composed in** (a schema here defines `Product.dimensions`, the supergraph
|
|
944
|
-
doesn't carry it — publish the subgraph). Comparison is deliberately looser
|
|
945
|
-
than field-set equality, which would be wrong both ways: a subgraph carries
|
|
946
|
-
federation plumbing no supergraph has, and `@external`/`@shareable` put a field
|
|
947
|
-
in more than one subgraph.
|
|
948
|
-
|
|
949
|
-
A supergraph is routinely only partly local, so the report names three states —
|
|
950
|
-
checked, not here, and answered with fabricated data — and the headline counts
|
|
951
|
-
them. Only drift fails; absence is a supported setup.
|
|
952
|
-
`GraphWeaver::Federation::Drift` is the same thing as data (`#to_h`, `#drift?`).
|
|
953
|
-
|
|
954
|
-
#### Validation errors name the subgraph behind the type
|
|
955
|
-
|
|
956
|
-
When the schema dump is a composed supergraph, `rake graph_weaver:queries:check`
|
|
957
|
-
brands each error with who resolves the type it points at:
|
|
958
|
-
|
|
959
|
-
app/graphql/queries/product.graphql
|
|
960
|
-
4:5 Field 'dimensions' doesn't exist on type 'Product' (products, reviews)
|
|
961
|
-
|
|
962
|
-
`Product.dimensions` says what broke; `(products, reviews)` says whose code to
|
|
963
|
-
look at. A plain schema has no routing table and is unaffected.
|
|
964
|
-
|
|
965
|
-
- New: `SchemaLoader::RoutingTable#declared_fields`, `#declares?`, `#responsible`.
|
|
966
|
-
|
|
967
|
-
#### A partly-local supergraph now works
|
|
968
|
-
|
|
969
|
-
The testing router serves a supergraph composed from several services when only
|
|
970
|
-
some of them run in this process. A subgraph no loaded schema defines is
|
|
971
|
-
**absent** rather than an error at construction, so the router builds and every
|
|
972
|
-
query that doesn't reach those fields runs normally. A query that does reach
|
|
973
|
-
them is refused at plan time, before anything executes, naming the subgraph and
|
|
974
|
-
the field that reached for it.
|
|
975
|
-
|
|
976
|
-
subgraphs: { "reviews" => :fake } # answer an absent subgraph with fabricated data
|
|
977
|
-
|
|
978
|
-
Faking is opt-in and never silent: a faked fetch is marked `faked: true` in
|
|
979
|
-
`router.trace`, logged at `:warn` per fetch, and listed by `router.faked` and
|
|
980
|
-
`#inspect`. It is deliberately not surfaced as a response error — that would
|
|
981
|
-
make `execute!` raise, defeating the point.
|
|
982
|
-
|
|
983
|
-
- `Testing::Subgraphs.resolve` now returns only the subgraphs this process
|
|
984
|
-
serves instead of raising when one has no candidate. Two candidates still
|
|
985
|
-
refuse, naming both.
|
|
986
|
-
|
|
987
|
-
#### One tag picks what a test runs against
|
|
988
|
-
|
|
989
|
-
`auto_fake` and `config.router` each installed a client for **every** example
|
|
990
|
-
and refused to coexist, so a suite had to choose fakes or real resolvers once,
|
|
991
|
-
for everything — and running in-process against a live schema had no configured
|
|
992
|
-
mode at all. Now an rspec tag says it per example, or per group:
|
|
993
|
-
|
|
994
|
-
it "renders the empty state", graphql: :fake do … end
|
|
995
|
-
it "authorizes drafts", graphql: :in_process do … end
|
|
996
|
-
describe "checkout", graphql: :router do … end
|
|
997
|
-
|
|
998
|
-
`rspec --tag graphql:router` runs one mode's examples. The tag is namespaced
|
|
999
|
-
under one `graphql:` key on purpose: a bare `:fake` or `:router` would collide
|
|
1000
|
-
with an app's own metadata and silently change which client an unrelated
|
|
1001
|
-
example runs against.
|
|
1002
|
-
|
|
1003
|
-
**Nothing needs configuring.** Each mode derives what it runs against and
|
|
1004
|
-
refuses — naming what it looked for — rather than guessing. The schema is
|
|
1005
|
-
`config.schema` if you set one, else the committed dump, else the schema
|
|
1006
|
-
`GraphWeaver.client` talks to. `:in_process` finds the live schema *class*: the
|
|
1007
|
-
one your client already runs in-process, else the loaded class defining
|
|
1008
|
-
everything that schema declares (the rule `subgraphs:` detection already uses).
|
|
1009
|
-
`:router` plans against the dump when the dump is a composed supergraph — a
|
|
1010
|
-
federated suite whose checked-in dump is the supergraph needs no config at all.
|
|
1011
|
-
|
|
1012
|
-
- **New:** `graphql_context(current_user: user)` sets the context your
|
|
1013
|
-
resolvers see. It merges onto `config.context` and is reset before the next
|
|
1014
|
-
example, so an example running as somebody else can't leak into the one
|
|
1015
|
-
after. Pass a block to scope it. Under `graphql: :fake` it refuses — there
|
|
1016
|
-
are no resolvers to receive it.
|
|
1017
|
-
- **New:** `config.default_mode` is what an untagged example runs against
|
|
1018
|
-
(`nil`, the default, leaves `GraphWeaver.client` alone). It replaces
|
|
1019
|
-
`config.auto_fake`, which still works as the old spelling of
|
|
1020
|
-
`default_mode = :fake`.
|
|
1021
|
-
- **New:** `config.context` — the baseline every `:in_process` and `:router`
|
|
1022
|
-
example starts from. `config.router = { context: … }` now refuses and points
|
|
1023
|
-
here; the per-example reset would have overwritten it.
|
|
1024
|
-
- `GraphWeaver.execute`, `.new` and `Client.new` now refuse a *client* where a
|
|
1025
|
-
schema source belongs — an `InProcess`, `Retry`, transport or fake used to
|
|
1026
|
-
crash with `undefined method 'lstrip'`. The message names both ways to say
|
|
1027
|
-
what you meant.
|
|
1028
|
-
|
|
1029
|
-
#### Ruby-keyword field names now generate
|
|
1030
|
-
|
|
1031
|
-
A result key that underscores to a Ruby keyword — `pageInfo { next }`,
|
|
1032
|
-
`filter { in }` — no longer refuses to generate. A prop is only ever read off
|
|
1033
|
-
a receiver, so `const :next` is fine; the one bare read, an `alias:`
|
|
1034
|
-
delegator's first hop, now spells `self.next`. Output props keep only the ban
|
|
1035
|
-
the input side already had: names every `T::Struct` already answers to
|
|
1036
|
-
(`class`, `hash`, `serialize`). If you aliased a query around this, you can
|
|
1037
|
-
drop the alias and regenerate. `GraphWeaver::Codegen::RESERVED_PROPS` is gone
|
|
1038
|
-
— `STRUCT_METHODS` is the whole rule now.
|
|
1039
|
-
|
|
1040
|
-
- `optional: true` on an `alias:` no longer hides a path segment the schema has
|
|
1041
|
-
no field for. It still skips a field this query didn't select — that is what
|
|
1042
|
-
it is for — but a typo, or the classic `findPets` where the path is the Ruby
|
|
1043
|
-
prop chain, now raises and says which of the two it looks like. If an
|
|
1044
|
-
optional alias resolved only through a query-level rename (`{ renamed: meta }`),
|
|
1045
|
-
it will now raise on queries that don't select that key.
|
|
1046
|
-
- **New:** `GraphWeaver.reset_enums!`, `GraphWeaver.reset_type_helpers!` and
|
|
1047
|
-
`GraphWeaver.reset_registrations!` — the registry resets scalars already had.
|
|
1048
|
-
`reset_registrations!` is the clean slate to reach for between tests.
|
|
1049
|
-
- An alias error no longer names the same type twice when a query module and
|
|
1050
|
-
its root type share a name.
|
|
1051
|
-
|
|
1052
|
-
#### Testing::Router now plans a real query, not just a single-subgraph one
|
|
1053
|
-
|
|
1054
|
-
`GraphWeaver::Testing::Router` used to hand one operation to one subgraph
|
|
1055
|
-
verbatim and refuse anything that crossed a boundary. It now splits at the
|
|
1056
|
-
crossing, refetches the entity from its `@key` through
|
|
1057
|
-
`_entities(representations:)`, and stitches — batching every node at a level
|
|
1058
|
-
into one call, running root query fields that span subgraphs as one fetch
|
|
1059
|
-
each, and fetching a `@requires` field set from the subgraph that holds it
|
|
1060
|
-
before the field that needs it. On the demo corpus that moves 10/17 queries
|
|
1061
|
-
plannable to 17/17. It still refuses, at plan time, every shape it can't
|
|
1062
|
-
answer the way a real router would.
|
|
1063
|
-
|
|
1064
|
-
`subgraphs:` is now **optional**: each subgraph's Ruby schema is derived from
|
|
1065
|
-
what the loaded schemas define, and refuses rather than guesses when two
|
|
1066
|
-
match or none do. An explicit map (or a partial one) still wins, and is now
|
|
1067
|
-
checked the same way — a mis-wired entry fails at construction naming what it
|
|
1068
|
-
doesn't define, instead of surfacing three fetches later.
|
|
1069
|
-
|
|
1070
|
-
- **New:** `config.router = { supergraph: "supergraph.graphql" }` in
|
|
1071
|
-
`graph_weaver/rspec` runs every example against your real subgraph
|
|
1072
|
-
resolvers.
|
|
1073
|
-
- **New:** `rake graph_weaver:federation:subgraphs` prints the subgraph map
|
|
1074
|
-
detection sees, with the evidence for each match.
|
|
1075
|
-
- **New:** `Testing::Router#context` is settable, so one example can run as a
|
|
1076
|
-
different user without rebuilding the router.
|
|
1077
|
-
- `Testing::Unplannable`'s `:requires` category is **gone** — the gap it named
|
|
1078
|
-
is closed. `:root_fields_span` now applies only to mutations (query roots
|
|
1079
|
-
are planned). New categories: `:no_key`, `:abstract_boundary`,
|
|
1080
|
-
`:nested_field_set`, `:shadowed_key`.
|
|
1081
|
-
- The coverage report's second line now names every subgraph a query touches
|
|
1082
|
-
(`accounts+reviews`), not just the one it ran in.
|
|
1083
|
-
- `rake graph_weaver:schema:diff`, `schema:refresh` and `cassettes:anonymize`
|
|
1084
|
-
now load the Rails environment first, so an initializer's settings apply.
|
|
1085
|
-
|
|
1086
|
-
**`GraphWeaver::Testing::Router` — a local federation router for tests.** Give
|
|
1087
|
-
it a supergraph and your subgraph schema classes and it satisfies the client
|
|
1088
|
-
slot, so `GraphWeaver.client = router` runs every generated module against real
|
|
1089
|
-
resolvers in-process: no gateway, no node, no sockets. It plans one shape — a
|
|
1090
|
-
query whose every field resolves in a single subgraph, passed to that subgraph
|
|
1091
|
-
verbatim — and raises `Unplannable` (a `GraphWeaver::Error`) for anything that
|
|
1092
|
-
crosses a boundary, at plan time, before any subgraph runs. See
|
|
1093
|
-
[docs/testing.md](docs/testing.md#a-local-federation-router).
|
|
1094
|
-
|
|
1095
|
-
**`rake graph_weaver:federation:coverage SUPERGRAPH=…` says how much of your
|
|
1096
|
-
query set that router can plan**, and groups every refusal by what stopped it —
|
|
1097
|
-
the number that decides whether wiring it up is worth it. Planning needs the
|
|
1098
|
-
supergraph alone, so it runs in CI with no subgraph loadable.
|
|
1099
|
-
|
|
1100
|
-
**A supergraph's routing table is now readable:
|
|
1101
|
-
`GraphWeaver::SchemaLoader.routing_table(supergraph)`.** `load` strips the
|
|
1102
|
-
`@join__*` machinery to get the API schema; this keeps it — `owners("Product",
|
|
1103
|
-
"shippingEstimate") # => ["reviews"]`, each type's `@key` field sets, and which
|
|
1104
|
-
copies are `@external`. A `@join__` directive it hasn't been taught lands in
|
|
1105
|
-
`unsupported` rather than being skipped.
|
|
1106
|
-
|
|
1107
|
-
**`Representations.<entity>` for an entity the query didn't select now says
|
|
1108
|
-
what to do.** Builders are query-driven, so `Representations.warehouse(...)`
|
|
1109
|
-
raised a bare `NoMethodError` naming nothing. It now names the builders this
|
|
1110
|
-
query does have and the selection to add (`... on Warehouse { __typename }`).
|
|
1111
|
-
|
|
1112
|
-
**Shared-fragment directories are scanned recursively, and `.gql` files count.**
|
|
1113
|
-
The scan was `fragments/*.graphql`, so `fragments/person/fields.graphql` — how
|
|
1114
|
-
anyone with sixty fragments organizes them — was skipped in silence, and a
|
|
1115
|
-
`.gql` file was ignored even though `parse("x.gql")` reads one. A duplicate
|
|
1116
|
-
fragment name now names both files that define it.
|
|
1117
|
-
|
|
1118
|
-
**Query directories are scanned the same way — recursively, `.gql` included.**
|
|
1119
|
-
`queries/admin/pets.graphql` produced nothing at all: no file, no error.
|
|
1120
|
-
`generate!`, `check_queries` and `client.load_queries!` now walk the tree, and
|
|
1121
|
-
`.gql` no longer leaks its extension into the module name. Directories organize
|
|
1122
|
-
queries but do not namespace them — `queries/admin/pets.graphql` is still
|
|
1123
|
-
`PetsQuery` in `pets_query.rb` — so two files with the same base name are
|
|
1124
|
-
refused at generation, naming both, rather than one silently overwriting the
|
|
1125
|
-
other's generated file. The scaffolded `graphql.config.yml` matches
|
|
1126
|
-
(`**/*.{graphql,gql}`).
|
|
1127
|
-
|
|
1128
|
-
**`execute` now takes one kwarg per declared variable, always — a single
|
|
1129
|
-
required input-object variable is no longer flattened into per-field kwargs.**
|
|
1130
|
-
`mutation($input: AdoptionInput!)` generated `execute!(name:, species:, …)`,
|
|
1131
|
-
but adding any second variable generated `execute!(input:, …)` instead — so an
|
|
1132
|
-
unrelated edit to a query silently reshaped every call site, and the rule
|
|
1133
|
-
couldn't be stated without its exception. It also made a schema's own field
|
|
1134
|
-
names load-bearing: a field named `client` or `in` can't be a kwarg and can't
|
|
1135
|
-
be renamed, so flattening quietly declined and the surface moved again.
|
|
1136
|
-
**Rewrite affected call sites to pass the input as one kwarg:**
|
|
1137
|
-
`AdoptMutation.execute!(input: { name: "Rex", species: "DOG" })`, or
|
|
1138
|
-
`input: AdoptMutation::AdoptionInput.new(name: "Rex", species: Species::Dog)`
|
|
1139
|
-
for the field-by-field static check.
|
|
1140
|
-
|
|
1141
|
-
**An input field named after a Ruby keyword no longer makes a schema
|
|
1142
|
-
ungeneratable.** `StringQueryOperatorInput.in` — the standard Hasura/Gatsby
|
|
1143
|
-
filter shape — raised "would become prop 'in', which collides with a Ruby
|
|
1144
|
-
keyword", with no way out: an input field is the schema's name, not yours, and
|
|
1145
|
-
`extend_type alias:` is output-only. But `prop :in` is legal Ruby, and nothing
|
|
1146
|
-
reads an input prop bare (`serialize` goes through `public_send`), so the
|
|
1147
|
-
refusal was over-broad. Input fields named `in`, `end`, `def`, `nil` and the
|
|
1148
|
-
rest now generate. A field colliding with a method every struct defines
|
|
1149
|
-
(`serialize`, `to_h`, `class`, `hash`) is still refused — those break at
|
|
1150
|
-
require time. Output structs are unchanged: a result key *can* be renamed, in
|
|
1151
|
-
the query.
|
|
1152
|
-
|
|
1153
|
-
**A variable named `$client` no longer generates a file that won't parse.**
|
|
1154
|
-
`query($client: ID!)` emitted `def self.execute(client = nil, client:)` — a
|
|
1155
|
-
`SyntaxError` raised at app boot from `load_generated!`, arbitrarily far from
|
|
1156
|
-
the query that caused it, while `verify_generated!` reported the tree as
|
|
1157
|
-
current. Generation now refuses `$client`, `$variables` and `$transport` — the
|
|
1158
|
-
three locals the generated `execute` body owns — naming the fix. **Rename such
|
|
1159
|
-
a variable in the query (`query($clientId: ID!)`) before regenerating.**
|
|
1160
|
-
|
|
1161
|
-
**`auto_coerce` no longer erases the typing of String/ID variables.** It mapped
|
|
1162
|
-
both to `#to_s`, which widened their kwargs to `T.anything` — the majority of
|
|
1163
|
-
real variables, statically unchecked, in exchange for a cast that can't fail.
|
|
1164
|
-
`auto_coerce` now covers only the conversions that are conversions (`Int`→`to_i`,
|
|
1165
|
-
`Float`→`to_f`) plus scalars with a full cast/serialize pair. **If you relied on
|
|
1166
|
-
a String/ID kwarg accepting anything, opt in per scalar:**
|
|
1167
|
-
`GraphWeaver.register_scalar("ID", String, coerce: :to_s)`.
|
|
1168
|
-
|
|
1169
|
-
**An anonymous operation is now named after its module — in the query text and
|
|
1170
|
-
in `OPERATION_NAME`.** Requests started carrying `operationName` so servers and
|
|
1171
|
-
APMs can attribute traffic, but the constant was only set when the `.graphql`
|
|
1172
|
-
document named its operation — and anonymous is what the docs show, so every
|
|
1173
|
-
trace arrived `anonymous` and the feature did nothing for the documented happy
|
|
1174
|
-
path. `person.graphql` holding `query($id: ID!) { ... }` now emits
|
|
1175
|
-
`query PersonQuery($id: ID!) { ... }` with `OPERATION_NAME = "PersonQuery"`.
|
|
1176
|
-
Both halves move together: a server rejects an `operationName` its document
|
|
1177
|
-
doesn't declare. A document that names its own operation is left untouched.
|
|
1178
|
-
|
|
1179
|
-
**Cassette files no longer store the request twice — re-record them.** Every
|
|
1180
|
-
entry carried a `key:` (the normalized query + variables) *and* a `query:` and
|
|
1181
|
-
`variables:` again, and replay matched on `key:` alone: editing the half a
|
|
1182
|
-
reviewer reads changed nothing, editing the other half broke replay while the
|
|
1183
|
-
file still looked right. The key is now derived from `query`/`variables`/
|
|
1184
|
-
`operationName` at load, so the file holds the request once and diffs are real.
|
|
1185
|
-
**Existing cassettes must be re-recorded** (`GRAPHWEAVER_RECORD=1`, or delete
|
|
1186
|
-
the file) — this also covers cassettes of anonymous operations, which stopped
|
|
1187
|
-
matching when entries started keying on `operationName`.
|
|
1188
|
-
|
|
1189
|
-
**`MissingRecording` now prints the variables — the part that usually differs.**
|
|
1190
|
-
It printed the whole query and omitted the variables entirely, so the common
|
|
1191
|
-
miss (same query, different variables) showed you 60 lines identical to the
|
|
1192
|
-
YAML and nothing about the mismatch. The message now leads with the request's
|
|
1193
|
-
variables, says what was recorded for that query (`1 entry recorded for this
|
|
1194
|
-
query, with variables {"id" => "1"}`), and prints the query as one truncated
|
|
1195
|
-
line.
|
|
1196
|
-
|
|
1197
|
-
**A first run with no cassette and no `client:` no longer raises
|
|
1198
|
-
`MissingRecording`.** There is no request yet, so it raises `GraphWeaver::Error`
|
|
1199
|
-
naming the actual situation. **Rescue `GraphWeaver::Error` if you were catching
|
|
1200
|
-
`MissingRecording` for this case.**
|
|
1201
|
-
|
|
1202
|
-
**`Cassette.use` is now `GraphWeaver::Testing.cassette` — rename your calls.**
|
|
1203
|
-
It never returned a `Cassette`; it returns a *client* (a recorder or a replayer)
|
|
1204
|
-
to hand to `execute`, and the name said otherwise. `Cassette` is now only the
|
|
1205
|
-
file — `.new`, `#size`, `#anonymize!`.
|
|
1206
|
-
|
|
1207
|
-
**Record mode with no `client:` now raises instead of replaying.**
|
|
1208
|
-
`GRAPHWEAVER_RECORD=1` on a `Testing.cassette(name)` call with nothing to record
|
|
1209
|
-
against quietly served the stale recording, so "re-record everything" produced a
|
|
1210
|
-
half-refreshed cassette set with no signal. **Pass `client:` to every call you
|
|
1211
|
-
want re-recorded.**
|
|
1212
|
-
|
|
1213
|
-
**`Recorder.new(..., anonymize:)` is gone.** It was unreachable through the
|
|
1214
|
-
factory and duplicated `Testing.config.anonymize`. **Set the config flag** —
|
|
1215
|
-
that's the one way to anonymize, with `rake graph_weaver:cassettes:anonymize`
|
|
1216
|
-
as the cleanup tool for cassettes recorded before you turned it on.
|
|
1217
|
-
|
|
1218
|
-
**`FakeClient.new` no longer requires `schema:`.** Every other option fell back
|
|
1219
|
-
to `Testing.config`; this one didn't, even though `config.schema` already
|
|
1220
|
-
auto-locates the committed dump. `FakeClient.new` now works on its own, and
|
|
1221
|
-
says what to set when no schema resolves at all.
|
|
1222
|
-
|
|
1223
|
-
**`GraphWeaver.queries_paths` (plural) is gone — use `queries_path`.**
|
|
1224
|
-
`generate!` and `check_queries` read the singular (the first entry) while
|
|
1225
|
-
`load_queries!` walked the whole list, so a second queries directory produced
|
|
1226
|
-
modules at runtime that `rake graph_weaver:generate` never generated and
|
|
1227
|
-
`verify` never checked — silently. Queries are single-schema by design. **If
|
|
1228
|
-
you appended a second queries directory, fold it into the first** (or run a
|
|
1229
|
-
second `generate!` with its own `queries:`). `generated_paths` and
|
|
1230
|
-
`fragments_paths` stay plural; they genuinely load from several places.
|
|
1231
|
-
|
|
1232
|
-
**One GraphQL enum is now one Ruby type.** A schema enum a query touches — as
|
|
1233
|
-
a variable, in a result, or both — is emitted once per schema into
|
|
1234
|
-
`generated/enums.rb` as `GraphQLEnums::<Enum>`, and every query module aliases
|
|
1235
|
-
it. Before, an enum read out of a result got a class named for the response key
|
|
1236
|
-
and nested in the struct that selected it (`SearchQuery::Result::Search::Pet::Species`),
|
|
1237
|
-
while the same enum used as a variable got a module-level one — so whether a
|
|
1238
|
-
schema enum was one Ruby type or three depended on what else the query happened
|
|
1239
|
-
to reference, and handing a value from one query into another's variable raised
|
|
1240
|
-
a `TypeError` that wasn't even a `GraphWeaver::Error`.
|
|
1241
|
-
|
|
1242
|
-
**Regenerate, and expect enum constants to move.** A nested enum path in app
|
|
1243
|
-
code becomes the query module's own alias — `SearchQuery::Species` — or
|
|
1244
|
-
`GraphQLEnums::Species`; `srb tc` finds them all. The enums a shared fragment's
|
|
1245
|
-
union members select are hoisted too, so `unions.rb` now aliases them rather
|
|
1246
|
-
than re-emitting them.
|
|
1247
|
-
|
|
1248
|
-
**The shared module names no longer depend on your output directory.** They are
|
|
1249
|
-
`GraphQLInputs`, `GraphQLUnions` and `GraphQLEnums`, full stop. The old rule
|
|
1250
|
-
camelized the parent of `generated/` unless it was on a hardcoded blocklist, so
|
|
1251
|
-
`output: "gen2"` gave you `Gen2Inputs` and renaming `app/graphql/generated` to
|
|
1252
|
-
`app/gql/generated` renamed a public constant. **A multi-schema layout must now
|
|
1253
|
-
name its modules explicitly** — `GraphWeaver.inputs_module=` /
|
|
1254
|
-
`unions_module=` / `enums_module=`, or `generate!(inputs_module:, ...)` — in the
|
|
1255
|
-
same initializer that already gives each schema its paths. `GraphWeaver.inputs_module`
|
|
1256
|
-
and `unions_module` no longer take an output-path argument.
|
|
1257
|
-
|
|
1258
|
-
**One registration registry, not two.** `Client#register_scalar`,
|
|
1259
|
-
`#register_enum`, `#register_enums` and `#extend_type` are **deleted** — a
|
|
1260
|
-
client-scoped registration was invisible to `GraphWeaver.generate!` (the rake
|
|
1261
|
-
tasks have no client), so the console typed a field richly and the checked-in
|
|
1262
|
-
code silently generated `T.untyped`. **Move any `client.register_*` /
|
|
1263
|
-
`client.extend_type` call to the `GraphWeaver.` form** (an initializer, next to
|
|
1264
|
-
the rest of your config). The one thing client scoping bought — two servers
|
|
1265
|
-
disagreeing about a scalar — is what the per-field coordinate form is for:
|
|
1266
|
-
`GraphWeaver.register_scalar("User.birthday", Date)`.
|
|
1267
|
-
|
|
1268
|
-
Also gone with it: `GraphWeaver.register_enums` (bulk) — there was never a
|
|
1269
|
-
`register_scalars` to match it, so call `register_enum` per line — and
|
|
1270
|
-
`GraphWeaver.reject_positional_map!`, now folded into the one
|
|
1271
|
-
`Codegen.register_enum` that every door reaches (so all three doors give the
|
|
1272
|
-
same "the value map is a keyword" error instead of a bare arity complaint).
|
|
1273
|
-
`Codegen.parse` / `.generate` / `.generate_inputs` / `.generate_unions` no
|
|
1274
|
-
longer take `scalars:`/`enums:`/`types:`.
|
|
1275
|
-
|
|
1276
|
-
**`generate!` now takes a Client where it takes a schema** — `GraphWeaver.generate!(schema: api)`,
|
|
1277
|
-
`verify_generated!`, `check_queries` and `parse` all accept one, so the object
|
|
1278
|
-
you built in the console is the object the build step wants and no schema dump
|
|
1279
|
-
is needed. `client:` still means what it meant (a constant name to bake as
|
|
1280
|
-
`DEFAULT_CLIENT`) and still refuses a live object.
|
|
1281
|
-
**Rails integration fixes, found by running the gem in a real Rails app.**
|
|
1282
|
-
|
|
1283
|
-
- **Production boot no longer raises `uninitialized constant
|
|
1284
|
-
Generated::PersonQuery`.** The default `generated_path` is
|
|
1285
|
-
`app/graphql/generated`, which Zeitwerk claims as an autoload root, while
|
|
1286
|
-
the files there define top-level constants. Development (lazy) was fine and
|
|
1287
|
-
eager loading was not, so this only showed up in production or
|
|
1288
|
-
`rails zeitwerk:check`. The Railtie now hides the generated directory from
|
|
1289
|
-
the loader; nothing to configure.
|
|
1290
|
-
- **`rake graph_weaver:generate` runs your initializer again.** The tasks
|
|
1291
|
-
asked whether Rails' `:environment` task existed at *load* time, but Rails
|
|
1292
|
-
defines it after every Railtie's `rake_tasks` block, so the answer was
|
|
1293
|
-
always no. Generation and `verify` therefore ran without booting the app —
|
|
1294
|
-
silently dropping every `register_scalar` / `register_enum` / `extend_type`
|
|
1295
|
-
in `config/initializers`, and generating code that disagreed with the
|
|
1296
|
-
running app. **Regenerate**: if you register anything in an initializer,
|
|
1297
|
-
your committed generated files are wrong, and `rake graph_weaver:verify`
|
|
1298
|
-
will now say so.
|
|
1299
|
-
- `generate`, `verify` and `schema:diff` report a `GraphWeaver::Error` the
|
|
1300
|
-
way `schema:refresh` already did — the message, and a non-zero exit,
|
|
1301
|
-
instead of a rake backtrace through codegen.
|
|
1302
|
-
|
|
1303
|
-
**`rails g graph_weaver:install` takes any source `GraphWeaver.new` takes.**
|
|
1304
|
-
The source is one positional argument — an endpoint, a schema class or an
|
|
1305
|
-
existing dump all work the same way:
|
|
1306
|
-
|
|
1307
|
-
```sh
|
|
1308
|
-
rails g graph_weaver:install https://api.example.com/graphql
|
|
1309
|
-
rails g graph_weaver:install MyApp::Schema # in-process, no socket
|
|
1310
|
-
rails g graph_weaver:install db/schema.graphql # a dump you already have
|
|
1311
|
-
```
|
|
1312
|
-
|
|
1313
|
-
The initializer reflects the form chosen: a schema class is resolved in a
|
|
1314
|
-
`to_prepare` block (it is autoloaded, so an initializer can not read it, and a
|
|
1315
|
-
dev reload replaces the class object), and a dump you already have becomes
|
|
1316
|
-
`GraphWeaver.schema_path` rather than being copied. `--auth` and the
|
|
1317
|
-
introspection step are url-only; a source that can not use them, a constant
|
|
1318
|
-
that does not resolve, and a class that is not a schema are all refused
|
|
1319
|
-
before any file is written.
|
|
1320
|
-
|
|
1321
|
-
**Generated struct names now come from the query's own field names.** A struct
|
|
1322
|
-
is named for the response key that selects it — `stargazers` becomes
|
|
1323
|
-
`Stargazers`, `edges` becomes `Edges` — so its name is a function of its own
|
|
1324
|
-
position in the query and nothing else. Names came from GraphQL *type* names
|
|
1325
|
-
before, disambiguated by field name only on collision, which meant **a second
|
|
1326
|
-
selection of the same type renamed the first**: a silent break in checked-in
|
|
1327
|
-
code your app references. Deep queries could also collide outright and refuse
|
|
1328
|
-
to generate.
|
|
1329
|
-
|
|
1330
|
-
**Regenerate, and expect renames.** Nearly every nested struct changes name
|
|
1331
|
-
(`PersonQuery::Result::Person::Pet` becomes `...::Person::Pets`), and app code
|
|
1332
|
-
naming one won't typecheck until it's updated — `srb tc` finds them all. The
|
|
1333
|
-
payoff: adding, removing, or reordering an unrelated selection can never move
|
|
1334
|
-
a name again.
|
|
1335
|
-
|
|
1336
|
-
- The key is used verbatim, with no pluralization heuristic — a list field
|
|
1337
|
-
`pets` generates `Pets`. To pick a different name, alias the field in the
|
|
1338
|
-
query: `pet: pets { name }` generates `Pet` (and a `.pet` accessor).
|
|
1339
|
-
- Union and interface members keep their type-condition names (`... on Book`
|
|
1340
|
-
gives `Book`), inside a container named for the field; a union hoisted from
|
|
1341
|
-
a shared fragment is still named for the fragment.
|
|
1342
|
-
- Two ties that walk order used to settle now resolve on their own: fields
|
|
1343
|
-
sharing one collapsed union type take the first of their keys
|
|
1344
|
-
alphabetically, and a name that would shadow the struct it nests in
|
|
1345
|
-
(`pet { pet { ... } }`) takes a numeric suffix (`Pet2`).
|
|
1346
|
-
|
|
1347
|
-
**Requests now send `operationName`** — every graph_weaver request used to be
|
|
1348
|
-
anonymous in Apollo Studio, Hasura, and any APM that keys traces, rate limits
|
|
1349
|
-
and slow-query reports on it. Generated modules emit their operation name as
|
|
1350
|
-
`OPERATION_NAME` beside `QUERY` and send it on the wire; a raw query string
|
|
1351
|
-
handed to a transport falls back to the name in the document. In-process
|
|
1352
|
-
execution passes it to `Schema.execute(operation_name:)`, which also makes a
|
|
1353
|
-
multi-operation document selectable there.
|
|
1354
|
-
|
|
1355
|
-
To get the benefit, **name your operations** — `query Person($id: ID!)`, not
|
|
1356
|
-
`query($id: ID!)` — and regenerate. An anonymous operation still works and
|
|
1357
|
-
sends no `operationName`.
|
|
1358
|
-
|
|
1359
|
-
Three breaking changes come with it:
|
|
1360
|
-
- **The client-slot contract widened to
|
|
1361
|
-
`execute(query, variables:, operation_name: nil)`.** If you wrote your own
|
|
1362
|
-
transport, client, or test double, add the kwarg — a client that doesn't
|
|
1363
|
-
accept it now raises `ArgumentError: unknown keyword: :operation_name`. A
|
|
1364
|
-
graphql-ruby `Schema` class already takes it, so bare schemas in the client
|
|
1365
|
-
slot are unaffected. Subclasses of `GraphWeaver::Transport` only implement
|
|
1366
|
-
`post(body)` and need no change.
|
|
1367
|
-
- **Cassettes are keyed on `operationName` too**, so two operations in one
|
|
1368
|
-
document can't collide. Cassettes recorded from a *named* operation before
|
|
1369
|
-
this release no longer match — re-record them
|
|
1370
|
-
(`GRAPHWEAVER_RECORD=1 bundle exec rspec`). Anonymous ones are unaffected.
|
|
1371
|
-
- **`GraphWeaver::Transport.log_tag` takes an operation name, not a query
|
|
1372
|
-
string** (`log_tag(query)` → `log_tag(operation_name)`); the constant
|
|
1373
|
-
`Transport::OPERATION_NAME` is now `Transport::OPERATION_NAME_PATTERN`, since
|
|
1374
|
-
generated modules define an `OPERATION_NAME` of their own.
|
|
1375
|
-
|
|
1376
|
-
Codegen bug fixes from the library review (all with regression coverage):
|
|
1377
|
-
- Narrowing (`... on X` and nothing else) now reads the match off `__typename`
|
|
1378
|
-
when the selection carries it, instead of off "the object came back empty".
|
|
1379
|
-
Selecting `__typename` guaranteed a non-empty object, so **every non-matching
|
|
1380
|
-
member was cast into `X`'s struct** — loudly when it had a non-null field,
|
|
1381
|
-
silently when all its fields were nullable. Regenerate: any query mixing
|
|
1382
|
-
`__typename` with a single type condition (the `_entities { __typename
|
|
1383
|
-
... on Widget { … } }` federation shape) was mistyped and now filters
|
|
1384
|
-
correctly.
|
|
1385
|
-
- A dispatched union/interface now requires its `__typename` to be unaliased and
|
|
1386
|
-
free of `@skip`/`@include` — `from_h` reads it unguarded, so either would have
|
|
1387
|
-
raised at runtime. Fix the selection if generation now refuses it.
|
|
1388
|
-
- **Unions and interfaces generate per named condition, plus one catch-all
|
|
1389
|
-
`Other`** — not one struct per schema member. A two-condition query against
|
|
1390
|
-
GitHub's `Node` (278 implementations) went from 5,386 lines / 279 structs to
|
|
1391
|
-
162 lines / 4. **Regenerate, and expect member names to move**: a type your
|
|
1392
|
-
query names no fields on is now `Other` rather than its own struct, so a
|
|
1393
|
-
`case` over the members needs an `Other` branch (`T.absurd` will tell you).
|
|
1394
|
-
In exchange, a `__typename` the query doesn't name — including a **member the
|
|
1395
|
-
schema grows after you generate** — deserializes into `Other` instead of
|
|
1396
|
-
raising `unexpected __typename`, so adding a union member upstream stays the
|
|
1397
|
-
non-breaking change GraphQL says it is.
|
|
1398
|
-
- `@skip`/`@include` on an inline fragment or a named spread now makes the
|
|
1399
|
-
fields under it nilable, as it always did for a directly-marked field —
|
|
1400
|
-
previously they kept non-null typing and a `data.fetch`, so a skipped block
|
|
1401
|
-
raised `key not found`. The narrowing guard sees the fragment's own directive
|
|
1402
|
-
too. Conversely, a field selected both conditionally and unconditionally is no
|
|
1403
|
-
longer over-nilable: one unguaranteed selection doesn't unmake the guarantee.
|
|
1404
|
-
- List variables coerce per element, so an enum inside a list accepts its wire
|
|
1405
|
-
value the way a scalar enum already did (`sort: ["POPULARITY_DESC"]` used to
|
|
1406
|
-
raise `NoMethodError: undefined method 'serialize' for String`). Input-object
|
|
1407
|
-
and custom-scalar elements coerce in lists too.
|
|
1408
|
-
|
|
1409
|
-
- Federation schemas that previously wouldn't load now do:
|
|
1410
|
-
- a supergraph whose `schema` definition carries a non-`@link` directive
|
|
1411
|
-
(`@tag`, `@composeDirective`, a composed custom one) no longer dies with a
|
|
1412
|
-
`GraphQL::ParseError` pointing into a document you never wrote.
|
|
1413
|
-
- **raw subgraph SDL loads** — what `rover subgraph fetch`, `_service { sdl }`,
|
|
1414
|
-
or your service repo's `.graphql` gives you. The federation directives a
|
|
1415
|
-
subgraph applies but doesn't declare (`@key`, `@external`, `@shareable`, …)
|
|
1416
|
-
are supplied on load, for both fed-1 and `@link`-style subgraphs. Note the
|
|
1417
|
-
`@inaccessible` subtraction stays supergraph-only: a subgraph keeps those
|
|
1418
|
-
fields, because it is not the public contract.
|
|
1419
|
-
- A schema that won't build now raises `GraphWeaver::Error` naming the artifact
|
|
1420
|
-
we took the source for (supergraph / subgraph / plain SDL / introspection),
|
|
1421
|
-
instead of whatever graphql-ruby's internals happened to raise — a
|
|
1422
|
-
`NoMethodError`, a `ParseError` pointing into a document you never wrote, a
|
|
1423
|
-
bare `RuntimeError`. **Rescuing the raw graphql-ruby classes no longer
|
|
1424
|
-
catches these.** The `@inaccessible` cascade also prunes a directive
|
|
1425
|
-
definition's own arguments.
|
|
1426
|
-
- **Single-line SDL loads.** `SchemaLoader.load("type Query { hi: String }")` —
|
|
1427
|
-
the shape you type in a console — was rejected as "unsupported schema format",
|
|
1428
|
-
because a string had to contain a newline to count as content rather than a
|
|
1429
|
-
path.
|
|
1430
|
-
- Rejecting a schema source is branded too, so the error class no longer depends
|
|
1431
|
-
on which branch rejected it: an unsupported format and an unreadable file both
|
|
1432
|
-
raise `GraphWeaver::Error` (were `ArgumentError` and `Errno::ENOENT`). A bare
|
|
1433
|
-
host now says so — `"graphql.anilist.co" looks like a host; did you mean
|
|
1434
|
-
"https://graphql.anilist.co"?` — instead of pointing at the file system.
|
|
1435
|
-
- Cassette recording accepts a `GraphWeaver::Client` — the call
|
|
1436
|
-
`docs/cassettes.md` shows (`Testing.cassette("github", client: live)`),
|
|
1437
|
-
which failed with `ArgumentError: missing keywords`. And a client that can't
|
|
1438
|
-
`execute` is now rejected on the spot, with its class named, rather than
|
|
1439
|
-
surfacing later as `NoMethodError … for an instance of Hash`.
|
|
1440
|
-
- Generated structs answer `respond_to?` the way `method_missing` behaves, so
|
|
1441
|
-
`struct.method(:nmae)` gets the same "did you mean" hint the direct call does.
|
|
1442
|
-
- `@oneOf` input objects enforce exactly one field. The schema can't express it
|
|
1443
|
-
— every `@oneOf` field is nullable — so the struct accepted zero or many and
|
|
1444
|
-
the server rejected the round trip; supplying the wrong number now raises
|
|
1445
|
-
`GraphWeaver::InputError` naming the type and the keys. **Regenerate** to pick
|
|
1446
|
-
it up.
|
|
1447
|
-
- An enum whose values differ only in case (`enum E { active ACTIVE }`) is
|
|
1448
|
-
refused at generation naming both wire values, instead of emitting two
|
|
1449
|
-
`Active` constants and raising `RuntimeError: Enum values must be assigned to
|
|
1450
|
-
constants` when the file loads. **Map such an enum onto one of yours**
|
|
1451
|
-
(`register_enum`). `AB`/`A_B` and `IN_PROGRESS`/`INPROGRESS` still generate
|
|
1452
|
-
fine — they name distinct constants.
|
|
1453
|
-
- A `.graphql` file that won't parse raises `GraphWeaver::ValidationError`
|
|
1454
|
-
**naming the file**, instead of a bare `GraphQL::ParseError` whose `[6, 1]`
|
|
1455
|
-
pointed into a document you never wrote — fragment inlining parses on the
|
|
1456
|
-
`generate!` path before `Codegen#generate`'s rescue could brand it. Fragment
|
|
1457
|
-
files get the same treatment.
|
|
1458
|
-
- Generated `from_response` shape-checks the envelope, so a malformed one stays
|
|
1459
|
-
under `GraphWeaver::Error`. A non-object `data`, a `Hash` (or an array of
|
|
1460
|
-
strings) for `errors`, and non-object `extensions` all escaped as a raw Sorbet
|
|
1461
|
-
`TypeError` — the `data` one from `from_h`'s sig, before the struct's own
|
|
1462
|
-
rescue could see it. A body that isn't an object at all deserialized to an
|
|
1463
|
-
empty envelope (`String#[]` answers `"data"` with nil); it now raises.
|
|
1464
|
-
- The generated `from_h` rescues `StandardError`, not just
|
|
1465
|
-
`TypeError`/`ArgumentError`/`KeyError` — a registered scalar whose cast raises
|
|
1466
|
-
anything else (`JSON::ParserError`, `URI::InvalidURIError`, your
|
|
1467
|
-
`Money::ParseError`) escaped the umbrella. **Regenerate** to pick both up.
|
|
1468
|
-
- A document holding more than one operation is refused at generation. Only the
|
|
1469
|
-
first was ever typed, and the whole document went on the wire with no
|
|
1470
|
-
`operationName`, so the request came back "Must provide operation name" —
|
|
1471
|
-
**split multi-operation files into one operation each.**
|
|
1472
|
-
- Result keys are checked before they become props, so generation refuses what
|
|
1473
|
-
used to be an unloadable file. Two keys that underscore to the same prop
|
|
1474
|
-
(`{ name Name: name }` — a plain alias, no exotic schema needed) raised
|
|
1475
|
-
`ArgumentError: Attempted to redefine prop :name` at require time; so did a
|
|
1476
|
-
field named `class`, `hash`, `send` or `frozen?`, which `T::Props` won't let a
|
|
1477
|
-
struct redefine. **Alias the field in the query** (`classValue: class`) — the
|
|
1478
|
-
error names the key and the spelling. The same reserved set now covers input
|
|
1479
|
-
fields, which only checked Ruby keywords and `serialize`/`to_h` before.
|
|
1480
|
-
- **Global registrations are validated against the schema**, like client-scoped
|
|
1481
|
-
ones always were: `GraphWeaver.extend_type("Medai", …)` (or `register_scalar` /
|
|
1482
|
-
`register_enum`) used to be a silent no-op, which is the failure mode
|
|
1483
|
-
`docs/getting_started.md` step 3 walks you straight into — it now raises at
|
|
1484
|
-
generation with the spellchecked hint. Registrations are global (see above),
|
|
1485
|
-
so **drop any that names a type the schema you generate against doesn't
|
|
1486
|
-
have**. The built-in scalars are exempt — a schema with no `Date` isn't a
|
|
1487
|
-
mistake.
|
|
1488
|
-
- `extend_type(requires:)` and `register_enum(requires:)` check each path is
|
|
1489
|
-
loadable at registration, as `register_scalar(requires:)` already did and
|
|
1490
|
-
`docs/scalars.md` already promised — a typo fails now, not in the generated
|
|
1491
|
-
file.
|
|
1492
|
-
- Docs: `docs/testing.md` passed the client to generated `execute` as a `client:`
|
|
1493
|
-
kwarg — it's positional. `README.md` had module naming backwards for the
|
|
1494
|
-
documented path (a file's module comes from the **file** name, not the
|
|
1495
|
-
operation name). `docs/federation.md` covers subgraph SDL, federation v1
|
|
1496
|
-
supergraphs, and that `@inaccessible` is subtracted only on the supergraph
|
|
1497
|
-
path. `docs/cassettes.md` names `MissingRecording` correctly.
|
|
1498
|
-
- **Federation namespaces are derived from the schema's own `@link`/`@core`
|
|
1499
|
-
declarations** instead of a hardcoded `join__`/`link__`/`core__` list — the
|
|
1500
|
-
spec URL's name segment gives the namespace, `as:` renames it, and `import:`
|
|
1501
|
-
binds names into the root namespace (`{name: "@key", as: "@myKey"}` included).
|
|
1502
|
-
Four things this fixes:
|
|
1503
|
-
- a graph using fed-2.5+ auth (`@requiresScopes`/`@policy`/`@context`) no
|
|
1504
|
-
longer leaks `federation__Scope`, `federation__Policy` or
|
|
1505
|
-
`context__ContextFieldValue` into `schema.types`;
|
|
1506
|
-
- a supergraph that renamed a spec (`@link(url: ".../join/v0.3", as: "j")`)
|
|
1507
|
-
strips its `j__*` machinery — it previously failed to load at all;
|
|
1508
|
-
- **a renamed `@inaccessible`** (`import: [{name: "@inaccessible", as:
|
|
1509
|
-
"@private"}]`, or `as:` on the inaccessible spec) hides what it marks. It
|
|
1510
|
-
was missed entirely before, so the derived API schema kept fields the
|
|
1511
|
-
router does not serve and codegen over-permitted them. **Regenerate** if
|
|
1512
|
-
your supergraph renames it.
|
|
1513
|
-
- a `@core`-only fed-1 schema, and any composed graph carrying no `@join__`
|
|
1514
|
-
marker, is now recognized as composed rather than loaded as plain SDL
|
|
1515
|
-
(`core__Purpose` used to survive, and `@inaccessible` went unsubtracted).
|
|
1516
|
-
|
|
1517
|
-
- **Subgraph SDL loads with the entity resolver it serves.** No published
|
|
1518
|
-
subgraph SDL contains `_entities`/`_service` — `rover subgraph fetch` and
|
|
1519
|
-
`_service { sdl }` both print the schema, where the plumbing is implicit — so
|
|
1520
|
-
the one query only a subgraph can describe couldn't be typed against the
|
|
1521
|
-
artifact you have. Weaver now supplies `_Any`, `_Service` and an `_Entity`
|
|
1522
|
-
union over the file's own `@key`'d types, alongside the `@key`/`@external`
|
|
1523
|
-
definitions it already supplied. Supergraphs and plain SDL are untouched;
|
|
1524
|
-
a file declaring its own `_entities` keeps it.
|
|
1525
|
-
- **Typed `_entities` representations.** A query selecting entities now
|
|
1526
|
-
generates a `Representations` builder per entity it can resolve, typed from
|
|
1527
|
-
the `@key(fields:)` directives the subgraph SDL carries:
|
|
1528
|
-
`UserQuery::Representations.user(id: "1")` → `{"__typename" => "User", "id"
|
|
1529
|
-
=> "1"}`. `__typename` is injected, key fields are typed from the schema, and
|
|
1530
|
-
a single `@key` makes them **required kwargs** — so an incomplete
|
|
1531
|
-
representation is an `srb tc` error, not a round trip. Compound (`"upc sku"`)
|
|
1532
|
-
and nested (`"organization { id }"`) key sets are parsed as the selection
|
|
1533
|
-
sets they are; a type with two alternative keys takes them optionally and
|
|
1534
|
-
raises `GraphWeaver::InputError` naming the type and what's missing when
|
|
1535
|
-
neither is satisfied. Builders are emitted only for entities the query
|
|
1536
|
-
actually reaches, and a key marked `resolvable: false` gets none.
|
|
1537
|
-
**`Representations` joins `Result`/`QUERY` as a reserved module-level name**
|
|
1538
|
-
— a shared fragment hoisting to it is now refused.
|
|
1539
|
-
|
|
1540
|
-
Transport improvements from the same review:
|
|
1541
|
-
- **`Transport::HTTP` pools its connections** (`pool_size:`, default 5) instead
|
|
1542
|
-
of serializing every request behind one socket and one mutex. The mutex was
|
|
1543
|
-
held across the whole network round trip, so one transport — which is what
|
|
1544
|
-
`GraphWeaver.client = api` gives a Rails app — allowed exactly one request in
|
|
1545
|
-
flight process-wide. Against a 10 ms-latency server, 8 threads × 10 calls:
|
|
1546
|
-
1059 ms before, 281 ms with the default pool of 5 (~3.8×). Sockets still open
|
|
1547
|
-
lazily, stay keep-alive, and are dropped on any error so the next call
|
|
1548
|
-
reconnects. **Lower `pool_size:` if your server counts connections per
|
|
1549
|
-
client**; raise it to match a threaded web server's thread count.
|
|
1550
|
-
- Both transports now send `Accept: application/graphql-response+json,
|
|
1551
|
-
application/json;q=0.9` — the media type GraphQL-over-HTTP requires a
|
|
1552
|
-
conforming client to accept, so a spec-conformant server can finally use the
|
|
1553
|
-
newer status-code semantics — and `User-Agent: graph_weaver/<version>`, so
|
|
1554
|
-
server operators can attribute the traffic. Previously the only header sent
|
|
1555
|
-
was `Content-Type`, and net/http supplied `Accept: */*`. `headers:` still
|
|
1556
|
-
overrides both; a prebuilt `Faraday::Connection` keeps whatever it carries.
|
|
1557
|
-
- **`Transport::Faraday` takes `open_timeout:`/`read_timeout:` and defaults them
|
|
1558
|
-
to 10s/30s**, the same as `Transport::HTTP`. It had no timeout knobs at all,
|
|
1559
|
-
so it inherited net/http's 60s/60s — 6× and 2× the documented defaults. Both
|
|
1560
|
-
timeouts now also thread through the client: `GraphWeaver.new(url,
|
|
1561
|
-
read_timeout: 5)` works whichever transport is picked. Passing a timeout
|
|
1562
|
-
alongside a prebuilt `Faraday::Connection` raises, as `headers:` already did.
|
|
1563
|
-
The Faraday transport also logs its adapter at `:info` — the default
|
|
1564
|
-
`net_http` one opens a connection per request, which was invisible.
|
|
1565
|
-
- **New `GraphWeaver::InProcess`**, wrapping a live graphql-ruby schema class —
|
|
1566
|
-
`GraphWeaver.new(MySchema, context: { current_user: user })`. In-process
|
|
1567
|
-
execution worked but was blind in three ways: nothing supplied a `context:`,
|
|
1568
|
-
so a resolver reading `context[:current_user]` got nil (surfacing as "Cannot
|
|
1569
|
-
return null for non-nullable field Query.me"); all logging lived in
|
|
1570
|
-
`Transport#execute`, which an in-process schema bypasses, so not one line at
|
|
1571
|
-
DEBUG; and a resolver raise came out as a bare `RuntimeError` where the same
|
|
1572
|
-
failure over HTTP is a `ServerError`, so `rescue GraphWeaver::Error` caught
|
|
1573
|
-
one and missed the other. A resolver raise is now a `ServerError` (status
|
|
1574
|
-
500) with the original kept as `#cause` — in-process, the real backtrace is
|
|
1575
|
-
the point. **A bare schema class still works in any client slot**; the
|
|
1576
|
-
wrapper is an upgrade, not a requirement.
|
|
1577
|
-
- **`ServerError` carries the response `#headers`** (names downcased), plus
|
|
1578
|
-
`#retry_after` (seconds or HTTP-date, per RFC 9110) and `#rate_limited?`. The
|
|
1579
|
-
`Net::HTTPResponse` was always in hand and thrown away, so recovering
|
|
1580
|
-
`x-ratelimit-remaining` or a request id meant monkey-patching the transport.
|
|
1581
|
-
A `post` override may now return a third element, the headers; returning the
|
|
1582
|
-
documented `[status, body]` pair stays correct.
|
|
1583
|
-
- **`Retry` honours `Retry-After`** — the server's delay wins over the
|
|
1584
|
-
configured backoff, clamped to `max:` and not jittered. Related: **408 and
|
|
1585
|
-
429 now retry by default.** They were treated as ordinary 4xx ("your bug,
|
|
1586
|
-
retrying won't fix it"), which for the one status that exists to say "come
|
|
1587
|
-
back later" was exactly backwards, and left `Retry` incorrect against GitHub
|
|
1588
|
-
and Shopify. Pass `retry_if:` to restore the old behaviour.
|
|
1589
|
-
- **A throttling predicate, spelled the same everywhere**: `ServerError#throttled?`
|
|
1590
|
-
(429, or a 503 that says when to come back) and `QueryError#throttled?` /
|
|
1591
|
-
`Response#throttled?` (a throttle code in the errors array). An API says "slow
|
|
1592
|
-
down" with an HTTP status or with a code in a 200 body, and callers shouldn't
|
|
1593
|
-
have to know which. The codes are `GraphWeaver::GraphQLError::THROTTLE_CODES`
|
|
1594
|
-
— Shopify's `THROTTLED`, GitHub's `RATE_LIMITED`, and the common Apollo/Hasura
|
|
1595
|
-
spellings — so `retry_codes:` takes the constant instead of a hand-written
|
|
1596
|
-
string. `QueryError#to_h` gains `"throttled"` alongside `"schema_stale"`.
|
|
1597
|
-
- `Transport::HTTP` takes `ca_file:`/`ca_path:`/`cert:`/`key:`/`verify_mode:`,
|
|
1598
|
-
forwarded to `Net::HTTP.start` — a private CA or mTLS no longer means
|
|
1599
|
-
switching to Faraday, which was the real but undiscoverable answer. Passing
|
|
1600
|
-
one to an `http://` url raises instead of quietly doing nothing.
|
|
1601
|
-
- **An instrumentation seam**: `GraphWeaver.instrumenter = ->(event, payload,
|
|
1602
|
-
&block) { ... }`, a no-op until set, wrapping every request — over the wire
|
|
1603
|
-
and in-process, one seam for both. `ActiveSupport::Notifications` becomes a
|
|
1604
|
-
two-line adapter. The one event is `GraphWeaver::EXECUTE_EVENT`; its payload
|
|
1605
|
-
carries `:url`, `:schema`, `:operation` and `:status`, and deliberately not
|
|
1606
|
-
the query or variables (those are PII, and belong at debug on the logger
|
|
1607
|
-
where the level gates them). See `docs/logging.md`.
|
|
1608
|
-
Developer-experience fixes (all with regression coverage):
|
|
1609
|
-
- **FakeClient override keys are validated against the schema.** A typo'd key
|
|
1610
|
-
(`"Person.nmae" => "Daniel"`) pinned nothing, and the example passed against
|
|
1611
|
-
random fake data — a test that had quietly stopped checking what it claims to.
|
|
1612
|
-
Keys now raise, spellchecked, at `FakeClient.new` and at `Testing.configure`
|
|
1613
|
-
when a schema is already set. Bare field-name keys (`"name"`) still work;
|
|
1614
|
-
**fix or drop any key that doesn't name a field in your schema.**
|
|
1615
|
-
- Codegen validation errors name the position they already captured: each
|
|
1616
|
-
message is prefixed `4:5`, and `queries/typo.graphql:4:5` when the file is
|
|
1617
|
-
known (`Codegen.new`/`Codegen.generate` take it as `path:`), instead of
|
|
1618
|
-
leaving a project of thirty query files to search by hand.
|
|
1619
|
-
- A strict `alias:` whose path doesn't fit a query now names the query that
|
|
1620
|
-
failed and ends with `— pass optional: true to skip selections that don't
|
|
1621
|
-
fit`, the documented way out.
|
|
1622
|
-
- Generation lists the custom scalars it had no registration for at `info`
|
|
1623
|
-
(`3 unregistered custom scalars → T.untyped: …`). Informational — a scalar
|
|
1624
|
-
without a codec is a legitimate choice, just no longer a silent one.
|
|
1625
|
-
- `Response#ok?` (and `#success?`) — the positive form of `errors?`.
|
|
1626
|
-
- `FakeClient#schema` reads back the schema responses are fabricated against,
|
|
1627
|
-
which is how to reach it under `auto_fake`, where `GraphWeaver.client` is the
|
|
1628
|
-
fake; `Testing.config.schema` reads back too.
|
|
1629
|
-
New:
|
|
1630
|
-
- **`rake graph_weaver:queries:check` — which of your queries a schema change
|
|
1631
|
-
broke.** Re-introspects the url the dump records (leaving the dump alone) and
|
|
1632
|
-
validates every checked-in query against the server as it is now,
|
|
1633
|
-
reporting file plus line:col plus message and exiting non-zero on any
|
|
1634
|
-
failure, so it drops into CI. `GraphWeaver.check_queries` returns the same
|
|
1635
|
-
thing as data (`{path => [{"message", "line", "column"}]}`, empty when
|
|
1636
|
-
everything validates); pass `schema:` to check a schema you already have
|
|
1637
|
-
without touching the network. Complements `graph_weaver:verify`, which asks
|
|
1638
|
-
the different question of whether the committed Ruby is stale.
|
|
1639
|
-
- `verify_generated!` (and `rake graph_weaver:verify`) compares generated files
|
|
1640
|
-
with line endings normalized, so a checkout under git's `autocrlf` no longer
|
|
1641
|
-
reports every generated file as stale.
|
|
1642
|
-
- New [editor support](docs/editors.md) doc: the `graphql.config.yml` that gives
|
|
1643
|
-
VS Code and RubyMine validation, autocomplete and hover docs in your
|
|
1644
|
-
`.graphql` files — no JS project, no gem code, five lines of YAML.
|
|
1645
|
-
- **Byte-identical generation is now a stated guarantee**, not just a property:
|
|
1646
|
-
the same schema and queries produce the same files on any machine, in any
|
|
1647
|
-
order (`docs/generated_modules.md`). It was already true and spec-enforced;
|
|
1648
|
-
it was documented nowhere.
|
|
1649
|
-
|
|
1650
|
-
**Faraday is no longer auto-selected — `GraphWeaver.new(url)` always builds
|
|
1651
|
-
`Transport::HTTP`.** Selection used to be `defined?(::Faraday)`, and faraday
|
|
1652
|
-
rides into most bundles transitively (stripe, octokit, ...), so adding an
|
|
1653
|
-
unrelated gem silently swapped your transport, its timeouts, and its connection
|
|
1654
|
-
behaviour. The accidental default was also the slower one: `Transport::HTTP`
|
|
1655
|
-
pools persistent sockets (1 TCP connection for 10 requests) where Faraday's
|
|
1656
|
-
default `net_http` adapter reconnects per request (10 for 10) — a full TLS
|
|
1657
|
-
handshake each time over HTTPS.
|
|
1658
|
-
|
|
1659
|
-
**What you must do:** if you were relying on the auto-pick, ask for Faraday
|
|
1660
|
-
explicitly — `GraphWeaver.new(url, transport: :faraday)`. A middleware block
|
|
1661
|
-
still implies it (`GraphWeaver.new(url) { |conn| ... }`), since the block is
|
|
1662
|
-
Faraday's. Faraday is otherwise unchanged and fully supported. Alongside a url,
|
|
1663
|
-
`transport:` now takes `:http` (the default) or `:faraday` rather than a
|
|
1664
|
-
built transport object — passing an object there used to raise "pass a url or
|
|
1665
|
-
transport:, not both" and now raises naming the two symbols. Alongside a schema
|
|
1666
|
-
source it still takes a built transport, and now rejects a Symbol. The client
|
|
1667
|
-
logs which transport it built at `info`.
|
|
1668
|
-
|
|
1669
|
-
`docs/transports.md` gains the recipe for giving Faraday the connection reuse
|
|
1670
|
-
`Transport::HTTP` has by default: the `:net_http_persistent` adapter, the two
|
|
1671
|
-
gems it needs, and the version pairing (Faraday 2.x requires
|
|
1672
|
-
`faraday-net_http_persistent` **2.x**; 1.2.0 raises `NoMethodError: undefined
|
|
1673
|
-
method 'dependency'` at load). graph_weaver depends on neither and never
|
|
1674
|
-
selects it for you.
|
|
1675
|
-
|
|
1676
|
-
**Generated files are pruned when their query disappears.** Renaming or
|
|
1677
|
-
deleting a `.graphql` used to leave its `.rb` behind forever: `load_generated!`
|
|
1678
|
-
kept requiring it, its module kept resolving against a query that no longer
|
|
1679
|
-
existed, and `verify_generated!` stayed silent — the pruning only covered
|
|
1680
|
-
`inputs/*.rb` and `unions.rb`. `generate!` now deletes any generated file the
|
|
1681
|
-
plan no longer produces, and `verify_generated!` reports it as stale.
|
|
1682
|
-
|
|
1683
|
-
Only files carrying the `# Generated by GraphWeaver — do not edit.` header are
|
|
1684
|
-
ever deleted, so a hand-written file in the output directory survives. **What
|
|
1685
|
-
you must do:** nothing, unless you were relying on a lingering module — the
|
|
1686
|
-
next `generate!` removes it, and CI's `rake graph_weaver:verify` will name it
|
|
1687
|
-
first.
|
|
1688
|
-
|
|
1689
|
-
**Mutations now generate `…Mutation` modules, not `…Query`.**
|
|
1690
|
-
`save_list_entry.graphql` holding a `mutation` produces
|
|
1691
|
-
`SaveListEntryMutation` in `save_list_entry_mutation.rb`;
|
|
1692
|
-
`SaveListEntryQuery.execute!` read wrong for a write. Queries are unchanged.
|
|
1693
|
-
The rule is one rule — the camelized file name plus the operation the file
|
|
1694
|
-
defines — and all three naming sites follow it: `generate!`,
|
|
1695
|
-
`GraphWeaver.parse(path)`, and `client.load_queries!`. The operation name
|
|
1696
|
-
written *inside* the file still names nothing; it goes on the wire as
|
|
1697
|
-
`operationName`.
|
|
1698
|
-
|
|
1699
|
-
**What you must do:** regenerate (`rake graph_weaver:generate`) and rename the
|
|
1700
|
-
call sites of any mutation module — `AdoptQuery` → `AdoptMutation`, including
|
|
1701
|
-
nested constants like `AdoptQuery::AdoptionInput`. Regeneration prunes the old
|
|
1702
|
-
`*_query.rb` files, and `rake graph_weaver:verify` names anything missed.
|
|
1703
|
-
Changing a file's `query` to `mutation` from here on renames its constant the
|
|
1704
|
-
same way, which CI now catches rather than letting it drift.
|
|
1705
|
-
|
|
1706
|
-
**Generated modules get their client plumbing from
|
|
1707
|
-
`GraphWeaver::QueryModule`.** `client`/`client=` carry no per-query type
|
|
1708
|
-
information, so every generated file repeated the same fifteen untyped lines;
|
|
1709
|
-
they now live in the gem, beside the input-struct runtime, and a module says
|
|
1710
|
-
`extend GraphWeaver::QueryModule` instead. `execute`, `execute!`,
|
|
1711
|
-
`from_response` and `from_response!` stay generated — their sigs are your
|
|
1712
|
-
query's types. A baked `client:` constant is emitted as `DEFAULT_CLIENT`,
|
|
1713
|
-
still resolved on first use so a module can load before the initializer that
|
|
1714
|
-
builds its client, and resolution is unchanged: per call → per module → baked
|
|
1715
|
-
constant → `GraphWeaver.client`.
|
|
1716
|
-
|
|
1717
|
-
**What you must do:** regenerate (`rake graph_weaver:generate`). The files
|
|
1718
|
-
change; nothing about how you call them does.
|
|
1719
|
-
Error-message and console ergonomics from the same review:
|
|
1720
|
-
- **Validation errors name the query file and render one per line**, compiler
|
|
1721
|
-
style — `invalid query in app/graphql/queries/person.graphql:` followed by an
|
|
1722
|
-
indented `4:5 Field 'nmae' doesn't exist on type 'Person'` per error. They
|
|
1723
|
-
arrived as one joined line with no file at all, because `generate!` had the
|
|
1724
|
-
path in hand and never passed it to codegen, so thirty query files left you
|
|
1725
|
-
hunting for a bare `4:5`. `ValidationError#errors` and `#to_h` keep the shape
|
|
1726
|
-
`rake graph_weaver:queries:check` reads; only the message text changed, and
|
|
1727
|
-
**it is multi-line now** — update anything matching on it.
|
|
1728
|
-
- **`register_enum("Species", PetKind, {"DOG" => :dog})` says the value map is a
|
|
1729
|
-
keyword**, and shows the call with `map:` in it. Guessing the map as a third
|
|
1730
|
-
positional argument used to get Ruby's `wrong number of arguments (given 3,
|
|
1731
|
-
expected 2)`, which never mentions `map:`.
|
|
1732
|
-
- **`load_queries!` logs when it replaces an already-loaded module**, at
|
|
1733
|
-
`:info`, before swapping the constant: `replacing PersonQuery — objects built
|
|
1734
|
-
from the previous module stay instances of it`. Reloading is unchanged and
|
|
1735
|
-
still what the method is for; it just isn't silent about the structs it
|
|
1736
|
-
orphans, which is how a console session ends up with an `is_a?` that fails
|
|
1737
|
-
for no visible reason.
|
|
1738
|
-
**Rails install generator.**
|
|
1739
|
-
`rails g graph_weaver:install https://api.example.com/graphql` writes
|
|
1740
|
-
`config/initializers/graph_weaver.rb`, the `app/graphql/queries` and
|
|
1741
|
-
`app/graphql/generated` directories, `graphql.config.yml` (schema autocomplete
|
|
1742
|
-
and validation for `.graphql` files in VS Code / RubyMine) and the schema dump
|
|
1743
|
-
— replacing the console step the getting-started guide used to open with.
|
|
1744
|
-
`--auth` names the ENV var holding the token (default `GRAPHWEAVER_AUTH`),
|
|
1745
|
-
`--no-schema` skips the introspection. Re-running prompts on conflict like any
|
|
1746
|
-
Rails generator.
|
|
1747
|
-
|
|
1748
|
-
**`rake graph_weaver:schema:refresh` can now create the first dump.** It read
|
|
1749
|
-
its url from an existing dump's provenance stamp, so it couldn't bootstrap one
|
|
1750
|
-
— pass `URL=https://api.example.com/graphql` and it will, and both the
|
|
1751
|
-
no-dump and no-provenance messages now name that fix. The same logic is
|
|
1752
|
-
`GraphWeaver::SchemaLoader.refresh!(url:, auth:)`, which is what the generator
|
|
1753
|
-
calls.
|
|
1754
|
-
|
|
1755
|
-
**Pointing a client at a url that isn't a GraphQL endpoint now says so.** A
|
|
1756
|
-
REST base url, a GraphiQL page or a proxy that ate the path answers 200 with
|
|
1757
|
-
well-formed JSON, and `.schema` raised a bare `KeyError`/`NoMethodError` out of
|
|
1758
|
-
graphql-ruby — unbranded, no url, and it escaped `rescue GraphWeaver::Error`
|
|
1759
|
-
(a 404 on the same path was already branded and clear). Introspection now
|
|
1760
|
-
checks for `data.__schema` and raises `GraphWeaver::Error` naming the endpoint
|
|
1761
|
-
and the first 200 characters of what came back.
|
|
1762
|
-
|
|
1763
|
-
**A subgraph's own `FieldSet` / `Scope` / `Policy` type no longer collides with
|
|
1764
|
-
weaver's.** Loading subgraph SDL injects the federation directive definitions
|
|
1765
|
-
the file applies but doesn't declare, and the scalars they reference went in
|
|
1766
|
-
unnamespaced — so a subgraph that owns a type by one of those names either had
|
|
1767
|
-
it shadowed or failed to build, with advice pointing at the wrong file. Those
|
|
1768
|
-
three are now `federation__FieldSet` / `federation__Scope` /
|
|
1769
|
-
`federation__Policy`. `_Any` / `_Entity` / `_Service` keep their names — those
|
|
1770
|
-
are spec-mandated and queryable.
|
|
1771
|
-
|
|
1772
|
-
**`rake graph_weaver:queries:check` no longer compares an in-process app's
|
|
1773
|
-
schema against itself.** For an app whose schema is its own graphql-ruby class
|
|
1774
|
-
there is no server to re-introspect, so the check degraded to re-reading the
|
|
1775
|
-
committed dump — reporting phantom errors about the app's own schema, a field
|
|
1776
|
-
you just added reading as "doesn't exist". When `GraphWeaver.client` executes
|
|
1777
|
-
in-process (a `Client` wrapping a schema class, or the class itself), the check
|
|
1778
|
-
now validates against the live class. Network clients are unchanged.
|
|
1779
|
-
|
|
1780
|
-
**The two dead-end "records no source url" messages now say what to do.** A
|
|
1781
|
-
dump taken from a schema class is rebuilt from code, not re-fetched — both
|
|
1782
|
-
`schema:refresh` and `schema:diff` say that instead of naming a `URL=` that
|
|
1783
|
-
doesn't exist for you.
|
|
1784
|
-
|
|
1785
|
-
**Two rake tasks are renamed so each one names its own subject.** There were
|
|
1786
|
-
three checks and two of them were called `verify`, while the one people run
|
|
1787
|
-
most — "did schema drift break my queries?" — lived under `schema:` and doesn't
|
|
1788
|
-
check the schema. **Update your CI:**
|
|
1789
|
-
|
|
1790
|
-
| Was | Now | Asks |
|
|
1791
|
-
|---|---|---|
|
|
1792
|
-
| `graph_weaver:schema:check` | `graph_weaver:queries:check` | do my checked-in queries still validate? |
|
|
1793
|
-
| `graph_weaver:schema:verify` | `graph_weaver:schema:diff` | has the server drifted from the dump? |
|
|
1794
|
-
|
|
1795
|
-
`graph_weaver:verify` (is the committed Ruby fresh?) and
|
|
1796
|
-
`graph_weaver:schema:refresh` are unchanged. No aliases — the old names are
|
|
1797
|
-
gone.
|
|
1798
|
-
|
|
1799
|
-
**The instrumentation payload now carries `:status` in-process too.** `InProcess`
|
|
1800
|
-
brands a resolver raise as `ServerError(500)` precisely so callers needn't
|
|
1801
|
-
branch on which side of the seam a query ran — but the payload had no `:status`
|
|
1802
|
-
in-process and no `:schema` over the wire, so a subscriber had to branch
|
|
1803
|
-
anyway. A successful in-process execute now sets `:status` to 200; a failure
|
|
1804
|
-
still rides the exception the hook already sees.
|
|
1805
|
-
|
|
1806
|
-
**`extend_type` and `alias:` moved from `docs/scalars.md` to
|
|
1807
|
-
`docs/generated_modules.md`** — they decorate a generated struct, and now sit
|
|
1808
|
-
next to what a generated struct looks like. **Update any bookmark to
|
|
1809
|
-
`scalars.md#type-helpers-your-logic-on-generated-structs`**; it is
|
|
1810
|
-
`generated_modules.md#type-helpers` now. `scalars.md` still owns
|
|
1811
|
-
`register_scalar` and `register_enum`.
|
|
1812
|
-
|
|
1813
|
-
- **`Testing::Router#trace` accumulates across executes and is reset
|
|
1814
|
-
explicitly** — `router.reset_trace`. It used to clear itself at the top of
|
|
1815
|
-
every `execute`, which made it answer about the *last* query rather than the
|
|
1816
|
-
code path: a service object running two queries reported only the second's
|
|
1817
|
-
fetches, and an example that ran nothing read the previous example's, so an
|
|
1818
|
-
assertion could pass on another example's work and fail under `--order rand`.
|
|
1819
|
-
The rspec `graphql: :router` tag resets it per example. **An example that
|
|
1820
|
-
asserts on the trace after more than one `execute` now sees both**, and
|
|
1821
|
-
wants a `reset_trace` in between if it meant only the last one.
|
|
1822
|
-
- **The absent-subgraph refusal leads with the half that applies.** It opened
|
|
1823
|
-
with "Rails autoloads, so the class is probably just not loaded yet" — right
|
|
1824
|
-
often enough to lead with, except when eager loading is already on, and then
|
|
1825
|
-
the library can *ask* rather than send you to a setting you already have. It
|
|
1826
|
-
now checks `config.eager_load` / `config.rake_eager_load` and, when either is
|
|
1827
|
-
on, says the subgraph runs elsewhere and puts `=> :fake` first instead of at
|
|
1828
|
-
the end of a 60-word sentence.
|
|
1829
|
-
- **A refusal spells a nested `@key`/`@requires` field set the way your schema
|
|
1830
|
-
does** — `"origin { lat lon }"`, not `"origin.lat", "origin.lon"`. The dotted
|
|
1831
|
-
form is this library's parse of it and matches nothing you can grep for.
|
|
1832
|
-
- **One `@interfaceObject` no longer disables the whole router.** It refused at
|
|
1833
|
-
construction, for the entire supergraph, so a single directive made
|
|
1834
|
-
`Testing::Router` unusable even for queries that never touch the type — one
|
|
1835
|
-
corpus had to be split into two graphs over it. It is now a per-query refusal
|
|
1836
|
-
(`Unplannable#category` `:interface_object`) keyed on the types the query
|
|
1837
|
-
actually reaches, and `federation:coverage` counts it as one refusal among
|
|
1838
|
-
others rather than aborting. Routing an `@interfaceObject` is still not
|
|
1839
|
-
implemented; this only makes the refusal proportionate.
|
|
1840
|
-
`RoutingTable#unsupported` no longer lists them —
|
|
1841
|
-
`RoutingTable#interface_objects` does, as `{"Media" => ["catalog"]}`.
|
|
1842
|
-
- `docs/federation.md`'s refusal table now lists **every** `Unplannable`
|
|
1843
|
-
category, and a spec keeps it that way. Five were missing, `chained_requires`
|
|
1844
|
-
and `conditional_fragment` among them.
|
|
1845
|
-
- **`federation:coverage` counts what your suite can *run*, not only what
|
|
1846
|
-
plans.** `5/5 queries plannable locally (100%)` was optimistic in exactly the
|
|
1847
|
-
partly-local shape the docs call the usual migration one: a query resolving
|
|
1848
|
-
in a subgraph another service serves plans fine and a spec still can't run
|
|
1849
|
-
it. The headline now reads `…, 2 servable here`, and the queries reaching
|
|
1850
|
-
past what's loaded are listed with the subgraph each needs. Plan-only is
|
|
1851
|
-
still the design — with no subgraph loaded (the SDL-alone CI run) the second
|
|
1852
|
-
number is dropped and the report says it counted planning only.
|
|
1853
|
-
`Coverage#servable` and `#elsewhere` are the programmatic side, and
|
|
1854
|
-
`Coverage::Result` gained `absent` / `servable?`.
|
|
1855
|
-
- **`Testing::Failure.stale_schema` drops its `schema:` / `seed:` sampling.**
|
|
1856
|
-
It picked a random real type/field so a fabricated error string would look
|
|
1857
|
-
plausible — but no assertion can depend on which one it picks, so it was
|
|
1858
|
-
decoration with three kwargs and an RNG behind it. `stale_schema(type:,
|
|
1859
|
-
field:)` names the casualty when the message matters, and the bare call still
|
|
1860
|
-
trips `schema_stale?`. Passing `schema:`/`seed:` now raises `ArgumentError`.
|
|
1861
|
-
- **`rake graph_weaver:cassettes:check`** — replays every recording through the
|
|
1862
|
-
generated modules and fails when one no longer casts. A cassette is the only
|
|
1863
|
-
artifact recorded from a foreign server, and nothing else here notices when
|
|
1864
|
-
that server's answers drift out of the shape the structs were generated for:
|
|
1865
|
-
`verify`, `queries:check` and `schema:diff` all ask about the local side. It
|
|
1866
|
-
needs no network, so it belongs beside `verify` in a PR run. A recording no
|
|
1867
|
-
generated module sends is skipped and counted, and checking *none* of them
|
|
1868
|
-
fails, like `federation:diff`. `Testing::Cassette#check` is the programmatic
|
|
1869
|
-
side.
|
|
1870
|
-
- **A cast failure no longer prints sorbet-runtime's `Caller:` frame.**
|
|
1871
|
-
`GraphWeaver::TypeError`'s message ended with `Caller:
|
|
1872
|
-
.../sorbet-runtime/.../call_validation.rb:331` — a path into the gem, never
|
|
1873
|
-
into the code with the problem, and the only location the message offered.
|
|
1874
|
-
|
|
1875
|
-
### v0.4.6 (2026-07-30)
|
|
1876
|
-
Bug fixes from a full-library review (all with regression coverage):
|
|
1877
|
-
- alias: a nested-object/enum leaf (`meta.sub`) now qualifies its constant
|
|
1878
|
-
(`Meta::Sub`) instead of emitting a bare `Sub` that raised NameError; alias
|
|
1879
|
-
names/segments are validated as identifiers (were interpolated verbatim,
|
|
1880
|
-
allowing injection); `optional:` no longer swallows a reserved-name/collision
|
|
1881
|
-
mistake; a real field named `first`/`last` resolves as a field.
|
|
1882
|
-
- Shared unions: a hoisted member selecting a mapped enum now emits its
|
|
1883
|
-
`<NAME>_FROM_WIRE` table into `unions.rb` (was a NameError at `from_h`); a
|
|
1884
|
-
fragment whose name collides with `Result`/`QUERY` is refused.
|
|
1885
|
-
- A named interface fragment holding inline `... on X` conditions now dispatches
|
|
1886
|
-
instead of silently dropping those fields; fragment cycles raise a clear error
|
|
1887
|
-
in the FakeClient/Anonymizer walkers instead of `SystemStackError`.
|
|
1888
|
-
- Malformed responses/inputs stay under `GraphWeaver::Error`: a non-2xx
|
|
1889
|
-
`errors: null` body keeps its status; a 2xx non-object body is a `ServerError`;
|
|
1890
|
-
`data!` on null-data/no-errors raises `QueryError`; `coerce(non-Hash)` raises
|
|
1891
|
-
`InputError`.
|
|
1892
|
-
- Testing harness: FakeClient/Anonymizer merge duplicate result keys (were
|
|
1893
|
-
fabricating shapes the generated struct couldn't cast); `fail_at` fires every
|
|
1894
|
-
execute; symbol-keyed cassette variables no longer crash on reload; the
|
|
1895
|
-
Anonymizer keeps concrete-fragment fields when data lacks `__typename`.
|
|
1896
|
-
- Federation: a user type named `link` is no longer dropped; an all-`@inaccessible`
|
|
1897
|
-
schema raises a pointed error. Client accepts `retries: nil` on a schema
|
|
1898
|
-
source; `register_scalar` rejects an anonymous class.
|
|
1899
|
-
- FakeClient: an Integer `list_size` now means exactly that length (a Range
|
|
1900
|
-
randomizes within it). Codegen rejects two variables that underscore to the
|
|
1901
|
-
same kwarg (`$userId` + `$user_id`). The Faraday transport raises rather than
|
|
1902
|
-
silently dropping `headers:`/block when handed a prebuilt connection.
|
|
1903
|
-
|
|
1904
|
-
### v0.4.5 (2026-07-30)
|
|
1905
|
-
- `alias:` paths gain list-element selectors: a `first`/`last` segment picks one
|
|
1906
|
-
element out of a list hop, always nilable — `alias: { entity: "_entities.first" }`
|
|
1907
|
-
yields `def entity = _entities&.first`, and navigation continues into the
|
|
1908
|
-
element (`_entities.first.name`). Typed from the selection: a single inline
|
|
1909
|
-
fragment lands on the concrete member (`T.nilable(Widget)`), a multi-fragment
|
|
1910
|
-
selection on the union. Selectors are checked against the node shape — `.first`
|
|
1911
|
-
on a non-list raises. Cleanly retires the `result._entities&.first&.field`
|
|
1912
|
-
boilerplate of single-entity federation `_entities` queries.
|
|
1913
|
-
- `extend_type(..., optional: true)` makes its aliases lenient: a query whose
|
|
1914
|
-
selection doesn't fit the path omits the accessor instead of failing
|
|
1915
|
-
generation. For an alias on a universal type (a `Query` accessor a strict alias
|
|
1916
|
-
would force every query to satisfy), or one that only fits some selections.
|
|
1917
|
-
|
|
1918
|
-
### v0.4.4 (2026-07-30)
|
|
1919
|
-
- Supergraph loading now derives the **API schema**: `@inaccessible` elements
|
|
1920
|
-
(present in the federated graph but hidden from what the router serves) are
|
|
1921
|
-
removed on load, cascading — a field/argument/union-member/interface
|
|
1922
|
-
referencing a removed type goes too, and a type left empty is removed in turn.
|
|
1923
|
-
So codegen validates against exactly what clients can query, with no
|
|
1924
|
-
over-permitting and no Apollo JS tooling to subtract the API schema first.
|
|
1925
|
-
Plain (non-federation) SDL is untouched.
|
|
1926
|
-
|
|
1927
|
-
### v0.4.3 (2026-07-30)
|
|
1928
|
-
- Federation-aware supergraph loading: `SchemaLoader` (and `Client.new`) now
|
|
1929
|
-
load a composed Apollo Federation v2 supergraph SDL directly. When the `@join__*`
|
|
1930
|
-
markers are present it strips the composition machinery — the synthetic
|
|
1931
|
-
`join__*`/`link__*` types and directive definitions, and every `@join__*`/`@link`
|
|
1932
|
-
application — via an AST rewrite before `from_definition`, so the merged type
|
|
1933
|
-
shapes load cleanly with nothing federation-internal leaking into
|
|
1934
|
-
`schema.types`. Plain SDL is untouched. A query can now be typed against the
|
|
1935
|
-
composed supergraph, not just per-subgraph schema objects.
|
|
1936
|
-
- Removed `directive_defaults_patch.rb` (the graphql-ruby monkeypatch); the
|
|
1937
|
-
preprocessor supersedes it and `graphql-ruby` fixed the underlying issue. The
|
|
1938
|
-
gem now requires `graphql >= 2.6.7`.
|
|
1939
|
-
|
|
1940
|
-
### v0.4.2 (2026-07-30)
|
|
1941
|
-
- `extend_type` accepts `alias:` — project a selected field (possibly nested)
|
|
1942
|
-
onto a flat, typed accessor emitted into the struct body:
|
|
1943
|
-
`extend_type("Widget", alias: { tag: "meta.tag" })` generates a sig'd
|
|
1944
|
-
`def tag = meta&.tag`. Retires hand-written value objects that only flattened
|
|
1945
|
-
a passthrough field. Takes a `{ name => path }` hash, a bare path string
|
|
1946
|
-
(accessor named after the last segment), or an array of paths. Typed from the
|
|
1947
|
-
selection — a nullable hop makes the accessor nilable and nil-safe; the leaf
|
|
1948
|
-
may be a scalar, enum, or nested struct. Validated per query at generation: an
|
|
1949
|
-
unselected/misspelled segment (with `did you mean`), a list hop, or a name
|
|
1950
|
-
collision raises. Stacks and is client-scopable like the mixin forms.
|
|
1951
|
-
|
|
1952
|
-
### v0.4.1 (2026-07-29)
|
|
1953
|
-
- Generated `execute!` forwards its kwargs to `execute` via hash shorthand
|
|
1954
|
-
(`execute(client, name:, species:)` rather than `name: name, species: species`)
|
|
1955
|
-
— cosmetic only. Regenerate to refresh (`verify` flags the drift otherwise).
|
|
1956
|
-
|
|
1957
|
-
### v0.4.0 (2026-07-28)
|
|
1958
|
-
- Shared unions: when a named shared fragment is the whole selection on a union
|
|
1959
|
-
field (`feed { ...FeedItemFields }`), its type is hoisted once into a
|
|
1960
|
-
`GraphQLUnions` module and every query that spreads it aliases the same type —
|
|
1961
|
-
so a union selected across many queries is one Ruby type family (one
|
|
1962
|
-
exhaustive `case … T.absurd`), not a fresh dispatch module per query. No flag:
|
|
1963
|
-
the shared fragment is the opt-in. Triggers only for an exact lone spread;
|
|
1964
|
-
mixing other fields, or shadowing with a query-local fragment, keeps the union
|
|
1965
|
-
inlined. Module name derives from the output path (override with
|
|
1966
|
-
`GraphWeaver.unions_module=`); dynamic `parse` still inlines.
|
|
1967
|
-
- Removed the `shared_inputs:` option from `generate!` / `verify_generated!`.
|
|
1968
|
-
Directory-based generation always emits each input type once into a shared
|
|
1969
|
-
module — the self-contained-module opt-out added complexity for little value.
|
|
1970
|
-
Single-query `parse` / `Codegen.generate` still inline their types (there's
|
|
1971
|
-
no set to share against). Only affects callers who passed
|
|
1972
|
-
`shared_inputs: false`.
|
|
1973
|
-
|
|
1974
|
-
### v0.3.4 (2026-07-29)
|
|
1975
|
-
- Shared fragments: define reusable named fragments once (default
|
|
1976
|
-
`app/graphql/fragments`, configurable via `GraphWeaver.fragments_paths`) and
|
|
1977
|
-
spread them from any query. Each query inlines only the fragments it
|
|
1978
|
-
transitively spreads, so the sent query stays self-contained. Fragment files
|
|
1979
|
-
hold only fragments; names are unique across them. Works in `generate!` and
|
|
1980
|
-
dynamic `parse`.
|
|
1981
|
-
|
|
1982
|
-
### v0.3.3 (2026-07-29)
|
|
1983
|
-
- Union member-type dedup: a union selected more than once on a struct now
|
|
1984
|
-
collapses to one Ruby type family instead of a distinct per-field family with
|
|
1985
|
-
identical members — so a consumer gets a single exhaustive
|
|
1986
|
-
`case … T.absurd` across every field of that union. Structurally different
|
|
1987
|
-
selections stay distinct types. (First cut: same-struct siblings; regenerate
|
|
1988
|
-
checked-in modules to pick it up.)
|
|
1989
|
-
|
|
1990
|
-
### v0.3.2 (2026-07-29)
|
|
1991
|
-
- `register_scalar` accepts a `Type.field` coordinate to override how one
|
|
1992
|
-
field's scalar deserializes — so the same scalar can be different Ruby types
|
|
1993
|
-
across fields (`register_scalar("User.birthday", Date)` while
|
|
1994
|
-
`ISO8601DateTime` stays a `Time` elsewhere). Field overrides win over the
|
|
1995
|
-
scalar-name registration; both stack global-then-client. The coordinate is
|
|
1996
|
-
validated against the schema (a typo'd or non-scalar field raises). Same
|
|
1997
|
-
method, same signature — a `.` in the name selects the field form.
|
|
1998
|
-
|
|
1999
|
-
### v0.3.1 (2026-07-28)
|
|
2000
|
-
- `GraphWeaver.extend_t_sig` controls whether generated modules/structs emit
|
|
2001
|
-
`extend T::Sig`. Default (`nil`) auto-detects a global T::Sig injection
|
|
2002
|
-
(`class Module; include T::Sig`) and skips the now-redundant `extend` — so
|
|
2003
|
-
generated code stays clean under rubocop's `Sorbet/RedundantExtendTSig`.
|
|
2004
|
-
Force with `true`/`false`; `false` requires the global include.
|
|
2005
|
-
|
|
2006
|
-
### v0.3.0 (2026-07-28)
|
|
2007
|
-
- Renamed `register_type` to `extend_type` to disambiguate intent: it
|
|
2008
|
-
*decorates* a generated struct with mixin modules/helpers — it doesn't
|
|
2009
|
-
define or replace a type's deserialization (that's `register_scalar` /
|
|
2010
|
-
`register_enum`, for leaf types, whose Ruby shape is fixed; a composite's
|
|
2011
|
-
shape is per-query, so there's nothing fixed to replace). No deprecation —
|
|
2012
|
-
the old name is dropped.
|
|
2013
|
-
- Invalid query input now raises `GraphWeaver::InputError` (under the
|
|
2014
|
-
`GraphWeaver::Error` umbrella) instead of a raw `ArgumentError` /
|
|
2015
|
-
`KeyError` / sorbet `TypeError`: an unknown or typo'd input key, a
|
|
2016
|
-
missing required field, an out-of-range enum, or a wrong-typed field
|
|
2017
|
-
when an input object is built from a hash through `coerce`. Carries
|
|
2018
|
-
`#field` / `#struct` and a JSON-ready `#to_h` — one rescue point for
|
|
2019
|
-
returning a 422 at an API boundary. Top-level *scalar* kwargs still
|
|
2020
|
-
fail like any Ruby method call (sorbet `TypeError` / `ArgumentError`).
|
|
2021
|
-
|
|
2022
|
-
### v0.2.2 (2026-07-22)
|
|
2023
|
-
- Generated modules expose from_response / from_response! alongside
|
|
2024
|
-
execute / execute!: deserialize a raw GraphQL response (fetched by any
|
|
2025
|
-
client) into the typed envelope without going through the transport.
|
|
2026
|
-
execute now delegates to from_response
|
|
2027
|
-
|
|
2028
|
-
### v0.2.1 (2026-07-13)
|
|
2029
|
-
- Conventional paths are appendable lists: queries_paths /
|
|
2030
|
-
generated_paths (singular accessors read the first entry, so existing
|
|
2031
|
-
config keeps working); load_generated!, Client#load_queries!, and the
|
|
2032
|
-
Railtie walk every entry — append spec/support/graphql/* from a spec
|
|
2033
|
-
helper to load test-only queries. Entries may be globs, and the
|
|
2034
|
-
generated default includes app/graphql/*/generated so per-schema
|
|
2035
|
-
layouts auto-load
|
|
2036
|
-
- inputs_module derives from the output path: multi-schema layouts name
|
|
2037
|
-
each schema's module after its directory
|
|
2038
|
-
(app/graphql/github/generated -> GithubInputs), the conventional
|
|
2039
|
-
layout keeps GraphQLInputs; GraphWeaver.inputs_module= and
|
|
2040
|
-
generate!(inputs_module:) still override
|
|
2041
|
-
- Shared types split one-file-per-type: generated/inputs/ holds each
|
|
2042
|
-
input struct/enum in its own small file (PokeAPI: 573 files, median
|
|
2043
|
-
24 lines vs one 11.5k-line blob) with inputs.rb as the manifest
|
|
2044
|
-
(forward declarations make load order irrelevant); regeneration
|
|
2045
|
-
prunes files for types the schema dropped, verify flags strays;
|
|
2046
|
-
generate!/verify take inputs_module: per invocation (multi-schema
|
|
2047
|
-
apps generate into different modules)
|
|
2048
|
-
- Shared input types: generate! emits every variable type (input
|
|
2049
|
-
structs + their enums + mapped-enum tables) ONCE per schema into
|
|
2050
|
-
generated/inputs.rb (module GraphQLInputs; GraphWeaver.inputs_module=
|
|
2051
|
-
renames, shared_inputs: false opts out), with query modules aliasing
|
|
2052
|
-
only what their own surface references — AdoptQuery::AdoptionInput
|
|
2053
|
-
keeps working and shared types gain one identity across modules.
|
|
2054
|
-
Three filtered Hasura queries: 34,684 lines inline -> 11,754 shared
|
|
2055
|
-
(~90 lines per query module)
|
|
2056
|
-
- BREAKING (vs 0.2.0): auto_fake is opt-in again — require
|
|
2057
|
-
"graph_weaver/rspec" no longer swaps every example onto a fake;
|
|
2058
|
-
set config.auto_fake = true explicitly (the schema still auto-locates
|
|
2059
|
-
once you do). Less magic, no unexpected behavior
|
|
2060
|
-
- Generated input structs are table-driven: typed consts + a per-field
|
|
2061
|
-
FIELDS table (conversions as lambdas) interpreted by the
|
|
2062
|
-
GraphWeaver::InputStruct runtime, replacing unrolled
|
|
2063
|
-
serialize/coerce/value_at per struct — a bool_exp-heavy PokeAPI module
|
|
2064
|
-
shrinks 29k -> 11.5k lines (-60%) with identical behavior (nil
|
|
2065
|
-
omission, wire-value enums, nested/recursive coercion, spellchecked
|
|
2066
|
-
unknown keys all covered by the existing suite)
|
|
2067
|
-
- Internal: Node base class for the codegen IR protocol; module
|
|
2068
|
-
assembly moved from Codegen#generate into Emit#emit_module
|
|
2069
|
-
(byte-identical output)
|
|
2070
|
-
|
|
2071
|
-
### v0.2.0 (2026-07-12)
|
|
2072
|
-
- Cleanup pass (staff-engineer review): scalar registrations get the
|
|
2073
|
-
same typo validation as enums/types; cassette replay stops recomputing
|
|
2074
|
-
its key per entry; dependency-order DFS uses hash bookkeeping (big
|
|
2075
|
-
bool_exp graphs); require/vocabulary residue swept; the vestigial
|
|
2076
|
-
graph_weaver/testing/rspec shim removed
|
|
2077
|
-
- BREAKING: "client" replaces "executor" across the whole surface.
|
|
2078
|
-
Generated modules: the per-call override is an optional POSITIONAL
|
|
2079
|
-
first argument — PersonQuery.execute(github, id: "1") — so variables
|
|
2080
|
-
own the entire kwarg namespace and NOTHING is reserved (a $client or
|
|
2081
|
-
$executor variable is fine; only Ruby keywords refuse); per-module is
|
|
2082
|
-
MyQuery.client=, the baked param is client:. GraphWeaver.executor= is
|
|
2083
|
-
gone — GraphWeaver.client= is the one ambient slot (auto_fake swaps
|
|
2084
|
-
it per example; explicit clients are self-contained and never see it).
|
|
2085
|
-
Client#executor is now Client#transport (transport: to bring your
|
|
2086
|
-
own); SchemaLoader.introspect/stale? speak transport. Renames:
|
|
2087
|
-
FakeExecutor => Testing::FakeClient, SequenceExecutor =>
|
|
2088
|
-
Testing::Sequence, RetryExecutor => GraphWeaver::Retry,
|
|
2089
|
-
Recording/ReplayExecutor => Recorder/Replayer, Cassette.use(client:)
|
|
2090
|
-
- Live federation integration: two Ruby subgraphs (apollo-federation
|
|
2091
|
-
gem) composed and routed by a real Apollo gateway (node harness under
|
|
2092
|
-
spec/support/federation), with GraphWeaver introspecting through the
|
|
2093
|
-
router and executing a query stitched across BOTH subgraphs — part of
|
|
2094
|
-
make integration. Complements the existing supergraph-SDL codegen spec
|
|
2095
|
-
- graphql-over-http: a non-2xx response carrying a GraphQL errors body
|
|
2096
|
-
(Apollo Server/Router send request errors as 4xx JSON) flows into the
|
|
2097
|
-
Response envelope so QueryError sees the structured errors; only
|
|
2098
|
-
non-GraphQL bodies (proxy pages) raise ServerError
|
|
2099
|
-
- Fix: input-struct serialize used bare locals (result/value) that a
|
|
2100
|
-
same-named prop silently shadowed — a field named "result" dropped
|
|
2101
|
-
its value onto the wrong target; generated locals now wear the
|
|
2102
|
-
reserved __gw prefix (GraphQL reserves __-names, so no collision is
|
|
2103
|
-
possible)
|
|
2104
|
-
- Input fields and variables whose Ruby name would be a keyword
|
|
2105
|
-
(nil/def/end/...), a generated method (serialize/to_h), or the
|
|
2106
|
-
reserved executor kwarg now refuse at generation with a pointed
|
|
2107
|
-
error instead of emitting broken code
|
|
2108
|
-
- Non-JSON 200 bodies (proxy error pages) classify as ServerError, and
|
|
2109
|
-
unserializable variables (NaN/Infinity) raise GraphWeaver::Error —
|
|
2110
|
-
raw JSON::* errors no longer escape the umbrella
|
|
2111
|
-
- Transports redact on inspect/to_s (class + url only) — Authorization
|
|
2112
|
-
headers can't leak through logs or exception dumps
|
|
2113
|
-
- Narrowed `... on X` selections require at least one unconditional
|
|
2114
|
-
field: with every field behind @skip/@include, a matching response is
|
|
2115
|
-
{} — byte-identical to a non-match — so generation refuses rather
|
|
2116
|
-
than silently dropping real matches to nil
|
|
2117
|
-
- Integration spec against Hasura's PokeAPI: snake_case codegen,
|
|
2118
|
-
recursive bool_exp variable filtering, untyped jsonb pass-through
|
|
2119
|
-
(make integration)
|
|
2120
|
-
- BREAKING: ValidationError now descends from GraphWeaver::Error (was
|
|
2121
|
-
ArgumentError) — one `rescue GraphWeaver::Error` catches everything
|
|
2122
|
-
- Input-struct .coerce raises on unknown hash keys with a spellchecked
|
|
2123
|
-
hint — a typo'd filter key no longer silently drops off the wire
|
|
2124
|
-
- Client registrations (register_type/register_enum) validate at the
|
|
2125
|
-
call site when the schema is already loaded; lazy clients still
|
|
2126
|
-
validate at generation
|
|
2127
|
-
- Unregistered custom scalars emit bare T.untyped (not
|
|
2128
|
-
T.nilable(T.untyped), an srb tc error under typed: strict)
|
|
2129
|
-
- Wire log lines carry [req N OperationName] tags; long queries
|
|
2130
|
-
(introspection) truncate at debug
|
|
2131
|
-
- Logging: GraphWeaver.logger (any stdlib-compatible Logger; Rails.logger
|
|
2132
|
-
auto-wired by the railtie) — wire traffic + timings at debug,
|
|
2133
|
-
introspection/cache/codegen at info, every raised error at warn
|
|
2134
|
-
- Recursive input types generate — self- and mutually-referential inputs
|
|
2135
|
-
(Hasura's bool_exp filter surface) emit dependency-ordered structs with
|
|
2136
|
-
runtime forward declarations for cycles, so variable-driven Hasura
|
|
2137
|
-
filtering works; previously raised "recursive input type"
|
|
2138
|
-
- Fix: snake_case GraphQL type names (Hasura, PostGraphile) camelize
|
|
2139
|
-
into valid Ruby constants — pokemon_v2_pokemon => PokemonV2Pokemon
|
|
2140
|
-
(previously generated a SyntaxError); wire names (__typename dispatch,
|
|
2141
|
-
registry keys) are untouched
|
|
2142
|
-
- Everything raised is rescuable: unparseable queries wrap as
|
|
2143
|
-
ValidationError (GraphQL::ParseError no longer leaks), and internal
|
|
2144
|
-
NotImplementedError raises (recursive inputs, unsupported kinds,
|
|
2145
|
-
subscriptions) became GraphWeaver::Error
|
|
2146
|
-
- Transport::HTTP takes open_timeout:/read_timeout: (defaults 10s/30s);
|
|
2147
|
-
timeouts surface as retriable TransportError
|
|
2148
|
-
- Transport::HTTP reuses its connection (keep-alive, mutex-serialized,
|
|
2149
|
-
keep_alive_timeout: for the idle window); any failure drops the socket
|
|
2150
|
-
so the next call starts fresh
|
|
2151
|
-
- GraphQLError#code also reads a top-level "type" (GitHub's dialect:
|
|
2152
|
-
NOT_FOUND, FORBIDDEN) when extensions.code is absent
|
|
2153
|
-
- Typo'd client-scoped registrations raise at generation with a
|
|
2154
|
-
spellchecked hint (register_type("Pett") => "did you mean 'Pet'?")
|
|
2155
|
-
instead of silently no-oping
|
|
2156
|
-
- Abstract selections narrow: __typename is only required when the
|
|
2157
|
-
selection varies by concrete type. Interface-level-fields-only
|
|
2158
|
-
selections generate one shared struct (no dispatch); a single
|
|
2159
|
-
`... on X` condition generates X's struct, always nilable — a
|
|
2160
|
-
non-matching runtime type casts to nil, so narrowing doubles as
|
|
2161
|
-
filtering
|
|
2162
|
-
- Zero-config rspec: require "graph_weaver/rspec" now defaults
|
|
2163
|
-
auto_fake on and auto-locates the schema from the committed dump
|
|
2164
|
-
(config.schema= / config.auto_fake = false to override) — one line is
|
|
2165
|
-
the whole test setup in a conventional app
|
|
2166
|
-
- examples/: runnable demos, all directly executable — countries.rb
|
|
2167
|
-
(public API, no auth, all dynamic), rick_and_morty.rb (filtered
|
|
2168
|
-
search, pagination, a block-built type helper), and github/ (auth,
|
|
2169
|
-
checked-in generated modules; stars the repo ⭐ then tours the
|
|
2170
|
-
stargazers, their top repos, and what else they've starred); excluded
|
|
2171
|
-
from the gem package
|
|
2172
|
-
- Fix: requires: now load before codec probing, so inference sees
|
|
2173
|
-
methods the required file provides — register_scalar("DateTime", Time,
|
|
2174
|
-
requires: "time") correctly infers Time.parse in a fresh process
|
|
2175
|
-
(previously the cast was silently skipped unless "time" was already
|
|
2176
|
-
loaded)
|
|
2177
|
-
- docs/quickstart.md renamed to docs/getting_started.md
|
|
2178
|
-
- Rails Railtie: the graph_weaver:* rake tasks self-register (no
|
|
2179
|
-
Rakefile edit) and depend on :environment, and generated modules load
|
|
2180
|
-
at boot (after initializers) when generated_path exists; outside
|
|
2181
|
-
Rails, require "graph_weaver/tasks" and call load_generated! as before
|
|
2182
|
-
- BREAKING (vs 0.1.0): reset_scalars! lost its coerce: flavor —
|
|
2183
|
-
GraphWeaver.auto_coerce = true is the one way to default-coerce
|
|
2184
|
-
(broader: convertible built-ins AND full cast/serialize scalars,
|
|
2185
|
-
resolved lazily, per-registration coerce: still wins)
|
|
2186
|
-
- GraphWeaver.client= — the blessed global wiring: assign the app's
|
|
2187
|
-
default client and generated modules resolve through it (per call ->
|
|
2188
|
-
per module -> baked -> executor= -> client). executor= stays as the
|
|
2189
|
-
low-level override, so test fakes still win
|
|
2190
|
-
- Enum mappings: register_enum("Species", PetKind) (+ bulk
|
|
2191
|
-
register_enums, client-scoped variants) — generated code speaks YOUR
|
|
2192
|
-
T::Enum, with the wire mapping inferred by name, pinned via map:,
|
|
2193
|
-
exhaustiveness-checked at generation (fails naming gaps), and
|
|
2194
|
-
fallback: to absorb unknown wire values on cast (inputs stay strict);
|
|
2195
|
-
translation tables emitted into the source (X_FROM_WIRE / X_TO_WIRE)
|
|
2196
|
-
- Type helpers: register_type("Pet", PetHelpers) (global or
|
|
2197
|
-
client-scoped, additive) — app-owned modules included into every
|
|
2198
|
-
struct generated from that GraphQL type, so derived values live as
|
|
2199
|
-
methods beside the honest wire data and srb tc checks them against
|
|
2200
|
-
each query's selection. Or build the mixin inline with a block
|
|
2201
|
-
(module_eval'd into an auto-named GraphWeaver::TypeHelpers constant —
|
|
2202
|
-
quick decoration, invisible to srb tc)
|
|
2203
|
-
- BREAKING (vs 0.1.0): register_scalar takes the type positionally —
|
|
2204
|
-
register_scalar("Money", Money, requires: ...) — matching the new
|
|
2205
|
-
registrars: the GraphQL name + your Ruby type up front, options as
|
|
2206
|
-
kwargs
|
|
2207
|
-
- GraphWeaver::Client — transport, schema, and scalars for one server in
|
|
2208
|
-
one object: GraphWeaver.new(url_or_schema) takes a url (transport
|
|
2209
|
-
built, schema introspected lazily per cache:/ttl:) or a schema source
|
|
2210
|
-
(live class — also the in-process executor — or a path/SDL/dump);
|
|
2211
|
-
#parse and #execute/#execute! bind the implicit schema + transport;
|
|
2212
|
-
#register_scalar scopes scalar mappings to the client (overlaying the
|
|
2213
|
-
global registry), so two servers can disagree about a scalar type
|
|
2214
|
-
- BREAKING: GraphWeaver.connect removed — GraphWeaver.new(url) replaces
|
|
2215
|
-
it (wire generated modules with GraphWeaver.executor = client.executor)
|
|
2216
|
-
- BREAKING: the one-shots are now GraphWeaver.execute(url_or_schema,
|
|
2217
|
-
query, **variables) / execute! — Client#execute on a throwaway client;
|
|
2218
|
-
variables are plain kwargs, as on a generated module
|
|
2219
|
-
- Client#load_queries! — parse every query file into modules named like
|
|
2220
|
-
generation would name them (reloadable; namespace: to scope): the
|
|
2221
|
-
no-build-step analog of generate! + load_generated!
|
|
2222
|
-
- Introspected schema dumps record provenance (source url + timestamp):
|
|
2223
|
-
a parsable SDL header comment, a "graph_weaver" sibling key in JSON —
|
|
2224
|
-
read it back with SchemaLoader.provenance(path), check drift with
|
|
2225
|
-
SchemaLoader.stale?(path) or rake graph_weaver:schema:verify, rewrite
|
|
2226
|
-
with rake graph_weaver:schema:refresh (GRAPHWEAVER_AUTH for tokens)
|
|
2227
|
-
- generate!/verify_generated!/rake auto-locate the schema dump at
|
|
2228
|
-
schema_path in any supported format; SchemaLoader.locate is public
|
|
2229
|
-
- Calling a result field by its camelCase wire name raises a pointed
|
|
2230
|
-
NoMethodError naming the snake_case prop that does exist
|
|
2231
|
-
(result.addPet => "use 'add_pet'"), and near-miss typos in either
|
|
2232
|
-
casing get a spellchecked suggestion (result.addPt => "did you mean
|
|
2233
|
-
'add_pet'?") — the runtime companion to srb tc's static flag
|
|
2234
|
-
- BREAKING: an operation whose only variable is a required input object
|
|
2235
|
-
(the Relay convention) now flattens the input's fields into execute's
|
|
2236
|
-
kwargs — AdoptQuery.execute!(name:, species:) instead of
|
|
2237
|
-
execute!(input: {...}); multi-variable / nullable-input operations
|
|
2238
|
-
keep the input: kwarg (struct or hash)
|
|
2239
|
-
- Enum kwargs accept the T::Enum or its wire value (T.any(Enum, String))
|
|
2240
|
-
everywhere — variables now match input-hash fields
|
|
2241
|
-
- BREAKING: HttpExecutor / FaradayExecutor are now Transport::HTTP /
|
|
2242
|
-
Transport::Faraday, subclasses of the new abstract GraphWeaver::Transport
|
|
2243
|
-
base, which owns the shared flow (encode, TransportError reclassify,
|
|
2244
|
-
non-2xx ServerError, parse) — a custom transport just implements
|
|
2245
|
-
post(body) => [status, body]. Opt-in require moved:
|
|
2246
|
-
"graph_weaver/faraday_executor" -> "graph_weaver/transport/faraday"
|
|
2247
|
-
- SchemaLoader.introspect cache: reuses a fresh dump in ANY supported
|
|
2248
|
-
format before re-introspecting (an existing schema.graphql wins over
|
|
2249
|
-
writing schema.json), and accepts :json / :graphql / :gql to pick the
|
|
2250
|
-
format at GraphWeaver.schema_path's location
|
|
2251
|
-
- rubydoc.info rendering: ship .yardopts (markdown markup, docs/ guides
|
|
2252
|
-
as extra files) and re-indent docstring examples so code blocks and
|
|
2253
|
-
backticks render; make docs previews locally
|
|
2254
|
-
- GraphWeaver.connect(url, auth:, headers:, retries:): one-shot setup —
|
|
2255
|
-
best transport (Faraday when the app loads it; detection is defined?,
|
|
2256
|
-
never a require), bearer/verbatim auth, opt-in RetryExecutor wrapping
|
|
2257
|
-
(true / options Hash; off by default), wired in as the global executor
|
|
2258
|
-
- Generation workflow: GraphWeaver.generate! (queries dir -> generated
|
|
2259
|
-
dir), verify_generated! (the freshness guard — raises naming stale
|
|
2260
|
-
files), load_generated! (factory_bot-style explicit loading), rake
|
|
2261
|
-
tasks (require "graph_weaver/tasks": graph_weaver:generate / :verify),
|
|
2262
|
-
all defaulting to configurable conventional paths (queries_path /
|
|
2263
|
-
generated_path / schema_path)
|
|
2264
|
-
- GraphWeaver.auto_coerce = true: default input coercion for scalars
|
|
2265
|
-
without an explicit coerce:, resolved lazily at generation time (no
|
|
2266
|
-
reset_scalars! ordering dance) — convertible built-ins take their
|
|
2267
|
-
conversion, cast/serialize pairs take parse-style coercion
|
|
2268
|
-
- SchemaLoader.introspect cache: true — caches at GraphWeaver.schema_path,
|
|
2269
|
-
in the format the extension picks: .json (verbatim wire artifact) or
|
|
2270
|
-
.graphql/.gql (SDL — human-readable, PR-reviewable diffs);
|
|
2271
|
-
the same dump rake graph_weaver:generate reads
|
|
2272
|
-
- docs/transports.md: connect, the executor contract, Faraday, retries
|
|
2273
|
-
- Cassette workflow: GRAPHWEAVER_RECORD=1 / config.record force
|
|
2274
|
-
re-recording; config.anonymize scrubs responses as they are recorded
|
|
2275
|
-
(caller sees the anonymized data too, so assertions hold on replay);
|
|
2276
|
-
rake graph_weaver:cassettes:anonymize; docs/cassettes.md guide
|
|
2277
|
-
- auto_coerce reaches input-object fields: raw scalar values inside
|
|
2278
|
-
input hashes coerce via the registry, mutations included
|
|
2279
|
-
- RetryExecutor: composable retries over any transport — tries:,
|
|
2280
|
-
exponential/linear/custom backoff with jitter and max clamp,
|
|
2281
|
-
retry-by-error-class (5xx yes, 4xx no by default; retry_if: override)
|
|
2282
|
-
and retry-by-GraphQL-code (retry_codes: ["THROTTLED"])
|
|
2283
|
-
|
|
2284
|
-
### v0.1.0 (2026-07-11)
|
|
2285
|
-
- Structured errors: execute returns a typed Response envelope (#data/#data!,
|
|
2286
|
-
#errors, #errors?, #extensions) instead of raising on GraphQL errors, so
|
|
2287
|
-
partial data and top-level extensions (cost/throttle) survive. Error classes
|
|
2288
|
-
under GraphWeaver::Error — TransportError (network), ServerError (non-2xx
|
|
2289
|
-
HTTP, #status/#body), QueryError (#errors/#data/#extensions/#codes),
|
|
2290
|
-
ValidationError (build-time) — plus a GraphQLError value object with #code.
|
|
2291
|
-
Transport-error classification is an extensible Set (GraphWeaver.transport_errors
|
|
2292
|
-
/ register_transport_error): each transport seeds its own network exceptions
|
|
2293
|
-
and apps can add more (e.g. a connection-pool timeout).
|
|
2294
|
-
The envelope is a single generic GraphWeaver::Response[Result] (no per-query
|
|
2295
|
-
wrapper class). execute! is the shortcut for execute(...).data! — the typed
|
|
2296
|
-
result or a raised QueryError — on both generated modules and the one-shot
|
|
2297
|
-
GraphWeaver.execute!/execute.
|
|
2298
|
-
BREAKING: module #execute returns Response; use #execute! (or #data!) for
|
|
2299
|
-
the old raise-or-result behavior. GraphWeaver.execute now returns the
|
|
2300
|
-
envelope too; GraphWeaver.execute! returns the result.
|
|
2301
|
-
- GraphWeaver.register_scalar: custom scalar deserialization into rich Ruby
|
|
2302
|
-
objects. cast/serialize inferred from a class type via paired codecs
|
|
2303
|
-
(.parse/#to_s or .load/.dump), or given as a Symbol/Proc (:itself opts out);
|
|
2304
|
-
requires: emits (validated, and require-checked when type: is a class)
|
|
2305
|
-
requires into generated source — the built-in Date scalar carries
|
|
2306
|
-
require "date" so Date-using queries are self-contained; coerce: true lets a
|
|
2307
|
-
variable accept the value or its raw input (coerce: :to_f for a built-in
|
|
2308
|
-
conversion), casting/converting the latter — reset_scalars!(coerce: true)
|
|
2309
|
-
reloads the built-ins coercible; built-in scalars pre-registered in one
|
|
2310
|
-
overridable registry (reset_scalars!/clear_scalars!)
|
|
2311
|
-
- FaradayExecutor: url, Faraday connection, or middleware block
|
|
2312
|
-
- GraphWeaver.executor default transport; per-module executor= override
|
|
2313
|
-
- GraphWeaver.parse and GraphWeaver.execute (dynamic queries)
|
|
2314
|
-
- Codegen.generate shorthand; executor: takes a constant; module_name
|
|
2315
|
-
derived from operation or file name
|
|
2316
|
-
- Error ergonomics: schema_stale? (validation-shaped rejections hint at
|
|
2317
|
-
regeneration), errors_at(path) + each_error/errors_by_field filtering,
|
|
2318
|
-
#report (field-keyed rollup with entity ids resolved from partial
|
|
2319
|
-
data), #to_h across the hierarchy (JSON-ready machine output), and
|
|
2320
|
-
GraphWeaver::TypeError wrapping cast failures with the failing struct
|
|
2321
|
-
- SchemaLoader: introspect(executor, cache:, ttl:) fetches schemas from
|
|
2322
|
-
live endpoints with file caching; load accepts introspection JSON /
|
|
2323
|
-
SDL content / Hashes as well as paths (cache round-trips)
|
|
2324
|
-
- GraphWeaver::Testing (require "graph_weaver/testing", or
|
|
2325
|
-
"graph_weaver/rspec" for the rspec integration): FakeExecutor
|
|
2326
|
-
fabricates schema-correct castable responses (mode: :faker semantic
|
|
2327
|
-
values / :literal; overrides by GraphQL name; seeded; list_size /
|
|
2328
|
-
null_chance), failure simulation (Failure.transport/server/graphql/
|
|
2329
|
-
throttled/stale_schema, SequenceExecutor for retries, fail_at: with
|
|
2330
|
-
spec-correct null propagation, corrupt: for derived type mismatches),
|
|
2331
|
-
cassette record/replay above the transport, and Cassette#anonymize!
|
|
2332
|
-
(shape-preserving, consistent id mapping). rspec: seed follows
|
|
2333
|
-
--seed; auto_fake installs a fake executor per example
|
|
2334
|
-
- one-off integration specs against live GitHub + Countries APIs
|
|
2335
|
-
(make integration)
|
|
2336
|
-
- Input objects: INPUT_OBJECT variables generate module-level T::Structs
|
|
2337
|
-
with serialize (aliased to_h) producing the wire hash; execute kwargs
|
|
2338
|
-
also accept plain hashes, normalized + type-checked via the generated
|
|
2339
|
-
.coerce (underscored Symbol/String keys, enums as instances or wire
|
|
2340
|
-
values, nested inputs as hashes)
|
|
2341
|
-
- fields under @skip/@include generate nilable regardless of schema
|
|
2342
|
-
nullability; FakeExecutor honors first/last/limit when sizing lists
|
|
2343
|
-
- eval hardening for parse: module names must be constant names, and
|
|
2344
|
-
QUERY heredocs can't be terminated early by block strings
|
|
2345
|
-
- GraphWeaver::Selection: one shared query-walk (codegen, FakeExecutor,
|
|
2346
|
-
anonymizer); codegen split into scalar_type / nodes / emit
|
|
2347
|
-
- docs/: generated_modules, real_world, scalars, errors, testing;
|
|
2348
|
-
README slimmed to pitch + quickstart
|
|
2349
|
-
|
|
2350
|
-
### v0.0.1 (2026-07-07)
|
|
2351
|
-
- voila: typed codegen (T::Structs, T::Enums, typed variable kwargs)
|
|
2352
|
-
- queries + mutations; fragments, unions, interfaces, enums, custom scalars
|
|
2353
|
-
- schema sources: live class, introspection JSON, SDL (incl. supergraph)
|
|
2354
|
-
- pluggable executor: in-process schema or HTTP
|
|
2355
|
-
- dynamic (no-build) mode for development
|