graph_weaver 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1447 -1
- data/Gemfile +8 -0
- data/Gemfile.lock +151 -2
- data/README.md +20 -6
- data/docs/alternatives.md +201 -0
- data/docs/cassettes.md +17 -1
- data/docs/errors.md +382 -17
- data/docs/federation.md +469 -63
- data/docs/generated_modules.md +231 -15
- data/docs/getting_started.md +497 -104
- data/docs/i18n.md +234 -0
- data/docs/logging.md +160 -24
- data/docs/real_world.md +28 -0
- data/docs/scalars.md +190 -26
- data/docs/testing.md +457 -58
- data/docs/transports.md +164 -19
- data/docs/upgrading.md +328 -3
- data/graph_weaver.gemspec +7 -0
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +47 -10
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +404 -197
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/errors.rb +264 -34
- data/lib/graph_weaver/federation.rb +119 -26
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +17 -11
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +78 -0
- data/lib/graph_weaver/internal/headers.rb +51 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +183 -1
- data/lib/graph_weaver/log_subscriber.rb +66 -0
- data/lib/graph_weaver/logging.rb +136 -12
- data/lib/graph_weaver/query_module.rb +36 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +33 -5
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +221 -49
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +6 -5
- data/lib/graph_weaver/testing/endpoint.rb +106 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +133 -44
- data/lib/graph_weaver/testing/router.rb +58 -11
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +46 -4
- data/lib/graph_weaver/transport.rb +109 -26
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +474 -106
- metadata +56 -1
data/docs/transports.md
CHANGED
|
@@ -21,11 +21,14 @@ without one. `load_queries!` is the same rule over a directory.
|
|
|
21
21
|
|
|
22
22
|
A *transport* is the network end of that contract — GraphQL-over-HTTP. The bundled
|
|
23
23
|
two — `Transport::HTTP` (net/http, zero dependencies, loaded by default)
|
|
24
|
-
and `Transport::Faraday` (opt-in
|
|
24
|
+
and `Transport::Faraday` (opt-in: naming it loads faraday, so an app that
|
|
25
|
+
doesn't needs no faraday) — subclass `GraphWeaver::Transport`,
|
|
25
26
|
which owns the shared flow: encode the request, reclassify network
|
|
26
27
|
failures as `TransportError`, raise `ServerError` on non-2xx, parse the
|
|
27
28
|
body. A subclass only implements `post(body) => [status, body]` — that's
|
|
28
|
-
the whole recipe for bringing your own HTTP client.
|
|
29
|
+
the whole recipe for bringing your own HTTP client. Return the response
|
|
30
|
+
headers as a third element, downcased, and `ServerError#headers` carries
|
|
31
|
+
them; two elements is still a complete answer.
|
|
29
32
|
|
|
30
33
|
## One-shot setup: a client
|
|
31
34
|
|
|
@@ -41,20 +44,32 @@ lazily, and `parse`/`run` bound to both. A `Client` answers the client
|
|
|
41
44
|
contract itself, so it goes anywhere a transport does — `Retry.new(client)`,
|
|
42
45
|
`subgraphs:`, a cassette recorder.
|
|
43
46
|
|
|
44
|
-
- `auth:` — a token
|
|
45
|
-
|
|
47
|
+
- `auth:` — a token, or something answering `#call` that returns one per
|
|
48
|
+
request; "Bearer" is assumed unless it carries its own scheme
|
|
49
|
+
(`"Basic dXNlcjpwYXNz..."`)
|
|
46
50
|
- `transport:` — `:http` (the default) or `:faraday`
|
|
47
51
|
- `headers:` — anything else (API keys, custom headers)
|
|
48
|
-
- `retries:` — off by default;
|
|
49
|
-
|
|
50
|
-
(`backoff:`, `retry_codes:`, ...)
|
|
52
|
+
- `retries:` — off by default; every other [`Retry`](#retries) option sits
|
|
53
|
+
beside it (`backoff:`, `retry_codes:`, ...)
|
|
51
54
|
- `open_timeout:` / `read_timeout:` — seconds, defaulting to 10 and 30 on
|
|
52
55
|
either transport
|
|
56
|
+
- `pool_size:` — how many sockets the bundled HTTP transport keeps open,
|
|
57
|
+
defaulting to `RAILS_MAX_THREADS` (else 5). Refused with
|
|
58
|
+
`transport: :faraday`, whose adapter owns its own connections — a ceiling
|
|
59
|
+
here would be a number nothing reads
|
|
53
60
|
- `cache:` / `ttl:` — schema introspection caching (see
|
|
54
61
|
[real world](real_world.md)); url clients only — a schema source never
|
|
55
62
|
introspects, so passing them raises
|
|
56
63
|
- a block customizes the Faraday connection (Faraday only — raises without it)
|
|
57
64
|
|
|
65
|
+
They combine, so the whole thing is still one call:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
GraphWeaver.new(url, transport: :faraday, retries: 2) do |conn|
|
|
69
|
+
conn.response :logger
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
58
73
|
What you pass is what you get; the client logs which transport it built at
|
|
59
74
|
`info`.
|
|
60
75
|
|
|
@@ -138,7 +153,55 @@ requires a conforming client to accept, with the legacy type as
|
|
|
138
153
|
fallback), and `User-Agent: graph_weaver/<version>` so a server operator
|
|
139
154
|
can attribute the traffic. Anything you pass in `headers:` wins over
|
|
140
155
|
these. A prebuilt `Faraday::Connection` owns its own headers; only the
|
|
141
|
-
ones it leaves unset are filled in
|
|
156
|
+
ones it leaves unset are filled in — and Faraday's stock
|
|
157
|
+
`User-Agent: Faraday v…`, which it fills in for every connection whether
|
|
158
|
+
you asked or not, counts as unset.
|
|
159
|
+
|
|
160
|
+
**Who the graph thinks is calling.** Both transports also send
|
|
161
|
+
`apollographql-client-name` and `apollographql-client-version`, which is
|
|
162
|
+
what an Apollo Router or GraphOS keys client attribution on — per-client
|
|
163
|
+
SLOs and rate limits, and "who still asks for this deprecated field". The
|
|
164
|
+
name is your Rails application's (`Storefront`), or `graph_weaver` outside
|
|
165
|
+
Rails, since Apollo means the consuming *application*; the version is the
|
|
166
|
+
gem's, because graph_weaver can't know what your app calls its releases.
|
|
167
|
+
Both are plain headers, so `headers:` overrides them — which is how one app
|
|
168
|
+
names its several clients apart:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
GraphWeaver::Transport::HTTP.new(url, headers: {
|
|
172
|
+
"apollographql-client-name" => "storefront-checkout",
|
|
173
|
+
"apollographql-client-version" => ENV.fetch("GIT_SHA"),
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**A header that expires.** A header *value* may be anything answering `#call`,
|
|
178
|
+
on either transport, resolved per request rather than captured when the
|
|
179
|
+
transport was built — the same way a graph's [`schema`](federation.md) takes a
|
|
180
|
+
lambda. A value (or a call) of `nil` sends no such header; anything else is
|
|
181
|
+
sent as its `to_s`, so a numeric tenant id needs no ceremony:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
GraphWeaver::Transport::HTTP.new(url, headers: {
|
|
185
|
+
"Authorization" => -> { "Bearer #{Tokens.fetch}" }, # rotating token
|
|
186
|
+
"X-Tenant" => -> { Current.tenant&.id }, # nil ⇒ header omitted
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`auth:` is that header under a shorter name, so a rotating credential is
|
|
191
|
+
`GraphWeaver.new(url, auth: -> { Tokens.fetch })`. A prebuilt
|
|
192
|
+
`Faraday::Connection` owns its own headers, so a rotating credential there is
|
|
193
|
+
Faraday's middleware (`conn.request :authorization, "Bearer", -> { ... }`).
|
|
194
|
+
|
|
195
|
+
**Compression and proxies** need no configuration on either transport.
|
|
196
|
+
`net/http` — which both use underneath — asks for `gzip`/`deflate` on every
|
|
197
|
+
request and decodes what comes back, and it reads `http_proxy` / `HTTPS_PROXY`
|
|
198
|
+
and `no_proxy` from the environment. A proxy is never used for a loopback
|
|
199
|
+
address, which is Ruby's rule, not ours.
|
|
200
|
+
|
|
201
|
+
**The endpoint an error names** is the url with its userinfo and any secret
|
|
202
|
+
query parameter folded to `[FILTERED]` — see [errors](errors.md). `#url` on a
|
|
203
|
+
transport stays the real endpoint; `#safe_url` is the one that goes in a log
|
|
204
|
+
line, an exception or an APM payload.
|
|
142
205
|
|
|
143
206
|
**Request body.** `{"query": ..., "variables": ...}`, plus
|
|
144
207
|
`"operationName"` when the operation has a name — the field Apollo Studio,
|
|
@@ -148,6 +211,40 @@ module at generation, so the name is declared in the query too. A raw query
|
|
|
148
211
|
string handed straight to a transport falls back to the name in the document,
|
|
149
212
|
and a genuinely anonymous one sends no `operationName` key at all.
|
|
150
213
|
|
|
214
|
+
**Variables have to be JSON.** The body is one `application/json` document, so
|
|
215
|
+
every variable value, at any depth, must be something JSON carries: a string,
|
|
216
|
+
a number, a boolean, null, a list, an object — or a value with an honest
|
|
217
|
+
string form, which is how a `Date`, a `Time`, a `BigDecimal` or a `Symbol`
|
|
218
|
+
travels. A `File`, an `IO`, a `Pathname` or a plain object is refused before
|
|
219
|
+
the body is built, naming the variable: JSON would otherwise render it as its
|
|
220
|
+
`#to_s`, so `$file` reaches the server as `"#<File:0x00007f…>"` and is stored
|
|
221
|
+
as if it meant something. graph_weaver does not implement the [GraphQL
|
|
222
|
+
multipart request
|
|
223
|
+
spec](https://github.com/jaydenseric/graphql-multipart-request-spec), so an
|
|
224
|
+
`Upload!` argument needs your own transport or a separate upload endpoint —
|
|
225
|
+
registering a scalar can't help, because multipart restructures the whole
|
|
226
|
+
request rather than one value.
|
|
227
|
+
|
|
228
|
+
**No persisted-query id goes with it**, so a gateway safelist configured with
|
|
229
|
+
`require_id` refuses every request this client makes; automatic persisted
|
|
230
|
+
queries (APQ) are an optimization, so those just never kick in. Until the gem
|
|
231
|
+
sends one, `post` is the seam — it sees the encoded body and can put the hash
|
|
232
|
+
beside it:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class APQ < GraphWeaver::Transport::HTTP
|
|
236
|
+
def post(body)
|
|
237
|
+
request = JSON.parse(body)
|
|
238
|
+
sha = Digest::SHA256.hexdigest(request.fetch("query"))
|
|
239
|
+
extensions = { "persistedQuery" => { "version" => 1, "sha256Hash" => sha } }
|
|
240
|
+
status, response, headers = super(JSON.generate(request.except("query").merge("extensions" => extensions)))
|
|
241
|
+
return [status, response, headers] unless response.to_s.include?("PersistedQueryNotFound")
|
|
242
|
+
|
|
243
|
+
super(JSON.generate(request.merge("extensions" => extensions))) # register on miss
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
151
248
|
**Concurrency.** One transport is normally the whole app's transport
|
|
152
249
|
(`GraphWeaver.client = api`), so it has to serve every thread.
|
|
153
250
|
`Transport::HTTP` opens up to `pool_size:` sockets lazily and reuses the
|
|
@@ -165,6 +262,16 @@ Rails sizes its own connection pool from, because it is the same question:
|
|
|
165
262
|
how many requests this process can have in flight at once. Lower it for a
|
|
166
263
|
server that counts connections.
|
|
167
264
|
|
|
265
|
+
**The pool is fork-safe**, which is what a Puma or Unicorn worker under
|
|
266
|
+
`preload_app!` needs. A socket warmed before the fork — an initializer that
|
|
267
|
+
introspects the schema is enough — is otherwise inherited by every worker, and
|
|
268
|
+
nothing in a round trip says which process opened it, so two workers
|
|
269
|
+
interleaving on one fd hand each other's answers back. A child notices the pid
|
|
270
|
+
changed and starts over: the inherited sockets are **abandoned rather than
|
|
271
|
+
closed** (closing would take down the fd the parent is still using) and
|
|
272
|
+
reconnect on first use, and the permits are rebuilt, since any held at fork time
|
|
273
|
+
went with the threads that held them. There is no `after_fork` hook to write.
|
|
274
|
+
|
|
168
275
|
Under a fiber scheduler (`async`, Falcon) everything here works unchanged —
|
|
169
276
|
`SizedQueue`, `Mutex`, `net/http` and `Kernel#sleep` are all scheduler-aware,
|
|
170
277
|
so requests multiplex on one thread at thread-equivalent throughput. But
|
|
@@ -181,8 +288,14 @@ takes a `Client` or any bare transport/fake):
|
|
|
181
288
|
1. per call: `execute(client: some_client, ...)` — a kwarg like the
|
|
182
289
|
variables, and a name no GraphQL variable is allowed to take
|
|
183
290
|
2. per module: `MyQuery.client = something`
|
|
184
|
-
3.
|
|
185
|
-
|
|
291
|
+
3. a test mode's stand-in: under `graphql: :fake` / `:in_process` /
|
|
292
|
+
`:router`, built from the graph this module was generated from
|
|
293
|
+
4. baked constant: `Codegen.generate(..., client: "MyApi::CLIENT")` — the
|
|
294
|
+
constant's *name*, not the object, because generated source spells it
|
|
295
|
+
5. the app default: `GraphWeaver.client=`
|
|
296
|
+
|
|
297
|
+
The mode replaces what codegen baked in, not what your example said — 1 and 2
|
|
298
|
+
still win.
|
|
186
299
|
|
|
187
300
|
Nothing set anywhere raises, naming the two you'd usually reach for:
|
|
188
301
|
`no client configured — set GraphWeaver.client= or pass a client`.
|
|
@@ -209,12 +322,29 @@ GraphWeaver.new(
|
|
|
209
322
|
`GraphWeaver::Retry.new(inner_transport, ...)` takes the same options and
|
|
210
323
|
wraps any client/transport directly — the client just passes them along.
|
|
211
324
|
|
|
212
|
-
Defaults: transport failures always retry;
|
|
213
|
-
**408
|
|
214
|
-
won't fix it.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
325
|
+
Defaults: transport failures always retry; a response retries when its
|
|
326
|
+
status is 5xx or **408 or 429** — the rest of 4xx is a bug in the request,
|
|
327
|
+
retrying won't fix it. That's one rule for both shapes a failure arrives
|
|
328
|
+
in: raised as a `ServerError`, or returned in the envelope because the
|
|
329
|
+
server sent GraphQL errors alongside the status. Apollo Router does the
|
|
330
|
+
latter for everything it decides itself — rate limiting is `503` with a
|
|
331
|
+
`REQUEST_RATE_LIMITED` body — so a policy that read only the raised half
|
|
332
|
+
made exactly one attempt behind a router. `retry_codes:` adds the other
|
|
333
|
+
signal: error codes, at any status (off by default — pass the codes your
|
|
334
|
+
API uses, or `GraphWeaver::GraphQLError::THROTTLE_CODES`). Exhausting the
|
|
335
|
+
retries re-raises the last error (or returns the last response).
|
|
336
|
+
|
|
337
|
+
A `200` is never retried on its status, whatever it carries. A router that
|
|
338
|
+
gives up on a slow subgraph answers `200` with partial data and a
|
|
339
|
+
`GATEWAY_TIMEOUT` error: the caller already has an answer, and whether a
|
|
340
|
+
partial one is worth repeating is a judgment only the caller can make —
|
|
341
|
+
`retry_codes: ["GATEWAY_TIMEOUT"]` is how they say yes.
|
|
342
|
+
|
|
343
|
+
**Nothing else retries**, which is the half a script author needs: a `200`
|
|
344
|
+
the server stands behind, and an `InputError` (the variables never left the
|
|
345
|
+
process), are permanent by construction — the identical request gets the
|
|
346
|
+
identical answer. Only a failure the server itself marked transient, by
|
|
347
|
+
status or by code, is worth repeating.
|
|
218
348
|
|
|
219
349
|
`retries:` counts the attempts *after* the first, so
|
|
220
350
|
`GraphWeaver.new(url, retries: 3)` makes up to four and `retries: 0` never
|
|
@@ -229,20 +359,35 @@ it, and a second `charge` is worse than a failed one.
|
|
|
229
359
|
`retry_mutations: true` opts an idempotent API back in; the skipped
|
|
230
360
|
retry says so on the logger.
|
|
231
361
|
|
|
362
|
+
The cap is on **attempts**, not on a kind of failure: a mutation gets its one
|
|
363
|
+
attempt whatever `retry_on:` says, so a `ServerError` is not retried either —
|
|
364
|
+
not a 500, not a 429 that named a `Retry-After`. `retry_mutations: true` puts
|
|
365
|
+
the mutation back on the same budget as a query, for every one of them.
|
|
366
|
+
|
|
367
|
+
**Idempotency is the server's.** GraphWeaver never reads an `idempotencyKey`
|
|
368
|
+
input: it is an argument like any other, and nothing in the client
|
|
369
|
+
deduplicates on it. So before turning `retry_mutations: true` on for a
|
|
370
|
+
checkout, the *server* has to dedupe on that key. And either way a failed
|
|
371
|
+
response does not mean nothing happened — the request that timed out was
|
|
372
|
+
still delivered, so the order may exist behind the error your controller
|
|
373
|
+
rendered. Reconcile; don't assume.
|
|
374
|
+
|
|
232
375
|
**`Retry-After` wins over the backoff.** When the server names a delay
|
|
233
376
|
(seconds or an HTTP-date), that's the wait — the server is the only
|
|
234
377
|
party that knows when its window reopens. It's clamped to `max_delay:` so a
|
|
235
378
|
"come back in an hour" can't park a thread for an hour, and not
|
|
236
379
|
jittered, since it's an instruction rather than a guess.
|
|
237
380
|
|
|
238
|
-
`ServerError` carries the response `#headers
|
|
239
|
-
|
|
240
|
-
|
|
381
|
+
`ServerError` carries the response `#headers`, so the rate-limit budget and
|
|
382
|
+
request id are in hand without monkey-patching a transport. Look one up in
|
|
383
|
+
whatever casing the server used — field names are case-insensitive; iterating
|
|
384
|
+
them yields the downcased spelling:
|
|
241
385
|
|
|
242
386
|
```ruby
|
|
243
387
|
rescue GraphWeaver::ServerError => e
|
|
244
388
|
e.throttled? # 429, or 503 + Retry-After
|
|
245
389
|
e.retry_after # seconds, or nil
|
|
390
|
+
e.headers["Retry-After"] # == e.headers["retry-after"]
|
|
246
391
|
e.headers["x-ratelimit-remaining"]
|
|
247
392
|
end
|
|
248
393
|
```
|
data/docs/upgrading.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Upgrading
|
|
2
2
|
|
|
3
|
+
[Regenerate](#regenerate-on-every-upgrade) whichever version you're on, then
|
|
4
|
+
read the one section that is yours: from [0.6.1](#upgrading-from-061), from
|
|
5
|
+
[0.5.1](#upgrading-from-051), or [to 0.5.0](#upgrading-to-050) from anything
|
|
6
|
+
older.
|
|
7
|
+
|
|
3
8
|
## Regenerate on every upgrade
|
|
4
9
|
|
|
5
10
|
**Any release can change what codegen emits.** Patch releases included — most of
|
|
@@ -19,6 +24,326 @@ moved. That's the reminder working, not a false alarm.
|
|
|
19
24
|
Generation is deterministic, so the diff is exactly what the new version emits
|
|
20
25
|
differently and nothing else — worth reading rather than rubber-stamping.
|
|
21
26
|
|
|
27
|
+
## Upgrading from 0.6.1
|
|
28
|
+
|
|
29
|
+
Mostly mechanical. Everything that wants your hands, or changes under you, is
|
|
30
|
+
one row below; read the left column and skip what isn't yours. A typical app
|
|
31
|
+
ticks two or three.
|
|
32
|
+
|
|
33
|
+
| applies if you… | what changed |
|
|
34
|
+
|---|---|
|
|
35
|
+
| run a Rails app that configures no logger or instrumenter | **you start logging one info line per GraphQL call** — a production log-volume change, [first bullet below](#behavior-that-changed-under-you) |
|
|
36
|
+
| wrap a gateway in `Retry` | **a 5xx/429 that arrives with an errors body retries now** — real traffic, [below](#behavior-that-changed-under-you); `retries: 0` opts out |
|
|
37
|
+
| commit a schema dump introspected through a url carrying a token | **refresh it, and rotate the token if that file was pushed** — the dump recorded the url verbatim |
|
|
38
|
+
| keep a schema dump deliberately behind your own schema class | `verify` fails on it now |
|
|
39
|
+
| check in a composed supergraph as your dump | `schema:refresh` refuses it rather than overwriting it with the API schema — recompose instead |
|
|
40
|
+
| use `@oneOf` input types and commit a `.json` dump | `@oneOf` starts being enforced client-side once you regenerate |
|
|
41
|
+
| call `result.to_json`, or `render json: result` | it is the wire shape now, not `#inspect` or your prop names |
|
|
42
|
+
| send a `File`, `IO` or `Pathname` as a variable | refused at the wire, where `JSON.generate` used to ship its `#to_s` |
|
|
43
|
+
| pin a lowercase type name in `graphql_fake` | it works now; a near-miss *keyword* raises `ArgumentError` |
|
|
44
|
+
| tag specs `graphql: false`, or set `config.default_mode = nil` | both refused — [renames](#renames) |
|
|
45
|
+
| `rescue GraphWeaver::TypeError` or `GraphWeaver::ValidationError` | both constants are gone, with no alias — [renames](#renames) |
|
|
46
|
+
| subscribe to `"graph_weaver.execute"` | the event is `"execute.graph_weaver"` — [renames](#renames) |
|
|
47
|
+
| index a hash by an `InputError`'s `#field` | it names the input field now, not the variable — **nothing raises** |
|
|
48
|
+
| read an `InputError`'s `#details[:type]` | it is the GraphQL type now, never a Ruby class — **nothing raises** |
|
|
49
|
+
| read `payload[:status]` in an instrumentation subscriber | it is a Symbol; the HTTP status moved to `:http_status` — **nothing raises** |
|
|
50
|
+
| call `respond_to?` on a result struct | it stopped answering true for props that don't exist — **nothing raises** |
|
|
51
|
+
| generate a module with a baked `client:` | a `graphql:` tag now reaches it |
|
|
52
|
+
| set `config.context`, `config.schema` or `config.router` from a `before` hook | all three refused — they are suite setup |
|
|
53
|
+
| pass a `DateTime` where the schema says `Date` | refused — pass `.to_date` |
|
|
54
|
+
| register a scalar with your own `cast:`/`serialize:` | the same guard as the built-ins, and a proc that returns a value is refused |
|
|
55
|
+
| have a field named `class`, `hash`, `display`, `to_json`, `each` or `supplied` | the prop takes a trailing underscore |
|
|
56
|
+
| have an entity `@key` that selects through a list | the kwarg is a list now, not one hash — regenerate |
|
|
57
|
+
| build a type helper with `extend_type("Widget") { … }` | its constant is named for its graph and its type — regenerate |
|
|
58
|
+
| adopt `GraphWeaver.graph` | every queries directory then needs one |
|
|
59
|
+
| write `config.graph_weaver.<anything but watch>` | refused at boot |
|
|
60
|
+
| pass `SUPERGRAPH=` to any task but `federation:*` | refused, where it was ignored — a CI step that did it goes red |
|
|
61
|
+
| compare a `Testing::Router` error hash whole in a spec | a subgraph error carries `extensions: {"service" => …}` now |
|
|
62
|
+
| pass `seed:` to `graphql_router(fake: …)` | refused |
|
|
63
|
+
| require `graph_weaver/rspec` from `spec/support/` | check the glob is uncommented — rspec-rails ships it commented out |
|
|
64
|
+
| adopt `graphql: :wire` | it needs `require "webmock/rspec"`, not just the gem |
|
|
65
|
+
|
|
66
|
+
Then five commands, in order:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# 1. the two renames your own code holds
|
|
70
|
+
grep -rn "GraphWeaver::TypeError\|GraphWeaver::ValidationError" app lib spec
|
|
71
|
+
grep -rn "graph_weaver.execute" app lib config spec # the old event name
|
|
72
|
+
|
|
73
|
+
# 2. rewrite the dump: it drops a credential the url carried, and picks up
|
|
74
|
+
# isOneOf. Skip only if your dump is SDL and records no source url — and
|
|
75
|
+
# a composed supergraph refuses, since introspection can't rebuild one:
|
|
76
|
+
# `rover supergraph compose` is what rewrites that.
|
|
77
|
+
rake graph_weaver:schema:refresh
|
|
78
|
+
|
|
79
|
+
# 3. regenerate — also the graph name in every module, the underscored
|
|
80
|
+
# reserved props, as_json, and the client: and cast:/serialize: refusals
|
|
81
|
+
rake graph_weaver:generate
|
|
82
|
+
|
|
83
|
+
# 4. the renamed tag, the deleted nil, the seed: refusal
|
|
84
|
+
bundle exec rspec
|
|
85
|
+
|
|
86
|
+
# 5. the gate: red while any checked-in file is still what 0.6.1 wrote
|
|
87
|
+
rake graph_weaver:verify
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Two kinds of file answer that first grep, and only one needs your hands.**
|
|
91
|
+
Hits under your generated directory (`app/graphql/generated/` by default) are the
|
|
92
|
+
old names in machine-written code — step 3 rewrites them. Hits anywhere else are
|
|
93
|
+
yours: `CastError` and `QueryValidationError`, renamed by hand.
|
|
94
|
+
|
|
95
|
+
### Renames
|
|
96
|
+
|
|
97
|
+
| before | after |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `graphql: false` (rspec tag) | `graphql: :live` — the opt-out is your own client, which is a mode like the other four; `false` is refused, naming it |
|
|
100
|
+
| `config.default_mode = nil` | `config.default_mode = :live`, which is now the **default** — every example has exactly one mode, and `nil` is no longer a value it reads back |
|
|
101
|
+
| `GraphWeaver::TypeError` | `GraphWeaver::CastError` — the response wouldn't cast into the generated structs; the old name shadowed a core class it doesn't descend from. No alias: the old constant is gone, so a stale `rescue` is a `NameError` |
|
|
102
|
+
| `GraphWeaver::ValidationError` | `GraphWeaver::QueryValidationError` — build time, the *query* against the schema. Your input's validation is `InputError`. No alias here either |
|
|
103
|
+
| `"graph_weaver.execute"` | `"execute.graph_weaver"` — `<event>.<namespace>`, the way every notification in this ecosystem is spelled, and what `LogSubscriber.attach_to` and an APM's namespace routing key on. Subscribe through `GraphWeaver::EXECUTE_EVENT` and there is nothing to rename; a hardcoded string silently stops matching |
|
|
104
|
+
|
|
105
|
+
### Behavior that changed under you
|
|
106
|
+
|
|
107
|
+
The first one reaches every Rails app that never configured logging, and it is
|
|
108
|
+
the only one here that shows up in production rather than in your code.
|
|
109
|
+
|
|
110
|
+
- **A Rails app logs one line per GraphQL call, and emits one notification.**
|
|
111
|
+
The railtie now sets `GraphWeaver.instrumenter` to the
|
|
112
|
+
`ActiveSupport::Notifications` adapter and attaches
|
|
113
|
+
`GraphWeaver::LogSubscriber`, so an app that configured neither gets
|
|
114
|
+
`GraphWeaver billing/InvoicesQuery (12.3ms) ok` at **info** — one line per
|
|
115
|
+
operation, carrying nothing that can hold PII; the query and variables stay
|
|
116
|
+
at debug. An instrumenter you set yourself is never replaced, and **to opt
|
|
117
|
+
out, set `GraphWeaver.logger = nil` or `GraphWeaver.instrumenter = nil` in
|
|
118
|
+
`config/initializers`** — which now takes effect, so an app that worked
|
|
119
|
+
around it with `config.after_initialize { GraphWeaver.logger = nil }` can
|
|
120
|
+
drop that. In-process calls are in scope too: a bare schema class in a client
|
|
121
|
+
slot (`GraphWeaver.client = MyApp::Schema`, `execute!(client: MyApp::Schema)`,
|
|
122
|
+
a graph's `client "Billing::Schema"`) goes through the same wrapper
|
|
123
|
+
`GraphWeaver.new(MyApp::Schema)` always used, so it produces events and log
|
|
124
|
+
lines where it produced none. See [logging](logging.md).
|
|
125
|
+
- **A `Retry` in front of a gateway starts actually retrying.** It read only
|
|
126
|
+
the failures that *raised*, and Apollo Router answers everything it decides
|
|
127
|
+
itself with a GraphQL errors body — rate limiting is `503` plus
|
|
128
|
+
`REQUEST_RATE_LIMITED`, its own faults are `500` plus a code — so the body
|
|
129
|
+
won over the status and `retries: 3` made one attempt. A response retries now
|
|
130
|
+
when its status is one a `ServerError` retries on (5xx, 408, 429), or when
|
|
131
|
+
its error codes are named in `retry_codes:`. A `200` is never retried on
|
|
132
|
+
status, and a mutation still gets one attempt unless `retry_mutations:
|
|
133
|
+
true`. **This is real traffic you weren't sending** — if the inert policy was
|
|
134
|
+
what you wanted, `retries: 0`. `#throttled?` answers
|
|
135
|
+
`REQUEST_RATE_LIMITED` too.
|
|
136
|
+
- **A task that can't honour `SUPERGRAPH=` refuses instead of ignoring it.**
|
|
137
|
+
The flag reaches the `federation:*` tasks and nothing else, so
|
|
138
|
+
`SUPERGRAPH=public.graphql rake graph_weaver:queries:check` reported every
|
|
139
|
+
query valid against a supergraph missing a field they select — the wrong
|
|
140
|
+
answer wearing a green tick. **A CI step that passes it to `generate`,
|
|
141
|
+
`verify` or `queries:check` goes red**; drop the flag, or declare the
|
|
142
|
+
supergraph on a graph so every run finds it.
|
|
143
|
+
- **`InputError#field` names the input field, not the variable.** It is now
|
|
144
|
+
`#path`'s last *named* segment — the slot that actually held the bad value,
|
|
145
|
+
which is the one a form highlights — where it used to be re-branded on the
|
|
146
|
+
way out with the *variable* name. Nothing raises; the value just differs once
|
|
147
|
+
a refusal happens inside an input object. **Read `error.path.first` wherever
|
|
148
|
+
you wanted the variable**, and `#field` wherever you wanted the field. On a
|
|
149
|
+
refusal that never got past the variable the two are the same, which is why
|
|
150
|
+
this can pass unnoticed until the first nested input fails. An index is a
|
|
151
|
+
position rather than a field, so it never becomes one: `execute(ids: [1, 2,
|
|
152
|
+
"x"])` reports `#path` `["ids", 2]` and `#field` `"ids"`. **Self-check:**
|
|
153
|
+
`grep -rn "\.field" app lib` — every hit that indexes or compares an
|
|
154
|
+
`InputError`'s `#field` is a place to decide which of the two you meant.
|
|
155
|
+
- **The instrumentation payload's `:status` is a Symbol, and the HTTP status
|
|
156
|
+
moved to `:http_status`.** `:status` is now `:ok`, `:errors` (the response
|
|
157
|
+
came back carrying GraphQL errors) or `:failed` (it raised) — a 200 carrying
|
|
158
|
+
errors is not a success, and only a symbol says that on both sides of the
|
|
159
|
+
seam. Nothing raises: a subscriber comparing it to an Integer just stops
|
|
160
|
+
matching. **A subscriber that branched on `payload[:status] == 200`, or on a
|
|
161
|
+
4xx/5xx, reads `:http_status` now** — which is nil in-process, where
|
|
162
|
+
`:status` used to be a fabricated 200 so one subscriber could read both
|
|
163
|
+
sides. The whole payload is a documented contract now; see
|
|
164
|
+
[logging](logging.md#the-payload). **Self-check:** nothing subscribing to
|
|
165
|
+
`execute.graph_weaver` means nothing to change — this reaches subscribers
|
|
166
|
+
only.
|
|
167
|
+
- **`respond_to?` on a result struct no longer answers true for a name that
|
|
168
|
+
doesn't exist.** It used to say true for any near miss, which broke the
|
|
169
|
+
standard duck-typing guard — `obj.pet if obj.respond_to?(:pet)` raised the
|
|
170
|
+
very `NoMethodError` the hint exists to explain. **A branch that read the old
|
|
171
|
+
answer now takes the other path**, and `struct.method(:nmae)` raises Ruby's
|
|
172
|
+
bare `NameError` rather than a hinted one; `struct.nmae` still hints.
|
|
173
|
+
- **A `graphql:` tag reaches a module generated with `client:`.** The baked
|
|
174
|
+
client used to sit above the slot a tag swaps, so a bound module ran against
|
|
175
|
+
its real endpoint under `graphql: :fake`. **If a spec relied on that**, it now
|
|
176
|
+
runs against the fake — pass `client:` on the call, set `MyQuery.client =`, or
|
|
177
|
+
tag the example `graphql: :live`.
|
|
178
|
+
- **`config.context`, `config.schema` and `config.router` are suite setup.**
|
|
179
|
+
Setting any of the three once an example is running refuses, naming the
|
|
180
|
+
per-example helper (`graphql_context`, `graphql_fake(schema:)`,
|
|
181
|
+
`graphql_router(fake:)`). The tag builds an example's clients in a `before`
|
|
182
|
+
hook of its own, which rspec runs ahead of any group `before`, so a set there
|
|
183
|
+
was read too late and silently changed nothing — a `config.context` that
|
|
184
|
+
never reached a resolver, a `config.schema` the fake never saw. The refusal
|
|
185
|
+
replaces a line that wasn't working. **Move it to an `around`, or to
|
|
186
|
+
`GraphWeaver::Testing.configure` in the spec helper**; `configure` and
|
|
187
|
+
`around` are unchanged.
|
|
188
|
+
- **`result.to_json` is real JSON, and it is the wire shape.** It used to be
|
|
189
|
+
Ruby's `Object#to_json` — the `#inspect` string, quoted — so a log line or a
|
|
190
|
+
cache write stored nothing, with no exception and no warning; under Rails
|
|
191
|
+
`render json: result` instead shipped the *Ruby* prop names, trailing
|
|
192
|
+
underscores included. Both now produce the response keys, each leaf back
|
|
193
|
+
through its scalar registration's `serialize:`, so
|
|
194
|
+
`Result.from_h(JSON.parse(result.to_json)) == result`. `#to_h` is unchanged
|
|
195
|
+
and still the Ruby view. **Anything that parsed the old output, or diffed a
|
|
196
|
+
cached copy of it, is reading something different now** — and `as_json` is
|
|
197
|
+
emitted code, so a struct generated by 0.6.1 raises `GraphWeaver::Error`
|
|
198
|
+
naming this until you regenerate.
|
|
199
|
+
- **A schema dump introspected through a credentialed url still holds the
|
|
200
|
+
token.** The provenance stamp wrote the transport's url verbatim, so a url
|
|
201
|
+
carrying userinfo or an `?access_token=` landed in a file that gets
|
|
202
|
+
committed. It records the endpoint bare now — userinfo and any query
|
|
203
|
+
parameter `filter_parameters` filters are dropped — and re-introspection
|
|
204
|
+
still authenticates from the dump's `auth_env`. **Run `rake
|
|
205
|
+
graph_weaver:schema:refresh` once, and rotate the token if that file was ever
|
|
206
|
+
pushed.**
|
|
207
|
+
- **`verify` fails when the dump has fallen behind the schema class it was
|
|
208
|
+
built from.** For an app that serves its own schema the dump is an artifact
|
|
209
|
+
derived from code in the same repo, and everything downstream reads it, so
|
|
210
|
+
`generate` and `verify` both called a tree up to date while the live
|
|
211
|
+
resolvers had already moved. **A dump you deliberately keep behind your own
|
|
212
|
+
schema is a red gate now** — `rake graph_weaver:schema:refresh`, or ask about
|
|
213
|
+
no dump at all with `verify_generated!(schema:)`. It costs one in-process
|
|
214
|
+
introspection per graph and never a network call.
|
|
215
|
+
- **`@oneOf` starts being enforced if your dump is `.json`.** graphql-ruby's
|
|
216
|
+
introspection query omits `isOneOf` unless asked, and its loader drops the
|
|
217
|
+
field even when it is there, so every dump this gem has written said "not
|
|
218
|
+
@oneOf" for every input object and the enforcing struct was never generated.
|
|
219
|
+
**Regenerate (`rake graph_weaver:schema:refresh && rake
|
|
220
|
+
graph_weaver:generate`) and the emitted `ONE_OF` starts refusing calls that
|
|
221
|
+
set two fields** — which your server was refusing all along, so the failure
|
|
222
|
+
moves from the wire into `execute`. SDL dumps, inline SDL and a live class
|
|
223
|
+
were always correct.
|
|
224
|
+
- **A fake pin is told from an option by a schema lookup, not by casing.** The
|
|
225
|
+
rule was "a dot or a leading capital is a pin", so a lowercase type could not
|
|
226
|
+
be pinned at all: `graphql_fake("pokemon_v2_pokemon" => …)` against a Hasura
|
|
227
|
+
API came back as `a fake doesn't take pokemon_v2_pokemon:`. Those pins work
|
|
228
|
+
now. The other side of it: **a keyword that is a near-miss for a pin
|
|
229
|
+
(`Persn: "Ada"`) raises `ArgumentError` from the fake** rather than
|
|
230
|
+
`GraphWeaver::Error` from the override check — the same key written in the
|
|
231
|
+
leading positional hash is unchanged, and is the spelling for a schema whose
|
|
232
|
+
vocabulary collides with an option name.
|
|
233
|
+
- **Regenerate**, as ever — generated modules carry a private `GRAPH` naming the
|
|
234
|
+
graph they were generated from, and a [multi-schema](getting_started.md#more-than-one-schema)
|
|
235
|
+
app whose modules predate it refuses rather than guessing which schema a
|
|
236
|
+
module belongs to. A generated `execute` also makes its request through the
|
|
237
|
+
gem now (`from_response(dispatch(variables, client:))`), which is what lets
|
|
238
|
+
an event name the graph; 0.6.1's modules keep working as they are, but `rake
|
|
239
|
+
graph_weaver:verify` reports the tree out of date until you regenerate.
|
|
240
|
+
Result structs also gained `==`/`eql?`/`hash`, `deconstruct_keys`, `#to_h`
|
|
241
|
+
and `#as_json`, and the emitted guard in front of a `cast:` changed (below).
|
|
242
|
+
- **Check that your `require "graph_weaver/rspec"` actually runs.** The old
|
|
243
|
+
setup put it in `spec/support/graph_weaver.rb`, and rspec-rails ships the
|
|
244
|
+
`spec/support` glob **commented out** — so if you never uncommented it, the
|
|
245
|
+
tag did nothing and every `graphql: :fake` example has been hitting the real
|
|
246
|
+
client. `rails g graph_weaver:install` now writes the line into
|
|
247
|
+
`spec/rails_helper.rb` instead; **move yours there** if the glob isn't live.
|
|
248
|
+
- **`graphql: :wire`, if you adopt it, needs webmock *enabled*** — `require
|
|
249
|
+
"webmock/rspec"` in the spec helper. Having it in the Gemfile is not enough:
|
|
250
|
+
`Bundler.require` loads webmock without installing its adapters, and the tag
|
|
251
|
+
refuses before the first request rather than letting it leave the suite.
|
|
252
|
+
- **A `DateTime` given for a `Date` variable is refused.** `DateTime` is a
|
|
253
|
+
`Date` to Ruby, so it used to pass the cast untouched and go on the wire as
|
|
254
|
+
`"2024-01-15T10:20:30+00:00"` where the schema said `ISO8601Date` — a lenient
|
|
255
|
+
server truncated it, a strict one refused it. Truncating it here would be the
|
|
256
|
+
same guess made silently, so it now raises an `InputError` naming the class
|
|
257
|
+
and the fix: `$d of On: expected a Date, got a DateTime — pass .to_date if
|
|
258
|
+
dropping the time of day is what you meant`. **Pass `.to_date` where a
|
|
259
|
+
`DateTime` reaches a `Date` variable.** The pairings that already raised —
|
|
260
|
+
a `Time` for a date, a `Date` for a timestamp — now raise that branded
|
|
261
|
+
`InputError` rather than Ruby's *"no implicit conversion of Time into
|
|
262
|
+
String"*, and a `DateTime` or `Time.zone.now` for a *timestamp* converts
|
|
263
|
+
losslessly where it used to raise.
|
|
264
|
+
- **A `cast:` of your own gets the same guard and the same verdict.** A
|
|
265
|
+
registration like `register_scalar("Date", Date, cast: :iso8601, serialize:
|
|
266
|
+
:iso8601)` emitted a bare `value.is_a?(Date)` pass-through, so a `DateTime`
|
|
267
|
+
went by untouched and your `serialize:` wrote a full timestamp into a date
|
|
268
|
+
field — **pass `.to_date` there too**. Anything else wrong used to arrive as
|
|
269
|
+
Ruby's own sentence about an argument you never wrote (`no implicit
|
|
270
|
+
conversion of Integer into String`) under `kind: :unparseable`; the verdict
|
|
271
|
+
is the library's now and splits the way Ruby does — a `TypeError` from a
|
|
272
|
+
codec reads `expected a Date, got 5` under `kind: :type_mismatch`, an
|
|
273
|
+
`ArgumentError` keeps the parser's words under `:unparseable`.
|
|
274
|
+
**`#details[:type]` is the GraphQL type now, never a Ruby class** — a
|
|
275
|
+
`register_scalar("Money", BigDecimal)` field reads `"Money"`, not
|
|
276
|
+
`"BigDecimal"`, and an input object reads its schema name rather than the
|
|
277
|
+
class generated for it; the *message* still names the Ruby you may pass.
|
|
278
|
+
**A spec matching the old
|
|
279
|
+
message, or branching on `:unparseable` for a wrong class, needs updating**
|
|
280
|
+
— and the guard is emitted into your generated files, so a checked-in one
|
|
281
|
+
keeps the old behavior until you regenerate.
|
|
282
|
+
- **A field whose name a struct already answers to now generates as `name_`.**
|
|
283
|
+
`class` becomes the prop `class_`, `hash` becomes `hash_`, and so on for
|
|
284
|
+
`display`, `to_json`, `each` and (on an input) `supplied`. Nothing that used
|
|
285
|
+
to work stops working: a key you aliased in the query to get past the old
|
|
286
|
+
*"alias it in the query"* refusal still generates from that alias — **drop
|
|
287
|
+
the alias and regenerate** if you want the field's own name back. Only the
|
|
288
|
+
Ruby name moves; the wire keeps the schema's spelling in both directions, so
|
|
289
|
+
`result.class` is still Ruby's `class` and `result.class_` is the field. The
|
|
290
|
+
prop is the field's one Ruby name, so `.coerce({ class_: … })` and a result's
|
|
291
|
+
`#to_h` and pattern matching all use it. An `InputError`'s structured half is
|
|
292
|
+
the wire's throughout, so a refusal on that field reports `#path` `["class"]`
|
|
293
|
+
and `#coordinate` `"Tricky.class"`. An **input** struct's `#to_h`
|
|
294
|
+
is the wire hash it would send, `{"class" => …}`, and input structs don't
|
|
295
|
+
pattern-match. Input types had no way past the old refusal at all, so a
|
|
296
|
+
schema with a `class` column — a Hasura `bool_exp` has one input field per
|
|
297
|
+
column — generates for the first time. The names that take an underscore are
|
|
298
|
+
a list the gem owns, rather than whatever `T::Struct` answered to in the
|
|
299
|
+
generating process: deriving them made generation depend on require order, so
|
|
300
|
+
with ActiveSupport loaded first a key named `asJson` was refused and loaded
|
|
301
|
+
second it became a prop that shadowed the real `#as_json`. The list is what a
|
|
302
|
+
struct answers — `T::Struct` and `Object`'s public instance methods, the
|
|
303
|
+
hooks Ruby and Rails call on an object that doesn't define one (`initialize`,
|
|
304
|
+
`to_ary`, `to_hash`, `to_json`, `as_json`, `to_param`, `try`, `presence`,
|
|
305
|
+
`each`, `deconstruct_keys`), and the methods the gem's own mixins define — so
|
|
306
|
+
a few more names move than 0.6.1 touched. Kernel's *private* methods are not
|
|
307
|
+
on it: `format`, `select`, `test`, `open`, `load` and `pp` are ordinary
|
|
308
|
+
column names, and the gem's mixins qualify their own calls (`Kernel.raise`)
|
|
309
|
+
so a prop may take one. A federation `@key` on such a field follows the same
|
|
310
|
+
rule instead of being refused: the kwarg takes the underscore
|
|
311
|
+
(`Representations.room(class_: …)`) and `"class"` still goes on the wire, so
|
|
312
|
+
**regenerate if a `@key` of yours names one**. Generated source marks each
|
|
313
|
+
rename on the line above the prop — `# wire: class — reserved as a prop
|
|
314
|
+
name` — so **read the regenerate diff** rather than grepping for the names
|
|
315
|
+
yourself.
|
|
316
|
+
- **If you adopt `GraphWeaver.graph`, every queries directory needs a graph.**
|
|
317
|
+
Declaring one replaces the implicit graph your top-level settings describe,
|
|
318
|
+
so an app that declares a graph beside its existing `app/graphql/queries`
|
|
319
|
+
leaves that directory unread — `generate` skipping it, `verify` calling the
|
|
320
|
+
tree up to date. `generate!`, `verify_generated!` and `check_queries` refuse
|
|
321
|
+
instead, naming the stray files. **Name the directory in a graph
|
|
322
|
+
(`queries`/`output`), declare a graph for it, or delete it.** An app that
|
|
323
|
+
declares no graph is unaffected.
|
|
324
|
+
- **A `client` that isn't a constant is refused at generation.** Its value is
|
|
325
|
+
spelled into every module the graph generates, so `client` given an endpoint
|
|
326
|
+
url emitted a file that doesn't parse, from a run that reported success.
|
|
327
|
+
Declare the constant and name it — `CLIENT = GraphWeaver.new(url)`, then
|
|
328
|
+
`client "CLIENT"` — which is what the message says.
|
|
329
|
+
- **A `cast:` or `serialize:` proc that returns a value is refused at
|
|
330
|
+
registration.** A proc there builds *source* for the generated file, so
|
|
331
|
+
`cast: ->(v) { v.to_sym }` interpolated to nothing and every response failed
|
|
332
|
+
far from the registration, blaming the codec. It is probed once when
|
|
333
|
+
registered now: return the source (`cast: ->(v) { "Money.parse(#{v})" }`) or
|
|
334
|
+
name a method instead (`cast: :parse`).
|
|
335
|
+
- **`config.graph_weaver` refuses a key the railtie doesn't read**, at boot. It
|
|
336
|
+
takes `watch`; `config.graph_weaver.queries_paths = …` was taken silently and
|
|
337
|
+
did nothing, so the refusal replaces a line that wasn't working — in every
|
|
338
|
+
spelling of that write, `config.graph_weaver[:queries_paths] = …` included.
|
|
339
|
+
**Move it to `GraphWeaver.queries_paths =`**, which is what the message says.
|
|
340
|
+
- **A router's `fake:` refuses `seed:`**, as `graphql_fake` already did. A
|
|
341
|
+
router is built once for the suite, so a seed inside
|
|
342
|
+
`graphql_router(fake: …)` would pin every example to one run — `rspec --seed
|
|
343
|
+
1234` reproduces the fabricated data along with the test order, and
|
|
344
|
+
`GraphWeaver::Testing.config.seed` is the override for a harness that isn't
|
|
345
|
+
rspec.
|
|
346
|
+
|
|
22
347
|
## Upgrading from 0.5.1
|
|
23
348
|
|
|
24
349
|
Much smaller than 0.5.0, and mostly mechanical. Three commands find most of it:
|
|
@@ -157,8 +482,8 @@ are emitted `private_constant`, so **regenerate**.
|
|
|
157
482
|
for the type** — `Testing.config.overrides = { "Money" => "12.00" }`, or the
|
|
158
483
|
same key on one example's `graphql_fake`. Without one, `FakeClient` and
|
|
159
484
|
cassette anonymization refuse rather than feeding your cast a `"Money-1"`
|
|
160
|
-
placeholder. Scalars registered as `Time`, `Date`, `Integer`,
|
|
161
|
-
`String` or `T::Boolean` need nothing.
|
|
485
|
+
placeholder. Scalars registered as `BigDecimal`, `Time`, `Date`, `Integer`,
|
|
486
|
+
`Float`, `String` or `T::Boolean` need nothing.
|
|
162
487
|
- **Re-run `rake graph_weaver:cassettes:anonymize`** on any committed cassette
|
|
163
488
|
holding a registered custom scalar: the anonymizer used to write a value the
|
|
164
489
|
generated codec couldn't read back.
|
|
@@ -283,7 +608,7 @@ carries it — `SearchQuery::Result::Search::Species` is `GraphQLTypes::Species`
|
|
|
283
608
|
|---|---|
|
|
284
609
|
| `Testing.config.auto_fake = true` | `Testing.config.default_mode = :fake` |
|
|
285
610
|
| a mutation's `…Query` module | `…Mutation` |
|
|
286
|
-
| `graphql: :none` (rspec tag) | `graphql:
|
|
611
|
+
| `graphql: :none` (rspec tag) | `graphql: :live` |
|
|
287
612
|
|
|
288
613
|
**The shared types module was three, and is now one.** `GraphQLInputs`,
|
|
289
614
|
`GraphQLEnums` and `GraphQLUnions` are all `GraphQLTypes`, and the files move
|
data/graph_weaver.gemspec
CHANGED
|
@@ -40,12 +40,19 @@ Gem::Specification.new do |s|
|
|
|
40
40
|
s.add_development_dependency "debug"
|
|
41
41
|
s.add_development_dependency "faker"
|
|
42
42
|
s.add_development_dependency "faraday"
|
|
43
|
+
s.add_development_dependency "rack" # WebMock's to_rack needs it; webmock doesn't depend on it
|
|
43
44
|
s.add_development_dependency "rake"
|
|
45
|
+
# spec/railtie_spec.rb boots a real Rails application: the railtie's bug of
|
|
46
|
+
# record was Rails' initializer TSort putting graph_weaver.logger after
|
|
47
|
+
# config/initializers, which a stand-in cannot model. Brings activesupport,
|
|
48
|
+
# which LogSubscriber is checked against for the same reason.
|
|
49
|
+
s.add_development_dependency "railties"
|
|
44
50
|
s.add_development_dependency "redcarpet" # yard --markup markdown
|
|
45
51
|
s.add_development_dependency "rspec"
|
|
46
52
|
s.add_development_dependency "simplecov"
|
|
47
53
|
s.add_development_dependency "sorbet"
|
|
48
54
|
s.add_development_dependency "tapioca"
|
|
55
|
+
s.add_development_dependency "webmock" # graphql: :wire serves its Rack app through it
|
|
49
56
|
s.add_development_dependency "webrick"
|
|
50
57
|
s.add_development_dependency "yard"
|
|
51
58
|
end
|