graph_weaver 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -4
- data/README.md +40 -88
- data/docs/alternatives.md +1 -7
- data/docs/cassettes.md +54 -59
- data/docs/editors.md +32 -47
- data/docs/errors.md +261 -369
- data/docs/federation.md +650 -837
- data/docs/generated_modules.md +380 -463
- data/docs/getting_started.md +211 -428
- data/docs/i18n.md +114 -177
- data/docs/logging.md +127 -116
- data/docs/real_world.md +26 -39
- data/docs/scalars.md +277 -310
- data/docs/testing.md +343 -486
- data/docs/transports.md +203 -268
- data/docs/upgrading.md +211 -560
- data/examples/README.md +38 -0
- data/examples/countries.rb +39 -0
- data/examples/federation.rb +62 -0
- data/examples/github/generate.rb +20 -0
- data/examples/github/generated/star_mutation.rb +126 -0
- data/examples/github/generated/stargazers_query.rb +232 -0
- data/examples/github/generated/starred_query.rb +151 -0
- data/examples/github/queries/star.graphql +8 -0
- data/examples/github/queries/stargazers.graphql +22 -0
- data/examples/github/queries/starred.graphql +11 -0
- data/examples/github/run.rb +43 -0
- data/examples/github/setup.rb +18 -0
- data/examples/rick_and_morty.rb +57 -0
- data/graph_weaver.gemspec +12 -3
- data/lib/graph_weaver/client.rb +30 -1
- data/lib/graph_weaver/codegen/emit.rb +5 -11
- data/lib/graph_weaver/codegen.rb +23 -55
- data/lib/graph_weaver/context_seam.rb +54 -0
- data/lib/graph_weaver/errors.rb +23 -15
- data/lib/graph_weaver/federation.rb +11 -2
- data/lib/graph_weaver/graph.rb +39 -29
- data/lib/graph_weaver/in_process.rb +15 -9
- data/lib/graph_weaver/internal/endpoint.rb +7 -5
- data/lib/graph_weaver/internal/headers.rb +19 -0
- data/lib/graph_weaver/internal/test_clients.rb +7 -11
- data/lib/graph_weaver/internal.rb +81 -13
- data/lib/graph_weaver/log_subscriber.rb +10 -2
- data/lib/graph_weaver/logging.rb +33 -13
- data/lib/graph_weaver/query_module.rb +44 -23
- data/lib/graph_weaver/retry.rb +12 -8
- data/lib/graph_weaver/rspec.rb +13 -24
- data/lib/graph_weaver/schema_loader.rb +52 -14
- data/lib/graph_weaver/tasks.rb +10 -2
- data/lib/graph_weaver/testing/cassette.rb +28 -5
- data/lib/graph_weaver/testing/endpoint.rb +14 -13
- data/lib/graph_weaver/testing/fake_client.rb +33 -3
- data/lib/graph_weaver/testing/router.rb +7 -3
- data/lib/graph_weaver/testing.rb +12 -4
- data/lib/graph_weaver/transport/http.rb +2 -2
- data/lib/graph_weaver/transport.rb +47 -23
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +32 -10
- metadata +16 -3
- data/CHANGELOG.md +0 -3801
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cb0372c9f8505bec490fbb13344cbd586da68cb4932cbce0f86de2467d0eaf4
|
|
4
|
+
data.tar.gz: 6bc17449eea717c8cb145c159a2395f61f484a2f585c016ee31521f979f1a6d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63670eee3838644725166822028b4816164450c853e24d74dd6ce47212e4e18c2bda7309d5549f8830e709d547e73678425ae9984d960a5367b97f5102613f5c
|
|
7
|
+
data.tar.gz: d7b30350095090f9c69e7b5316137ac5324b60965a242ffe5c9be4a893812a48351a172bde26f36000797d830eb01c3e15ea0352eea8ecda9ac775f55b12ac82
|
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.2)
|
|
5
5
|
graphql (>= 2.6.7)
|
|
6
6
|
sorbet-runtime
|
|
7
7
|
|
|
@@ -240,7 +240,7 @@ GEM
|
|
|
240
240
|
crack (>= 0.3.2)
|
|
241
241
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
242
242
|
webrick (1.9.2)
|
|
243
|
-
yard (0.9.
|
|
243
|
+
yard (0.9.45)
|
|
244
244
|
zeitwerk (2.8.3)
|
|
245
245
|
|
|
246
246
|
PLATFORMS
|
|
@@ -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.2)
|
|
303
303
|
graphql (2.6.10) sha256=9b7c8633767f516ff9d48a8d6305b2a00a2101c82aa871b92e69086944f9f83e
|
|
304
304
|
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
|
|
305
305
|
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
|
@@ -367,7 +367,7 @@ CHECKSUMS
|
|
|
367
367
|
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
|
|
368
368
|
webmock (3.26.4) sha256=8d8da206d217ebe6968cfb09c77f4533c23074e1432bad865f3994eacbaad50d
|
|
369
369
|
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
|
370
|
-
yard (0.9.
|
|
370
|
+
yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51
|
|
371
371
|
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
|
|
372
372
|
|
|
373
373
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -39,21 +39,15 @@ result.person&.nmae
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
`person` is `T.nilable` because the schema says the field is nullable — the `&.`
|
|
42
|
-
isn't defensive, it's the schema talking.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`NoMethodError` the first time the line runs instead of in CI — [Sorbet, with or
|
|
42
|
+
isn't defensive, it's the schema talking. Static Sorbet is optional:
|
|
43
|
+
`sorbet-runtime` is the only Sorbet gem this one needs, so in an app that doesn't
|
|
44
|
+
run `srb tc` the same typo surfaces as a `NoMethodError` the first time the line
|
|
45
|
+
runs, rather than in CI — [Sorbet, with or
|
|
47
46
|
without](docs/getting_started.md#sorbet-with-or-without).
|
|
48
47
|
|
|
49
48
|
## Start here
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
# Gemfile
|
|
53
|
-
gem "graph_weaver"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
In Rails, setup is one command:
|
|
50
|
+
Add `gem "graph_weaver"` to your Gemfile. In Rails, the rest is one command:
|
|
57
51
|
|
|
58
52
|
```sh
|
|
59
53
|
rails g graph_weaver:install https://api.example.com/graphql
|
|
@@ -63,41 +57,19 @@ which writes the initializer, the `app/graphql` layout, the editor config and th
|
|
|
63
57
|
schema dump. **[Getting started](docs/getting_started.md)** walks the production
|
|
64
58
|
setup end to end.
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
client, namespace and scalar registrations — and the same one command generates
|
|
72
|
-
and verifies the app: **[more than one
|
|
73
|
-
schema](docs/getting_started.md#more-than-one-schema)**.
|
|
74
|
-
|
|
75
|
-
Or skip the build step and poke at an API from a console —
|
|
76
|
-
anything holding a schema parses, and the module runs on what parsed it:
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
api = GraphWeaver.new("https://countries.trevorblades.com/")
|
|
80
|
-
CountryQuery = api.parse("queries/country.graphql") # a path or a raw string
|
|
81
|
-
CountryQuery.execute!(code: "JP").country&.capital # => "Tokyo"
|
|
82
|
-
|
|
83
|
-
api.run!("query { continents { name } }").continents # or no module at all
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
The **[examples](examples/)** run that path for real, smallest first: a public API
|
|
87
|
-
in 30 lines, a paginated search, the production path against GitHub, and the
|
|
60
|
+
Or skip the build step and poke at an API from a console: `GraphWeaver.new(url)`
|
|
61
|
+
parses a query into a module on the spot, and `run!` executes one without a module
|
|
62
|
+
at all — [against a real API](docs/real_world.md). The
|
|
63
|
+
**[examples](examples/)** run that path for real, smallest first: a public API in
|
|
64
|
+
30 lines, a paginated search, the production path against GitHub, and the
|
|
88
65
|
federated graph below.
|
|
89
66
|
|
|
90
67
|
## Precise types are expensive to fake, so it fakes them for you
|
|
91
68
|
|
|
92
69
|
Generation makes result types exact, which makes them tedious to build by hand —
|
|
93
70
|
and most generators stop there and leave you the fixtures. GraphWeaver ships the
|
|
94
|
-
fakes. One line in the spec helper
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
require "graph_weaver/rspec"
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
then one tag says what an example runs against:
|
|
71
|
+
fakes. One line in the spec helper (`require "graph_weaver/rspec"`), then one tag
|
|
72
|
+
says what an example runs against:
|
|
101
73
|
|
|
102
74
|
```ruby
|
|
103
75
|
it "shows the profile", graphql: :fake do
|
|
@@ -113,71 +85,51 @@ No fixture, no stub, no HTTP — and the values are seeded from rspec's own seed
|
|
|
113
85
|
`--seed 4242` hands back that same person and a failure reproduces.
|
|
114
86
|
|
|
115
87
|
Random data answers "does this render". When the example is *about* the data, pin
|
|
116
|
-
the fields it's about
|
|
88
|
+
the fields it's about — `graphql_fake("Person.name" => "Ada")`, or a whole type
|
|
89
|
+
off your factory — and everything else in the selection stays fabricated. Pins are
|
|
90
|
+
schema names, checked and spellchecked, so a typo raises rather than leaving the
|
|
91
|
+
example green against random data.
|
|
117
92
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
person.pets.size # => 2 — a pinned list is as long as you write it
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Keys are schema names — a field, or a whole type: `"Person" => build(:person)`
|
|
127
|
-
reads the selected fields off your factory's object and fabricates the rest. They
|
|
128
|
-
are checked and spellchecked, so a typo raises instead of leaving the example
|
|
129
|
-
green against random data. The tag also picks a *real* client
|
|
130
|
-
when you want one: `:in_process` runs your resolvers, `:router` runs them across a
|
|
131
|
-
federated graph, and `:wire` serves either at your own endpoint, so the transport
|
|
132
|
-
you ship runs too. Field-level failure simulation and record/replay cassettes with
|
|
133
|
-
anonymization are in [testing](docs/testing.md).
|
|
93
|
+
The tag also picks a *real* client when you want one: `:in_process` runs your
|
|
94
|
+
resolvers, `:router` runs them across a federated graph, and `:wire` serves either
|
|
95
|
+
at your own endpoint, so the transport you ship runs too. Field-level failure
|
|
96
|
+
simulation and record/replay cassettes with anonymization are in
|
|
97
|
+
[testing](docs/testing.md).
|
|
134
98
|
|
|
135
99
|
## Federation without a gateway
|
|
136
100
|
|
|
137
101
|
When your app is both a GraphQL client and a subgraph, the local router plans a
|
|
138
102
|
query across the composed supergraph and runs your **real resolvers** over the
|
|
139
|
-
boundary — no gateway process, no node, no sockets.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
fetches:
|
|
145
|
-
→ accounts root fields
|
|
146
|
-
→ reviews _entities × 1 User
|
|
147
|
-
→ products _entities × 2 Product
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
The trace is the query plan: every node at a level goes in one `_entities` call,
|
|
151
|
-
so two products cost one fetch. Anything it can't answer *faithfully* it refuses
|
|
152
|
-
at plan time rather than guessing — the example prints one of those too, naming
|
|
153
|
-
the coordinate that stopped it and what to rename. And it's diffed against a real
|
|
154
|
-
`@apollo/gateway` over the same supergraph:
|
|
155
|
-
currently 73 queries identical, 2 refused, 0 wrong
|
|
103
|
+
boundary — no gateway process, no node, no sockets. It prints the plan as it
|
|
104
|
+
fetches, batching every node at a level into one `_entities` call, and anything it
|
|
105
|
+
can't answer *faithfully* it refuses at plan time rather than guessing, naming the
|
|
106
|
+
coordinate that stopped it. It is diffed against a real `@apollo/gateway` over the
|
|
107
|
+
same supergraph: currently 73 queries identical, 2 refused, 0 wrong
|
|
156
108
|
([`spec/integration/router_parity_spec.rb`](spec/integration/router_parity_spec.rb)).
|
|
157
|
-
|
|
109
|
+
[`examples/federation.rb`](examples/federation.rb) runs the whole thing with no
|
|
110
|
+
network; see [federation](docs/federation.md).
|
|
158
111
|
|
|
159
112
|
## The schema keeps itself honest
|
|
160
113
|
|
|
161
114
|
The lifecycle is rake tasks, not a CI pipeline you assemble yourself:
|
|
162
115
|
`schema:refresh` re-introspects the committed dump, `schema:diff` names what
|
|
163
|
-
changed when whatever that dump came from
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
regenerating never shows a diff you didn't earn. See
|
|
116
|
+
changed when whatever that dump came from has moved past it, `queries:check` names
|
|
117
|
+
the queries that drift broke and where, `unused` names the selections your app
|
|
118
|
+
stopped reading, and `verify` fails when the checked-in Ruby is stale. Generation
|
|
119
|
+
is deterministic — same schema and queries, byte-identical files — so regenerating
|
|
120
|
+
never shows a diff you didn't earn. See
|
|
169
121
|
[getting started](docs/getting_started.md#5-verify-in-ci).
|
|
170
122
|
|
|
171
|
-
**Any release can change what codegen emits**, patch releases included
|
|
172
|
-
|
|
173
|
-
|
|
123
|
+
**Any release can change what codegen emits**, patch releases included. So `rake
|
|
124
|
+
graph_weaver:generate` is part of every upgrade, and `verify` is what tells you
|
|
125
|
+
when you've skipped it.
|
|
174
126
|
|
|
175
127
|
#### Also in the box
|
|
176
128
|
|
|
177
|
-
- **
|
|
178
|
-
- **Fragments
|
|
179
|
-
- **Any transport
|
|
180
|
-
- **Structured errors
|
|
129
|
+
- **Typed variable kwargs** — enums as `T::Enum`s, input objects as `T::Struct`s, required vs optional falling out of nullability and defaults
|
|
130
|
+
- **Fragments**, **unions and interfaces** (member structs, `__typename` dispatch), `@skip`/`@include` nullability
|
|
131
|
+
- **Any transport** — in-process, a zero-dependency HTTP client, or Faraday with your own middleware, plus a composable `Retry`
|
|
132
|
+
- **Structured errors** — an envelope that keeps partial data and extensions, a hierarchy split by failure site, field-level reports, stale-schema detection
|
|
181
133
|
|
|
182
134
|
#### Dig deeper
|
|
183
135
|
|
data/docs/alternatives.md
CHANGED
|
@@ -27,12 +27,6 @@ expensive.
|
|
|
27
27
|
The honest counter: if your app doesn't use Sorbet, most of that value
|
|
28
28
|
evaporates, and [graphlient](#graphlient) is the better answer.
|
|
29
29
|
|
|
30
|
-
This idea was tried once before. `yogurt` generated Sorbet types from GraphQL
|
|
31
|
-
documents, shipped two versions 70 minutes apart in November 2020, and stopped —
|
|
32
|
-
its own README concedes it lacked named fragments and that the author "probably
|
|
33
|
-
got a lot of the decisions wrong".<!-- https://raw.githubusercontent.com/theorygeek/yogurt/master/README.md ; versions 0.1.1 16:52 and 0.2.0 18:02 on 2020-11-26 — https://rubygems.org/api/v1/versions/yogurt.json ; last real commit 29ef902 2020-11-26 -->
|
|
34
|
-
So the thesis is unproven, not proven wrong.
|
|
35
|
-
|
|
36
30
|
## The table
|
|
37
31
|
|
|
38
32
|
`graphql-ruby` isn't a column because it isn't a client — see
|
|
@@ -53,7 +47,7 @@ vendor SDKs actually do.
|
|
|
53
47
|
| **Errors** | typed envelope keeping partial data, hierarchy by failure site<!-- docs/errors.md; lib/graph_weaver/errors.rb --> | raw hashes; HTTP errors become a fake `errors` array<!-- lib/graphql/client/http.rb:80-85; https://github.com/github-community-projects/graphql-client/issues/67 --> | raises a real class hierarchy on every failure<!-- lib/graphlient/errors/ --> | thin hierarchy, mostly never raised<!-- lib/artemis/exceptions.rb — GraphQLError/GraphQLServerError defined, never raised --> | yours to write |
|
|
54
48
|
| **Transport** | `Net::HTTP` (pooled) or Faraday, plus `Retry`<!-- docs/transports.md; lib/graph_weaver/transport/ --> | stock adapter self-described as trivial<!-- lib/graphql/client/http.rb:17-19 "Production applications should consider implementing their own network adapter" --> | Faraday 2.x, full middleware access<!-- lib/graphlient/adapters/http/faraday_adapter.rb:36-48 --> | four adapters, no middleware layer<!-- lib/artemis/adapters.rb; https://github.com/yuki24/artemis/issues/57 open since 2019 --> | whatever you picked |
|
|
55
49
|
| **Rails** | generator, railtie, reload-on-edit, rake lifecycle<!-- lib/generators/graph_weaver/install_generator.rb; lib/graph_weaver/railtie.rb; lib/graph_weaver/tasks.rb --> | opt-in railtie, no generators; docs call the boot order "a mess"<!-- lib/graphql/client/railtie.rb:34-37 TODO; https://github.com/github-community-projects/graphql-client/blob/master/guides/rails-configuration.md --> | none | the whole pitch: generators, config, callbacks<!-- lib/artemis/railtie.rb; lib/generators/artemis/ --> | n/a |
|
|
56
|
-
| **Last release** | v0.7.
|
|
50
|
+
| **Last release** | v0.7.1, 2026-09-13 | v0.26.0, 2025-05-29<!-- https://rubygems.org/api/v1/gems/graphql-client.json --> | v0.9.0, 2026-08-02<!-- https://rubygems.org/api/v1/gems/graphlient.json --> | v1.1.0, 2024-08-16<!-- https://rubygems.org/api/v1/gems/artemis.json --> | n/a |
|
|
57
51
|
| **Downloads** | 7.5k<!-- 7,478 — https://rubygems.org/api/v1/gems/graph_weaver.json --> | 94M<!-- 94,408,657 --> | 32M<!-- 32,287,626 --> | 430k<!-- 425,737 --> | n/a |
|
|
58
52
|
|
|
59
53
|
## graphql-client
|
data/docs/cassettes.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# Cassettes: capture and replay
|
|
2
2
|
|
|
3
3
|
Cassettes record real API responses and replay them in tests — above the
|
|
4
|
-
transport (a client wrapping a client), so there's no HTTP interception and
|
|
5
|
-
|
|
6
|
-
is a YAML list of `{query, variables, operationName, response}` entries,
|
|
7
|
-
matched on everything but the response — the request's identity as the server
|
|
8
|
-
sees it.
|
|
4
|
+
transport (a client wrapping a client), so there's no HTTP interception and they
|
|
5
|
+
work identically over HTTP, Faraday, or in-process execution.
|
|
9
6
|
|
|
10
7
|
`Testing.cassette(name, client:)` returns a client that replays
|
|
11
8
|
`spec/cassettes/<name>.yml`, recording it through `client:` first if the file
|
|
12
|
-
doesn't exist yet
|
|
9
|
+
doesn't exist yet:
|
|
13
10
|
|
|
14
11
|
```ruby
|
|
15
12
|
client = GraphWeaver::Testing.cassette("github", client: live)
|
|
@@ -17,7 +14,7 @@ result = RepoQuery.execute!(client:, owner: "dpep", name: "graph_weaver")
|
|
|
17
14
|
```
|
|
18
15
|
|
|
19
16
|
That first run writes `spec/cassettes/github.yml` (`Testing.config.cassette_dir`
|
|
20
|
-
resolves bare names). Commit it — with anonymization on
|
|
17
|
+
resolves bare names). Commit it — with [anonymization](#anonymization) on, since
|
|
21
18
|
recordings hold real data — and the suite runs offline from then on. Re-record
|
|
22
19
|
when the API's real behavior changes:
|
|
23
20
|
|
|
@@ -27,20 +24,27 @@ GRAPHWEAVER_RECORD=1 bundle exec rspec # every Testing.cassette records afresh
|
|
|
27
24
|
|
|
28
25
|
(`Testing.config.record = true` is the programmatic equivalent.) A call with no
|
|
29
26
|
`client:` raises there, rather than quietly replaying the recording it was told
|
|
30
|
-
to refresh
|
|
31
|
-
`GraphWeaver::Testing::MissingRecording`, naming the variables it was called
|
|
32
|
-
|
|
27
|
+
to refresh; a *request* with no recording raises
|
|
28
|
+
`GraphWeaver::Testing::MissingRecording`, naming the variables it was called with
|
|
29
|
+
and the ones recorded for that same query — what usually differs.
|
|
33
30
|
|
|
34
|
-
**One entry per request.**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
**One entry per request.** A cassette is a YAML list of
|
|
32
|
+
`{query, variables, operationName, response}` entries, and the first three
|
|
33
|
+
together are the request key — the request's identity as the server sees it.
|
|
34
|
+
Re-recording a request *replaces* its entry and a request the file hasn't seen
|
|
35
|
+
appends one, so however often `GRAPHWEAVER_RECORD=1` runs, no cassette ends up
|
|
36
|
+
with two entries for the same request.
|
|
39
37
|
|
|
40
38
|
Editing a query changes the key, so the re-record writes a new entry and the old
|
|
41
39
|
one stays behind — a recording of a request nothing sends any more.
|
|
42
|
-
`cassettes:check` counts those ("1 not sent by any query module", below); the
|
|
43
|
-
|
|
40
|
+
`cassettes:check` counts those ("1 not sent by any query module", below); the way
|
|
41
|
+
to clear them is to delete the cassette and record it afresh.
|
|
42
|
+
|
|
43
|
+
**Recording under `parallel_tests` is safe.** Writing rewrites the whole file, so
|
|
44
|
+
a recorder re-reads it inside an exclusive lock on a `<cassette>.yml.lock`
|
|
45
|
+
sidecar — a mutex for this process's threads, an `flock` for the other
|
|
46
|
+
processes — and every worker's entries survive. That sidecar is an empty lock
|
|
47
|
+
target: gitignore it, or let it sit; nothing reads it.
|
|
44
48
|
|
|
45
49
|
## Has a recording gone stale?
|
|
46
50
|
|
|
@@ -48,16 +52,11 @@ A cassette is the one artifact here recorded from *someone else's* server, and
|
|
|
48
52
|
none of the other checks can see it drift: `verify` asks whether the generated
|
|
49
53
|
Ruby is fresh, `queries:check` whether a query still validates, `schema:diff`
|
|
50
54
|
whether the server's schema moved. When the recorded *answers* stop fitting the
|
|
51
|
-
structs your schema generated — a field that was `Int!` when you recorded and
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rake graph_weaver:cassettes:check
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
It replays every recording through the generated modules — no network — so it
|
|
60
|
-
belongs in the normal PR run beside `verify`, and exits non-zero on drift:
|
|
55
|
+
structs your schema generated — a field that was `Int!` when you recorded and is
|
|
56
|
+
`String!` now — nothing notices until a spec dies mid-run on a cast error naming
|
|
57
|
+
a struct and nothing else. `rake graph_weaver:cassettes:check` replays every
|
|
58
|
+
recording through the generated modules — no network — so it belongs in the
|
|
59
|
+
normal PR run beside `verify`, and exits non-zero on drift:
|
|
61
60
|
|
|
62
61
|
```
|
|
63
62
|
spec/cassettes/dashboard.yml: 1 stale (3 checked, 1 not sent by any query module)
|
|
@@ -65,17 +64,17 @@ spec/cassettes/dashboard.yml: 1 stale (3 checked, 1 not sent by any query module
|
|
|
65
64
|
failed to cast response into DashboardQuery::Result::Me::Reviews::Book: Parameter 'price_cents': Can't set …price_cents to 4200 (instance of Integer) - need a String
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
A recording is matched to the module that sends its query, so one written by
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
A recording is matched to the module that sends its query, so one written by hand
|
|
68
|
+
is skipped and counted rather than guessed at. Checking **none** of them fails
|
|
69
|
+
too: a green run that compared nothing would pass whatever the recordings said.
|
|
70
|
+
The fix is a re-record (`GRAPHWEAVER_RECORD=1`, with a live `client:`) — or
|
|
71
|
+
`rake graph_weaver:generate`, if it was the schema dump that moved first.
|
|
73
72
|
|
|
74
73
|
## Anonymization
|
|
75
74
|
|
|
76
|
-
Cassettes hold real responses, so scrub them as they're recorded: real data
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
Cassettes hold real responses, so scrub them as they're recorded: real data never
|
|
76
|
+
reaches disk, and the caller sees the anonymized response too, so assertions
|
|
77
|
+
written during the recording run still hold on replay.
|
|
79
78
|
|
|
80
79
|
```ruby
|
|
81
80
|
GraphWeaver::Testing.configure do |config|
|
|
@@ -94,21 +93,21 @@ preserving everything that makes the recording faithful:
|
|
|
94
93
|
| id *relationships* (same original id → same fake id) | the id values themselves |
|
|
95
94
|
|
|
96
95
|
**Every plain string goes**, not the PII-shaped ones — nothing here can tell a
|
|
97
|
-
user's name from a product's, so a recorded `"pikachu"` replays as `"name-1"`
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
user's name from a product's, so a recorded `"pikachu"` replays as `"name-1"` and
|
|
97
|
+
an assertion pinned to it fails. Leave it off for a public, non-sensitive API,
|
|
98
|
+
where the real values *are* the point of the cassette.
|
|
100
99
|
|
|
101
100
|
`data` is walked against the schema — which is why it needs one, to know which
|
|
102
|
-
values are enums, dates, ids. `errors` and `extensions` have none behind them,
|
|
103
|
-
|
|
101
|
+
values are enums, dates, ids. `errors` and `extensions` have none behind them, so
|
|
102
|
+
they're walked by shape instead: keys, nesting and structure survive, every
|
|
104
103
|
string and number is replaced. `path`, `locations` and an error's
|
|
105
104
|
`extensions.code` are kept, because they describe the request rather than the
|
|
106
105
|
data — and call sites branch on `code` the way they branch on an enum.
|
|
107
106
|
|
|
108
107
|
**The query and its variables are not anonymized.** They're the key replay
|
|
109
108
|
matches on, so scrubbing them would make the recording unfindable. A mutation's
|
|
110
|
-
input is often the sensitive part, so record with placeholder variables, or
|
|
111
|
-
|
|
109
|
+
input is often the sensitive part, so record with placeholder variables, or don't
|
|
110
|
+
record that request.
|
|
112
111
|
|
|
113
112
|
Recording says so when the bytes it wrote look like a credential:
|
|
114
113
|
|
|
@@ -118,20 +117,16 @@ cassette is committed as written, so review this one first. …
|
|
|
118
117
|
```
|
|
119
118
|
|
|
120
119
|
It recognizes tokens by shape — a JWT, `AKIA…`, `ghp_…`, `xox…`, `sk_live_…`, a
|
|
121
|
-
PEM block, a `Bearer` header — which is every credential that is unmistakable
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
For cassettes recorded before the flag was on
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Anonymization preserves shape, so an anonymized cassette still passes
|
|
132
|
-
`cassettes:check` — including a custom scalar, whose replacement is the same
|
|
133
|
-
one [`FakeClient`](testing.md#fabricated-data--graphql-fake) would fabricate.
|
|
134
|
-
A scalar registered as *your own* class needs a pin for the type in
|
|
120
|
+
PEM block, a `Bearer` header — which is every credential that is unmistakable and
|
|
121
|
+
nothing else. A password like `hunter2` has no shape, so a quiet run is not a
|
|
122
|
+
clean bill of health: **read a cassette before committing it.**
|
|
123
|
+
|
|
124
|
+
For cassettes recorded before the flag was on,
|
|
125
|
+
`rake graph_weaver:cassettes:anonymize` does every cassette in `cassette_dir`, in
|
|
126
|
+
place. Anonymization preserves shape, so an anonymized cassette still passes
|
|
127
|
+
`cassettes:check` — including a custom scalar, whose replacement is the same one
|
|
128
|
+
[`FakeClient`](testing.md#fabricated-data--graphql-fake) would fabricate. A scalar
|
|
129
|
+
registered as *your own* class needs a pin for the type in
|
|
135
130
|
`Testing.config.overrides` (`{ "Money" => "12.00" }` — [pins](testing.md#pins)),
|
|
136
131
|
which the anonymizer reads too; without one, anonymizing refuses rather than
|
|
137
132
|
writing a value the codec can't read back.
|
|
@@ -139,6 +134,6 @@ writing a value the codec can't read back.
|
|
|
139
134
|
## Cassette or FakeClient?
|
|
140
135
|
|
|
141
136
|
[FakeClient](testing.md) needs no recording and is the better default for unit
|
|
142
|
-
tests. Reach for a cassette when the *shape* of a real API's answers is the
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
tests. Reach for a cassette when the *shape* of a real API's answers is the point
|
|
138
|
+
— pagination quirks, which union member came back, where that server puts its
|
|
139
|
+
nulls — and for pinning a regression.
|
data/docs/editors.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# Editor support: five lines of YAML
|
|
2
2
|
|
|
3
|
-
Your `.graphql` files are plain GraphQL documents and your schema dump is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Ruby developers mostly don't know this, which is the only reason it's worth a
|
|
9
|
-
page.
|
|
10
|
-
|
|
11
|
-
## The file
|
|
3
|
+
Your `.graphql` files are plain GraphQL documents and your schema dump is a plain
|
|
4
|
+
introspection result, so the whole JavaScript GraphQL editor toolchain works on a
|
|
5
|
+
Ruby repo — **with no JS project, no `package.json`, and no `npm install`**. It
|
|
6
|
+
just needs one config file telling it where the two live. Ruby developers mostly
|
|
7
|
+
don't know this, which is the only reason it's worth a page.
|
|
12
8
|
|
|
13
9
|
```yaml
|
|
14
10
|
# graphql.config.yml — repo root
|
|
@@ -18,58 +14,47 @@ documents:
|
|
|
18
14
|
- app/graphql/fragments/**/*.{graphql,gql}
|
|
19
15
|
```
|
|
20
16
|
|
|
21
|
-
That's the whole setup
|
|
22
|
-
(`GraphWeaver.schema_path`, `queries_paths`,
|
|
23
|
-
them, move these to match. Include the
|
|
24
|
-
validating a query that spreads a shared fragment
|
|
25
|
-
unless the fragment files are in `documents` too. The
|
|
26
|
-
line whether or not you have fragments yet
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
An SDL dump works just as well if you took one (`cache: :graphql`):
|
|
30
|
-
|
|
31
|
-
```yaml
|
|
32
|
-
schema: app/graphql/schema.graphql
|
|
33
|
-
```
|
|
17
|
+
That's the whole setup, and `rails g graph_weaver:install` writes it. The paths
|
|
18
|
+
are graph_weaver's conventions (`GraphWeaver.schema_path`, `queries_paths`,
|
|
19
|
+
`fragments_paths`) — if you moved them, move these to match. Include the
|
|
20
|
+
fragments directory: an editor validating a query that spreads a shared fragment
|
|
21
|
+
reports `Unknown fragment` unless the fragment files are in `documents` too. The
|
|
22
|
+
generator writes that line whether or not you have fragments yet, and a glob
|
|
23
|
+
matching nothing is fine.
|
|
34
24
|
|
|
35
25
|
Introspection JSON is read directly — graphql-config ships a JSON loader, so
|
|
36
|
-
`schema.json` needs no conversion step.
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
`schema.json` needs no conversion step. An SDL dump works just as well if you
|
|
27
|
+
took one (`cache: :graphql`): point `schema:` at `app/graphql/schema.graphql`.
|
|
28
|
+
graph_weaver also writes a `graph_weaver` provenance key alongside the
|
|
29
|
+
introspection result; if some tool objects to it, use the SDL dump instead.
|
|
39
30
|
|
|
40
31
|
## What it buys you
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- **The JetBrains GraphQL plugin**, bundled with recent RubyMine, reads the
|
|
48
|
-
same file.
|
|
49
|
-
|
|
50
|
-
Either one gives you, inside a `.graphql` file:
|
|
33
|
+
Two editor plugins read this file:
|
|
34
|
+
**[vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql)**,
|
|
35
|
+
whose README states it **requires** a graphql-config file — which is why nothing
|
|
36
|
+
works without the YAML above — and **the JetBrains GraphQL plugin**, bundled with
|
|
37
|
+
recent RubyMine. Either one gives you, inside a `.graphql` file:
|
|
51
38
|
|
|
52
39
|
- validation as you type — a typo'd field is red before you run anything
|
|
53
40
|
- field and argument autocomplete off the real schema
|
|
54
|
-
- go-to-definition and hover docs into schema types, including the
|
|
55
|
-
|
|
41
|
+
- go-to-definition and hover docs into schema types, including the descriptions
|
|
42
|
+
the API author wrote
|
|
56
43
|
|
|
57
44
|
That is the same feedback the generator gives you, one round trip earlier — you
|
|
58
|
-
find the typo while typing the query, not at `rake graph_weaver:generate`.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[@graphql-eslint](https://the-guild.dev/graphql/eslint/docs) for lint rules
|
|
64
|
-
over your documents.
|
|
45
|
+
find the typo while typing the query, not at `rake graph_weaver:generate`. The
|
|
46
|
+
same globs also feed the JS CI tools, if you want them (these *do* need npm,
|
|
47
|
+
unlike the editor path): [graphql-inspector](https://the-guild.dev/graphql/inspector)
|
|
48
|
+
`validate` and [@graphql-eslint](https://the-guild.dev/graphql/eslint/docs) for
|
|
49
|
+
lint rules over your documents.
|
|
65
50
|
|
|
66
51
|
## What it doesn't buy you
|
|
67
52
|
|
|
68
53
|
**Nothing links a `.graphql` file to the Ruby it generates.** There is no
|
|
69
|
-
go-to-definition from a query field to its `T::Struct`, no rename that moves
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
go-to-definition from a query field to its `T::Struct`, no rename that moves both,
|
|
55
|
+
no warning that a struct went unused. The editor plugin understands GraphQL and
|
|
56
|
+
Sorbet understands Ruby, and no tool in any ecosystem bridges the two except
|
|
57
|
+
where documents and types share a single language service.
|
|
73
58
|
|
|
74
59
|
So the division of labour is:
|
|
75
60
|
|