graph_weaver 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -4
- data/README.md +40 -88
- data/docs/alternatives.md +1 -7
- data/docs/cassettes.md +54 -59
- data/docs/editors.md +32 -47
- data/docs/errors.md +261 -369
- data/docs/federation.md +650 -837
- data/docs/generated_modules.md +380 -463
- data/docs/getting_started.md +211 -428
- data/docs/i18n.md +114 -177
- data/docs/logging.md +127 -116
- data/docs/real_world.md +26 -39
- data/docs/scalars.md +277 -310
- data/docs/testing.md +343 -486
- data/docs/transports.md +203 -268
- data/docs/upgrading.md +211 -560
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +12 -3
- data/lib/graph_weaver/client.rb +30 -1
- data/lib/graph_weaver/codegen/emit.rb +5 -11
- data/lib/graph_weaver/codegen.rb +23 -55
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +23 -15
- data/lib/graph_weaver/federation.rb +11 -2
- data/lib/graph_weaver/graph.rb +39 -29
- data/lib/graph_weaver/in_process.rb +15 -9
- data/lib/graph_weaver/internal/endpoint.rb +7 -5
- data/lib/graph_weaver/internal/headers.rb +19 -0
- data/lib/graph_weaver/internal/test_clients.rb +7 -11
- data/lib/graph_weaver/internal.rb +81 -13
- data/lib/graph_weaver/log_subscriber.rb +10 -2
- data/lib/graph_weaver/logging.rb +33 -13
- data/lib/graph_weaver/query_module.rb +44 -23
- data/lib/graph_weaver/retry.rb +12 -8
- data/lib/graph_weaver/rspec.rb +13 -24
- data/lib/graph_weaver/schema_loader.rb +52 -14
- data/lib/graph_weaver/tasks.rb +10 -2
- data/lib/graph_weaver/testing/cassette.rb +28 -5
- data/lib/graph_weaver/testing/endpoint.rb +14 -13
- data/lib/graph_weaver/testing/fake_client.rb +33 -3
- data/lib/graph_weaver/testing/router.rb +7 -3
- data/lib/graph_weaver/testing.rb +12 -4
- data/lib/graph_weaver/transport/http.rb +2 -2
- data/lib/graph_weaver/transport.rb +47 -23
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +32 -10
- metadata +16 -3
- data/CHANGELOG.md +0 -3801
data/docs/getting_started.md
CHANGED
|
@@ -2,15 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
The setup that ships, end to end: queries live as `.graphql` files, generation
|
|
4
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.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
covers the differences. Still deciding whether to adopt at all?
|
|
10
|
-
[Alternatives](alternatives.md) compares the field, this gem included.
|
|
11
|
-
No Sorbet in your app? None needed: `sorbet-runtime` comes with the gem and the
|
|
12
|
-
generated code checks itself at runtime — running `srb tc` on top is your call,
|
|
13
|
-
and [Sorbet, with or without](#sorbet-with-or-without) says what that buys.
|
|
5
|
+
Follow it once when you add the gem to an app. Rails is assumed;
|
|
6
|
+
[not Rails?](#not-rails) covers the differences. Exploring an API from a console
|
|
7
|
+
instead? Start with [dynamic mode](real_world.md) — no build step. No Sorbet in
|
|
8
|
+
your app? None needed — [Sorbet, with or without](#sorbet-with-or-without).
|
|
14
9
|
|
|
15
10
|
## 1. Install
|
|
16
11
|
|
|
@@ -35,9 +30,8 @@ rails g graph_weaver:install https://api.example.com/graphql
|
|
|
35
30
|
introspect app/graphql/schema.json from https://api.example.com/graphql
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
The argument is whatever you'd pass to `GraphWeaver.new` — the
|
|
39
|
-
|
|
40
|
-
initializer that fits:
|
|
33
|
+
The argument is whatever you'd pass to `GraphWeaver.new` — the same three source
|
|
34
|
+
forms the library takes — and the generator writes the initializer that fits:
|
|
41
35
|
|
|
42
36
|
| source | |
|
|
43
37
|
|---|---|
|
|
@@ -47,43 +41,35 @@ initializer that fits:
|
|
|
47
41
|
|
|
48
42
|
| flag | |
|
|
49
43
|
|---|---|
|
|
50
|
-
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH
|
|
44
|
+
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`, url only, omitted for a public API. The name is recorded into the dump, so `schema:refresh`/`schema:diff`/`queries:check` read the same one the initializer does |
|
|
51
45
|
| `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh` does it later (`URL=...` to name an endpoint the first time) |
|
|
52
46
|
|
|
53
|
-
Re-running is safe
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
records, which `schema:diff`/`:refresh` read:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
keep app/graphql/schema.json — delete it and re-run to re-introspect
|
|
61
|
-
```
|
|
47
|
+
Re-running is safe: every file goes through the usual Rails conflict prompt, so
|
|
48
|
+
an initializer you've edited is never overwritten silently. The schema dump is
|
|
49
|
+
the exception — it is **kept** rather than prompted for, since replacing it
|
|
50
|
+
would drop the source url it records; delete it and re-run to re-introspect.
|
|
62
51
|
|
|
63
52
|
What it wrote:
|
|
64
53
|
|
|
65
54
|
- **`config/initializers/graph_weaver.rb`.** `GraphWeaver.client =` is the
|
|
66
|
-
load-bearing line: generated modules
|
|
55
|
+
load-bearing line: generated modules belonging to no declared graph resolve to
|
|
67
56
|
it at execute time (the full
|
|
68
57
|
[resolution order](transports.md#client-resolution)). Custom
|
|
69
|
-
scalars/enums/type helpers register here too — the rake tasks bake them
|
|
70
|
-
|
|
58
|
+
scalars/enums/type helpers register here too — the rake tasks bake them into
|
|
59
|
+
generated source, so they have to run first ([scalars](scalars.md)):
|
|
71
60
|
|
|
72
61
|
```ruby
|
|
73
62
|
GraphWeaver.register_scalar("Money", Money) # a scalar the registry can't know
|
|
74
63
|
```
|
|
75
64
|
|
|
76
|
-
Every setting also takes the block form
|
|
77
|
-
|
|
78
|
-
itself, so the two spellings are one call.
|
|
65
|
+
Every setting also takes the block form — `GraphWeaver.configure { |config| … }`,
|
|
66
|
+
where `config` is `GraphWeaver` itself, so the two spellings are one call.
|
|
79
67
|
|
|
80
|
-
A registration that names one of your own constants — a `T::Enum` for
|
|
68
|
+
A registration that names one of your **own** constants — a `T::Enum` for
|
|
81
69
|
`register_enum`, a mixin for [`extend_type`](generated_modules.md#type-helpers)
|
|
82
|
-
— goes in a `to_prepare` block,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`:environment`, which runs `to_prepare` too, so the registration is in place
|
|
86
|
-
before it emits.
|
|
70
|
+
— goes in a `to_prepare` block instead, because autoloading is set up after
|
|
71
|
+
`config/initializers` run. Generation depends on `:environment`, which runs
|
|
72
|
+
`to_prepare` too, so the registration is still in place before it emits:
|
|
87
73
|
|
|
88
74
|
```ruby
|
|
89
75
|
Rails.application.config.to_prepare do
|
|
@@ -91,42 +77,34 @@ What it wrote:
|
|
|
91
77
|
GraphWeaver.extend_type("Pet", PetHelpers)
|
|
92
78
|
end
|
|
93
79
|
```
|
|
94
|
-
|
|
95
|
-
A custom `GraphQL::Schema::Validator` ([the `extensions.input`
|
|
96
|
-
recipe](errors.md#what-your-server-can-send)) is installed by *symbol*, so
|
|
97
|
-
nothing references its constant and Zeitwerk never autoloads it — name it in
|
|
98
|
-
the same `to_prepare` block, above the schema, or the schema raises
|
|
99
|
-
`unknown validation: :your_rule` on whichever file boots first.
|
|
100
|
-
|
|
101
80
|
- **`app/graphql/schema.json`.** The schema dump codegen reads
|
|
102
81
|
(`GraphWeaver.schema_path`) — never written by hand, always committed.
|
|
103
|
-
`cache: true` in the initializer reuses it; delete the file to
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
`cache: true` in the initializer reuses it; delete the file to re-introspect.
|
|
83
|
+
Prefer PR-reviewable diffs? `cache: :graphql` writes SDL instead; both
|
|
84
|
+
generate identical code. (`cache:`/`ttl:` apply only to url clients — a schema
|
|
85
|
+
source never introspects, so passing them raises.) A dump records the url it
|
|
86
|
+
came from, so a second client at a second origin caches under
|
|
87
|
+
`app/graphql/schema-<digest>.json` rather than overwriting the first's; name
|
|
88
|
+
the file yourself (`cache: "app/graphql/billing.json"`) to say which is which.
|
|
89
|
+
- **`graphql.config.yml`.** Five lines of YAML that give VS Code and RubyMine
|
|
90
|
+
schema autocomplete, hover docs, and validation as you type in `.graphql`
|
|
91
|
+
files — no JS project, no `npm install`. See [editors](editors.md).
|
|
111
92
|
- **`app/graphql/queries/`, `app/graphql/fragments/`, `app/graphql/generated/`.**
|
|
112
93
|
Where you write queries, where shared fragments live, and where generation
|
|
113
94
|
writes Ruby.
|
|
114
95
|
- **`.rubocop.yml`**, if you have one. Generated code is machine-written and
|
|
115
|
-
marked "do not edit," so the output directory is added to `AllCops: Exclude
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Everything above describes **one** schema, which is the usual case. An app
|
|
128
|
-
with a second one declares each as a graph — [more than one
|
|
129
|
-
schema](#more-than-one-schema), at the end; skip it until you have two.
|
|
96
|
+
marked "do not edit," so the output directory is added to `AllCops: Exclude:`.
|
|
97
|
+
An `AllCops:` you already have is left alone (a second one would replace it,
|
|
98
|
+
not merge); the generator prints the line to add.
|
|
99
|
+
|
|
100
|
+
Rake needs no wiring: in Rails the `graph_weaver:*` tasks register themselves and
|
|
101
|
+
depend on `:environment`, so your initializer runs first. The generated modules
|
|
102
|
+
load at boot from a `to_prepare` block, so a helper or enum you registered is
|
|
103
|
+
already in place when the file that names it loads.
|
|
104
|
+
|
|
105
|
+
All of that describes **one** schema, which is the usual case; an app with a
|
|
106
|
+
second one declares each as a graph ([more than one
|
|
107
|
+
schema](#more-than-one-schema)).
|
|
130
108
|
|
|
131
109
|
## 3. Write a query, generate, commit
|
|
132
110
|
|
|
@@ -157,19 +135,16 @@ where the schema calls the type `Country`. The corners are in
|
|
|
157
135
|
[generated modules](generated_modules.md#naming).
|
|
158
136
|
|
|
159
137
|
**Not sure what the API offers?** `app/graphql/schema.json` is the whole schema
|
|
160
|
-
as plain JSON — types, fields, descriptions — already in your repo
|
|
161
|
-
|
|
162
|
-
`.graphql` files as you type, with autocomplete and hover docs off that same
|
|
163
|
-
dump — see [editors](editors.md).
|
|
138
|
+
as plain JSON — types, fields, descriptions — already in your repo, and
|
|
139
|
+
[your editor reads it](editors.md) as you type the query.
|
|
164
140
|
|
|
165
141
|
**In development you don't type that command again.** While the server is
|
|
166
|
-
running, a `.graphql` edit — or a refreshed schema dump — regenerates before
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
graph_weaver:verify` in CI.
|
|
142
|
+
running, a `.graphql` edit — or a refreshed schema dump — regenerates before the
|
|
143
|
+
next request, the way a route change takes effect; a query that doesn't compile
|
|
144
|
+
is logged with its file and position while the modules already loaded keep
|
|
145
|
+
serving, so a file saved mid-edit doesn't take the server down. Development
|
|
146
|
+
only, and `config.graph_weaver.watch = false` turns it off. The generated files
|
|
147
|
+
are still what ships: commit them, and keep `rake graph_weaver:verify` in CI.
|
|
173
148
|
|
|
174
149
|
### Shared fragments
|
|
175
150
|
|
|
@@ -189,31 +164,16 @@ Fragment files hold only fragments (no operations), and names are unique across
|
|
|
189
164
|
them. Point elsewhere with `GraphWeaver.fragments_paths` (an appendable list,
|
|
190
165
|
default `app/graphql/fragments`).
|
|
191
166
|
|
|
192
|
-
One payoff
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
[abstract types](generated_modules.md#abstract-types).
|
|
167
|
+
One payoff: when a shared fragment *is* the whole selection on a union field,
|
|
168
|
+
its type is hoisted once into `GraphQLTypes` and every query that spreads it
|
|
169
|
+
gets the same Ruby type — so one exhaustive `case … T.absurd` works everywhere.
|
|
170
|
+
See [abstract types](generated_modules.md#abstract-types).
|
|
197
171
|
|
|
198
172
|
## 4. Test against fakes
|
|
199
173
|
|
|
200
174
|
The generator put `require "graph_weaver/rspec"` in your `spec/rails_helper.rb`
|
|
201
|
-
— add it there yourself if rspec arrived after the install, since the tags
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```ruby
|
|
205
|
-
it "renders the empty state", graphql: :fake do … end # or tag the describe
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
The tag installs a seeded, schema-correct `FakeClient` for that example — and
|
|
209
|
-
**nothing leaves the process**: no server, no HTTP at all, so no webmock and
|
|
210
|
-
no VCR. `rspec --seed 1234` reproduces the fake data along with test order.
|
|
211
|
-
The schema it fabricates from is derived (the committed dump, or your
|
|
212
|
-
client's), so there is nothing to configure. Tag `graphql: :in_process`
|
|
213
|
-
instead and the same example runs against your real resolvers.
|
|
214
|
-
|
|
215
|
-
A request spec is the usual shape: tag it, pin the value the assertion is
|
|
216
|
-
about, and everything else in the selection is still fabricated.
|
|
175
|
+
— add it there yourself if rspec arrived after the install, since the tags below
|
|
176
|
+
do nothing without it.
|
|
217
177
|
|
|
218
178
|
```ruby
|
|
219
179
|
# spec/requests/people_spec.rb
|
|
@@ -228,13 +188,17 @@ RSpec.describe "People", type: :request do
|
|
|
228
188
|
end
|
|
229
189
|
```
|
|
230
190
|
|
|
231
|
-
|
|
191
|
+
The tag installs a seeded, schema-correct `FakeClient` for that example, and
|
|
192
|
+
**nothing leaves the process**: no server, no HTTP, so no webmock and no VCR.
|
|
193
|
+
`rspec --seed 1234` reproduces the fake data along with test order. Pin the value
|
|
194
|
+
the assertion is about and everything else in the selection is still fabricated.
|
|
195
|
+
Tag `graphql: :in_process` instead and the same example runs against your real
|
|
196
|
+
resolvers. Pins, simulating failures, and the federated `graphql: :router` are in
|
|
232
197
|
[testing](testing.md).
|
|
233
198
|
|
|
234
|
-
A fresh `rails g rspec:install` leaves the `spec/support` glob commented
|
|
235
|
-
|
|
236
|
-
`rails_helper.rb` itself. Nothing warns you that a support file went
|
|
237
|
-
unread.
|
|
199
|
+
A fresh `rails g rspec:install` leaves the `spec/support` glob commented out in
|
|
200
|
+
`spec/rails_helper.rb`, so uncomment it — or put the require in
|
|
201
|
+
`rails_helper.rb` itself. Nothing warns you that a support file went unread.
|
|
238
202
|
|
|
239
203
|
## 5. Verify in CI
|
|
240
204
|
|
|
@@ -248,22 +212,17 @@ Five questions, five tasks — the last only on a federated graph:
|
|
|
248
212
|
| does the app still read what it selects? | `rake graph_weaver:unused` | no |
|
|
249
213
|
| did a subgraph change without a recompose? | `rake graph_weaver:federation:diff` | no |
|
|
250
214
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
215
|
+
Every one of them exits non-zero on a finding, so the gate is a chain.
|
|
216
|
+
`verify` compares the committed generated files against what the current schema
|
|
217
|
+
+ queries + registrations would produce, so it belongs in every CI build.
|
|
218
|
+
`schema:diff` asks whatever the dump came from — a recorded source url (with
|
|
219
|
+
`GRAPHWEAVER_AUTH` for private APIs), or your own schema class when the app
|
|
220
|
+
[serves the schema itself](#your-apps-own-schema-in-process) — and
|
|
221
|
+
`rake graph_weaver:schema:refresh` is the repair either way. On an app with more
|
|
222
|
+
than one schema, `rake graph_weaver:graphs` lists which graphs are configured,
|
|
223
|
+
where each generates, and what each registers.
|
|
255
224
|
|
|
256
|
-
|
|
257
|
-
schema + queries + registrations would produce, so it belongs in every CI
|
|
258
|
-
build. `schema:diff` asks whatever the dump came from — a recorded source url
|
|
259
|
-
(with `GRAPHWEAVER_AUTH` for private APIs), or your own schema class when the
|
|
260
|
-
app [serves the schema itself](#your-apps-own-schema-in-process) — and
|
|
261
|
-
`rake graph_weaver:schema:refresh` is the repair either way.
|
|
262
|
-
`federation:diff` needs no network, so it goes in the same PR run;
|
|
263
|
-
see [federation](federation.md#has-the-supergraph-been-recomposed).
|
|
264
|
-
|
|
265
|
-
Every one of them exits non-zero on a finding, so the gate is a chain. The two
|
|
266
|
-
topologies differ only in what reaches a network:
|
|
225
|
+
The two topologies differ only in what reaches a network:
|
|
267
226
|
|
|
268
227
|
```sh
|
|
269
228
|
# an API you don't own — the dump records the url it was introspected from
|
|
@@ -282,55 +241,20 @@ bundle exec rake graph_weaver:cassettes:check # do the recordings still cast?
|
|
|
282
241
|
```
|
|
283
242
|
|
|
284
243
|
In-process the order is the repair order: refresh the dump, regenerate,
|
|
285
|
-
re-record.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
on: [push]
|
|
291
|
-
jobs:
|
|
292
|
-
graph_weaver:
|
|
293
|
-
runs-on: ubuntu-latest
|
|
294
|
-
steps:
|
|
295
|
-
- uses: actions/checkout@v4
|
|
296
|
-
- uses: ruby/setup-ruby@v1
|
|
297
|
-
with:
|
|
298
|
-
bundler-cache: true
|
|
299
|
-
- run: bundle exec rake graph_weaver:verify
|
|
300
|
-
- run: bundle exec rake graph_weaver:cassettes:check
|
|
301
|
-
- run: bundle exec rake graph_weaver:queries:check
|
|
302
|
-
- run: bundle exec rake graph_weaver:schema:diff
|
|
303
|
-
env:
|
|
304
|
-
GRAPHWEAVER_AUTH: ${{ secrets.GRAPHWEAVER_AUTH }}
|
|
305
|
-
# federated apps only — needs no network, so it runs beside the rest
|
|
306
|
-
- run: bundle exec rake graph_weaver:federation:diff
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
An in-process schema needs no `env:` — it answers introspection itself.
|
|
244
|
+
re-record. In GitHub Actions each line is one `- run:` step; the remote chain
|
|
245
|
+
wants `GRAPHWEAVER_AUTH: ${{ secrets.GRAPHWEAVER_AUTH }}` in the `env:` of the
|
|
246
|
+
two steps that re-introspect, and an in-process schema needs no `env:` at all,
|
|
247
|
+
since it answers introspection itself. A federated app adds `rake
|
|
248
|
+
graph_weaver:federation:diff` to either chain — it needs no network.
|
|
310
249
|
|
|
311
250
|
**On a federated graph, none of the five looks at the schema production is
|
|
312
|
-
serving
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
endpoint serves it. Hot-reload a router onto a supergraph that dropped a field
|
|
316
|
-
your queries select and all five still exit 0 while every one of those requests
|
|
317
|
-
fails; the first detector is a user's failed request, answered by the runtime
|
|
318
|
-
[drift message](errors.md#stale-schemas). The pre-deploy check against the
|
|
319
|
-
live graph is Apollo's, not this gem's — run it in the same job:
|
|
320
|
-
|
|
321
|
-
```sh
|
|
322
|
-
rover subgraph check my-graph@prod --name products --schema products.graphql
|
|
323
|
-
rover supergraph fetch my-graph@prod # then recompose and diff what you get back
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
`rover subgraph check` asks GraphOS whether publishing this subgraph would break
|
|
327
|
-
the composition or a client's registered operations; `rover supergraph fetch`
|
|
328
|
-
hands you what the router is actually running, which is the artifact
|
|
329
|
-
`federation:diff` should have been pointed at all along. See
|
|
251
|
+
serving**, because no endpoint serves a composed supergraph. The pre-deploy
|
|
252
|
+
check against the live graph is Apollo's, not this gem's — run `rover subgraph
|
|
253
|
+
check` and `rover supergraph fetch` in the same job. See
|
|
330
254
|
[federation → in CI](federation.md#in-ci).
|
|
331
255
|
|
|
332
|
-
`schema:diff` names what moved, breaking changes first — breaking meaning
|
|
333
|
-
|
|
256
|
+
`schema:diff` names what moved, breaking changes first — breaking meaning a
|
|
257
|
+
query written against your dump stops validating, or stops casting:
|
|
334
258
|
|
|
335
259
|
```
|
|
336
260
|
app/graphql/schema.json vs https://api.example.com/graphql: 8 changes, 5 breaking
|
|
@@ -350,21 +274,17 @@ other:
|
|
|
350
274
|
app/graphql/schema.json is stale — the server's schema has drifted (rake graph_weaver:schema:refresh)
|
|
351
275
|
```
|
|
352
276
|
|
|
353
|
-
Nullability is judged from your side, which is why the two above point
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
`GraphWeaver::SchemaLoader.diff(path)` is the same summary as an object —
|
|
360
|
-
`#breaking`, `#compatible`, `#to_h`, and `#empty?` for the plain "has it
|
|
361
|
-
drifted" question.
|
|
277
|
+
Nullability is judged from your side, which is why the two above point opposite
|
|
278
|
+
ways: `Person.pets` losing its `!` hands a generated struct the nil it declared
|
|
279
|
+
it wouldn't get, while `AdoptionInput.nickname` gaining one rejects a query that
|
|
280
|
+
omits it. Any drift exits non-zero — whether a change matters is yours to judge.
|
|
281
|
+
`GraphWeaver::SchemaLoader.diff(path)` is the same summary as an object
|
|
282
|
+
(`#breaking`, `#compatible`, `#to_h`, `#empty?`).
|
|
362
283
|
|
|
363
|
-
`queries:check` answers the question that
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
error's line and column, and exits non-zero:
|
|
284
|
+
`queries:check` answers the question that matters when the schema *has* moved:
|
|
285
|
+
**which of your queries no longer validate, and why.** It re-introspects the
|
|
286
|
+
recorded url (without rewriting the dump) and validates every `.graphql` file
|
|
287
|
+
against the schema as it is right now, naming each error's line and column:
|
|
368
288
|
|
|
369
289
|
```
|
|
370
290
|
app/graphql/queries/person.graphql
|
|
@@ -373,90 +293,50 @@ app/graphql/queries/person.graphql
|
|
|
373
293
|
1 invalid query
|
|
374
294
|
```
|
|
375
295
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
# => { "app/graphql/queries/person.graphql" =>
|
|
382
|
-
# [{ "message" => "Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)",
|
|
383
|
-
# "line" => 4, "column" => 5 }] }
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Empty means everything validates. Pass `schema:` a *loaded* schema (not a path)
|
|
387
|
-
and nothing touches the network — handy for checking a proposed subgraph before
|
|
388
|
-
it's live:
|
|
389
|
-
|
|
390
|
-
```ruby
|
|
391
|
-
GraphWeaver.check_queries(schema: GraphWeaver::SchemaLoader.load("proposed.graphql"))
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
Left off, it re-introspects the url the dump records — and when that dump is a
|
|
395
|
-
composed supergraph, each error also names the subgraphs behind the type it
|
|
396
|
-
points at ([federation](federation.md#the-routing-table)).
|
|
296
|
+
`GraphWeaver.check_queries` returns the same findings as data — a hash of file
|
|
297
|
+
to `message`/`line`/`column`, empty when everything validates — so you can wire
|
|
298
|
+
it into a spec, a Slack ping, an issue. Pass it `schema:` a *loaded* schema (not
|
|
299
|
+
a path) and nothing touches the network, which is how you check your queries
|
|
300
|
+
against a proposed subgraph before it's live.
|
|
397
301
|
|
|
398
302
|
### The selections nothing reads
|
|
399
303
|
|
|
400
304
|
`rake graph_weaver:unused` asks the one question the others can't: not "is the
|
|
401
305
|
Ruby fresh" but "does the app still use what the query asks for". A field
|
|
402
306
|
someone stopped rendering stays in the `.graphql` forever — the query keeps
|
|
403
|
-
validating, the struct keeps casting, and the server keeps paying to resolve
|
|
404
|
-
it. graphql-client catches that at runtime by masking the data a caller didn't
|
|
405
|
-
declare; the structs are checked in here, so it can be recovered without
|
|
406
|
-
running anything:
|
|
307
|
+
validating, the struct keeps casting, and the server keeps paying to resolve it.
|
|
407
308
|
|
|
408
309
|
```
|
|
409
310
|
app/graphql/queries/products.graphql: Products.sku — selected, never read (Catalog::ProductsQuery::Result::Products#sku)
|
|
410
|
-
app/graphql/queries/products.graphql: Products.blurb — selected, never read (Catalog::ProductsQuery::Result::Products#blurb)
|
|
411
311
|
|
|
412
312
|
13 selections, 2 unread — 2 queries, 58 files swept under .
|
|
413
313
|
```
|
|
414
314
|
|
|
415
315
|
Each line names the query file, the selection to go and delete, and the
|
|
416
|
-
generated prop behind it. It reads the generated structs for the props a
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
it consumes. Nothing is edited and the exit is 0; `STRICT=1` exits 1 when
|
|
428
|
-
anything is unread, for teams who want the gate.
|
|
429
|
-
|
|
430
|
-
A line handing a query module straight to a serializer — `render json:`,
|
|
431
|
-
`to_h`, `to_json`, `as_json`, `serialize`, `deconstruct_keys` — reads every
|
|
432
|
-
prop at once, so that module is excused and the line is quoted, because
|
|
433
|
-
matching a serializer by name is the softest thing here and a wrong excuse
|
|
434
|
-
should be obvious:
|
|
435
|
-
|
|
436
|
-
```
|
|
437
|
-
Accounts::MeQuery: every prop counted as read — handed whole to a serializer at app/controllers/accounts_controller.rb:3
|
|
438
|
-
render json: Accounts::MeQuery.execute!.me
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
A local counts too, which is what makes the ordinary two-line controller work
|
|
442
|
-
— `result = Accounts::MeQuery.execute!` on one line, `render json: result.me`
|
|
443
|
-
on the next — and the excuse names the local it followed.
|
|
316
|
+
generated prop behind it. It reads the generated structs for the props a query
|
|
317
|
+
produced, then sweeps your `.rb`, `.rake`, `.builder`, `.erb`, `.slim`, `.haml`
|
|
318
|
+
and `.jbuilder` for every name they could be read by — `.sku`, `sku:`, `:sku`,
|
|
319
|
+
`"sku"`. `PATHS=app,lib` narrows the sweep (a `PATHS=` naming a directory that
|
|
320
|
+
isn't there is refused rather than swept as nothing); anything under a directory
|
|
321
|
+
named `generated`, plus `vendor`, `node_modules`, `tmp` and `log`, is skipped,
|
|
322
|
+
as is any file defining a graphql-ruby **type** — a `field :sku` there is your
|
|
323
|
+
*server* offering a field, not this app reading one back. A module handed whole
|
|
324
|
+
to a serializer (`render json:`, `to_h`, `as_json`, a local and all) counts every
|
|
325
|
+
prop as read, and the report quotes the line it followed. Nothing is edited and
|
|
326
|
+
the exit is 0; `STRICT=1` exits 1 when anything is unread.
|
|
444
327
|
|
|
445
328
|
**It is a lint, not a proof**, and the task's own footer says so. It matches
|
|
446
|
-
names as text, so a prop called `name` counts as read the moment anything
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Silence is the safe direction here. Treat a finding as a prompt to go and
|
|
453
|
-
look, and a clean run as nothing more than the absence of an obvious one —
|
|
454
|
-
which is why it exits 0 unless you ask it not to.
|
|
329
|
+
names as text, so a prop called `name` counts as read the moment anything says
|
|
330
|
+
`.name`, and it can't see a prop reached by `public_send`. Measured against real
|
|
331
|
+
corpora, **half to two thirds of genuinely unread selections go unreported**,
|
|
332
|
+
the share rising with the size of the app. Silence is the safe direction: treat
|
|
333
|
+
a finding as a prompt to go and look, and a clean run as nothing more than the
|
|
334
|
+
absence of an obvious one.
|
|
455
335
|
|
|
456
336
|
## Your app's own schema, in-process
|
|
457
337
|
|
|
458
|
-
An app that *serves* GraphQL with graphql-ruby can have the same typed
|
|
459
|
-
|
|
338
|
+
An app that *serves* GraphQL with graphql-ruby can have the same typed access to
|
|
339
|
+
its own API — same generated structs, no socket, no HTTP:
|
|
460
340
|
|
|
461
341
|
```sh
|
|
462
342
|
rails g graph_weaver:install MyApp::Schema
|
|
@@ -470,13 +350,13 @@ Rails.application.config.to_prepare do
|
|
|
470
350
|
end
|
|
471
351
|
```
|
|
472
352
|
|
|
473
|
-
`to_prepare`, not a bare assignment: the schema class is autoloaded, so it
|
|
474
|
-
|
|
475
|
-
|
|
353
|
+
`to_prepare`, not a bare assignment: the schema class is autoloaded, so it isn't
|
|
354
|
+
resolvable while initializers run, and a dev reload replaces it with a new class
|
|
355
|
+
object that a captured one would go stale against.
|
|
476
356
|
|
|
477
357
|
**Context is per request, not per app.** A resolver reading
|
|
478
|
-
`context[:current_user]` gets nil from the app default — build a client
|
|
479
|
-
|
|
358
|
+
`context[:current_user]` gets nil from the app default — build a client where
|
|
359
|
+
you know the request and pass it per call:
|
|
480
360
|
|
|
481
361
|
```ruby
|
|
482
362
|
client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
|
|
@@ -484,14 +364,14 @@ PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
|
|
|
484
364
|
```
|
|
485
365
|
|
|
486
366
|
Each query gets its own copy of that hash, so a resolver writing
|
|
487
|
-
`context[:loader] =` can't hand what it wrote to the next request
|
|
488
|
-
matters because one in-process client is normally the whole app's.
|
|
367
|
+
`context[:loader] =` can't hand what it wrote to the next request.
|
|
489
368
|
|
|
490
|
-
**Keep the dump in step with the schema.** Codegen reads the committed
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
369
|
+
**Keep the dump in step with the schema.** Codegen reads the committed dump at
|
|
370
|
+
`GraphWeaver.schema_path`, never the live class — that's what makes `rake
|
|
371
|
+
graph_weaver:verify` a deterministic CI check, and why it **fails** when the
|
|
372
|
+
dump has fallen behind the class rather than calling the tree up to date. The
|
|
373
|
+
same two tasks a remote schema uses keep it in step, and neither touches a
|
|
374
|
+
network here:
|
|
495
375
|
|
|
496
376
|
```sh
|
|
497
377
|
rake graph_weaver:schema:diff # what has the class changed since the dump?
|
|
@@ -499,26 +379,16 @@ rake graph_weaver:schema:refresh # rewrite the dump from the class
|
|
|
499
379
|
rake graph_weaver:generate
|
|
500
380
|
```
|
|
501
381
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
up: `schema:refresh` writes to the path graph_weaver already reads, and is
|
|
507
|
-
what `schema:diff` and a runtime "the schema may have changed" error both
|
|
508
|
-
name.
|
|
509
|
-
|
|
510
|
-
`verify` **fails** when the dump has fallen behind the class, rather than
|
|
511
|
-
calling the tree up to date: generating from a dump that old would produce
|
|
512
|
-
Ruby for a schema your resolvers have already left. `queries:check` is
|
|
513
|
-
unaffected either way — running in-process it validates against the live
|
|
514
|
-
class, not the dump — so the two can disagree about the same query while the
|
|
515
|
-
dump is stale, and `schema:refresh` is what settles it.
|
|
382
|
+
`refresh` rewrites the dump in whatever format it already is, so a repo that
|
|
383
|
+
chose `cache: :graphql` keeps SDL. `queries:check` validates against the live
|
|
384
|
+
class rather than the dump, so the two can disagree about one query while the
|
|
385
|
+
dump is stale; `schema:refresh` settles it.
|
|
516
386
|
|
|
517
387
|
**Scaffolding the app too?** On a `rails new --skip-active-record`,
|
|
518
388
|
`rails g graphql:install` writes `config.active_record.query_log_tags` lines
|
|
519
|
-
into `config/application.rb` that an app without ActiveRecord can't boot
|
|
520
|
-
|
|
521
|
-
|
|
389
|
+
into `config/application.rb` that an app without ActiveRecord can't boot with —
|
|
390
|
+
a graphql-ruby bug. Run it as `rails g graphql:install --skip-query-logs`, or
|
|
391
|
+
delete the lines it wrote.
|
|
522
392
|
|
|
523
393
|
## A schema dump you already have
|
|
524
394
|
|
|
@@ -526,14 +396,14 @@ with — a graphql-ruby bug. Run it as
|
|
|
526
396
|
rails g graph_weaver:install db/schema.graphql
|
|
527
397
|
```
|
|
528
398
|
|
|
529
|
-
Sets `GraphWeaver.schema_path` to that file rather than writing a second
|
|
530
|
-
|
|
531
|
-
|
|
399
|
+
Sets `GraphWeaver.schema_path` to that file rather than writing a second copy,
|
|
400
|
+
and introspects nothing. A dump has no resolvers, so it can't execute — set
|
|
401
|
+
`GraphWeaver.client` to whatever serves the API.
|
|
532
402
|
|
|
533
403
|
## More than one schema
|
|
534
404
|
|
|
535
|
-
The five steps above describe one graph — a schema, its queries, its output.
|
|
536
|
-
|
|
405
|
+
The five steps above describe one graph — a schema, its queries, its output. An
|
|
406
|
+
app with a second schema declares each one:
|
|
537
407
|
|
|
538
408
|
```ruby
|
|
539
409
|
# config/initializers/graph_weaver.rb
|
|
@@ -545,176 +415,89 @@ GraphWeaver.graph :billing do
|
|
|
545
415
|
namespace "Billing"
|
|
546
416
|
register_scalar "Money", BigDecimal
|
|
547
417
|
end
|
|
548
|
-
|
|
549
|
-
GraphWeaver.graph :github do
|
|
550
|
-
schema "db/github.json"
|
|
551
|
-
queries "app/graphql/github/queries"
|
|
552
|
-
output "app/graphql/github/generated"
|
|
553
|
-
client "GITHUB"
|
|
554
|
-
namespace "GitHub"
|
|
555
|
-
end
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**Declaring graph two means declaring graph one.** A declared graph *replaces*
|
|
559
|
-
the implicit one the top-level settings describe, so the moment any graph is
|
|
560
|
-
named, the `app/graphql/queries` and `app/graphql/generated` an existing app was
|
|
561
|
-
already using belong to no graph — nothing generates or prunes them, and
|
|
562
|
-
`generate` refuses the whole app rather than leave them there silently. Wrap
|
|
563
|
-
them in a graph of their own, its directories and nothing else:
|
|
564
|
-
|
|
565
|
-
```ruby
|
|
566
|
-
GraphWeaver.graph :app do
|
|
567
|
-
queries "app/graphql/queries"
|
|
568
|
-
output "app/graphql/generated"
|
|
569
|
-
end
|
|
570
418
|
```
|
|
571
419
|
|
|
572
|
-
No `schema`, so it keeps reading the one `schema_path` names; no `namespace`,
|
|
573
|
-
so every constant keeps the name it has. Nothing about the existing queries or
|
|
574
|
-
generated files changes.
|
|
575
|
-
|
|
576
420
|
One `rake graph_weaver:generate` does the app, one `rake graph_weaver:verify`
|
|
577
|
-
gates it, and `rake graph_weaver:graphs` lists what is configured.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
the block calls is refused naming the nine it takes.
|
|
421
|
+
gates it, and `rake graph_weaver:graphs` lists what is configured. Each setting
|
|
422
|
+
falls back to the matching top-level one, so a graph says only what differs, and
|
|
423
|
+
anything else the block calls is refused naming the nine it takes (these six,
|
|
424
|
+
plus `register_scalar`, `register_enum` and `extend_type`).
|
|
582
425
|
|
|
583
426
|
| setting | takes |
|
|
584
427
|
|---|---|
|
|
585
428
|
| `schema` | a graphql-ruby schema class, a [`Client`](transports.md), a path to a dump, SDL, or a lambda returning one |
|
|
586
429
|
| `queries` | a directory, or a list of them — the `.graphql` files this graph generates from |
|
|
587
430
|
| `output` | one directory — where this graph's generated Ruby is written |
|
|
588
|
-
| `client` | a
|
|
431
|
+
| `client` | what this graph's modules execute against — a client, or the name of the constant holding one |
|
|
589
432
|
| `namespace` | a constant, or its name — what every constant this graph generates nests under |
|
|
590
433
|
| `types_module` | a constant name for the shared types module (default: `GraphQLTypes`, under `namespace`) |
|
|
591
434
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
Resolving at first use rather than at declaration is what lets an initializer
|
|
602
|
-
name `Billing::Schema` before Zeitwerk has loaded it, and what lets a dev reload
|
|
603
|
-
swap the class object underneath. A graph with no `client` generates modules
|
|
604
|
-
that fall back to `GraphWeaver.client`, the app default.
|
|
605
|
-
|
|
606
|
-
`schema "x"` sets and a bare `schema` reads back. There is no `schema = "x"`
|
|
607
|
-
form: the block is `instance_eval`'d, so that would be a local variable that
|
|
608
|
-
silently does nothing — the same reason graphql-ruby writes `field :name`.
|
|
609
|
-
|
|
610
|
-
### Two remote APIs, and no schema of your own
|
|
611
|
-
|
|
612
|
-
An app that is a pure client of someone else's GraphQL owns no schema class at
|
|
613
|
-
all, so every graph's schema is a dump — and the dumps have to come from
|
|
614
|
-
somewhere. Name the file you want and the client that can fetch it, and
|
|
615
|
-
`schema:refresh` writes it:
|
|
435
|
+
**Declaring graph two means declaring graph one.** A declared graph *replaces*
|
|
436
|
+
the implicit one the top-level settings describe, so the moment any graph is
|
|
437
|
+
named, the `app/graphql/queries` and `app/graphql/generated` an existing app was
|
|
438
|
+
already using belong to no graph — and `generate` refuses the whole app rather
|
|
439
|
+
than leave them there silently. Wrap them in a graph of their own, its
|
|
440
|
+
directories and nothing else: no `schema`, so it keeps reading the one
|
|
441
|
+
`schema_path` names, and no `namespace`, so every constant keeps the name it has.
|
|
616
442
|
|
|
617
443
|
```ruby
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
GraphWeaver.graph :countries do
|
|
623
|
-
schema "app/graphql/countries/schema.json"
|
|
624
|
-
queries "app/graphql/countries/queries"
|
|
625
|
-
output "app/graphql/countries/generated"
|
|
626
|
-
client "COUNTRIES"
|
|
627
|
-
namespace "Countries"
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
GraphWeaver.graph :poke do
|
|
631
|
-
schema "app/graphql/poke/schema.json"
|
|
632
|
-
queries "app/graphql/poke/queries"
|
|
633
|
-
output "app/graphql/poke/generated"
|
|
634
|
-
client "POKE"
|
|
635
|
-
namespace "Poke"
|
|
444
|
+
GraphWeaver.graph :app do
|
|
445
|
+
queries "app/graphql/queries"
|
|
446
|
+
output "app/graphql/generated"
|
|
636
447
|
end
|
|
637
448
|
```
|
|
638
449
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
450
|
+
**`client` is where this graph's endpoint lives** — its modules say which graph
|
|
451
|
+
they belong to and nothing about transport, so they read it when they execute.
|
|
452
|
+
A graph with no `client` falls back to `GraphWeaver.client`, the app default.
|
|
453
|
+
Name the object (`client GraphWeaver.new(url, auth: …)`) or, when the constant
|
|
454
|
+
holding it is defined later than the graph block, its name (`client "GITHUB"`),
|
|
455
|
+
which is resolved on first use. `schema "x"` sets and a bare `schema` reads
|
|
456
|
+
back; there is no `schema = "x"` form, since the block is `instance_eval`'d and
|
|
457
|
+
that would be a local variable that silently does nothing.
|
|
458
|
+
|
|
459
|
+
**`namespace` nests everything that graph generates** — `person.graphql` becomes
|
|
460
|
+
`Billing::PersonQuery` ([naming](generated_modules.md#naming)). Constants are
|
|
461
|
+
global, so two schemas that both have a `person.graphql` would otherwise fight
|
|
462
|
+
over one name; without a namespace the collision is refused at generation,
|
|
463
|
+
naming both files. **The block's registrations reach that graph alone**, laid
|
|
464
|
+
over the top-level ones — so `Money` is checked against the schema it was
|
|
465
|
+
registered for, and against no other.
|
|
466
|
+
|
|
467
|
+
An app that is a pure client of someone else's GraphQL owns no schema class, so
|
|
468
|
+
every graph's `schema` is a dump. Give each the file you want and a `client`
|
|
469
|
+
that can fetch it: `rake graph_weaver:schema:refresh` introspects each graph's
|
|
470
|
+
client into its own dump, recording the url so every later `schema:refresh` and
|
|
471
|
+
`schema:diff` re-reads the right server. (`URL=` is for the app that has one
|
|
472
|
+
dump and no graphs.)
|
|
649
473
|
|
|
650
474
|
In specs, `graph:` is how an example says which graph a helper stands in for —
|
|
651
475
|
`graphql_fake(graph: :poke, "pokemon_v2_pokemon.name" => "pikachu")`. See
|
|
652
|
-
[testing
|
|
476
|
+
[testing](testing.md).
|
|
653
477
|
|
|
654
478
|
**In Rails, declare graphs in the initializer itself, and name an autoloaded
|
|
655
|
-
schema class with a lambda** — `schema -> { Billing::Schema }
|
|
479
|
+
schema class with a lambda** — `schema -> { Billing::Schema }`, as above.
|
|
656
480
|
Zeitwerk is set up *after* `config/initializers` run, so a bare
|
|
657
481
|
`Billing::Schema` there raises `uninitialized constant`; the lambda is resolved
|
|
658
|
-
when generation asks, and
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
`to_prepare`
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
GraphWeaver.graph :billing do
|
|
670
|
-
schema Billing::Schema
|
|
671
|
-
queries "app/graphql/billing/queries"
|
|
672
|
-
output "app/graphql/billing/generated"
|
|
673
|
-
namespace "Billing"
|
|
674
|
-
register_enum "Species", PetKind
|
|
675
|
-
end
|
|
676
|
-
end
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
Re-running is safe — the name is the identity, so the second declaration
|
|
680
|
-
replaces the first — and watch mode sees the graph either way.
|
|
681
|
-
|
|
682
|
-
One constraint: `to_prepare` runs after Rails has set Zeitwerk up, and Zeitwerk
|
|
683
|
-
reads its ignore list only then, so an `output` declared there can't be hidden
|
|
684
|
-
from autoloading. Under the conventional `app/graphql/*/generated` it already
|
|
685
|
-
is; anywhere else is refused at boot, naming the two fixes — declare the graph
|
|
686
|
-
in `config/initializers` with `schema -> { Billing::Schema }`, or add the
|
|
687
|
-
directory to `GraphWeaver.generated_paths` there.
|
|
688
|
-
|
|
689
|
-
Two things are worth knowing:
|
|
690
|
-
|
|
691
|
-
- **`namespace` nests everything that graph generates** — `person.graphql`
|
|
692
|
-
becomes `Billing::PersonQuery`, and its shared types module becomes
|
|
693
|
-
`Billing::GraphQLTypes`. Constants are global, so two schemas that both have a
|
|
694
|
-
`person.graphql`, or that both hoist an enum, would otherwise fight over one
|
|
695
|
-
name. Without a namespace the collision is refused at generation, naming both
|
|
696
|
-
files.
|
|
697
|
-
- **The block's registrations reach that graph alone**, laid over the top-level
|
|
698
|
-
ones. That is what makes the build quiet: `Money` is checked against the
|
|
699
|
-
schema it was registered for, and against no other. The top-level layer is
|
|
700
|
-
read when generation asks, not when the graph is declared, so a
|
|
701
|
-
`register_scalar` in another initializer reaches every graph whichever
|
|
702
|
-
initializer Rails happened to run first.
|
|
703
|
-
|
|
704
|
-
Declaring any graph replaces the implicit one the settings describe — an app
|
|
705
|
-
either has graphs or has settings, never a silent third thing. The name is the
|
|
706
|
-
identity, so re-declaring `:billing` replaces it rather than adding a second
|
|
707
|
-
one; a `to_prepare` block that re-runs on every reload is safe.
|
|
482
|
+
when generation asks, and again after a dev reload has replaced the class
|
|
483
|
+
object.
|
|
484
|
+
|
|
485
|
+
A block **runs where you write it**, registrations included, so a graph whose
|
|
486
|
+
registrations name your own constants is declared from a `to_prepare` block for
|
|
487
|
+
the reason [above](#2-run-the-generator) — safely, since the name is the
|
|
488
|
+
identity and the second declaration replaces the first. One constraint there:
|
|
489
|
+
Zeitwerk reads its ignore list before `to_prepare` runs, so an `output` declared
|
|
490
|
+
in one can't be hidden from autoloading. Under the conventional
|
|
491
|
+
`app/graphql/*/generated` it already is; anywhere else is refused at boot,
|
|
492
|
+
naming the two fixes.
|
|
708
493
|
|
|
709
494
|
## Sorbet, with or without
|
|
710
495
|
|
|
711
496
|
`sorbet-runtime` is a hard dependency, so generated `T::Struct`s and sigs
|
|
712
|
-
enforce at runtime in every app — no Sorbet setup required on your end.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
works without Sorbet; codegen plus Sorbet is what moves type errors from
|
|
717
|
-
runtime to CI.
|
|
497
|
+
enforce at runtime in every app — no Sorbet setup required on your end. The
|
|
498
|
+
*static* layer (`srb tc` flagging a typo'd field before anything runs) applies
|
|
499
|
+
only when your app runs Sorbet, and only to checked-in generated files — dynamic
|
|
500
|
+
`parse` is invisible to `srb tc`.
|
|
718
501
|
|
|
719
502
|
A misspelled field is caught either way — by `srb tc` before it runs, or by
|
|
720
503
|
`NoMethodError` the first time it does. Nullability is the gap:
|
|
@@ -724,14 +507,14 @@ may be none of your dev data and plenty of production's.
|
|
|
724
507
|
|
|
725
508
|
If your app globally injects `T::Sig` (`class Module; include T::Sig`), the
|
|
726
509
|
per-struct `extend T::Sig` in generated files is redundant — rubocop's
|
|
727
|
-
`Sorbet/RedundantExtendTSig` flags it. GraphWeaver
|
|
728
|
-
|
|
729
|
-
|
|
510
|
+
`Sorbet/RedundantExtendTSig` flags it. GraphWeaver detects that at generation
|
|
511
|
+
time and skips the `extend`; override with `GraphWeaver.extend_t_sig =
|
|
512
|
+
true`/`false`.
|
|
730
513
|
|
|
731
514
|
## Not Rails?
|
|
732
515
|
|
|
733
|
-
There's no generator, but what it writes is short — a few lines wherever
|
|
734
|
-
|
|
516
|
+
There's no generator, but what it writes is short — a few lines wherever your
|
|
517
|
+
app boots, two directories, and the schema dump:
|
|
735
518
|
|
|
736
519
|
```ruby
|
|
737
520
|
GraphWeaver.client = GraphWeaver.new(
|
|
@@ -748,12 +531,12 @@ rake graph_weaver:schema:refresh URL=https://api.example.com/graphql
|
|
|
748
531
|
```
|
|
749
532
|
|
|
750
533
|
Those paths are the Rails convention, not a requirement: put the files where
|
|
751
|
-
your project already puts things and say so with `GraphWeaver.queries_paths`
|
|
752
|
-
|
|
534
|
+
your project already puts things and say so with `GraphWeaver.queries_paths` and
|
|
535
|
+
`GraphWeaver.generated_paths`.
|
|
753
536
|
|
|
754
|
-
Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks —
|
|
755
|
-
|
|
756
|
-
|
|
537
|
+
Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks — and,
|
|
538
|
+
since there's no `:environment` hook to run your registrations, require the file
|
|
539
|
+
that does them from the Rakefile too.
|
|
757
540
|
|
|
758
541
|
**Or skip rake too.** The tasks are a thin wrapper over public calls, so a
|
|
759
542
|
script of your own does the same work — and `cache: true` writes the dump on
|
|
@@ -768,8 +551,8 @@ GraphWeaver.generate!(schema:) # => every file the plan produces
|
|
|
768
551
|
GraphWeaver.changed_files # => only the ones whose bytes moved
|
|
769
552
|
```
|
|
770
553
|
|
|
771
|
-
Pruning, the shared types module, and `verify_generated!` — the freshness
|
|
772
|
-
|
|
554
|
+
Pruning, the shared types module, and `verify_generated!` — the freshness guard
|
|
555
|
+
`rake graph_weaver:verify` runs — are in
|
|
773
556
|
[generated modules](generated_modules.md#generating).
|
|
774
557
|
|
|
775
558
|
`graphql.config.yml` is copy/paste from [editors](editors.md).
|