graph_weaver 0.7.5 → 0.7.6
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 +2 -2
- data/docs/errors.md +7 -3
- data/docs/generated_modules.md +10 -6
- data/docs/getting_started.md +34 -17
- data/docs/logging.md +79 -35
- data/docs/migrating.md +11 -4
- data/docs/scalars.md +12 -3
- data/docs/testing.md +75 -13
- data/docs/upgrading.md +25 -3
- data/examples/README.md +4 -2
- data/examples/github/generate.rb +22 -8
- data/examples/github/generated/star_mutation.rb +2 -2
- data/examples/github/generated/stargazers_query.rb +2 -2
- data/examples/github/generated/starred_query.rb +2 -2
- data/examples/github/run.rb +1 -0
- data/examples/github/setup.rb +16 -8
- data/graph_weaver.gemspec +15 -6
- data/lib/generators/graph_weaver/install_generator.rb +20 -2
- data/lib/graph_weaver/client.rb +0 -23
- data/lib/graph_weaver/codegen/aliases.rb +15 -3
- data/lib/graph_weaver/codegen/emit.rb +7 -6
- data/lib/graph_weaver/codegen/enum_type.rb +26 -2
- data/lib/graph_weaver/codegen/nodes.rb +21 -2
- data/lib/graph_weaver/codegen.rb +106 -54
- data/lib/graph_weaver/graph.rb +55 -5
- data/lib/graph_weaver/in_process.rb +1 -1
- data/lib/graph_weaver/input_struct.rb +29 -4
- data/lib/graph_weaver/internal/overrides.rb +126 -14
- data/lib/graph_weaver/internal/test_clients.rb +29 -7
- data/lib/graph_weaver/internal/unused.rb +30 -11
- data/lib/graph_weaver/internal/values.rb +17 -5
- data/lib/graph_weaver/internal.rb +6 -6
- data/lib/graph_weaver/log_subscriber.rb +27 -17
- data/lib/graph_weaver/logging.rb +89 -53
- data/lib/graph_weaver/parsing.rb +32 -3
- data/lib/graph_weaver/query_module.rb +48 -8
- data/lib/graph_weaver/rspec.rb +41 -17
- data/lib/graph_weaver/schema_diff.rb +24 -5
- data/lib/graph_weaver/schema_loader.rb +22 -9
- data/lib/graph_weaver/tasks.rb +71 -44
- data/lib/graph_weaver/testing/fake_client.rb +24 -21
- data/lib/graph_weaver/testing.rb +27 -7
- data/lib/graph_weaver/transport.rb +1 -1
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +59 -35
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a68eaeb8cd0f3139cc9e50b88d98a50e04acd0c2c94c7825233f04405d30c42
|
|
4
|
+
data.tar.gz: '08df5051075500cb81c710cd65705f387dec16bc29a626e2b8069bbf5c61b3a1'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee7b2971697409f7180e31d40569fd44108404c0f1d811b259bec6e000f7cf82142e7640de164510d1dbd253916392c6cc866619ee2d37e35e610b1ed0be9741
|
|
7
|
+
data.tar.gz: 9d219008a1db9bc353ef3b033b9c39f64657e58d1d2548fb074fc1a60973052e4abd0c1950d93622ad1cfc0414e7ca1219d9c770e32e445cedba287636f58a73
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
graph_weaver (0.7.
|
|
4
|
+
graph_weaver (0.7.6)
|
|
5
5
|
graphql (>= 2.6.7)
|
|
6
6
|
sorbet-runtime
|
|
7
7
|
|
|
@@ -299,7 +299,7 @@ CHECKSUMS
|
|
|
299
299
|
google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
|
|
300
300
|
google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
|
|
301
301
|
google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
|
|
302
|
-
graph_weaver (0.7.
|
|
302
|
+
graph_weaver (0.7.6)
|
|
303
303
|
graphql (2.6.10) sha256=9b7c8633767f516ff9d48a8d6305b2a00a2101c82aa871b92e69086944f9f83e
|
|
304
304
|
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
|
|
305
305
|
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
data/docs/errors.md
CHANGED
|
@@ -106,6 +106,7 @@ subclass says where it failed:
|
|
|
106
106
|
| `InputError` | the variables wouldn't build into the generated input structs — unknown/typo'd key, missing required field, out-of-range enum, wrong-typed field, wrong number of @oneOf fields — `#kind`, `#path`, `#coordinate`, `#value`, `#details`, `#field`, `#struct` |
|
|
107
107
|
| `QueryValidationError` | build time: the query didn't validate against the schema |
|
|
108
108
|
| `Codegen::Aliases::UnknownSegment` | build time: an [`alias:`](generated_modules.md#flat-accessors-with-alias) path names a field no type here has — a typo, so `optional: true` won't skip it |
|
|
109
|
+
| `Codegen::Aliases::HoistedSegment` | build time: an [`alias:`](generated_modules.md#flat-accessors-with-alias) path reads *into* a struct [hoisted](generated_modules.md#a-shared-fragment-is-one-type) from a shared fragment — `optional: true` skips it, but only at the cost of the accessor for every query that spreads the fragment |
|
|
109
110
|
| `ConfigurationError` | setup judged against your schema — which Ruby schema serves which subgraph (`Testing::Router`, `federation:diff`) |
|
|
110
111
|
| `Testing::Unplannable` | the local test router won't plan this operation — `#category`, `#detail` |
|
|
111
112
|
| `Testing::MissingRecording` | a [cassette](cassettes.md) holds no entry for this request — the message prints the variables, and the ones it did record |
|
|
@@ -219,9 +220,12 @@ a form keyed on `e.field` finds the same slot for a refusal raised before the
|
|
|
219
220
|
request left and for one the server sent back.
|
|
220
221
|
|
|
221
222
|
The **prop** (`issued_on`) is what you type in Ruby — `.new`, `.coerce`, the kwargs
|
|
222
|
-
of `execute` — and it is `#message`, the developer's line, that names it
|
|
223
|
-
|
|
224
|
-
the
|
|
223
|
+
of `execute` — and it is `#message`, the developer's line, that names it, with the
|
|
224
|
+
schema's spelling beside it where the two differ (`issued_on (issuedOn): expected
|
|
225
|
+
a String`): the prop is what you fix, and the wire name is what you grep the
|
|
226
|
+
`.graphql` for. In a Rails form the field names are the props, so underscore on
|
|
227
|
+
the way in — and give the nil case a home, because **`#field` is `nil` whenever
|
|
228
|
+
nothing named a slot**:
|
|
225
229
|
|
|
226
230
|
```ruby
|
|
227
231
|
form.errors.add(e.field&.underscore || :base, render_input_error(e))
|
data/docs/generated_modules.md
CHANGED
|
@@ -461,10 +461,12 @@ off `__typename` when the selection carries one unaliased and unguarded, and off
|
|
|
461
461
|
narrowed fragment, or one whose `__typename` is itself guarded, is refused: a
|
|
462
462
|
match would be indistinguishable from a miss.
|
|
463
463
|
|
|
464
|
-
A whole
|
|
464
|
+
A whole abstract field selected as one named *shared* fragment
|
|
465
465
|
(`{ ...FeedItemFields }`) is [hoisted](#a-shared-fragment-is-one-type) into
|
|
466
|
-
`GraphQLTypes` like any other,
|
|
467
|
-
|
|
466
|
+
`GraphQLTypes` like any other, in whichever of the three shapes above the
|
|
467
|
+
fragment has — a dispatch module, one struct, or a nilable narrowed struct. So
|
|
468
|
+
which shape it is never decides whether consuming queries share a type, and
|
|
469
|
+
adding a second `... on` to a shared fragment doesn't move their constant.
|
|
468
470
|
|
|
469
471
|
### Consuming a union — dispatch on the class, not `__typename`
|
|
470
472
|
|
|
@@ -673,7 +675,7 @@ to replace it; what the *example* says still wins.
|
|
|
673
675
|
generated module). A generated file says nothing about transport — only a private
|
|
674
676
|
`GRAPH` naming its graph, which is also how `graphql: :fake` fabricates each
|
|
675
677
|
module's own schema with two graphs in play, and the
|
|
676
|
-
`:graph` on every [instrumentation event](logging.md#
|
|
678
|
+
`:graph` on every [instrumentation event](logging.md#instrumentation) the module's
|
|
677
679
|
`execute` produces.
|
|
678
680
|
|
|
679
681
|
## Deserializing a response from another client
|
|
@@ -848,8 +850,10 @@ you — a script that generates its own modules sets all four:
|
|
|
848
850
|
other [client](#clients) to reach for.
|
|
849
851
|
|
|
850
852
|
Miss (3) and the script gets a `NameError` for its own module; miss (4) and it
|
|
851
|
-
gets `
|
|
852
|
-
|
|
853
|
+
gets `no client configured — set GraphWeaver.client= or pass a client` from a
|
|
854
|
+
module that otherwise looks fine. Set the slot to something that can't execute
|
|
855
|
+
and the module names the contract instead: `PersonQuery: client must respond to
|
|
856
|
+
#execute(query, variables:, operation_name:), got Hash`.
|
|
853
857
|
|
|
854
858
|
Every directory setting is a list — `queries_paths`, `generated_paths`,
|
|
855
859
|
`fragments_paths` — and every entry is read (entries may be globs; the generated
|
data/docs/getting_started.md
CHANGED
|
@@ -292,9 +292,11 @@ omits it. Any drift exits non-zero — whether a change matters is yours to judg
|
|
|
292
292
|
(`#breaking`, `#compatible`, `#to_h`, `#empty?`).
|
|
293
293
|
|
|
294
294
|
`queries:check` answers the question that matters when the schema *has* moved:
|
|
295
|
-
**which of your queries no longer validate, and why.** It re-introspects
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
**which of your queries no longer validate, and why.** It re-introspects
|
|
296
|
+
whatever is behind each graph's dump (without rewriting the dump) — the same
|
|
297
|
+
source `schema:refresh` rewrites from and `schema:diff` compares against — and
|
|
298
|
+
validates every `.graphql` file against the schema as it is right now, naming
|
|
299
|
+
each error's line and column:
|
|
298
300
|
|
|
299
301
|
```
|
|
300
302
|
app/graphql/queries/person.graphql
|
|
@@ -355,8 +357,11 @@ named `generated`, plus `vendor`, `node_modules`, `tmp` and `log`, is skipped,
|
|
|
355
357
|
as is any file defining a graphql-ruby **type** — a `field :sku` there is your
|
|
356
358
|
*server* offering a field, not this app reading one back. A module handed whole
|
|
357
359
|
to a serializer (`render json:`, `to_h`, `as_json`, a local and all) counts every
|
|
358
|
-
prop as read, and the report quotes the line it followed
|
|
359
|
-
the
|
|
360
|
+
prop as read, and the report quotes the line it followed — a plain local stands
|
|
361
|
+
for the module only inside the method it was assigned in, so a same-named block
|
|
362
|
+
param in the next method credits nothing, while an `@ivar` crosses that boundary
|
|
363
|
+
the way a `before_action` does. Nothing is edited and the exit is 0;
|
|
364
|
+
`STRICT=1` exits 1 when anything is unread.
|
|
360
365
|
|
|
361
366
|
**It is a lint, not a proof**, and the task's own footer says so. It matches
|
|
362
367
|
names as text, so a prop called `name` counts as read the moment anything says
|
|
@@ -441,18 +446,25 @@ the client the graph names, rewrites the dump, and records the source, so every
|
|
|
441
446
|
later refresh and `schema:diff` re-read the right server. `URL=` names the
|
|
442
447
|
endpoint instead, if you'd rather say it once than configure the client first.
|
|
443
448
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`
|
|
449
|
+
Until there is something behind the dump to re-read — the url it records, the
|
|
450
|
+
server the graph's client posts to, or the graphql-ruby class that client runs
|
|
451
|
+
in-process — `queries:check` validates against the committed file: a real check,
|
|
452
|
+
but `verify`'s question rather than this one's, and the verdict says which:
|
|
447
453
|
|
|
448
454
|
```
|
|
449
455
|
every query validates against db/schema.graphql as committed — not the server (rake graph_weaver:schema:diff asks whether the server moved)
|
|
450
456
|
```
|
|
451
457
|
|
|
452
|
-
A dump with
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
458
|
+
A dump with none of those behind it *is* the schema — a hand-maintained SDL
|
|
459
|
+
nothing serves. All three tasks say so in one sentence:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
db/schema.graphql records no source url and the graph names no server behind it — no client posting to one, and no graphql-ruby schema class in this process — so the file is the schema and nothing here can re-read it
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
`schema:refresh` steps over that graph and refreshes the rest; `schema:diff`
|
|
466
|
+
still exits 1, because a gate that passes on having compared nothing is worse
|
|
467
|
+
than one that says so.
|
|
456
468
|
|
|
457
469
|
## More than one schema
|
|
458
470
|
|
|
@@ -506,9 +518,10 @@ they belong to and nothing about transport, so they read it when they execute.
|
|
|
506
518
|
A graph with no `client` falls back to `GraphWeaver.client`, the app default.
|
|
507
519
|
Name the object (`client GraphWeaver.new(url, auth: …)`) or, when the constant
|
|
508
520
|
holding it is defined later than the graph block, its name (`client "GITHUB"`),
|
|
509
|
-
which is resolved on first use
|
|
510
|
-
|
|
511
|
-
|
|
521
|
+
which is resolved on first use — and not a lambda, which `schema` needs and this
|
|
522
|
+
doesn't, since the name is resolved at call time already. `schema "x"` sets and
|
|
523
|
+
a bare `schema` reads back; there is no `schema = "x"` form, since the block is
|
|
524
|
+
`instance_eval`'d and that would be a local variable that silently does nothing.
|
|
512
525
|
|
|
513
526
|
**`namespace` nests everything that graph generates** — `person.graphql` becomes
|
|
514
527
|
`Billing::PersonQuery` ([naming](generated_modules.md#naming)). Constants are
|
|
@@ -522,8 +535,9 @@ An app that is a pure client of someone else's GraphQL owns no schema class, so
|
|
|
522
535
|
every graph's `schema` is a dump. Give each the file you want and a `client`
|
|
523
536
|
that can fetch it: `rake graph_weaver:schema:refresh` introspects each graph's
|
|
524
537
|
client into its own dump, recording the url so every later `schema:refresh` and
|
|
525
|
-
`schema:diff` re-reads the right server.
|
|
526
|
-
|
|
538
|
+
`schema:diff` re-reads the right server. Every graph is its own job — one with
|
|
539
|
+
no server behind its dump is left as checked in, one whose server is down is
|
|
540
|
+
reported under its own heading, and the rest still refresh. (`URL=` is for the
|
|
527
541
|
app that has one dump and no graphs.)
|
|
528
542
|
|
|
529
543
|
In specs, `graph:` is how an example says which graph a helper stands in for —
|
|
@@ -680,6 +694,9 @@ GraphWeaver.client = GraphWeaver.new(
|
|
|
680
694
|
GraphWeaver.load_generated! # no Railtie to require the generated files
|
|
681
695
|
```
|
|
682
696
|
|
|
697
|
+
**A public API needs no `auth:`** — leave the line out. It is in the example
|
|
698
|
+
because most endpoints want a token, not because the constructor asks for one.
|
|
699
|
+
|
|
683
700
|
**`load_generated!` goes before your own requires** when anything your app loads
|
|
684
701
|
names a generated constant as it loads — a `STATUS_LABELS` table keyed on
|
|
685
702
|
`GraphQLTypes::ShipmentStatus` raises `uninitialized constant` otherwise, and
|
data/docs/logging.md
CHANGED
|
@@ -13,8 +13,8 @@ Pick the level, get the story:
|
|
|
13
13
|
|
|
14
14
|
| Level | What you see |
|
|
15
15
|
|-------|--------------|
|
|
16
|
-
| `debug` | the wire: query + variables per call (long queries truncated), response status/bytes/content type, request timing, connection open/drop, dynamically parsed modules |
|
|
17
|
-
| `info` | one line per
|
|
16
|
+
| `debug` | the wire: query + variables per call (long queries truncated), response status/bytes/content type, request timing, one line per attempt (its url status and which retry it was), connection open/drop, dynamically parsed modules |
|
|
17
|
+
| `info` | one line per call in Rails (see [Instrumentation](#instrumentation)), schema introspection (with timing) and cache hits/misses, the transport a client built, generated files written and any unregistered scalars, query modules loaded, a retry's wait and attempt number — and in development, what's being watched and what a save regenerated |
|
|
18
18
|
| `warn` | every GraphWeaver error raised — `TransportError`, `ServerError`, `QueryError`, `QueryValidationError`, `CastError` — registrations the schema being generated against can't match, a retry skipped because the operation was a mutation, and every fetch the test router answered with fabricated data |
|
|
19
19
|
| `error` | development only: a `.graphql` edit that won't compile, with its file and position — the modules already loaded keep serving |
|
|
20
20
|
|
|
@@ -61,9 +61,10 @@ value, since `expected an Int, got "lots"` is the whole diagnosis.
|
|
|
61
61
|
## Instrumentation
|
|
62
62
|
|
|
63
63
|
A logger tells a human what happened; an APM needs to time it and count it.
|
|
64
|
-
`GraphWeaver.instrumenter` is one callable wrapping every
|
|
65
|
-
*and* in-process. **In Rails you set nothing**: the
|
|
66
|
-
below and attaches `GraphWeaver::LogSubscriber` on
|
|
64
|
+
`GraphWeaver.instrumenter` is one callable wrapping every call GraphWeaver
|
|
65
|
+
makes — over the wire *and* in-process. **In Rails you set nothing**: the
|
|
66
|
+
railtie installs the adapter below and attaches `GraphWeaver::LogSubscriber` on
|
|
67
|
+
top of it. Everywhere else:
|
|
67
68
|
|
|
68
69
|
```ruby
|
|
69
70
|
GraphWeaver.instrumenter = lambda do |event, payload, &block|
|
|
@@ -76,40 +77,74 @@ An instrumenter you set yourself is never replaced — including
|
|
|
76
77
|
and return its value; a failure propagates through it, so the hook sees the
|
|
77
78
|
exception and can record it.
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
There are two events, and the second happens inside the first:
|
|
81
|
+
|
|
82
|
+
| Event | One of these is |
|
|
83
|
+
|-------|-----------------|
|
|
84
|
+
| `GraphWeaver::OPERATION_EVENT` (`"operation.graph_weaver"`) | one call of a generated module's `execute`/`execute!`, start to typed result or raise — the request it made, every retry and backoff beneath it, and the cast into your structs |
|
|
85
|
+
| `GraphWeaver::EXECUTE_EVENT` (`"execute.graph_weaver"`) | one request, start to parsed response, whichever client slot served it — so a call a [`Retry`](transports.md#retries) took three goes at is one operation event and three of these |
|
|
86
|
+
|
|
87
|
+
**Alert on the operation event**: it is the one that says what the *caller*
|
|
88
|
+
got. A `CastError` is raised after the response is back, so the request has
|
|
89
|
+
already closed `:ok` by the time the app sees a failure. It also fires at the
|
|
90
|
+
module seam, which every client slot passes through — a [test mode's](testing.md)
|
|
91
|
+
fake, the router and a cassette all report here, so a spec can assert on
|
|
92
|
+
instrumentation under any `graphql:` tag.
|
|
81
93
|
|
|
82
94
|
```ruby
|
|
83
|
-
ActiveSupport::Notifications.subscribe(GraphWeaver::
|
|
95
|
+
ActiveSupport::Notifications.subscribe(GraphWeaver::OPERATION_EVENT) do |*, payload|
|
|
84
96
|
StatsD.timing("graphql.#{payload[:operation] || "anonymous"}", payload[:duration_ms],
|
|
85
97
|
tags: ["status:#{payload[:status]}", "kind:#{payload[:kind]}", "code:#{payload[:code]}"])
|
|
86
98
|
end
|
|
87
99
|
```
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
Two calls don't produce one. `from_response` on its own reports nothing — no
|
|
102
|
+
call was made. And a module generated by 0.7.5 or earlier shows this seam
|
|
103
|
+
only half of one, so it emits the request event alone until you regenerate:
|
|
104
|
+
half a call reported `:ok` is the very thing the event exists to stop.
|
|
105
|
+
|
|
106
|
+
**A subscriber that raises takes the call down with it** — the response was
|
|
90
107
|
computed and is then thrown away. That is `ActiveSupport::Notifications`' own
|
|
91
108
|
semantics, the same on `sql.active_record`, so rescue inside the block. A
|
|
92
109
|
`GraphWeaver::LogSubscriber` subclass already does.
|
|
93
110
|
|
|
94
|
-
### The payload
|
|
111
|
+
### The operation payload
|
|
95
112
|
|
|
96
113
|
| Key | When | |
|
|
97
114
|
|-----|------|--|
|
|
98
115
|
| `:operation` | always | the operation name sent with the request, nil for an anonymous document — what a trace keys on (a generated module always has one) |
|
|
99
|
-
| `:
|
|
116
|
+
| `:module` | always | the generated module's name, as a String — `nil` for one with no name (a [`GraphWeaver.parse`](getting_started.md)). The fact this seam has and the request doesn't: two graphs can name the same operation, and a trace that is slow wants the file |
|
|
117
|
+
| `:graph` | always | the [graph](getting_started.md#more-than-one-schema) the module was declared under, as a Symbol — `nil` for a module that names none. Never inferred from the client |
|
|
100
118
|
| `:kind` | always | `:query`, `:mutation` or `:subscription` — what the document runs, so a write failure rate is a payload question rather than a guess at the operation's name. The shorthand `{ ... }` document is a `:query`. The same reading decides whether [`Retry`](transports.md#retries) may repeat the request |
|
|
119
|
+
| `:client` | always | the client the module **resolved to**: a test mode's stand-in names itself, a graph's client names itself, and over the wire it is `GraphWeaver::Client` — the request event beneath names the transport that carried it |
|
|
120
|
+
| `:status` | always | `:ok` (a typed result), `:errors` (the response carried GraphQL errors — what `execute!` raises `QueryError` for), or `:failed` (the call raised: the transport, a `CastError`, anything) |
|
|
121
|
+
| `:duration_ms` | always | the caller's wall clock — dispatch, every retry *and its backoff*, and the cast |
|
|
122
|
+
| `:code` | on `:errors` | the machine-readable reason, as on the request event |
|
|
123
|
+
| `:error` | on `:failed` | the exception's class name |
|
|
124
|
+
|
|
125
|
+
Not on it: `:url`, `:http_status`, `:retries`. Those describe one attempt, and
|
|
126
|
+
a call that took three goes has no single answer for any of them — read them
|
|
127
|
+
off the request events nested inside.
|
|
128
|
+
|
|
129
|
+
### The request payload
|
|
130
|
+
|
|
131
|
+
| Key | When | |
|
|
132
|
+
|-----|------|--|
|
|
133
|
+
| `:operation` | always | the operation name sent with the request, nil for an anonymous document |
|
|
134
|
+
| `:client` | always | the class that ran it: `GraphWeaver::Transport::HTTP`, `GraphWeaver::InProcess`, your own |
|
|
135
|
+
| `:kind` | always | `:query`, `:mutation` or `:subscription`, read off the document |
|
|
101
136
|
| `:status` | always | `:ok`, `:errors` (a response carrying GraphQL errors), or `:failed` (it raised) |
|
|
102
|
-
| `:duration_ms` | always | start to parsed response, for **this attempt** —
|
|
137
|
+
| `:duration_ms` | always | start to parsed response, for **this attempt** — the backoff between attempts belongs to the operation event above, which is where the caller's wall clock lives |
|
|
103
138
|
| `:url` | over the wire | the endpoint; nil in-process |
|
|
104
139
|
| `:http_status` | over the wire | what the server answered with, success or not; nil in-process |
|
|
105
140
|
| `:schema` | in-process | the schema class's name, as a String, so a payload logs as it stands |
|
|
106
141
|
| `:code` | on `:errors` | the machine-readable reason, always a String or `nil` — the first `extensions.code` *any* of the errors carries, not the first error's, since a code that exists beats the absence of one at position 0. Present and `nil` when the errors carry none. Never an HTTP status: that is `:http_status` |
|
|
107
142
|
| `:error` | on `:failed` | the exception's class name |
|
|
108
143
|
| `:retries` | under a `Retry` | how many retries this attempt follows. Each attempt is its own event, so one retried call is three events reading 0, 1, 2 — present at 0 rather than absent, so its absence means nothing was retrying |
|
|
109
|
-
| `:graph` | always | the
|
|
144
|
+
| `:graph` | always | the same label the operation above it carries, and `nil` for a request no generated module made — a client called directly. Never inferred from the client: a wrong graph on a request is worse than no graph |
|
|
110
145
|
|
|
111
|
-
Every key is filled in before your callable's block returns, so a subscriber
|
|
112
|
-
a complete payload; `ActiveSupport::Notifications` adds `:exception` and
|
|
146
|
+
Every key is filled in before your callable's block returns, so a subscriber
|
|
147
|
+
reads a complete payload; `ActiveSupport::Notifications` adds `:exception` and
|
|
113
148
|
`:exception_object` of its own when the block raises.
|
|
114
149
|
|
|
115
150
|
**`:graph` labels one request**, never what a *server* does while answering one:
|
|
@@ -117,33 +152,35 @@ an in-process resolver that calls out produces an event of its own. The label is
|
|
|
117
152
|
fiber-local, so a dispatch that crosses a `Fiber` — graphql-ruby's `Dataloader`
|
|
118
153
|
does — arrives with `:graph` unset. No label rather than a wrong one.
|
|
119
154
|
|
|
120
|
-
**Never the query text or the variables
|
|
121
|
-
that know none of the filtering rules, so the rule here isn't
|
|
122
|
-
that it was never there. `:url` is the one thing on
|
|
123
|
-
scrubbed, because a url can itself be a credential — and the
|
|
124
|
-
to its query parameters even when you have emptied or
|
|
125
|
-
`filter_parameters`. Your list widens that; it can't narrow it.
|
|
155
|
+
**Never the query text or the variables**, on either event: the payload fans out
|
|
156
|
+
to subscribers that know none of the filtering rules, so the rule here isn't
|
|
157
|
+
"scrub it", it's that it was never there. `:url` is the one thing on either
|
|
158
|
+
payload that *is* scrubbed, because a url can itself be a credential — and the
|
|
159
|
+
default names apply to its query parameters even when you have emptied or
|
|
160
|
+
narrowed `filter_parameters`. Your list widens that; it can't narrow it.
|
|
126
161
|
|
|
127
162
|
### One line per operation
|
|
128
163
|
|
|
129
|
-
In Rails the railtie also attaches `GraphWeaver::LogSubscriber`, which turns
|
|
130
|
-
event into one line — the shape ActiveRecord uses for a query:
|
|
164
|
+
In Rails the railtie also attaches `GraphWeaver::LogSubscriber`, which turns
|
|
165
|
+
each operation event into one line — the shape ActiveRecord uses for a query:
|
|
131
166
|
|
|
132
167
|
```
|
|
133
168
|
GraphWeaver PersonQuery (12.3ms) ok
|
|
134
169
|
GraphWeaver PersonQuery (8.1ms) errors [THROTTLED]
|
|
135
170
|
GraphWeaver PersonQuery (31.2ms) failed GraphWeaver::TransportError
|
|
136
|
-
GraphWeaver PersonQuery (
|
|
171
|
+
GraphWeaver PersonQuery (44.0ms) failed GraphWeaver::CastError
|
|
137
172
|
GraphWeaver billing/InvoicesQuery (12.3ms) ok
|
|
138
173
|
```
|
|
139
174
|
|
|
140
|
-
The operation is prefixed by its graph when the
|
|
175
|
+
The operation is prefixed by its graph when the call carried one, so an app
|
|
141
176
|
with several graphs sorts its own log and an app with one never sees the prefix.
|
|
142
177
|
|
|
143
|
-
**The
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
**The operation is info, the attempt is debug.** The info line is one *call*, so
|
|
179
|
+
it says what the caller got and a production log gets one per call — a retried
|
|
180
|
+
call included, whose attempts and their backoff are inside that one duration.
|
|
181
|
+
Debug adds a line per attempt in the same shape, carrying the url's status and
|
|
182
|
+
which try it was, beneath the query and the variables. It writes through
|
|
183
|
+
`GraphWeaver.logger`.
|
|
147
184
|
|
|
148
185
|
Outside Rails the same line is an instrumenter of your own, writing
|
|
149
186
|
`payload[:operation]`, `[:duration_ms]` and `[:status]` from an `ensure` — the
|
|
@@ -151,11 +188,15 @@ shape the two tracing examples below use.
|
|
|
151
188
|
|
|
152
189
|
### OpenTelemetry
|
|
153
190
|
|
|
191
|
+
A span per operation, with a span per attempt inside it:
|
|
192
|
+
|
|
154
193
|
```ruby
|
|
155
194
|
tracer = OpenTelemetry.tracer_provider.tracer("graph_weaver")
|
|
156
195
|
|
|
157
|
-
GraphWeaver.instrumenter = lambda do |
|
|
158
|
-
|
|
196
|
+
GraphWeaver.instrumenter = lambda do |event, payload, &block|
|
|
197
|
+
name = event == GraphWeaver::OPERATION_EVENT ? "graphql #{payload[:operation] || "query"}" : "graphql request"
|
|
198
|
+
|
|
199
|
+
tracer.in_span(name) do |span|
|
|
159
200
|
block.call
|
|
160
201
|
ensure
|
|
161
202
|
span.add_attributes(payload.compact.transform_keys { "graphql.#{_1}" }.transform_values(&:to_s))
|
|
@@ -171,7 +212,8 @@ span status itself for a *raise* only, which is the last line's reason to exist.
|
|
|
171
212
|
**Span status is not the alerting signal; `payload[:status]` is.** A response
|
|
172
213
|
carrying GraphQL errors is a 200 that returned normally, so nothing raises and a
|
|
173
214
|
span left to itself is `UNSET` — an SLO built on span status alone misses every
|
|
174
|
-
GraphQL-level failure there is. Alert on `:status` and
|
|
215
|
+
GraphQL-level failure there is. Alert on the operation event's `:status` and
|
|
216
|
+
group by `:code`.
|
|
175
217
|
|
|
176
218
|
**Propagating the trace outward** is a header, and a
|
|
177
219
|
[header value may be a callable](transports.md#headers) resolved per request —
|
|
@@ -191,9 +233,11 @@ GraphWeaver.instrumenter = lambda do |event, payload, &block|
|
|
|
191
233
|
end
|
|
192
234
|
```
|
|
193
235
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
236
|
+
One hook covers both events, and the event name is the span name — so an
|
|
237
|
+
operation and its attempts nest under distinct resources. Datadog's Net::HTTP
|
|
238
|
+
and Faraday contribs already trace the transport layer, so with them on you have
|
|
239
|
+
a span for the POST as well. This adds the spans *above* it, and the one named
|
|
240
|
+
for the operation is the one that means anything, since every GraphQL call is a
|
|
197
241
|
POST to the same url.
|
|
198
242
|
|
|
199
243
|
## Details
|
data/docs/migrating.md
CHANGED
|
@@ -27,10 +27,17 @@ two side by side is the right shape anyway.
|
|
|
27
27
|
`queries:check` have no server to ask. `rake graph_weaver:schema:refresh`
|
|
28
28
|
rewrites it from the client the app points at (or `URL=<your endpoint>` once)
|
|
29
29
|
and records the provenance. Do it now: until you do, `queries:check` is
|
|
30
|
-
re-reading the file it is meant to be checking against, and says so.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
re-reading the file it is meant to be checking against, and says so. It
|
|
31
|
+
rewrites the file **in place, in graph_weaver's format** — pretty-printed
|
|
32
|
+
introspection JSON with a `graph_weaver:` provenance key beside `data`.
|
|
33
|
+
graphql-client goes on reading it, since `load_schema` hands the parsed hash
|
|
34
|
+
to graphql-ruby and graphql-ruby takes `data` and ignores its siblings; a dump
|
|
35
|
+
anything *else* reads is worth checking once.
|
|
36
|
+
3. **Register scalars and enums, and regenerate** — if the API declares any.
|
|
37
|
+
Plenty don't: one whose leaves are all `String`, `Int`, `ID` and `Boolean` has
|
|
38
|
+
nothing to register and skips this step whole. Registrations are baked into
|
|
39
|
+
generated source, so one added later reaches nothing until the next
|
|
40
|
+
`rake graph_weaver:generate` ([scalars](scalars.md)).
|
|
34
41
|
4. **Port one query end to end** — write the `.graphql` file, generate, rewrite
|
|
35
42
|
its call site — and leave the specs alone. They pass untouched. That single
|
|
36
43
|
commit is the proof for every one after it.
|
data/docs/scalars.md
CHANGED
|
@@ -433,7 +433,12 @@ It raises naming every scalar that disagreed and which way:
|
|
|
433
433
|
|
|
434
434
|
The fabricated value is all it has to work with, so pin the one that matters:
|
|
435
435
|
`config.overrides = { "Decimal" => "123456789.123456789" }` is how the precision
|
|
436
|
-
case gets exercised at all — two decimal places always survive a Float.
|
|
436
|
+
case gets exercised at all — two decimal places always survive a Float. That is
|
|
437
|
+
the only door onto a pin here: `check_scalars!` takes the schema and nothing
|
|
438
|
+
else, and runs outside every fake, so `graphql_fake` would be a no-op. The last
|
|
439
|
+
leg compares with `==`, so a registered class that defines none is reported as
|
|
440
|
+
uncheckable rather than lossy — two identical spellings mean identity, not a
|
|
441
|
+
loss. Pass the
|
|
437
442
|
schema **class**; a dump's scalars pass values through, so against one this checks
|
|
438
443
|
only that a registration's `cast:` accepts what its own `serialize:` writes, which
|
|
439
444
|
is a different question (see below).
|
|
@@ -501,7 +506,9 @@ Two safety properties do the real work:
|
|
|
501
506
|
a member, or generation fails naming the gaps (`PetKind has no member for
|
|
502
507
|
Species value(s) DOG — add them, pin with map:, or absorb with fallback:`), so
|
|
503
508
|
your enum drifting from the server's is caught by `rake graph_weaver:generate`,
|
|
504
|
-
not in production.
|
|
509
|
+
not in production. The other direction only *warns* — one app-wide `T::Enum` can
|
|
510
|
+
serve two APIs that each expose a subset — and a kwarg carrying a member this
|
|
511
|
+
schema declares no value for is refused by name at the call.
|
|
505
512
|
- **`fallback:` for forward-compat**: `fallback: PetKind::Unknown` makes *casting*
|
|
506
513
|
absorb wire values the server added after you generated, so responses keep
|
|
507
514
|
flowing instead of raising. Inputs stay strict either way: a typo'd input is your
|
|
@@ -536,7 +543,9 @@ Three things follow from that, and they are the whole rule:
|
|
|
536
543
|
singleton, so there is nowhere to put it; the cast writes one debug line
|
|
537
544
|
(`GraphQLTypes::Species absorbed "AXOLOTL" into Other`) and that is the record
|
|
538
545
|
— so a presenter humanising the server's spelling can't, and falls back to a
|
|
539
|
-
label of its own.
|
|
546
|
+
label of its own. `as_json` writes the member's own wire value, `"__other__"`,
|
|
547
|
+
which `from_h` reads straight back — but `render json: result` hands that
|
|
548
|
+
sentinel to whoever is downstream, and no server declares it.
|
|
540
549
|
- **Inputs stay strict.** No wire value means `Other`, so a variable carrying it
|
|
541
550
|
is refused rather than sent. Everything else is unchanged: a typo'd input is
|
|
542
551
|
your bug, not drift.
|
data/docs/testing.md
CHANGED
|
@@ -136,6 +136,15 @@ leaving the example green against random data. **Schema vocabulary, not Ruby:**
|
|
|
136
136
|
spelled the way *that* schema spells it, so a Hasura table type is
|
|
137
137
|
`"pokemon_v2_pokemon"` and not a Ruby-cased guess at it.
|
|
138
138
|
|
|
139
|
+
**The key is checked; the value is yours.** `"Pet.species" => "RABBIT"` is
|
|
140
|
+
accepted on an enum declaring no `RABBIT`, and the fake serves it as written — a
|
|
141
|
+
pin is the escape hatch, so nothing second-guesses what you put through it. On an
|
|
142
|
+
enum registered
|
|
143
|
+
[`fallback: true`](scalars.md#values-the-server-hasnt-told-you-about-yet) that is
|
|
144
|
+
how you rehearse drift, since the value lands in the catch-all member exactly as
|
|
145
|
+
a real server's new one would; without a fallback the cast raises where a real
|
|
146
|
+
response would.
|
|
147
|
+
|
|
139
148
|
A pin **merges**, and pins a **subtree** as readily as a leaf: name the fields
|
|
140
149
|
the example is about and everything else in the selection is still fabricated. A
|
|
141
150
|
pinned list is exactly as long as you write it — `{}` means "another one, all
|
|
@@ -158,7 +167,8 @@ snake_cased reader, puts the value on the wire the way its
|
|
|
158
167
|
a `T::Enum` as its value), recurses into nested objects and arrays of them, and
|
|
159
168
|
**fabricates any field the object doesn't answer**. Readers are field names, not
|
|
160
169
|
aliases; `__typename` comes from the key, so at a union or interface pin the
|
|
161
|
-
concrete type
|
|
170
|
+
concrete type — `"Person"` or `"Person.name"`, never `"Named"` or
|
|
171
|
+
`"Named.name"`, and both spellings of the abstract one are refused. A FactoryBot sequence advances on
|
|
162
172
|
its own counter, which is the one thing `--seed` can't reproduce.
|
|
163
173
|
|
|
164
174
|
A **scalar type pin** is the one thing a scalar registered as *your own class*
|
|
@@ -217,9 +227,12 @@ end
|
|
|
217
227
|
```
|
|
218
228
|
|
|
219
229
|
The live schema *class* is found for you (a schema dump has no resolvers, so it
|
|
220
|
-
won't do)
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
won't do), from the three places one can be named: `config.schema` when you set
|
|
231
|
+
it to a class, else the class [that graph](getting_started.md#more-than-one-schema)
|
|
232
|
+
names, else the class your own `GraphWeaver.client` already runs in-process.
|
|
233
|
+
Nothing is *scanned* for: with none of the three it refuses, and names the fix
|
|
234
|
+
that fits — declare the class on the graph, write `graphql_in_process(MySchema)`
|
|
235
|
+
in the example, or set `config.schema = MySchema` for the suite.
|
|
223
236
|
|
|
224
237
|
A federated app has no one live class, so the example says which subgraph it
|
|
225
238
|
means: `graphql_in_process(Reviews::Schema)`. Testing one subgraph's resolvers
|
|
@@ -253,7 +266,11 @@ block to scope it for the example that needs two identities —
|
|
|
253
266
|
`graphql_context(admin: true) { … }` — and call it with nothing to read the
|
|
254
267
|
context back. Under `graphql: :fake` it refuses: there are no resolvers to
|
|
255
268
|
receive a context, and silently ignoring one would leave an example asserting on
|
|
256
|
-
data nothing scoped. Pin the data itself instead.
|
|
269
|
+
data nothing scoped. Pin the data itself instead. Under `graphql: :live` it
|
|
270
|
+
refuses too — including in an untagged example, which is `:live` by default, and
|
|
271
|
+
including when your own client is an `InProcess` running your resolvers: that
|
|
272
|
+
client carries the context it was *built* with, and `graphql_context` reaches
|
|
273
|
+
only the stand-ins the mode built for this example.
|
|
257
274
|
|
|
258
275
|
## A federated graph — `graphql: :router`
|
|
259
276
|
|
|
@@ -360,8 +377,17 @@ that is a pure *client* of someone else's API gets a schema-correct server
|
|
|
360
377
|
without writing one. A graph with no schema at all is refused, **by `:wire`'s own
|
|
361
378
|
name** — the one fallback the other tags have and this one can't use is your
|
|
362
379
|
client's own schema, since reading it means introspecting the endpoint `:wire`
|
|
363
|
-
has just stubbed. Commit a dump, or set `config.schema`.
|
|
364
|
-
|
|
380
|
+
has just stubbed. Commit a dump, or set `config.schema`. That refusal waits until
|
|
381
|
+
one of *that graph's* modules runs, so a graph the example never touches never
|
|
382
|
+
refuses it.
|
|
383
|
+
|
|
384
|
+
**A graph whose `client` posts nowhere runs above the wire** — there is no
|
|
385
|
+
endpoint to stub, so it is served in the client slot, exactly as
|
|
386
|
+
`graphql: :in_process` would serve it. That is how an app that owns resolvers
|
|
387
|
+
*and* calls someone else's API tests the remote half over the wire: its
|
|
388
|
+
in-process graph runs in-process, and every graph posting to a url is still
|
|
389
|
+
served at that url. An example where *no* graph posts anywhere is refused — a
|
|
390
|
+
`:wire` that serves nothing tests no transport.
|
|
365
391
|
|
|
366
392
|
**It says which, on the logger** — the choice is the one thing this tag makes for
|
|
367
393
|
you, and it is invisible from inside the example. One line per endpoint, at
|
|
@@ -377,7 +403,12 @@ set `GraphWeaver::Testing.config.schema` — the case worth catching, because an
|
|
|
377
403
|
app that owns real resolvers otherwise goes green against fabricated data with
|
|
378
404
|
nothing said. A warning rather than a refusal, because a loaded class isn't proof
|
|
379
405
|
you meant it *here* — a federated suite loads every subgraph's — and a fake
|
|
380
|
-
behind the wire is a thing to want.
|
|
406
|
+
behind the wire is a thing to want. A graph that ran above the wire gets a line
|
|
407
|
+
of its own, since the transport the example asked for never ran for it:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
graph_weaver: :wire has no endpoint for graph :orders — its client posts to none, so its modules run above the wire, as graphql: :in_process would
|
|
411
|
+
```
|
|
381
412
|
|
|
382
413
|
**A helper says what goes behind the wire.** Under the other tags a `graphql_*`
|
|
383
414
|
helper takes the client slot; under `:wire` it is served instead — the client
|
|
@@ -548,10 +579,14 @@ rather than guessing**:
|
|
|
548
579
|
the wire value that graph's generated cast expects. (Pins and `overrides:` stay
|
|
549
580
|
suite-wide, keyed by scalar name — one `"Money"` override for the run.)
|
|
550
581
|
- **`:in_process`** needs the live schema *class*, since only that has resolvers:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
[
|
|
582
|
+
`config.schema` when you set it to a class, else the one that graph names, else
|
|
583
|
+
the one your client already runs in-process. Nothing is scanned for — a loaded
|
|
584
|
+
class is no evidence the app meant it *here* — so past those three
|
|
585
|
+
`:in_process` refuses, while [`:wire`](#over-the-wire--graphql-wire) reads the
|
|
586
|
+
same list and serves a fake, **warning** and naming the class it saw.
|
|
587
|
+
(Deriving a schema from what *is* loaded is the
|
|
588
|
+
[subgraph rule](federation.md#which-schema-serves-which-subgraph), where the
|
|
589
|
+
routing table says what a match has to define.)
|
|
555
590
|
- **`:router`** plans against the composed supergraph **that graph** names, else
|
|
556
591
|
`config.router = { supergraph: … }`, else the committed dump when *that*
|
|
557
592
|
carries `@join__*` markers, else the dump your own client was built from
|
|
@@ -619,7 +654,17 @@ one ahead of yours, so a `before` setting `config.schema`, `config.router` or
|
|
|
619
654
|
**refused**, not ignored — a green example running against the wrong stand-in is
|
|
620
655
|
the expensive outcome.
|
|
621
656
|
|
|
622
|
-
|
|
657
|
+
That hook is also why **a refusal the tag itself raises can't be asserted with
|
|
658
|
+
`expect { }.to raise_error`**: it happens before the example body, and rspec
|
|
659
|
+
records it as the example's failure rather than letting anything catch it — an
|
|
660
|
+
`around` hook included, since `example.run` returns normally there. To assert
|
|
661
|
+
one, call the helper in an **untagged** example, where the same refusal is
|
|
662
|
+
raised in the body: `expect { graphql_in_process }.to raise_error(...)`, and the
|
|
663
|
+
same for `graphql_fake` and `graphql_router`. `:wire` has no helper, so its
|
|
664
|
+
whole-example refusal can only be read off the failure — the per-graph one it
|
|
665
|
+
raises when a module runs is in the body, and assertable.
|
|
666
|
+
|
|
667
|
+
### Fabricated list lengths and nulls
|
|
623
668
|
|
|
624
669
|
`list_size` is how long an **unbounded** list is — an Integer exactly that many,
|
|
625
670
|
a Range randomized within it, or a Hash saying it per list. A list with a
|
|
@@ -634,6 +679,23 @@ for the rest — `config.list_size = { "Row.tags" => 3, default: 1000 }` holds t
|
|
|
634
679
|
inner list at 3 however large the outer one grows. Or cap it in the query
|
|
635
680
|
(`tags(first: 3)`), where the query is yours to change.
|
|
636
681
|
|
|
682
|
+
`null_chance` takes the same two shapes, and the same keys: a number from 0 to 1
|
|
683
|
+
for every nullable field, or a Hash saying it per field with `default:` for the
|
|
684
|
+
rest. So the example about one missing value says only that —
|
|
685
|
+
`graphql_fake(null_chance: { "Person.nickname" => 1.0 })` — instead of nilling
|
|
686
|
+
everything else alongside it. A misspelled key is refused and spellchecked, the
|
|
687
|
+
way a pin's is.
|
|
688
|
+
|
|
689
|
+
**Both are keyed by field, and the key has to be one the option can reach**: a
|
|
690
|
+
type name (`"Person"`) is refused rather than read as the nearest field, a
|
|
691
|
+
`null_chance` key naming a non-null field is refused (nothing there ever comes
|
|
692
|
+
back null), and a `list_size` key naming a field that isn't a list likewise —
|
|
693
|
+
all three are keys that would validate clean, fabricate nothing and leave the
|
|
694
|
+
example green. `"default"` is always the fallback, never a field, so a schema
|
|
695
|
+
field actually called `default` is reachable only as `"Type.default"`. And a
|
|
696
|
+
[pin](#pins) on the same field beats both: a pinned value is used as written,
|
|
697
|
+
so it is neither nulled nor resized.
|
|
698
|
+
|
|
637
699
|
**A list field whose name ends in `errors` fabricates empty** — `userErrors`,
|
|
638
700
|
`errors`, `mutationErrors`. The Relay/Shopify payload
|
|
639
701
|
(`placeOrder { order userErrors }`) is the ecosystem's mutation shape, and a
|