graph_weaver 0.4.6 → 0.5.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 +1314 -0
- data/CLAUDE.md +100 -8
- data/DECISIONS.md +309 -0
- data/Gemfile.lock +23 -23
- data/NOTES.md +5 -5
- data/PLAN.md +106 -135
- data/README.md +115 -96
- data/REVIEW.md +946 -0
- data/docs/cassettes.md +75 -48
- data/docs/editors.md +82 -0
- data/docs/errors.md +32 -30
- data/docs/federation.md +520 -48
- data/docs/generated_modules.md +352 -137
- data/docs/getting_started.md +237 -67
- data/docs/logging.md +35 -6
- data/docs/real_world.md +21 -15
- data/docs/scalars.md +49 -154
- data/docs/testing.md +299 -52
- data/docs/transports.md +129 -30
- data/docs/upgrading.md +112 -0
- data/graph_weaver.gemspec +3 -1
- data/lib/generators/graph_weaver/install_generator.rb +259 -0
- data/lib/graph_weaver/client.rb +114 -111
- data/lib/graph_weaver/codegen/aliases.rb +217 -0
- data/lib/graph_weaver/codegen/emit.rb +272 -258
- data/lib/graph_weaver/codegen/enum_type.rb +27 -124
- data/lib/graph_weaver/codegen/nodes.rb +72 -13
- data/lib/graph_weaver/codegen/scalar_type.rb +68 -66
- data/lib/graph_weaver/codegen/type_helpers.rb +142 -0
- data/lib/graph_weaver/codegen.rb +593 -334
- data/lib/graph_weaver/errors.rb +127 -10
- data/lib/graph_weaver/federation.rb +272 -0
- data/lib/graph_weaver/hints.rb +9 -1
- data/lib/graph_weaver/in_process.rb +90 -0
- data/lib/graph_weaver/input_struct.rb +14 -2
- data/lib/graph_weaver/logging.rb +29 -0
- data/lib/graph_weaver/parsing.rb +67 -0
- data/lib/graph_weaver/query_module.rb +55 -0
- data/lib/graph_weaver/railtie.rb +23 -1
- data/lib/graph_weaver/representation.rb +74 -0
- data/lib/graph_weaver/response.rb +7 -0
- data/lib/graph_weaver/retry.rb +29 -8
- data/lib/graph_weaver/rspec.rb +214 -16
- data/lib/graph_weaver/schema_loader.rb +794 -59
- data/lib/graph_weaver/schemas.rb +46 -0
- data/lib/graph_weaver/selection.rb +43 -8
- data/lib/graph_weaver/tasks.rb +216 -21
- data/lib/graph_weaver/testing/cassette.rb +160 -61
- data/lib/graph_weaver/testing/coverage.rb +165 -0
- data/lib/graph_weaver/testing/failure.rb +10 -23
- data/lib/graph_weaver/testing/fake_client.rb +181 -21
- data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
- data/lib/graph_weaver/testing/router.rb +1431 -0
- data/lib/graph_weaver/testing/subgraphs.rb +130 -0
- data/lib/graph_weaver/testing.rb +204 -14
- data/lib/graph_weaver/transport/faraday.rb +28 -10
- data/lib/graph_weaver/transport/http.rb +99 -36
- data/lib/graph_weaver/transport.rb +67 -14
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +389 -170
- metadata +20 -3
data/docs/getting_started.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The setup that ships: queries live as `.graphql` files, generation writes
|
|
4
4
|
`# typed: strict` Ruby you check in, and CI fails when anything drifts.
|
|
5
|
-
|
|
6
|
-
[dynamic mode](real_world.md) — no build step.)
|
|
5
|
+
In Rails one generator does the setup. (Exploring an API from a console
|
|
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
|
|
9
|
+
covers the differences.
|
|
10
10
|
|
|
11
11
|
## 1. Install
|
|
12
12
|
|
|
@@ -15,55 +15,150 @@ covers the one difference.
|
|
|
15
15
|
gem "graph_weaver"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
## 2.
|
|
18
|
+
## 2. Run the generator
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
```sh
|
|
21
|
+
rails g graph_weaver:install https://api.example.com/graphql
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
create config/initializers/graph_weaver.rb
|
|
26
|
+
create app/graphql/queries/.keep
|
|
27
|
+
create app/graphql/generated/.keep
|
|
28
|
+
create graphql.config.yml
|
|
29
|
+
introspect app/graphql/schema.json from https://api.example.com/graphql
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The argument is whatever you'd pass to `GraphWeaver.new` — the generator
|
|
33
|
+
takes the same three source forms the library does, and writes the
|
|
34
|
+
initializer that fits:
|
|
35
|
+
|
|
36
|
+
| source | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `https://api.example.com/graphql` | an endpoint: introspected now, and the dump committed |
|
|
39
|
+
| `MyApp::Schema` | your own graphql-ruby schema, executing [in-process](#your-apps-own-schema-in-process) |
|
|
40
|
+
| `db/schema.graphql` | a [dump you already have](#a-schema-dump-you-already-have) — pointed at, not copied |
|
|
41
|
+
|
|
42
|
+
| flag | |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `--auth` | name of the ENV var holding the auth token — default `GRAPHWEAVER_AUTH`. Url only. Name a different one and the initializer follows, but `schema:refresh`/`schema:diff` still read `GRAPHWEAVER_AUTH` — set both |
|
|
45
|
+
| `--no-schema` | skip writing the dump; `rake graph_weaver:schema:refresh URL=...` does it later |
|
|
46
|
+
|
|
47
|
+
Re-running is safe — every file goes through the usual Rails conflict
|
|
48
|
+
prompt, so an initializer you've edited is never overwritten silently.
|
|
49
|
+
|
|
50
|
+
What it wrote:
|
|
51
|
+
|
|
52
|
+
- **`config/initializers/graph_weaver.rb`.** `GraphWeaver.client =` is the
|
|
53
|
+
load-bearing line: generated modules without a baked transport resolve to
|
|
54
|
+
it at execute time (the full
|
|
55
|
+
[resolution order](transports.md#client-resolution)). Custom
|
|
56
|
+
scalars/enums/type helpers register here too — the rake tasks bake them
|
|
57
|
+
into generated source, so they have to run first:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A registration that names one of your own constants — a `T::Enum` for
|
|
64
|
+
`register_enum`, a mixin for `extend_type` — goes in a `to_prepare` block,
|
|
65
|
+
the same place the in-process client goes and for the same reason:
|
|
66
|
+
autoloading is set up after `config/initializers` run. Generation depends on
|
|
67
|
+
`:environment`, which runs `to_prepare` too, so the registration is in place
|
|
68
|
+
before it emits.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Rails.application.config.to_prepare do
|
|
72
|
+
GraphWeaver.register_enum("Species", PetKind, fallback: PetKind::Unknown)
|
|
73
|
+
GraphWeaver.extend_type("Pet", PetHelpers)
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **`app/graphql/schema.json`.** The schema dump codegen reads
|
|
78
|
+
(`GraphWeaver.schema_path`) — never written by hand, always committed.
|
|
79
|
+
`cache: true` in the initializer reuses it; delete the file to
|
|
80
|
+
re-introspect. Prefer PR-reviewable diffs? `cache: :graphql` writes SDL
|
|
81
|
+
instead; both generate identical code. (`cache:`/`ttl:` apply only to url
|
|
82
|
+
clients — a schema source never introspects, so passing them raises.)
|
|
83
|
+
- **`graphql.config.yml`.** Five lines of YAML that give VS Code and
|
|
84
|
+
RubyMine schema autocomplete, hover docs, and validation as you type in
|
|
85
|
+
`.graphql` files — no JS project, no `npm install`. Details and the honest
|
|
86
|
+
limits in [editors](editors.md).
|
|
87
|
+
- **`app/graphql/queries/`, `app/graphql/generated/`.** Where you write
|
|
88
|
+
queries and where generation writes Ruby.
|
|
89
|
+
|
|
90
|
+
Rake needs no wiring either: in Rails the `graph_weaver:*` tasks register
|
|
91
|
+
themselves (a Railtie) and depend on `:environment`, so your initializer —
|
|
92
|
+
and its registrations — runs first. The generated modules load at boot the
|
|
93
|
+
same way, after `config/initializers`.
|
|
94
|
+
|
|
95
|
+
### Your app's own schema, in-process
|
|
96
|
+
|
|
97
|
+
An app that *serves* GraphQL with graphql-ruby can have the same typed
|
|
98
|
+
access to its own API — same generated structs, no socket, no HTTP:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
rails g graph_weaver:install MyApp::Schema
|
|
102
|
+
```
|
|
24
103
|
|
|
25
104
|
```ruby
|
|
26
|
-
|
|
105
|
+
# config/initializers/graph_weaver.rb
|
|
106
|
+
Rails.application.config.to_prepare do
|
|
107
|
+
# queries run in-process against the app's own schema — no socket
|
|
108
|
+
GraphWeaver.client = GraphWeaver.new(MyApp::Schema)
|
|
109
|
+
end
|
|
27
110
|
```
|
|
28
111
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
112
|
+
`to_prepare`, not a bare assignment: the schema class is autoloaded, so it
|
|
113
|
+
isn't resolvable while initializers run, and a dev reload replaces it with
|
|
114
|
+
a new class object that a captured one would go stale against.
|
|
32
115
|
|
|
33
|
-
|
|
34
|
-
|
|
116
|
+
**Context is per request, not per app.** A resolver reading
|
|
117
|
+
`context[:current_user]` gets nil from the app default — build a client
|
|
118
|
+
where you know the request and pass it per call:
|
|
35
119
|
|
|
36
|
-
|
|
120
|
+
```ruby
|
|
121
|
+
client = GraphWeaver.new(MyApp::Schema, context: { current_user: })
|
|
122
|
+
PetQuery.execute!(client:, id: "1").pet.owner # => the context's user
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Keep the dump in step with the schema.** Codegen reads the committed
|
|
126
|
+
dump at `GraphWeaver.schema_path`, never the live class — that's what
|
|
127
|
+
makes `rake graph_weaver:verify` a deterministic CI check. The generator
|
|
128
|
+
writes the first dump; after that it's an artifact derived from code in
|
|
129
|
+
your own repo, so rebuild it with graphql-ruby's own rake task:
|
|
37
130
|
|
|
38
131
|
```ruby
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
132
|
+
# lib/tasks/graphql.rake
|
|
133
|
+
require "graphql/rake_task"
|
|
134
|
+
GraphQL::RakeTask.new(schema_name: "MyApp::Schema", directory: "app/graphql",
|
|
135
|
+
dependencies: [:environment])
|
|
136
|
+
```
|
|
45
137
|
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
138
|
+
```sh
|
|
139
|
+
rake graphql:schema:json # rewrites app/graphql/schema.json
|
|
140
|
+
rake graph_weaver:generate
|
|
49
141
|
```
|
|
50
142
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
143
|
+
Run the dump step ahead of `rake graph_weaver:verify` in CI — that check
|
|
144
|
+
compares committed Ruby against the committed dump, so a stale dump makes
|
|
145
|
+
it fail on a query that is fine. `rake graph_weaver:queries:check` is
|
|
146
|
+
unaffected: when `GraphWeaver.client` runs in-process it validates
|
|
147
|
+
against the live class, not the dump. (`graph_weaver:schema:diff` and
|
|
148
|
+
`:refresh` are for servers you *don't* own; a dump taken from a schema
|
|
149
|
+
class records no url, and they say so.)
|
|
58
150
|
|
|
59
|
-
|
|
151
|
+
### A schema dump you already have
|
|
60
152
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Outside Rails, add `require "graph_weaver/tasks"` to your Rakefile.
|
|
153
|
+
```sh
|
|
154
|
+
rails g graph_weaver:install db/schema.graphql
|
|
155
|
+
```
|
|
65
156
|
|
|
66
|
-
|
|
157
|
+
Sets `GraphWeaver.schema_path` to that file rather than writing a second
|
|
158
|
+
copy, and introspects nothing. A dump has no resolvers, so it can't
|
|
159
|
+
execute — set `GraphWeaver.client` to whatever serves the API.
|
|
160
|
+
|
|
161
|
+
## 3. Write a query, generate, commit
|
|
67
162
|
|
|
68
163
|
```graphql
|
|
69
164
|
# app/graphql/queries/person.graphql
|
|
@@ -86,6 +181,12 @@ reviewed like any other code — and never edited by hand.
|
|
|
86
181
|
PersonQuery.execute!(id: "1").person&.name # typed, via GraphWeaver.client
|
|
87
182
|
```
|
|
88
183
|
|
|
184
|
+
### Autocomplete while you write the query
|
|
185
|
+
|
|
186
|
+
`graphql.config.yml` is already there, so VS Code and RubyMine validate the
|
|
187
|
+
`.graphql` files as you type, with schema autocomplete and hover docs — see
|
|
188
|
+
[editors](editors.md).
|
|
189
|
+
|
|
89
190
|
### Shared fragments
|
|
90
191
|
|
|
91
192
|
Define reusable fragments once and spread them from any query:
|
|
@@ -107,8 +208,8 @@ default `app/graphql/fragments`).
|
|
|
107
208
|
### Shared unions
|
|
108
209
|
|
|
109
210
|
When a shared fragment *is* the whole selection on a union field, its type is
|
|
110
|
-
hoisted once into
|
|
111
|
-
aliases the same type — so a `union` selected across many queries becomes one
|
|
211
|
+
hoisted once into the shared `GraphQLTypes` module and every query that spreads
|
|
212
|
+
it aliases the same type — so a `union` selected across many queries becomes one
|
|
112
213
|
Ruby type family, and you write one exhaustive `case … when … T.absurd` that
|
|
113
214
|
works everywhere:
|
|
114
215
|
|
|
@@ -124,43 +225,96 @@ fragment FeedItemFields on FeedItem {
|
|
|
124
225
|
query { feed { ...FeedItemFields } } # feed : T::Array[FeedItemFields::Type]
|
|
125
226
|
```
|
|
126
227
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path (`GraphQLUnions`, or `GithubUnions` in a multi-schema layout); override
|
|
132
|
-
with `GraphWeaver.unions_module=`.
|
|
228
|
+
There's no flag: hoisting triggers when the union field's selection is exactly
|
|
229
|
+
that one spread. Mix in other fields, or shadow the fragment with a query-local
|
|
230
|
+
one of the same name, and the union stays inlined in that query — see
|
|
231
|
+
[abstract types](generated_modules.md#abstract-types).
|
|
133
232
|
|
|
134
|
-
##
|
|
233
|
+
## 4. Test against fakes
|
|
135
234
|
|
|
136
235
|
```ruby
|
|
137
236
|
# spec/support/graph_weaver.rb
|
|
138
237
|
require "graph_weaver/rspec"
|
|
238
|
+
```
|
|
139
239
|
|
|
140
|
-
|
|
240
|
+
```ruby
|
|
241
|
+
it "renders the empty state", graphql: :fake do … end # or tag the describe
|
|
141
242
|
```
|
|
142
243
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
244
|
+
A fresh `rails g rspec:install` leaves the `spec/support` glob commented
|
|
245
|
+
out in `spec/rails_helper.rb`, so uncomment it — or put the require in
|
|
246
|
+
`rails_helper.rb` itself. Nothing warns you that a support file went
|
|
247
|
+
unread.
|
|
248
|
+
|
|
249
|
+
The tag installs a seeded, schema-correct `FakeClient` for that example —
|
|
250
|
+
no server, no stubs, and `rspec --seed 1234` reproduces the fake data along
|
|
251
|
+
with test order. The schema it fabricates from is derived (the committed
|
|
252
|
+
dump, or your client's), so there's nothing to configure. Tag
|
|
253
|
+
`graphql: :in_process` instead and the same example runs against your real
|
|
254
|
+
resolvers; pin values with `overrides:`, simulate failures with `Failure.*`
|
|
148
255
|
— see [testing](testing.md).
|
|
149
256
|
|
|
150
|
-
##
|
|
257
|
+
## 5. Verify in CI
|
|
151
258
|
|
|
152
259
|
```sh
|
|
153
|
-
rake graph_weaver:verify
|
|
154
|
-
rake graph_weaver:schema:
|
|
260
|
+
rake graph_weaver:verify # generated code fresh? fails on any drift
|
|
261
|
+
rake graph_weaver:schema:diff # server drifted? re-introspects and compares
|
|
262
|
+
rake graph_weaver:queries:check # did that drift break any of your queries?
|
|
155
263
|
```
|
|
156
264
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
265
|
+
Three different questions — four on a federated graph, where
|
|
266
|
+
`rake graph_weaver:federation:diff` asks whether anyone changed a subgraph
|
|
267
|
+
without recomposing the supergraph you committed. It needs no network
|
|
268
|
+
either, so it belongs in the same PR run; see
|
|
269
|
+
[federation](federation.md#has-the-supergraph-been-recomposed).
|
|
270
|
+
|
|
271
|
+
`graph_weaver:verify` compares the committed generated files against what the
|
|
272
|
+
current schema + queries + registrations would produce. No network — run it in
|
|
273
|
+
every CI build.
|
|
274
|
+
|
|
275
|
+
`graph_weaver:schema:diff` asks whether the *server* has moved since the
|
|
276
|
+
dump was taken. It needs network, a dump with a recorded source url
|
|
277
|
+
(introspected dumps have one), and `GRAPHWEAVER_AUTH` for private APIs;
|
|
278
|
+
run it on a schedule and refresh with `rake graph_weaver:schema:refresh`.
|
|
279
|
+
|
|
280
|
+
`graph_weaver:queries:check` answers the question that actually matters
|
|
281
|
+
when it *has* moved: **which of your queries no longer validate, and
|
|
282
|
+
why.** It re-introspects the recorded url (without rewriting the dump) and
|
|
283
|
+
validates every `.graphql` file against the schema as it is right now,
|
|
284
|
+
naming each error's line and column:
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
app/graphql/queries/person.graphql
|
|
288
|
+
4:5 Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)
|
|
289
|
+
|
|
290
|
+
1 invalid query
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
It exits non-zero when anything fails, so it drops straight into CI or a
|
|
294
|
+
scheduled job.
|
|
295
|
+
|
|
296
|
+
The Ruby behind it returns the same thing as data, so you can wire it into
|
|
297
|
+
whatever you already have (a spec, a Slack ping, an issue):
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
GraphWeaver.check_queries
|
|
301
|
+
# => { "app/graphql/queries/person.graphql" =>
|
|
302
|
+
# [{ "message" => "Field 'nmae' doesn't exist on type 'Person' (Did you mean `name`?)",
|
|
303
|
+
# "line" => 4, "column" => 5 }] }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Empty means everything validates. Pass `schema:` a loaded schema and nothing
|
|
307
|
+
touches the network — handy for checking a *proposed* schema (a subgraph about
|
|
308
|
+
to ship) before it's live:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
GraphWeaver.check_queries(schema: GraphWeaver::SchemaLoader.load("proposed.graphql"))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
It wants the loaded schema, not the path. Left off, it re-introspects the url
|
|
315
|
+
the dump records — and when that dump is a composed supergraph, each error also
|
|
316
|
+
names the subgraphs behind the type it points at
|
|
317
|
+
([federation](federation.md#the-routing-table)).
|
|
164
318
|
|
|
165
319
|
## Sorbet, with or without
|
|
166
320
|
|
|
@@ -181,8 +335,24 @@ and marked "do not edit," so excluding `generated/**` from rubocop is also fine.
|
|
|
181
335
|
|
|
182
336
|
## Not Rails?
|
|
183
337
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
338
|
+
There's no generator, but what it writes is short — a few lines wherever
|
|
339
|
+
your app boots, two directories, and the schema dump:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
GraphWeaver.client = GraphWeaver.new(
|
|
343
|
+
"https://api.example.com/graphql",
|
|
344
|
+
auth: ENV["GRAPHWEAVER_AUTH"],
|
|
345
|
+
cache: true,
|
|
346
|
+
)
|
|
347
|
+
GraphWeaver.load_generated! # no Railtie to require the generated files
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
mkdir -p app/graphql/queries app/graphql/generated
|
|
352
|
+
rake graph_weaver:schema:refresh URL=https://api.example.com/graphql
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Add `require "graph_weaver/tasks"` to your Rakefile for the rake tasks —
|
|
356
|
+
and, since there's no `:environment` hook to run your registrations,
|
|
357
|
+
require the file that does them from the Rakefile too. `graphql.config.yml`
|
|
358
|
+
is copy/paste from [editors](editors.md).
|
data/docs/logging.md
CHANGED
|
@@ -14,7 +14,7 @@ What logs at which level — pick the level, get the story:
|
|
|
14
14
|
| Level | What you see |
|
|
15
15
|
|-------|--------------|
|
|
16
16
|
| `debug` | the wire: query + variables per call (long queries truncated), response status/bytes, request timing, connection open/drop, dynamically parsed modules |
|
|
17
|
-
| `info` | schema introspection (with timing) and cache hits/misses, generated files written, query modules loaded |
|
|
17
|
+
| `info` | schema introspection (with timing) and cache hits/misses, generated files written and any unregistered scalars, query modules loaded |
|
|
18
18
|
| `warn` | every GraphWeaver error raised — `TransportError`, `ServerError`, `QueryError`, `ValidationError`, `TypeError` |
|
|
19
19
|
|
|
20
20
|
Every line carries `graph_weaver` as the progname, so formatter-based
|
|
@@ -22,12 +22,41 @@ filtering works out of the box. Wire lines are tagged
|
|
|
22
22
|
`[req 3 FilteredPokemon]` — a per-process request id plus the operation
|
|
23
23
|
name — so a request's lines stay paired when threads interleave.
|
|
24
24
|
|
|
25
|
-
Debugging a misbehaving integration is the intended use: crank to
|
|
26
|
-
`Logger::DEBUG` and you'll see exactly what went on the wire, what came
|
|
27
|
-
back, whether the schema came from cache or a live introspection, and
|
|
28
|
-
which connection served it.
|
|
29
|
-
|
|
30
25
|
**PII note**: queries, variables, and response sizes appear at debug
|
|
31
26
|
only — variables can carry user data, so keep production loggers at
|
|
32
27
|
info or above (or scrub in your formatter). Auth headers never log at
|
|
33
28
|
any level.
|
|
29
|
+
|
|
30
|
+
## Instrumentation
|
|
31
|
+
|
|
32
|
+
A logger tells a human what happened; an APM needs to time it and count
|
|
33
|
+
it. `GraphWeaver.instrumenter` is one callable wrapping every request —
|
|
34
|
+
over the wire *and* in-process, one seam for both paths. It's a no-op
|
|
35
|
+
until you set one, and `ActiveSupport::Notifications` is a two-line
|
|
36
|
+
adapter:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
GraphWeaver.instrumenter = lambda do |event, payload, &block|
|
|
40
|
+
ActiveSupport::Notifications.instrument(event, payload, &block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ActiveSupport::Notifications.subscribe(GraphWeaver::EXECUTE_EVENT) do |*, payload|
|
|
44
|
+
StatsD.timing("graphql.#{payload[:operation] || "anonymous"}", ...)
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The one event is `GraphWeaver::EXECUTE_EVENT`
|
|
49
|
+
(`"graph_weaver.execute"`), a single request from start to parsed
|
|
50
|
+
response. Its payload carries:
|
|
51
|
+
|
|
52
|
+
| Key | |
|
|
53
|
+
|-----|--|
|
|
54
|
+
| `:url` | the endpoint — nil in-process |
|
|
55
|
+
| `:schema` | the schema class, in-process only |
|
|
56
|
+
| `:operation` | the operation name sent with the request (a generated module always has one) — what a trace keys on |
|
|
57
|
+
| `:status` | the HTTP status, added once the response lands |
|
|
58
|
+
|
|
59
|
+
Your callable **must** call the block and return its value. A failure
|
|
60
|
+
propagates through it, so the hook sees the exception and can record it.
|
|
61
|
+
The query text and the variables are deliberately absent: they carry
|
|
62
|
+
PII, and belong at debug on the logger where the level gates them.
|
data/docs/real_world.md
CHANGED
|
@@ -6,24 +6,23 @@ ships is the checked-in codegen path in the [getting started](getting_started.md
|
|
|
6
6
|
this page is how you get there (the `parse` below becomes a `.graphql`
|
|
7
7
|
file plus `rake graph_weaver:generate`, everything else stays).
|
|
8
8
|
|
|
9
|
-
Everything hangs off a client — transport
|
|
10
|
-
|
|
9
|
+
Everything hangs off a client — transport and schema for one server. GitHub's
|
|
10
|
+
API, end to end:
|
|
11
11
|
|
|
12
12
|
```ruby
|
|
13
13
|
require "graph_weaver"
|
|
14
14
|
|
|
15
|
-
# transport + auth in one object (
|
|
15
|
+
# transport + auth in one object (docs/transports.md for retries and
|
|
16
16
|
# advanced setup). cache: true dumps the schema at GraphWeaver.schema_path
|
|
17
|
-
# on first introspection — the same file rake graph_weaver:generate reads
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# diffs) — or say cache: :graphql. Introspected dumps record their source
|
|
21
|
-
# url in a header, so a stale dump says where it came from.
|
|
17
|
+
# on first introspection — the same file rake graph_weaver:generate reads
|
|
18
|
+
# (docs/getting_started.md for the formats), with the source url recorded
|
|
19
|
+
# in a header, so a stale dump says where it came from.
|
|
22
20
|
github = GraphWeaver.new("https://api.github.com/graphql", auth: `gh auth token`.strip, cache: true)
|
|
23
21
|
|
|
24
|
-
# map GitHub's DateTime scalar onto Time (cast inferred from Time.parse)
|
|
25
|
-
#
|
|
26
|
-
|
|
22
|
+
# map GitHub's DateTime scalar onto Time (cast inferred from Time.parse).
|
|
23
|
+
# Registrations are global and codegen-time, so this line types your
|
|
24
|
+
# console and your checked-in code identically (docs/scalars.md).
|
|
25
|
+
GraphWeaver.register_scalar("DateTime", Time, serialize: :iso8601, requires: "time")
|
|
27
26
|
|
|
28
27
|
RepoQuery = github.parse(<<~GRAPHQL)
|
|
29
28
|
query($owner: String!, $name: String!) {
|
|
@@ -41,10 +40,17 @@ repo&.created_at # => 2026-07-07 ... (a real Time)
|
|
|
41
40
|
repo&.stargazer_count # => Integer
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
Clients are independent — build one per server, each with its own
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
Clients are independent — build one per server, each with its own transport
|
|
44
|
+
and schema. When you're ready to check the generated code in, the same client
|
|
45
|
+
is the schema `generate!` wants:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
GraphWeaver.generate!(schema: github) # no dump on disk needed
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The introspection step (seconds on a big API) happens lazily on first
|
|
52
|
+
`schema`/`parse` and caches per `cache:`/`ttl:`; for finer control the
|
|
53
|
+
pieces are all public
|
|
48
54
|
(`GraphWeaver::SchemaLoader.introspect(transport, cache:, ttl:)`, or cache
|
|
49
55
|
`introspect(transport).to_json` in Rails.cache and `SchemaLoader.load` it).
|
|
50
56
|
|