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/getting_started.md
CHANGED
|
@@ -6,7 +6,11 @@ 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
|
|
9
|
-
covers the differences.
|
|
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.
|
|
10
14
|
|
|
11
15
|
## 1. Install
|
|
12
16
|
|
|
@@ -27,6 +31,7 @@ rails g graph_weaver:install https://api.example.com/graphql
|
|
|
27
31
|
create app/graphql/fragments/.keep
|
|
28
32
|
create app/graphql/generated/.keep
|
|
29
33
|
create graphql.config.yml
|
|
34
|
+
insert spec/rails_helper.rb
|
|
30
35
|
introspect app/graphql/schema.json from https://api.example.com/graphql
|
|
31
36
|
```
|
|
32
37
|
|
|
@@ -42,11 +47,18 @@ initializer that fits:
|
|
|
42
47
|
|
|
43
48
|
| flag | |
|
|
44
49
|
|---|---|
|
|
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 |
|
|
46
|
-
| `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh
|
|
50
|
+
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only, and omitted entirely for a public API that needs no token. The name is recorded into the dump, so `schema:refresh`/`schema:diff`/`queries:check` read the same one the initializer does |
|
|
51
|
+
| `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh` does it later (`URL=...` to name an endpoint the first time) |
|
|
47
52
|
|
|
48
|
-
Re-running is safe — every file goes through the usual Rails
|
|
49
|
-
prompt, so an initializer you've edited is never overwritten
|
|
53
|
+
Re-running is safe — every file it writes goes through the usual Rails
|
|
54
|
+
conflict prompt, so an initializer you've edited is never overwritten
|
|
55
|
+
silently. The schema dump is the one file that isn't Thor's to diff, so it is
|
|
56
|
+
**kept** rather than prompted for — replacing it would drop the source url it
|
|
57
|
+
records, which `schema:diff`/`:refresh` read:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
keep app/graphql/schema.json — delete it and re-run to re-introspect
|
|
61
|
+
```
|
|
50
62
|
|
|
51
63
|
What it wrote:
|
|
52
64
|
|
|
@@ -55,15 +67,20 @@ What it wrote:
|
|
|
55
67
|
it at execute time (the full
|
|
56
68
|
[resolution order](transports.md#client-resolution)). Custom
|
|
57
69
|
scalars/enums/type helpers register here too — the rake tasks bake them
|
|
58
|
-
into generated source, so they have to run first:
|
|
70
|
+
into generated source, so they have to run first ([scalars](scalars.md)):
|
|
59
71
|
|
|
60
72
|
```ruby
|
|
61
73
|
GraphWeaver.register_scalar("Money", Money) # a scalar the registry can't know
|
|
62
74
|
```
|
|
63
75
|
|
|
76
|
+
Every setting also takes the block form the file's neighbours use —
|
|
77
|
+
`GraphWeaver.configure { |config| … }`, where `config` is `GraphWeaver`
|
|
78
|
+
itself, so the two spellings are one call.
|
|
79
|
+
|
|
64
80
|
A registration that names one of your own constants — a `T::Enum` for
|
|
65
|
-
`register_enum`, a mixin for `extend_type`
|
|
66
|
-
the same place the in-process client goes and
|
|
81
|
+
`register_enum`, a mixin for [`extend_type`](generated_modules.md#type-helpers)
|
|
82
|
+
— goes in a `to_prepare` block, the same place the in-process client goes and
|
|
83
|
+
for the same reason:
|
|
67
84
|
autoloading is set up after `config/initializers` run. Generation depends on
|
|
68
85
|
`:environment`, which runs `to_prepare` too, so the registration is in place
|
|
69
86
|
before it emits.
|
|
@@ -75,6 +92,12 @@ What it wrote:
|
|
|
75
92
|
end
|
|
76
93
|
```
|
|
77
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
|
+
|
|
78
101
|
- **`app/graphql/schema.json`.** The schema dump codegen reads
|
|
79
102
|
(`GraphWeaver.schema_path`) — never written by hand, always committed.
|
|
80
103
|
`cache: true` in the initializer reuses it; delete the file to
|
|
@@ -88,6 +111,12 @@ What it wrote:
|
|
|
88
111
|
- **`app/graphql/queries/`, `app/graphql/fragments/`, `app/graphql/generated/`.**
|
|
89
112
|
Where you write queries, where shared fragments live, and where generation
|
|
90
113
|
writes Ruby.
|
|
114
|
+
- **`.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
|
+
— otherwise `Style/Documentation`, `Style/ClassAndModuleChildren` and
|
|
117
|
+
`Metrics/*` fire on every generated file. An `AllCops:` you already have is
|
|
118
|
+
left alone (a second one would replace it, not merge); the generator prints
|
|
119
|
+
the line to add.
|
|
91
120
|
|
|
92
121
|
Rake needs no wiring either: in Rails the `graph_weaver:*` tasks register
|
|
93
122
|
themselves (a Railtie) and depend on `:environment`, so your initializer —
|
|
@@ -95,79 +124,9 @@ and its registrations — runs first. The generated modules load at boot from
|
|
|
95
124
|
a `to_prepare` block, so a helper or enum you registered in one is already
|
|
96
125
|
in place when the file that names it loads.
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
access to its own API — same generated structs, no socket, no HTTP:
|
|
102
|
-
|
|
103
|
-
```sh
|
|
104
|
-
rails g graph_weaver:install MyApp::Schema
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
# config/initializers/graph_weaver.rb
|
|
109
|
-
Rails.application.config.to_prepare do
|
|
110
|
-
# queries run in-process against the app's own schema — no socket
|
|
111
|
-
GraphWeaver.client = GraphWeaver.new(MyApp::Schema)
|
|
112
|
-
end
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`to_prepare`, not a bare assignment: the schema class is autoloaded, so it
|
|
116
|
-
isn't resolvable while initializers run, and a dev reload replaces it with
|
|
117
|
-
a new class object that a captured one would go stale against.
|
|
118
|
-
|
|
119
|
-
**Context is per request, not per app.** A resolver reading
|
|
120
|
-
`context[:current_user]` gets nil from the app default — build a client
|
|
121
|
-
where you know the request and pass it per call:
|
|
122
|
-
|
|
123
|
-
```ruby
|
|
124
|
-
client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
|
|
125
|
-
PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
|
|
126
|
-
```
|
|
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
|
-
|
|
132
|
-
**Keep the dump in step with the schema.** Codegen reads the committed
|
|
133
|
-
dump at `GraphWeaver.schema_path`, never the live class — that's what
|
|
134
|
-
makes `rake graph_weaver:verify` a deterministic CI check. The generator
|
|
135
|
-
writes the first dump; after that it's an artifact derived from code in
|
|
136
|
-
your own repo, so rebuild it with graphql-ruby's own rake task, ahead of
|
|
137
|
-
`verify` in CI:
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
# lib/tasks/graphql.rake
|
|
141
|
-
require "graphql/rake_task"
|
|
142
|
-
GraphQL::RakeTask.new(schema_name: "MyApp::Schema", directory: "app/graphql",
|
|
143
|
-
dependencies: [:environment])
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
```sh
|
|
147
|
-
rake graphql:schema:json # rewrites app/graphql/schema.json
|
|
148
|
-
rake graph_weaver:generate
|
|
149
|
-
```
|
|
150
|
-
|
|
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.
|
|
161
|
-
|
|
162
|
-
### A schema dump you already have
|
|
163
|
-
|
|
164
|
-
```sh
|
|
165
|
-
rails g graph_weaver:install db/schema.graphql
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Sets `GraphWeaver.schema_path` to that file rather than writing a second
|
|
169
|
-
copy, and introspects nothing. A dump has no resolvers, so it can't
|
|
170
|
-
execute — set `GraphWeaver.client` to whatever serves the API.
|
|
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.
|
|
171
130
|
|
|
172
131
|
## 3. Write a query, generate, commit
|
|
173
132
|
|
|
@@ -190,13 +149,18 @@ PersonQuery.execute!(id: "1").person&.name # typed, via GraphWeaver.client
|
|
|
190
149
|
```
|
|
191
150
|
|
|
192
151
|
Commit the schema dump and the generated files. Generated code is reviewed like
|
|
193
|
-
any other code — and never edited by hand.
|
|
194
|
-
name
|
|
152
|
+
any other code — and never edited by hand. One rule covers the names: the file
|
|
153
|
+
name becomes the module (`person.graphql` → `PersonQuery`), and every selection
|
|
154
|
+
inside it becomes a struct named for its **response key**, not its schema type
|
|
155
|
+
— a `countries { … }` selection is `CountriesQuery::Result::Countries` even
|
|
156
|
+
where the schema calls the type `Country`. The corners are in
|
|
195
157
|
[generated modules](generated_modules.md#naming).
|
|
196
158
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
159
|
+
**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. And
|
|
161
|
+
`graphql.config.yml` is there too, so VS Code and RubyMine validate the
|
|
162
|
+
`.graphql` files as you type, with autocomplete and hover docs off that same
|
|
163
|
+
dump — see [editors](editors.md).
|
|
200
164
|
|
|
201
165
|
**In development you don't type that command again.** While the server is
|
|
202
166
|
running, a `.graphql` edit — or a refreshed schema dump — regenerates before
|
|
@@ -233,21 +197,38 @@ works everywhere. See
|
|
|
233
197
|
|
|
234
198
|
## 4. Test against fakes
|
|
235
199
|
|
|
200
|
+
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
|
+
below do nothing without it.
|
|
203
|
+
|
|
236
204
|
```ruby
|
|
237
|
-
#
|
|
238
|
-
require "graph_weaver/rspec"
|
|
205
|
+
it "renders the empty state", graphql: :fake do … end # or tag the describe
|
|
239
206
|
```
|
|
240
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.
|
|
217
|
+
|
|
241
218
|
```ruby
|
|
242
|
-
|
|
219
|
+
# spec/requests/people_spec.rb
|
|
220
|
+
RSpec.describe "People", type: :request do
|
|
221
|
+
it "lists people", graphql: :fake do
|
|
222
|
+
graphql_fake("Person" => { "name" => "Ada Lovelace" })
|
|
223
|
+
|
|
224
|
+
get "/people"
|
|
225
|
+
|
|
226
|
+
expect(response.body).to include("Ada Lovelace")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
243
229
|
```
|
|
244
230
|
|
|
245
|
-
|
|
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
|
|
231
|
+
Pins, simulating failures, and the federated `graphql: :router` are in
|
|
251
232
|
[testing](testing.md).
|
|
252
233
|
|
|
253
234
|
A fresh `rails g rspec:install` leaves the `spec/support` glob commented
|
|
@@ -257,23 +238,97 @@ unread.
|
|
|
257
238
|
|
|
258
239
|
## 5. Verify in CI
|
|
259
240
|
|
|
260
|
-
|
|
241
|
+
Five questions, five tasks — the last only on a federated graph:
|
|
261
242
|
|
|
262
243
|
| ask | task | needs network |
|
|
263
244
|
|---|---|---|
|
|
264
245
|
| is the checked-in Ruby fresh? | `rake graph_weaver:verify` | no |
|
|
265
246
|
| has the server's schema drifted from the dump? | `rake graph_weaver:schema:diff` | yes |
|
|
266
247
|
| did that drift break any of my queries? | `rake graph_weaver:queries:check` | yes |
|
|
248
|
+
| does the app still read what it selects? | `rake graph_weaver:unused` | no |
|
|
267
249
|
| did a subgraph change without a recompose? | `rake graph_weaver:federation:diff` | no |
|
|
268
250
|
|
|
251
|
+
(`rake graph_weaver:graphs` answers a sixth, when an app has more than one
|
|
252
|
+
schema: which graphs are configured, where each generates, and what each
|
|
253
|
+
registers — the one place those registrations are listed, since a registration
|
|
254
|
+
belongs to one graph.)
|
|
255
|
+
|
|
269
256
|
`verify` compares the committed generated files against what the current
|
|
270
257
|
schema + queries + registrations would produce, so it belongs in every CI
|
|
271
|
-
build. `schema:diff`
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
`
|
|
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;
|
|
275
263
|
see [federation](federation.md#has-the-supergraph-been-recomposed).
|
|
276
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:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
# an API you don't own — the dump records the url it was introspected from
|
|
270
|
+
bundle exec rake graph_weaver:verify # offline checks first, so a
|
|
271
|
+
bundle exec rake graph_weaver:cassettes:check # network blip can't mask one
|
|
272
|
+
bundle exec rake graph_weaver:queries:check # re-introspects the recorded url
|
|
273
|
+
bundle exec rake graph_weaver:schema:diff # …so does this one
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
# your own graphql-ruby schema, in-process — none of this touches a network
|
|
278
|
+
bundle exec rake graph_weaver:schema:diff # has the class moved past the dump?
|
|
279
|
+
bundle exec rake graph_weaver:queries:check # do the queries still validate?
|
|
280
|
+
bundle exec rake graph_weaver:verify # is the checked-in Ruby current?
|
|
281
|
+
bundle exec rake graph_weaver:cassettes:check # do the recordings still cast?
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
In-process the order is the repair order: refresh the dump, regenerate,
|
|
285
|
+
re-record. As a GitHub Actions job:
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
# .github/workflows/graphql.yml
|
|
289
|
+
name: graphql
|
|
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.
|
|
310
|
+
|
|
311
|
+
**On a federated graph, none of the five looks at the schema production is
|
|
312
|
+
serving.** `verify`, `queries:check`, `unused` and `federation:diff` compare the
|
|
313
|
+
app to artifacts checked in beside it, and `schema:diff` reads a live source
|
|
314
|
+
only where the dump records one — a composed supergraph doesn't, because no
|
|
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
|
|
330
|
+
[federation → in CI](federation.md#in-ci).
|
|
331
|
+
|
|
277
332
|
`schema:diff` names what moved, breaking changes first — breaking meaning
|
|
278
333
|
a query written against your dump stops validating, or stops casting:
|
|
279
334
|
|
|
@@ -340,6 +395,317 @@ Left off, it re-introspects the url the dump records — and when that dump is a
|
|
|
340
395
|
composed supergraph, each error also names the subgraphs behind the type it
|
|
341
396
|
points at ([federation](federation.md#the-routing-table)).
|
|
342
397
|
|
|
398
|
+
### The selections nothing reads
|
|
399
|
+
|
|
400
|
+
`rake graph_weaver:unused` asks the one question the others can't: not "is the
|
|
401
|
+
Ruby fresh" but "does the app still use what the query asks for". A field
|
|
402
|
+
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:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
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
|
+
|
|
412
|
+
13 selections, 2 unread — 2 queries, 58 files swept under .
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
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
|
+
query produced, then sweeps your `.rb`, `.rake`, `.builder`, `.erb`, `.slim`,
|
|
418
|
+
`.haml` and `.jbuilder` **once** for every name they could be read by —
|
|
419
|
+
`.sku`, `sku:`, `:sku`, `"sku"`. `PATHS=app,lib` narrows the sweep, and a
|
|
420
|
+
`PATHS=` naming a directory that isn't there is refused rather than swept as
|
|
421
|
+
nothing. Everything under a directory named `generated`, plus `vendor`,
|
|
422
|
+
`node_modules`, `tmp` and `log`, is skipped either way, as is any file
|
|
423
|
+
defining a graphql-ruby **type** — `< GraphQL::Schema::Object` or the
|
|
424
|
+
`< Types::BaseObject` the generator writes — since a `field :sku` there is
|
|
425
|
+
your *server* offering a field, not this app reading one back. A `Resolver` or
|
|
426
|
+
a `Mutation` is swept like any other code: that is where a BFF reads the graph
|
|
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.
|
|
444
|
+
|
|
445
|
+
**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 at
|
|
447
|
+
all says `.name`; and it can't see a prop reached by `public_send` or a read
|
|
448
|
+
in a file type it doesn't sweep. The example above is the best case, not the
|
|
449
|
+
typical one: measured against real corpora, **half to two thirds of genuinely
|
|
450
|
+
unread selections go unreported**, the share rising with the size of the app,
|
|
451
|
+
because common prop names collide with ordinary words somewhere in it.
|
|
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.
|
|
455
|
+
|
|
456
|
+
## Your app's own schema, in-process
|
|
457
|
+
|
|
458
|
+
An app that *serves* GraphQL with graphql-ruby can have the same typed
|
|
459
|
+
access to its own API — same generated structs, no socket, no HTTP:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
rails g graph_weaver:install MyApp::Schema
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
# config/initializers/graph_weaver.rb
|
|
467
|
+
Rails.application.config.to_prepare do
|
|
468
|
+
# queries run in-process against the app's own schema — no socket
|
|
469
|
+
GraphWeaver.client = GraphWeaver.new(MyApp::Schema)
|
|
470
|
+
end
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`to_prepare`, not a bare assignment: the schema class is autoloaded, so it
|
|
474
|
+
isn't resolvable while initializers run, and a dev reload replaces it with
|
|
475
|
+
a new class object that a captured one would go stale against.
|
|
476
|
+
|
|
477
|
+
**Context is per request, not per app.** A resolver reading
|
|
478
|
+
`context[:current_user]` gets nil from the app default — build a client
|
|
479
|
+
where you know the request and pass it per call:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
|
|
483
|
+
PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
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 — which
|
|
488
|
+
matters because one in-process client is normally the whole app's.
|
|
489
|
+
|
|
490
|
+
**Keep the dump in step with the schema.** Codegen reads the committed
|
|
491
|
+
dump at `GraphWeaver.schema_path`, never the live class — that's what
|
|
492
|
+
makes `rake graph_weaver:verify` a deterministic CI check. The generator
|
|
493
|
+
writes the first dump; after that it's an artifact derived from code in
|
|
494
|
+
your own repo, and the same two tasks a remote schema uses keep it in step:
|
|
495
|
+
|
|
496
|
+
```sh
|
|
497
|
+
rake graph_weaver:schema:diff # what has the class changed since the dump?
|
|
498
|
+
rake graph_weaver:schema:refresh # rewrite the dump from the class
|
|
499
|
+
rake graph_weaver:generate
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Neither touches a network here — your schema class answers introspection
|
|
503
|
+
itself — and `refresh` rewrites the dump in whatever format it already is, so
|
|
504
|
+
a repo that chose `cache: :graphql` keeps SDL. graphql-ruby's own
|
|
505
|
+
`GraphQL::RakeTask` writes the same artifact, but you don't need to wire it
|
|
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.
|
|
516
|
+
|
|
517
|
+
**Scaffolding the app too?** On a `rails new --skip-active-record`,
|
|
518
|
+
`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
|
+
with — a graphql-ruby bug. Run it as
|
|
521
|
+
`rails g graphql:install --skip-query-logs`, or delete the lines it wrote.
|
|
522
|
+
|
|
523
|
+
## A schema dump you already have
|
|
524
|
+
|
|
525
|
+
```sh
|
|
526
|
+
rails g graph_weaver:install db/schema.graphql
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Sets `GraphWeaver.schema_path` to that file rather than writing a second
|
|
530
|
+
copy, and introspects nothing. A dump has no resolvers, so it can't
|
|
531
|
+
execute — set `GraphWeaver.client` to whatever serves the API.
|
|
532
|
+
|
|
533
|
+
## More than one schema
|
|
534
|
+
|
|
535
|
+
The five steps above describe one graph — a schema, its queries, its output.
|
|
536
|
+
An app with a second schema declares each one:
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
# config/initializers/graph_weaver.rb
|
|
540
|
+
GraphWeaver.graph :billing do
|
|
541
|
+
schema -> { Billing::Schema }
|
|
542
|
+
queries "app/graphql/billing/queries"
|
|
543
|
+
output "app/graphql/billing/generated"
|
|
544
|
+
client "Billing::Schema"
|
|
545
|
+
namespace "Billing"
|
|
546
|
+
register_scalar "Money", BigDecimal
|
|
547
|
+
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
|
+
```
|
|
571
|
+
|
|
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
|
+
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. Everything a
|
|
578
|
+
graph knows is said inside the block — six settings, and the same three
|
|
579
|
+
registrations you write at the top level. Each setting falls back to the
|
|
580
|
+
matching top-level one, so a graph says only what differs, and anything else
|
|
581
|
+
the block calls is refused naming the nine it takes.
|
|
582
|
+
|
|
583
|
+
| setting | takes |
|
|
584
|
+
|---|---|
|
|
585
|
+
| `schema` | a graphql-ruby schema class, a [`Client`](transports.md), a path to a dump, SDL, or a lambda returning one |
|
|
586
|
+
| `queries` | a directory, or a list of them — the `.graphql` files this graph generates from |
|
|
587
|
+
| `output` | one directory — where this graph's generated Ruby is written |
|
|
588
|
+
| `client` | a constant, or its name — what this graph's modules execute against |
|
|
589
|
+
| `namespace` | a constant, or its name — what every constant this graph generates nests under |
|
|
590
|
+
| `types_module` | a constant name for the shared types module (default: `GraphQLTypes`, under `namespace`) |
|
|
591
|
+
|
|
592
|
+
**`client` names a constant, not a url.** Its value is spelled into every module
|
|
593
|
+
this graph generates and resolved the first time one of them executes — so it
|
|
594
|
+
has to be something generated source can write down, and a url is not. Build
|
|
595
|
+
the client wherever you like and put the constant holding it here:
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
GITHUB = GraphWeaver.new("https://api.github.com/graphql", auth: ENV["GITHUB_TOKEN"])
|
|
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:
|
|
616
|
+
|
|
617
|
+
```ruby
|
|
618
|
+
# config/initializers/graph_weaver.rb
|
|
619
|
+
COUNTRIES = GraphWeaver.new("https://countries.trevorblades.com/")
|
|
620
|
+
POKE = GraphWeaver.new("https://beta.pokeapi.co/graphql/v1beta")
|
|
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"
|
|
636
|
+
end
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
```sh
|
|
640
|
+
rake graph_weaver:schema:refresh # introspects each graph's client into its schema
|
|
641
|
+
rake graph_weaver:generate
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
A graph whose dump isn't there yet is introspected from the url its own client
|
|
645
|
+
posts to, and the dump records that url — so every later `schema:refresh` and
|
|
646
|
+
`schema:diff` re-reads the right server without being told again. `URL=` is for
|
|
647
|
+
the app that has one dump and no graphs; it names a single endpoint, and here
|
|
648
|
+
each graph has its own.
|
|
649
|
+
|
|
650
|
+
In specs, `graph:` is how an example says which graph a helper stands in for —
|
|
651
|
+
`graphql_fake(graph: :poke, "pokemon_v2_pokemon.name" => "pikachu")`. See
|
|
652
|
+
[testing.md](testing.md).
|
|
653
|
+
|
|
654
|
+
**In Rails, declare graphs in the initializer itself, and name an autoloaded
|
|
655
|
+
schema class with a lambda** — `schema -> { Billing::Schema }` — as above.
|
|
656
|
+
Zeitwerk is set up *after* `config/initializers` run, so a bare
|
|
657
|
+
`Billing::Schema` there raises `uninitialized constant`; the lambda is resolved
|
|
658
|
+
when generation asks, and resolved again after a dev reload has replaced the
|
|
659
|
+
class object. (`client` and `namespace` take the constant or its name, since
|
|
660
|
+
either way it is baked into generated source as a name.)
|
|
661
|
+
|
|
662
|
+
**The block runs where you write it**, registrations included — so a
|
|
663
|
+
registration naming one of your own constants is in exactly the position a
|
|
664
|
+
top-level one is, and has the same answer: declare that graph from a
|
|
665
|
+
`to_prepare` block, as [above](#2-run-the-generator).
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
Rails.application.config.to_prepare do
|
|
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.
|
|
708
|
+
|
|
343
709
|
## Sorbet, with or without
|
|
344
710
|
|
|
345
711
|
`sorbet-runtime` is a hard dependency, so generated `T::Struct`s and sigs
|
|
@@ -350,12 +716,17 @@ generated files — dynamic `parse` is invisible to `srb tc`. Everything
|
|
|
350
716
|
works without Sorbet; codegen plus Sorbet is what moves type errors from
|
|
351
717
|
runtime to CI.
|
|
352
718
|
|
|
719
|
+
A misspelled field is caught either way — by `srb tc` before it runs, or by
|
|
720
|
+
`NoMethodError` the first time it does. Nullability is the gap:
|
|
721
|
+
`country.capital.upcase` is a typecheck error because `capital` is `T.nilable`,
|
|
722
|
+
but at runtime it only raises on the rows where `capital` really is nil — which
|
|
723
|
+
may be none of your dev data and plenty of production's.
|
|
724
|
+
|
|
353
725
|
If your app globally injects `T::Sig` (`class Module; include T::Sig`), the
|
|
354
726
|
per-struct `extend T::Sig` in generated files is redundant — rubocop's
|
|
355
727
|
`Sorbet/RedundantExtendTSig` flags it. GraphWeaver auto-detects that at
|
|
356
728
|
generation time and skips the `extend`; override with
|
|
357
|
-
`GraphWeaver.extend_t_sig = true`/`false`.
|
|
358
|
-
and marked "do not edit," so excluding `generated/**` from rubocop is also fine.)
|
|
729
|
+
`GraphWeaver.extend_t_sig = true`/`false`.
|
|
359
730
|
|
|
360
731
|
## Not Rails?
|
|
361
732
|
|
|
@@ -376,7 +747,29 @@ mkdir -p app/graphql/queries app/graphql/generated
|
|
|
376
747
|
rake graph_weaver:schema:refresh URL=https://api.example.com/graphql
|
|
377
748
|
```
|
|
378
749
|
|
|
750
|
+
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
|
+
and `GraphWeaver.generated_paths`.
|
|
753
|
+
|
|
379
754
|
Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks —
|
|
380
755
|
and, since there's no `:environment` hook to run your registrations,
|
|
381
|
-
require the file that does them from the Rakefile too.
|
|
382
|
-
|
|
756
|
+
require the file that does them from the Rakefile too.
|
|
757
|
+
|
|
758
|
+
**Or skip rake too.** The tasks are a thin wrapper over public calls, so a
|
|
759
|
+
script of your own does the same work — and `cache: true` writes the dump on
|
|
760
|
+
that first introspection, so there's nothing to refresh first:
|
|
761
|
+
|
|
762
|
+
```ruby
|
|
763
|
+
client = GraphWeaver.new("https://api.example.com/graphql", cache: true)
|
|
764
|
+
client.schema # introspects once, writing the dump
|
|
765
|
+
|
|
766
|
+
schema = GraphWeaver::SchemaLoader.load(GraphWeaver.schema_path)
|
|
767
|
+
GraphWeaver.generate!(schema:) # => every file the plan produces
|
|
768
|
+
GraphWeaver.changed_files # => only the ones whose bytes moved
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Pruning, the shared types module, and `verify_generated!` — the freshness
|
|
772
|
+
guard `rake graph_weaver:verify` runs — are in
|
|
773
|
+
[generated modules](generated_modules.md#generating).
|
|
774
|
+
|
|
775
|
+
`graphql.config.yml` is copy/paste from [editors](editors.md).
|