graph_weaver 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +537 -0
- data/Gemfile.lock +19 -19
- data/README.md +74 -53
- data/docs/cassettes.md +29 -4
- data/docs/editors.md +3 -1
- data/docs/errors.md +75 -16
- data/docs/federation.md +206 -155
- data/docs/generated_modules.md +223 -166
- data/docs/getting_started.md +106 -82
- data/docs/logging.md +35 -5
- data/docs/scalars.md +119 -24
- data/docs/testing.md +196 -155
- data/docs/transports.md +47 -19
- data/docs/upgrading.md +243 -22
- data/graph_weaver.gemspec +16 -2
- data/lib/generators/graph_weaver/install_generator.rb +31 -16
- data/lib/graph_weaver/client.rb +52 -15
- data/lib/graph_weaver/codegen/aliases.rb +15 -8
- data/lib/graph_weaver/codegen/emit.rb +107 -42
- data/lib/graph_weaver/codegen/enum_type.rb +4 -3
- data/lib/graph_weaver/codegen/nodes.rb +42 -21
- data/lib/graph_weaver/codegen/scalar_type.rb +87 -83
- data/lib/graph_weaver/codegen/type_helpers.rb +2 -3
- data/lib/graph_weaver/codegen.rb +382 -105
- data/lib/graph_weaver/coerce.rb +113 -0
- data/lib/graph_weaver/errors.rb +57 -13
- data/lib/graph_weaver/federation.rb +10 -22
- data/lib/graph_weaver/hints.rb +76 -2
- data/lib/graph_weaver/in_process.rb +11 -8
- data/lib/graph_weaver/inflect.rb +2 -0
- data/lib/graph_weaver/input_struct.rb +115 -12
- data/lib/graph_weaver/internal/overrides.rb +101 -0
- data/lib/graph_weaver/internal/planner.rb +868 -0
- data/lib/graph_weaver/internal/schemas.rb +50 -0
- data/lib/graph_weaver/internal/selection.rb +127 -0
- data/lib/graph_weaver/{testing → internal}/subgraphs.rb +45 -43
- data/lib/graph_weaver/internal/values.rb +181 -0
- data/lib/graph_weaver/internal.rb +206 -0
- data/lib/graph_weaver/logging.rb +108 -20
- data/lib/graph_weaver/parsing.rb +6 -13
- data/lib/graph_weaver/query_module.rb +2 -0
- data/lib/graph_weaver/railtie.rb +113 -14
- data/lib/graph_weaver/representation.rb +30 -2
- data/lib/graph_weaver/response.rb +15 -0
- data/lib/graph_weaver/retry.rb +54 -22
- data/lib/graph_weaver/rspec.rb +63 -18
- data/lib/graph_weaver/schema_diff.rb +293 -0
- data/lib/graph_weaver/schema_loader.rb +126 -35
- data/lib/graph_weaver/tasks.rb +88 -36
- data/lib/graph_weaver/testing/cassette.rb +131 -78
- data/lib/graph_weaver/testing/coverage.rb +11 -15
- data/lib/graph_weaver/testing/failure.rb +14 -8
- data/lib/graph_weaver/testing/fake_client.rb +253 -60
- data/lib/graph_weaver/testing/fake_subgraph.rb +19 -8
- data/lib/graph_weaver/testing/router.rb +147 -840
- data/lib/graph_weaver/testing.rb +40 -83
- data/lib/graph_weaver/transport/faraday.rb +1 -1
- data/lib/graph_weaver/transport/http.rb +29 -12
- data/lib/graph_weaver/transport.rb +11 -34
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +221 -118
- metadata +17 -13
- data/CLAUDE.md +0 -161
- data/DECISIONS.md +0 -309
- data/Makefile +0 -23
- data/NOTES.md +0 -182
- data/PLAN.md +0 -115
- data/REVIEW.md +0 -946
- data/lib/graph_weaver/schemas.rb +0 -46
- data/lib/graph_weaver/selection.rb +0 -120
- data/lib/graph_weaver/testing/values.rb +0 -98
data/docs/getting_started.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Getting started: the production path (Rails)
|
|
2
2
|
|
|
3
|
-
The setup that ships: queries live as `.graphql` files, generation
|
|
4
|
-
`# typed: strict` Ruby you check in, and CI fails when anything drifts.
|
|
5
|
-
|
|
3
|
+
The setup that ships, end to end: queries live as `.graphql` files, generation
|
|
4
|
+
writes `# typed: strict` Ruby you check in, and CI fails when anything drifts.
|
|
5
|
+
Follow it once when you add the gem to an app. (Exploring an API from a console
|
|
6
6
|
instead? Start with [dynamic mode](real_world.md) — no build step.)
|
|
7
7
|
|
|
8
8
|
Rails is assumed below; the [non-Rails note](#not-rails) at the bottom
|
|
@@ -24,6 +24,7 @@ rails g graph_weaver:install https://api.example.com/graphql
|
|
|
24
24
|
```
|
|
25
25
|
create config/initializers/graph_weaver.rb
|
|
26
26
|
create app/graphql/queries/.keep
|
|
27
|
+
create app/graphql/fragments/.keep
|
|
27
28
|
create app/graphql/generated/.keep
|
|
28
29
|
create graphql.config.yml
|
|
29
30
|
introspect app/graphql/schema.json from https://api.example.com/graphql
|
|
@@ -41,7 +42,7 @@ initializer that fits:
|
|
|
41
42
|
|
|
42
43
|
| flag | |
|
|
43
44
|
|---|---|
|
|
44
|
-
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only.
|
|
45
|
+
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only. The name is recorded into the dump, so `schema:refresh`/`schema:diff`/`queries:check` read the same one the initializer does |
|
|
45
46
|
| `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh URL=...` does it later |
|
|
46
47
|
|
|
47
48
|
Re-running is safe — every file goes through the usual Rails conflict
|
|
@@ -84,13 +85,15 @@ What it wrote:
|
|
|
84
85
|
RubyMine schema autocomplete, hover docs, and validation as you type in
|
|
85
86
|
`.graphql` files — no JS project, no `npm install`. Details and the honest
|
|
86
87
|
limits in [editors](editors.md).
|
|
87
|
-
- **`app/graphql/queries/`, `app/graphql/generated/`.**
|
|
88
|
-
queries and where generation
|
|
88
|
+
- **`app/graphql/queries/`, `app/graphql/fragments/`, `app/graphql/generated/`.**
|
|
89
|
+
Where you write queries, where shared fragments live, and where generation
|
|
90
|
+
writes Ruby.
|
|
89
91
|
|
|
90
92
|
Rake needs no wiring either: in Rails the `graph_weaver:*` tasks register
|
|
91
93
|
themselves (a Railtie) and depend on `:environment`, so your initializer —
|
|
92
|
-
and its registrations — runs first. The generated modules load at boot
|
|
93
|
-
|
|
94
|
+
and its registrations — runs first. The generated modules load at boot from
|
|
95
|
+
a `to_prepare` block, so a helper or enum you registered in one is already
|
|
96
|
+
in place when the file that names it loads.
|
|
94
97
|
|
|
95
98
|
### Your app's own schema, in-process
|
|
96
99
|
|
|
@@ -122,11 +125,16 @@ client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
|
|
|
122
125
|
PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
|
|
123
126
|
```
|
|
124
127
|
|
|
128
|
+
Each query gets its own copy of that hash, so a resolver writing
|
|
129
|
+
`context[:loader] =` can't hand what it wrote to the next request — which
|
|
130
|
+
matters because one in-process client is normally the whole app's.
|
|
131
|
+
|
|
125
132
|
**Keep the dump in step with the schema.** Codegen reads the committed
|
|
126
133
|
dump at `GraphWeaver.schema_path`, never the live class — that's what
|
|
127
134
|
makes `rake graph_weaver:verify` a deterministic CI check. The generator
|
|
128
135
|
writes the first dump; after that it's an artifact derived from code in
|
|
129
|
-
your own repo, so rebuild it with graphql-ruby's own rake task
|
|
136
|
+
your own repo, so rebuild it with graphql-ruby's own rake task, ahead of
|
|
137
|
+
`verify` in CI:
|
|
130
138
|
|
|
131
139
|
```ruby
|
|
132
140
|
# lib/tasks/graphql.rake
|
|
@@ -140,13 +148,16 @@ rake graphql:schema:json # rewrites app/graphql/schema.json
|
|
|
140
148
|
rake graph_weaver:generate
|
|
141
149
|
```
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
A stale dump makes `verify` fail on a query that is fine. `queries:check` is
|
|
152
|
+
unaffected: running in-process it validates against the live class, not the
|
|
153
|
+
dump. (`schema:diff` and `:refresh` are for servers you *don't* own; a dump
|
|
154
|
+
taken from a schema class records no url, and they say so.)
|
|
155
|
+
|
|
156
|
+
**Scaffolding the app too?** On a `rails new --skip-active-record`,
|
|
157
|
+
`rails g graphql:install` writes `config.active_record.query_log_tags` lines
|
|
158
|
+
into `config/application.rb` that an app without ActiveRecord can't boot
|
|
159
|
+
with — a graphql-ruby bug. Run it as
|
|
160
|
+
`rails g graphql:install --skip-query-logs`, or delete the lines it wrote.
|
|
150
161
|
|
|
151
162
|
### A schema dump you already have
|
|
152
163
|
|
|
@@ -174,19 +185,28 @@ query($id: ID!) {
|
|
|
174
185
|
rake graph_weaver:generate # writes app/graphql/generated/person_query.rb
|
|
175
186
|
```
|
|
176
187
|
|
|
177
|
-
Commit the schema dump and the generated files. Generated code is
|
|
178
|
-
reviewed like any other code — and never edited by hand.
|
|
179
|
-
|
|
180
188
|
```ruby
|
|
181
189
|
PersonQuery.execute!(id: "1").person&.name # typed, via GraphWeaver.client
|
|
182
190
|
```
|
|
183
191
|
|
|
184
|
-
|
|
192
|
+
Commit the schema dump and the generated files. Generated code is reviewed like
|
|
193
|
+
any other code — and never edited by hand. The module name comes from the file
|
|
194
|
+
name; the full set of naming rules is in
|
|
195
|
+
[generated modules](generated_modules.md#naming).
|
|
185
196
|
|
|
186
197
|
`graphql.config.yml` is already there, so VS Code and RubyMine validate the
|
|
187
198
|
`.graphql` files as you type, with schema autocomplete and hover docs — see
|
|
188
199
|
[editors](editors.md).
|
|
189
200
|
|
|
201
|
+
**In development you don't type that command again.** While the server is
|
|
202
|
+
running, a `.graphql` edit — or a refreshed schema dump — regenerates before
|
|
203
|
+
the next request, the way a route or a locale change takes effect. A query that
|
|
204
|
+
doesn't compile is logged with its file and position while the modules already
|
|
205
|
+
loaded keep serving, so a file saved mid-edit doesn't take the server down.
|
|
206
|
+
Development only, and `config.graph_weaver.watch = false` turns it off. The
|
|
207
|
+
generated files are still what ships: commit them, and keep `rake
|
|
208
|
+
graph_weaver:verify` in CI.
|
|
209
|
+
|
|
190
210
|
### Shared fragments
|
|
191
211
|
|
|
192
212
|
Define reusable fragments once and spread them from any query:
|
|
@@ -205,29 +225,10 @@ Fragment files hold only fragments (no operations), and names are unique across
|
|
|
205
225
|
them. Point elsewhere with `GraphWeaver.fragments_paths` (an appendable list,
|
|
206
226
|
default `app/graphql/fragments`).
|
|
207
227
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
it aliases the same type — so a `union` selected across many queries becomes one
|
|
213
|
-
Ruby type family, and you write one exhaustive `case … when … T.absurd` that
|
|
214
|
-
works everywhere:
|
|
215
|
-
|
|
216
|
-
```graphql
|
|
217
|
-
# app/graphql/fragments/feed_item.graphql
|
|
218
|
-
fragment FeedItemFields on FeedItem {
|
|
219
|
-
__typename
|
|
220
|
-
... on Post { title }
|
|
221
|
-
... on Photo { url }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
# any query
|
|
225
|
-
query { feed { ...FeedItemFields } } # feed : T::Array[FeedItemFields::Type]
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
There's no flag: hoisting triggers when the union field's selection is exactly
|
|
229
|
-
that one spread. Mix in other fields, or shadow the fragment with a query-local
|
|
230
|
-
one of the same name, and the union stays inlined in that query — see
|
|
228
|
+
One payoff worth knowing about: when a shared fragment *is* the whole selection
|
|
229
|
+
on a union field, its type is hoisted once into `GraphQLTypes` and every query
|
|
230
|
+
that spreads it gets the same Ruby type — so one exhaustive `case … T.absurd`
|
|
231
|
+
works everywhere. See
|
|
231
232
|
[abstract types](generated_modules.md#abstract-types).
|
|
232
233
|
|
|
233
234
|
## 4. Test against fakes
|
|
@@ -241,47 +242,74 @@ require "graph_weaver/rspec"
|
|
|
241
242
|
it "renders the empty state", graphql: :fake do … end # or tag the describe
|
|
242
243
|
```
|
|
243
244
|
|
|
245
|
+
The tag installs a seeded, schema-correct `FakeClient` for that example — no
|
|
246
|
+
server, no stubs, and `rspec --seed 1234` reproduces the fake data along with
|
|
247
|
+
test order. The schema it fabricates from is derived (the committed dump, or
|
|
248
|
+
your client's), so there's nothing to configure. Tag `graphql: :in_process`
|
|
249
|
+
instead and the same example runs against your real resolvers. Pinning values,
|
|
250
|
+
simulating failures, and the federated `graphql: :router` are in
|
|
251
|
+
[testing](testing.md).
|
|
252
|
+
|
|
244
253
|
A fresh `rails g rspec:install` leaves the `spec/support` glob commented
|
|
245
254
|
out in `spec/rails_helper.rb`, so uncomment it — or put the require in
|
|
246
255
|
`rails_helper.rb` itself. Nothing warns you that a support file went
|
|
247
256
|
unread.
|
|
248
257
|
|
|
249
|
-
The tag installs a seeded, schema-correct `FakeClient` for that example —
|
|
250
|
-
no server, no stubs, and `rspec --seed 1234` reproduces the fake data along
|
|
251
|
-
with test order. The schema it fabricates from is derived (the committed
|
|
252
|
-
dump, or your client's), so there's nothing to configure. Tag
|
|
253
|
-
`graphql: :in_process` instead and the same example runs against your real
|
|
254
|
-
resolvers; pin values with `overrides:`, simulate failures with `Failure.*`
|
|
255
|
-
— see [testing](testing.md).
|
|
256
|
-
|
|
257
258
|
## 5. Verify in CI
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
Four questions, four tasks — the last only on a federated graph:
|
|
261
|
+
|
|
262
|
+
| ask | task | needs network |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| is the checked-in Ruby fresh? | `rake graph_weaver:verify` | no |
|
|
265
|
+
| has the server's schema drifted from the dump? | `rake graph_weaver:schema:diff` | yes |
|
|
266
|
+
| did that drift break any of my queries? | `rake graph_weaver:queries:check` | yes |
|
|
267
|
+
| did a subgraph change without a recompose? | `rake graph_weaver:federation:diff` | no |
|
|
268
|
+
|
|
269
|
+
`verify` compares the committed generated files against what the current
|
|
270
|
+
schema + queries + registrations would produce, so it belongs in every CI
|
|
271
|
+
build. `schema:diff` needs a dump with a recorded source url (introspected
|
|
272
|
+
dumps have one) and `GRAPHWEAVER_AUTH` for private APIs — run it on a
|
|
273
|
+
schedule and repair with `rake graph_weaver:schema:refresh`.
|
|
274
|
+
`federation:diff` needs no network either, so it goes in the same PR run;
|
|
275
|
+
see [federation](federation.md#has-the-supergraph-been-recomposed).
|
|
276
|
+
|
|
277
|
+
`schema:diff` names what moved, breaking changes first — breaking meaning
|
|
278
|
+
a query written against your dump stops validating, or stops casting:
|
|
279
|
+
|
|
263
280
|
```
|
|
281
|
+
app/graphql/schema.json vs https://api.example.com/graphql: 8 changes, 5 breaking
|
|
282
|
+
|
|
283
|
+
breaking:
|
|
284
|
+
AdoptionInput.nickname String -> String!
|
|
285
|
+
Person.email removed
|
|
286
|
+
Person.pets [Pet!]! -> [Pet!]
|
|
287
|
+
Query.person(includeArchived:) argument added: Boolean! — required
|
|
288
|
+
Species.CAT enum value removed
|
|
264
289
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
[federation](federation.md#has-the-supergraph-been-recomposed).
|
|
290
|
+
other:
|
|
291
|
+
Person.birthday deprecated: use bornOn
|
|
292
|
+
Pet.nickname added: String
|
|
293
|
+
Species.BIRD enum value added
|
|
270
294
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
every CI build.
|
|
295
|
+
app/graphql/schema.json is stale — the server's schema has drifted (rake graph_weaver:schema:refresh)
|
|
296
|
+
```
|
|
274
297
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
Nullability is judged from your side, which is why the two above point
|
|
299
|
+
opposite ways: `Person.pets` losing its `!` hands a generated struct the
|
|
300
|
+
nil it declared it wouldn't get, while `AdoptionInput.nickname` gaining
|
|
301
|
+
one rejects a query that omits it. Any drift exits non-zero — whether a
|
|
302
|
+
change matters is yours to judge.
|
|
279
303
|
|
|
280
|
-
`
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
304
|
+
`GraphWeaver::SchemaLoader.diff(path)` is the same summary as an object —
|
|
305
|
+
`#breaking`, `#compatible`, `#to_h`, and `#empty?` for the plain "has it
|
|
306
|
+
drifted" question.
|
|
307
|
+
|
|
308
|
+
`queries:check` answers the question that actually matters when the schema
|
|
309
|
+
*has* moved: **which of your queries no longer validate, and why.** It
|
|
310
|
+
re-introspects the recorded url (without rewriting the dump) and validates
|
|
311
|
+
every `.graphql` file against the schema as it is right now, naming each
|
|
312
|
+
error's line and column, and exits non-zero:
|
|
285
313
|
|
|
286
314
|
```
|
|
287
315
|
app/graphql/queries/person.graphql
|
|
@@ -290,9 +318,6 @@ app/graphql/queries/person.graphql
|
|
|
290
318
|
1 invalid query
|
|
291
319
|
```
|
|
292
320
|
|
|
293
|
-
It exits non-zero when anything fails, so it drops straight into CI or a
|
|
294
|
-
scheduled job.
|
|
295
|
-
|
|
296
321
|
The Ruby behind it returns the same thing as data, so you can wire it into
|
|
297
322
|
whatever you already have (a spec, a Slack ping, an issue):
|
|
298
323
|
|
|
@@ -303,18 +328,17 @@ GraphWeaver.check_queries
|
|
|
303
328
|
# "line" => 4, "column" => 5 }] }
|
|
304
329
|
```
|
|
305
330
|
|
|
306
|
-
Empty means everything validates. Pass `schema:` a loaded schema
|
|
307
|
-
touches the network — handy for checking a
|
|
308
|
-
|
|
331
|
+
Empty means everything validates. Pass `schema:` a *loaded* schema (not a path)
|
|
332
|
+
and nothing touches the network — handy for checking a proposed subgraph before
|
|
333
|
+
it's live:
|
|
309
334
|
|
|
310
335
|
```ruby
|
|
311
336
|
GraphWeaver.check_queries(schema: GraphWeaver::SchemaLoader.load("proposed.graphql"))
|
|
312
337
|
```
|
|
313
338
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
([federation](federation.md#the-routing-table)).
|
|
339
|
+
Left off, it re-introspects the url the dump records — and when that dump is a
|
|
340
|
+
composed supergraph, each error also names the subgraphs behind the type it
|
|
341
|
+
points at ([federation](federation.md#the-routing-table)).
|
|
318
342
|
|
|
319
343
|
## Sorbet, with or without
|
|
320
344
|
|
data/docs/logging.md
CHANGED
|
@@ -14,8 +14,9 @@ What logs at which level — pick the level, get the story:
|
|
|
14
14
|
| Level | What you see |
|
|
15
15
|
|-------|--------------|
|
|
16
16
|
| `debug` | the wire: query + variables per call (long queries truncated), response status/bytes, request timing, connection open/drop, dynamically parsed modules |
|
|
17
|
-
| `info` | schema introspection (with timing) and cache hits/misses, generated files written and any unregistered scalars, query modules loaded |
|
|
18
|
-
| `warn` | every GraphWeaver error raised — `TransportError`, `ServerError`, `QueryError`, `ValidationError`, `TypeError` |
|
|
17
|
+
| `info` | schema introspection (with timing) and cache hits/misses, the transport a client built, generated files written and any unregistered scalars, query modules loaded, a retry's wait and attempt number — and in development, what's being watched and what a save regenerated |
|
|
18
|
+
| `warn` | every GraphWeaver error raised — `TransportError`, `ServerError`, `QueryError`, `ValidationError`, `TypeError` — registrations the schema being generated against can't match, a retry skipped because the operation was a mutation, and every fetch the test router answered with fabricated data |
|
|
19
|
+
| `error` | development only: a `.graphql` edit that won't compile, with its file and position — the modules already loaded keep serving |
|
|
19
20
|
|
|
20
21
|
Every line carries `graph_weaver` as the progname, so formatter-based
|
|
21
22
|
filtering works out of the box. Wire lines are tagged
|
|
@@ -24,8 +25,37 @@ name — so a request's lines stay paired when threads interleave.
|
|
|
24
25
|
|
|
25
26
|
**PII note**: queries, variables, and response sizes appear at debug
|
|
26
27
|
only — variables can carry user data, so keep production loggers at
|
|
27
|
-
info or above
|
|
28
|
-
|
|
28
|
+
info or above. Auth headers never log at any level.
|
|
29
|
+
|
|
30
|
+
## Filtered variables
|
|
31
|
+
|
|
32
|
+
Debug gets switched on during an incident, which is exactly when a
|
|
33
|
+
`login(password:)` mutation's variables must not land in the log. So the
|
|
34
|
+
values of sensitive keys are replaced with `[FILTERED]` before the line is
|
|
35
|
+
written — at any depth, including inside input objects.
|
|
36
|
+
|
|
37
|
+
In Rails you configure nothing: the railtie adopts the app's own
|
|
38
|
+
`config.filter_parameters`, so GraphWeaver scrubs whatever the request logs
|
|
39
|
+
already scrub.
|
|
40
|
+
|
|
41
|
+
Everywhere else, one list:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
GraphWeaver.filter_parameters = [:password, /token/]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Strings and Symbols match as case-insensitive substrings — `:token` covers
|
|
48
|
+
`apiToken` — and Regexps match themselves. The default is `[:password,
|
|
49
|
+
:token, :secret, :authorization]`, which covers the usual names before
|
|
50
|
+
anyone configures anything; assigning replaces it rather than adding to it,
|
|
51
|
+
and `[]` turns filtering off. Anything answering `#filter(hash)` is used
|
|
52
|
+
as-is, which is how the railtie hands over an
|
|
53
|
+
`ActiveSupport::ParameterFilter`.
|
|
54
|
+
|
|
55
|
+
The same list scrubs error messages, which reach the log at `warn` rather
|
|
56
|
+
than `debug`: a variable, input field, or entity key whose name is filtered
|
|
57
|
+
is rejected with `[FILTERED]` in place of the value — everything else keeps
|
|
58
|
+
quoting it, since `expected an Int, got "lots"` is the whole diagnosis.
|
|
29
59
|
|
|
30
60
|
## Instrumentation
|
|
31
61
|
|
|
@@ -52,7 +82,7 @@ response. Its payload carries:
|
|
|
52
82
|
| Key | |
|
|
53
83
|
|-----|--|
|
|
54
84
|
| `:url` | the endpoint — nil in-process |
|
|
55
|
-
| `:schema` | the schema class, in-process only |
|
|
85
|
+
| `:schema` | the schema class's name, in-process only — a String, so a payload logs as it stands |
|
|
56
86
|
| `:operation` | the operation name sent with the request (a generated module always has one) — what a trace keys on |
|
|
57
87
|
| `:status` | the HTTP status, added once the response lands |
|
|
58
88
|
|
data/docs/scalars.md
CHANGED
|
@@ -22,11 +22,18 @@ GraphWeaver.register_scalar("User.birthday", Date) # this field only
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
A field override wins over the scalar-name registration — which is also how two
|
|
25
|
-
servers that disagree about a `DateTime` coexist in one process.
|
|
26
|
-
validated against the schema, so a typo'd field raises.
|
|
25
|
+
servers that disagree about a `DateTime` coexist in one process.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Registrations are validated against the schema you generate against, and only
|
|
28
|
+
what that schema can **disprove** fails generation: a name it declares as
|
|
29
|
+
something else (`register_scalar("Species")` where `Species` is an enum), or a
|
|
30
|
+
coordinate whose field it declares as a composite. A name it simply can't match
|
|
31
|
+
only warns — one registry serves a whole graph, so that name may belong to the
|
|
32
|
+
subgraph next door (see
|
|
33
|
+
[federation](federation.md#generating-for-a-federated-graph)).
|
|
34
|
+
|
|
35
|
+
Pass a real class as the second argument and the cast/serialize are
|
|
36
|
+
**inferred** from it by probing the deserialize side and pairing its serializer:
|
|
30
37
|
|
|
31
38
|
| the class defines | cast | serialize |
|
|
32
39
|
|-------------------|---------------|----------------|
|
|
@@ -42,40 +49,128 @@ you need to:
|
|
|
42
49
|
- a `Proc` for anything a method name can't express: `cast: ->(expr) { "Money.new(#{expr})" }`
|
|
43
50
|
- `:itself` to force pass-through, opting out of inference (rare)
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
The type also accepts a plain string (`"BigDecimal"`) when you'd rather not
|
|
46
53
|
reference the class. `requires:` (a string or array) names files emitted as
|
|
47
|
-
`require`s atop the generated source so the cast/type resolve. When
|
|
54
|
+
`require`s atop the generated source so the cast/type resolve. When the type is
|
|
48
55
|
a real class (so the runtime is loaded), each path is also `require`d at
|
|
49
56
|
registration — a typo fails now, not in the generated file.
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
The testing harness can't invent a wire value for a scalar registered as your
|
|
59
|
+
own class — only `Money.parse` knows what it accepts — so it refuses rather than
|
|
60
|
+
guess. Say it in test config, where that answer belongs: a pin for the type,
|
|
61
|
+
`GraphWeaver::Testing.config.overrides = { "Money" => "12.00" }`, or per example
|
|
62
|
+
([testing → pins](testing.md#pins)). A scalar registered as `Time`, `Date`,
|
|
63
|
+
`Integer`, `Float`, `String` or `T::Boolean` needs nothing.
|
|
64
|
+
|
|
65
|
+
A registration whose type is a class **JSON can't parse into** needs a
|
|
66
|
+
`cast:` to build one — `BigDecimal` is the one people reach for, and it defines
|
|
67
|
+
neither `.parse` nor `.load`, so inference finds no codec and the prop would be
|
|
68
|
+
unsatisfiable. Generation refuses it where a query reads that scalar back,
|
|
69
|
+
naming the field:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
register_scalar("Money", BigDecimal) has no cast, so nothing builds a BigDecimal
|
|
73
|
+
out of the JSON at Product.price — give it one ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A registration used only for a variable is untouched: nothing casts it.
|
|
77
|
+
|
|
78
|
+
`cast:` is also what a *variable* of this scalar coerces through, so the same
|
|
79
|
+
registration gets you both directions with nothing to switch on:
|
|
53
80
|
|
|
54
81
|
```ruby
|
|
55
|
-
GraphWeaver.register_scalar("Money", Money
|
|
56
|
-
# generated execute now takes T.any(Money, String); "12.00" is parsed
|
|
82
|
+
GraphWeaver.register_scalar("Money", Money)
|
|
57
83
|
StoreQuery.execute(budget: "12.00") # Money.parse("12.00") under the hood
|
|
58
|
-
StoreQuery.execute(budget: Money.new(1200)) # passed straight through
|
|
84
|
+
StoreQuery.execute(budget: Money.new(1200)) # already a Money — passed straight through
|
|
59
85
|
```
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
cast/serialize pair (`Date`, your `Money`) parses, and bad input still explodes
|
|
68
|
-
because the cast raises. A pass-through scalar — `String`, `ID`, `Boolean` — has
|
|
69
|
-
neither a conversion nor a codec pair, so it can't coerce at all: `coerce: true`
|
|
70
|
-
on one raises rather than emitting a no-op.
|
|
87
|
+
The kwarg is still typed `Money`, not `T.any(Money, String)`: `execute`'s sig
|
|
88
|
+
stays as narrow as the schema and the conversion happens in its body (see
|
|
89
|
+
[typed variables](generated_modules.md#variables-become-typed-kwargs)). So
|
|
90
|
+
`budget: "12.00"` written literally in a `# typed:` file is still an `srb tc`
|
|
91
|
+
error — as it should be, since you have a `Money` right there — while
|
|
92
|
+
`budget: params[:budget]` typechecks and converts.
|
|
71
93
|
|
|
72
94
|
The built-in scalars (`Date`, `ID`, `Int`, …) are pre-registered through the
|
|
73
95
|
same path (`Date` even carries its own `require "date"`), so a later
|
|
74
96
|
`register_scalar` overrides them.
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
## What the wire carries
|
|
99
|
+
|
|
100
|
+
The rule is one sentence: **generated code takes every JSON spelling a
|
|
101
|
+
spec-compliant server may write, and refuses the rest.** The tables below are
|
|
102
|
+
the whole of it, and [`bin/round-trip`](../bin/round-trip) fuzzes both
|
|
103
|
+
directions against them — the accepted spellings as real values, the refused
|
|
104
|
+
ones under `--hostile`, where generated code has to name what it turned down.
|
|
105
|
+
|
|
106
|
+
The one place "spec-compliant" is doing real work is `Float`. JSON has a single
|
|
107
|
+
number type and encoders write the shortest form, so `1.0` reaches Ruby as `1`
|
|
108
|
+
from graphql-js and from Go. Nothing does the reverse: `2.0` for an `Int` is the
|
|
109
|
+
server writing a non-integer where the spec says integer, so it is refused.
|
|
110
|
+
|
|
111
|
+
### Coming back — what `from_h` accepts
|
|
112
|
+
|
|
113
|
+
| scalar | accepted | refused |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `Int` | any JSON integer, including past 2³¹ and 2⁵³ (lossless in Ruby) | `2.0`, `1.5`, `"1"`, `true` |
|
|
116
|
+
| `Float` | any JSON number, `3` and `-0.0` and `1e308` included; also a decimal string | a non-numeric string, `true`, a list/object |
|
|
117
|
+
| `String` | any JSON string — empty, unicode, newlines, control characters | a number, `true`, a list/object |
|
|
118
|
+
| `ID` | any JSON string | a number or `true` — **refused with a hint**: the server didn't quote it |
|
|
119
|
+
| `Boolean` | `true`, `false` | `"true"`, `1`, `0` |
|
|
120
|
+
| `Date` | ISO-8601: `"2024-01-01"`, `"20240101"`, and a full timestamp (truncated) | any other spelling, an epoch integer |
|
|
121
|
+
| `DateTime`/`Time` (registered as `Time`) | RFC 3339 with `Z` or an offset, with or without fractional seconds, seconds optional; also a bare date and `Time.parse`'s looser forms | an epoch integer, an unparseable string |
|
|
122
|
+
| an enum | a declared value, as a string | an undeclared value, a non-string |
|
|
123
|
+
| unregistered | anything — `T.untyped`, straight through | nothing |
|
|
124
|
+
|
|
125
|
+
A refusal is a [`GraphWeaver::TypeError`](errors.md) naming the field and the
|
|
126
|
+
generated struct (which names the query). Two refusals carry advice rather than
|
|
127
|
+
only sorbet's words: an unquoted `ID`, and a registration with no cast (above).
|
|
128
|
+
|
|
129
|
+
Numeric strings — here, and in the going-out table below — are read as a wire
|
|
130
|
+
format, not as Ruby source: `"010"` is ten, and `"0x1f"` and `"1_0"` are
|
|
131
|
+
refused. `Kernel#Integer` and `Kernel#Float` accept all three as literals,
|
|
132
|
+
which would let a zero-padded form field silently mean something else.
|
|
133
|
+
|
|
134
|
+
### Going out — what a variable kwarg accepts
|
|
135
|
+
|
|
136
|
+
The kwarg's **type** is what `srb tc` holds a call site to, and it is exactly
|
|
137
|
+
what the schema says. The **value** reaching `execute` at runtime is coerced,
|
|
138
|
+
because a Rails param is a String whatever the sig says (see
|
|
139
|
+
[typed variables](generated_modules.md#variables-become-typed-kwargs) for why
|
|
140
|
+
the sig is `.checked(:never)`).
|
|
141
|
+
|
|
142
|
+
| scalar | kwarg is typed | also accepts, at runtime | on the wire |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `Int` | `Integer` | a decimal string, a whole `Float` | the integer |
|
|
145
|
+
| `Float` | `Float` | a decimal string, an `Integer` | the float |
|
|
146
|
+
| `String` | `String` | nothing | the string |
|
|
147
|
+
| `ID` | `String` | an `Integer` — `execute(id: user.id)` | the string |
|
|
148
|
+
| `Boolean` | `true`/`false` | nothing | the boolean |
|
|
149
|
+
| `Date` | `Date` | an ISO-8601 string | `iso8601` |
|
|
150
|
+
| `Time` | `Time` | a string `Time.parse` takes | `iso8601` |
|
|
151
|
+
| an enum | the member **or** its wire value | — | the wire value |
|
|
152
|
+
| an input object | the struct **or** a Hash | — | the wire hash |
|
|
153
|
+
| a registered custom scalar | its Ruby type | whatever its `cast:` takes | its `serialize:` |
|
|
154
|
+
| unregistered | `T.untyped` | anything | straight through |
|
|
155
|
+
|
|
156
|
+
Two rows are judgment calls worth stating. **`ID` takes an `Integer`** because
|
|
157
|
+
the GraphQL spec says an ID serializes as a string but accepts an integer input,
|
|
158
|
+
and `execute(id: user.id)` off a model is the everyday call; `String` gets no
|
|
159
|
+
such license, since an `Integer` where a `String` belongs is more often a bug
|
|
160
|
+
than a spelling. **`Boolean` takes no string** — Ruby has no `Kernel#Boolean`,
|
|
161
|
+
so every rule for reading `"0"`, `"off"`, `"no"` is somebody's convention, and
|
|
162
|
+
the library will not pick one for you; convert at the call site.
|
|
163
|
+
|
|
164
|
+
Anything the table refuses raises `GraphWeaver::InputError` naming the variable,
|
|
165
|
+
the operation and the value — `$count of Compute: expected an Int, got "lots"`
|
|
166
|
+
— which is the same [422 rescue point](errors.md) as a bad input-object field.
|
|
167
|
+
Input-object fields go through this table too, so `{first: "20"}` inside a
|
|
168
|
+
filter hash reads the same as `first: "20"` as a kwarg.
|
|
169
|
+
|
|
170
|
+
`GraphWeaver.reset_registrations!` is the clean slate between tests, or between
|
|
171
|
+
generations for different schemas: built-in scalars restored, enum mappings and
|
|
172
|
+
type helpers dropped. To reset one registry rather than all of them,
|
|
173
|
+
`GraphWeaver::Codegen` has the pieces —
|
|
79
174
|
`reset_scalars!` (restore the built-ins), `clear_scalars!` (empty the registry
|
|
80
175
|
entirely), `reset_enums!`, `reset_type_helpers!`.
|
|
81
176
|
|