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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +153 -4
  4. data/README.md +45 -79
  5. data/docs/alternatives.md +195 -0
  6. data/docs/cassettes.md +61 -50
  7. data/docs/editors.md +32 -47
  8. data/docs/errors.md +360 -103
  9. data/docs/federation.md +692 -473
  10. data/docs/generated_modules.md +441 -314
  11. data/docs/getting_started.md +370 -194
  12. data/docs/i18n.md +171 -0
  13. data/docs/logging.md +197 -50
  14. data/docs/real_world.md +42 -27
  15. data/docs/scalars.md +307 -176
  16. data/docs/testing.md +473 -220
  17. data/docs/transports.md +224 -151
  18. data/docs/upgrading.md +258 -305
  19. data/examples/README.md +38 -0
  20. data/examples/countries.rb +39 -0
  21. data/examples/federation.rb +62 -0
  22. data/examples/github/generate.rb +20 -0
  23. data/examples/github/generated/star_mutation.rb +126 -0
  24. data/examples/github/generated/stargazers_query.rb +232 -0
  25. data/examples/github/generated/starred_query.rb +151 -0
  26. data/examples/github/queries/star.graphql +8 -0
  27. data/examples/github/queries/stargazers.graphql +22 -0
  28. data/examples/github/queries/starred.graphql +11 -0
  29. data/examples/github/run.rb +43 -0
  30. data/examples/github/setup.rb +18 -0
  31. data/examples/rick_and_morty.rb +57 -0
  32. data/graph_weaver.gemspec +19 -3
  33. data/lib/generators/graph_weaver/install_generator.rb +138 -4
  34. data/lib/graph_weaver/client.rb +69 -11
  35. data/lib/graph_weaver/codegen/aliases.rb +7 -5
  36. data/lib/graph_weaver/codegen/emit.rb +98 -29
  37. data/lib/graph_weaver/codegen/enum_type.rb +2 -1
  38. data/lib/graph_weaver/codegen/nodes.rb +39 -6
  39. data/lib/graph_weaver/codegen/registry.rb +175 -0
  40. data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
  41. data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
  42. data/lib/graph_weaver/codegen.rb +406 -195
  43. data/lib/graph_weaver/coerce.rb +155 -26
  44. data/lib/graph_weaver/context_seam.rb +54 -0
  45. data/lib/graph_weaver/errors.rb +284 -46
  46. data/lib/graph_weaver/federation.rb +129 -27
  47. data/lib/graph_weaver/graph.rb +315 -0
  48. data/lib/graph_weaver/hints.rb +100 -24
  49. data/lib/graph_weaver/in_process.rb +27 -15
  50. data/lib/graph_weaver/input_struct.rb +119 -32
  51. data/lib/graph_weaver/internal/endpoint.rb +80 -0
  52. data/lib/graph_weaver/internal/headers.rb +70 -0
  53. data/lib/graph_weaver/internal/overrides.rb +67 -5
  54. data/lib/graph_weaver/internal/planner.rb +45 -15
  55. data/lib/graph_weaver/internal/refusal.rb +49 -0
  56. data/lib/graph_weaver/internal/schemas.rb +23 -9
  57. data/lib/graph_weaver/internal/selection.rb +34 -0
  58. data/lib/graph_weaver/internal/server_input.rb +251 -0
  59. data/lib/graph_weaver/internal/test_clients.rb +276 -0
  60. data/lib/graph_weaver/internal/unused.rb +287 -0
  61. data/lib/graph_weaver/internal/values.rb +40 -4
  62. data/lib/graph_weaver/internal.rb +249 -14
  63. data/lib/graph_weaver/log_subscriber.rb +74 -0
  64. data/lib/graph_weaver/logging.rb +163 -19
  65. data/lib/graph_weaver/query_module.rb +44 -3
  66. data/lib/graph_weaver/railtie.rb +237 -17
  67. data/lib/graph_weaver/representation.rb +55 -17
  68. data/lib/graph_weaver/result_struct.rb +90 -0
  69. data/lib/graph_weaver/retry.rb +45 -13
  70. data/lib/graph_weaver/rspec.rb +404 -93
  71. data/lib/graph_weaver/schema_loader.rb +266 -56
  72. data/lib/graph_weaver/tasks.rb +380 -89
  73. data/lib/graph_weaver/testing/cassette.rb +34 -10
  74. data/lib/graph_weaver/testing/endpoint.rb +107 -0
  75. data/lib/graph_weaver/testing/failure.rb +69 -12
  76. data/lib/graph_weaver/testing/fake_client.rb +164 -45
  77. data/lib/graph_weaver/testing/router.rb +64 -13
  78. data/lib/graph_weaver/testing.rb +200 -58
  79. data/lib/graph_weaver/transport/faraday.rb +41 -8
  80. data/lib/graph_weaver/transport/http.rb +48 -6
  81. data/lib/graph_weaver/transport.rb +134 -27
  82. data/lib/graph_weaver/version.rb +1 -1
  83. data/lib/graph_weaver.rb +495 -106
  84. metadata +71 -3
  85. 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