graph_weaver 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1447 -1
- data/Gemfile +8 -0
- data/Gemfile.lock +151 -2
- data/README.md +20 -6
- data/docs/alternatives.md +201 -0
- data/docs/cassettes.md +17 -1
- data/docs/errors.md +382 -17
- data/docs/federation.md +469 -63
- data/docs/generated_modules.md +231 -15
- data/docs/getting_started.md +497 -104
- data/docs/i18n.md +234 -0
- data/docs/logging.md +160 -24
- data/docs/real_world.md +28 -0
- data/docs/scalars.md +190 -26
- data/docs/testing.md +457 -58
- data/docs/transports.md +164 -19
- data/docs/upgrading.md +328 -3
- data/graph_weaver.gemspec +7 -0
- data/lib/generators/graph_weaver/install_generator.rb +138 -4
- data/lib/graph_weaver/client.rb +47 -10
- data/lib/graph_weaver/codegen/aliases.rb +7 -5
- data/lib/graph_weaver/codegen/emit.rb +98 -29
- data/lib/graph_weaver/codegen/enum_type.rb +2 -1
- data/lib/graph_weaver/codegen/nodes.rb +39 -6
- data/lib/graph_weaver/codegen/registry.rb +175 -0
- data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
- data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
- data/lib/graph_weaver/codegen.rb +404 -197
- data/lib/graph_weaver/coerce.rb +155 -26
- data/lib/graph_weaver/errors.rb +264 -34
- data/lib/graph_weaver/federation.rb +119 -26
- data/lib/graph_weaver/graph.rb +315 -0
- data/lib/graph_weaver/hints.rb +100 -24
- data/lib/graph_weaver/in_process.rb +17 -11
- data/lib/graph_weaver/input_struct.rb +119 -32
- data/lib/graph_weaver/internal/endpoint.rb +78 -0
- data/lib/graph_weaver/internal/headers.rb +51 -0
- data/lib/graph_weaver/internal/overrides.rb +67 -5
- data/lib/graph_weaver/internal/planner.rb +45 -15
- data/lib/graph_weaver/internal/refusal.rb +49 -0
- data/lib/graph_weaver/internal/schemas.rb +23 -9
- data/lib/graph_weaver/internal/selection.rb +34 -0
- data/lib/graph_weaver/internal/server_input.rb +251 -0
- data/lib/graph_weaver/internal/test_clients.rb +276 -0
- data/lib/graph_weaver/internal/unused.rb +287 -0
- data/lib/graph_weaver/internal/values.rb +40 -4
- data/lib/graph_weaver/internal.rb +183 -1
- data/lib/graph_weaver/log_subscriber.rb +66 -0
- data/lib/graph_weaver/logging.rb +136 -12
- data/lib/graph_weaver/query_module.rb +36 -3
- data/lib/graph_weaver/railtie.rb +237 -17
- data/lib/graph_weaver/representation.rb +55 -17
- data/lib/graph_weaver/result_struct.rb +90 -0
- data/lib/graph_weaver/retry.rb +33 -5
- data/lib/graph_weaver/rspec.rb +404 -93
- data/lib/graph_weaver/schema_loader.rb +221 -49
- data/lib/graph_weaver/tasks.rb +380 -89
- data/lib/graph_weaver/testing/cassette.rb +6 -5
- data/lib/graph_weaver/testing/endpoint.rb +106 -0
- data/lib/graph_weaver/testing/failure.rb +69 -12
- data/lib/graph_weaver/testing/fake_client.rb +133 -44
- data/lib/graph_weaver/testing/router.rb +58 -11
- data/lib/graph_weaver/testing.rb +200 -58
- data/lib/graph_weaver/transport/faraday.rb +41 -8
- data/lib/graph_weaver/transport/http.rb +46 -4
- data/lib/graph_weaver/transport.rb +109 -26
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +474 -106
- metadata +56 -1
data/docs/generated_modules.md
CHANGED
|
@@ -49,11 +49,21 @@ GraphWeaver.generate!(schema:) # write the modules
|
|
|
49
49
|
GraphWeaver.verify_generated!(schema:) # the freshness guard, one line in a spec
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
**`verify_generated!` costs what `generate!` costs**, minus the writes — it
|
|
53
|
+
recomputes the whole plan, every file's content, and diffs each against disk,
|
|
54
|
+
whether nothing is stale or everything is. It doesn't get cheaper because only
|
|
55
|
+
one query changed, so it belongs in *one* example per suite run, not in a
|
|
56
|
+
`before` or an assertion per example, where it reads like a cheap check and
|
|
57
|
+
isn't.
|
|
58
|
+
|
|
52
59
|
`generate!` returns every file the plan produces, but rewrites only the ones
|
|
53
60
|
whose bytes changed; `GraphWeaver.changed_files` is that subset. So
|
|
54
61
|
`rake graph_weaver:generate` prints `wrote` for what moved and `N already up to
|
|
55
62
|
date` for the rest, and a watching dev server has one module to reload instead
|
|
56
|
-
of all of them.
|
|
63
|
+
of all of them. The unregistered-scalar report is the rake task's `puts`, not
|
|
64
|
+
`generate!`'s, so off rake read `GraphWeaver.untyped_scalars` for the unioned
|
|
65
|
+
list — or set [`GraphWeaver.logger`](logging.md), which `generate!` names them
|
|
66
|
+
on at `info` as it goes.
|
|
57
67
|
|
|
58
68
|
The schema dump is step 0 — codegen reads it, never a live endpoint.
|
|
59
69
|
`cache: true` on a url client writes it on first introspection
|
|
@@ -89,6 +99,21 @@ explicit, factory_bot-style:
|
|
|
89
99
|
GraphWeaver.load_generated! # require every file under generated_paths
|
|
90
100
|
```
|
|
91
101
|
|
|
102
|
+
**Outside Rails, four things have to agree**, and nothing wires them together
|
|
103
|
+
for you — a script that generates its own modules sets all four:
|
|
104
|
+
|
|
105
|
+
1. `queries_paths` — where `generate!` reads `.graphql` files.
|
|
106
|
+
2. `generated_paths` — where it writes, and where `load_generated!` reads.
|
|
107
|
+
Point them at the same directory or generation is invisible.
|
|
108
|
+
3. the call above, before the first `execute` — nothing else requires the
|
|
109
|
+
files.
|
|
110
|
+
4. `GraphWeaver.client =` — a module generated without a baked
|
|
111
|
+
[`client:`](#clients) has none of its own.
|
|
112
|
+
|
|
113
|
+
Miss (3) and the script gets a `NameError` for its own module; miss (4) and it
|
|
114
|
+
gets `PersonQuery: client must respond to #execute(query, variables:), got
|
|
115
|
+
NilClass` from a module that otherwise looks fine.
|
|
116
|
+
|
|
92
117
|
Every directory setting is a list — `queries_paths`, `generated_paths`,
|
|
93
118
|
`fragments_paths` — and every entry is read (entries may be globs; the
|
|
94
119
|
generated default includes `app/graphql/*/generated`, so per-schema layouts
|
|
@@ -168,6 +193,58 @@ end
|
|
|
168
193
|
**result**, or a raised `GraphWeaver::QueryError`. See [errors](errors.md).
|
|
169
194
|
- `from_response` / `from_response!` are the **network-free half** of the
|
|
170
195
|
pair — same envelope, but from a response hash you already have (below).
|
|
196
|
+
- A `Result` is an **ordinary Ruby object**: value `==` (with `eql?` and
|
|
197
|
+
`hash`, so a result works as a hash key), `deconstruct_keys` for pattern
|
|
198
|
+
matching, `#to_h`, and `#to_json`/`#as_json`. All of them go the whole way
|
|
199
|
+
down a nested result.
|
|
200
|
+
Immutable as far as its props go, like `Struct` or `Data` — and no further:
|
|
201
|
+
the `String` or `Hash` a leaf holds is the one the response carried, so
|
|
202
|
+
`result.name << "!"` changes the result, and its `hash` with it.
|
|
203
|
+
|
|
204
|
+
**Cache a result with `Marshal`, not YAML.** A `T::Enum` member is a
|
|
205
|
+
singleton that sorbet compares by identity, and Psych allocates an object
|
|
206
|
+
before filling it in, so YAML has no way to hand back the canonical one:
|
|
207
|
+
after a round trip `pet.species == Species::Dog` is false and the result no
|
|
208
|
+
longer equals itself. `Marshal` restores it intact — as does JSON, since
|
|
209
|
+
`#to_json` writes the wire shape and `from_h` reads it back (below).
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
PersonQuery.from_response!(raw) == PersonQuery.from_response!(raw) # true — value, not identity
|
|
213
|
+
|
|
214
|
+
case PersonQuery.execute!(id: "1")
|
|
215
|
+
in { person: { name:, pets: [{ name: first_pet }, *] } } then "#{name} and #{first_pet}"
|
|
216
|
+
in { person: { name: } } then "#{name}, petless"
|
|
217
|
+
in { person: nil } then "nobody"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
PersonQuery.execute!(id: "1").to_h
|
|
221
|
+
# => { person: { id: "1", name: "Daniel", birthday: #<Date 1984-05-06>,
|
|
222
|
+
# pets: [{ name: "Nibbler" }] } }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`#to_h` is the **Ruby** shape, not the wire's: snake_case prop names as
|
|
226
|
+
Symbols, nils kept, enums as their `T::Enum` members, and a registered
|
|
227
|
+
scalar as whatever object its codec built. It is a view, for Ruby to read.
|
|
228
|
+
|
|
229
|
+
`#to_json` — and `#as_json`, which `render json:` goes through — is the
|
|
230
|
+
**wire** shape instead: the response keys, and every leaf back through its
|
|
231
|
+
scalar registration's `serialize:`. So a result's JSON is the inverse of
|
|
232
|
+
`from_h`:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
PersonQuery::Result.from_h(JSON.parse(result.to_json)) == result # true
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
which is what a cache entry, a log line or a JSON API response wants. That
|
|
239
|
+
split is deliberate: a Symbol-keyed Ruby hash can't be mistaken for a
|
|
240
|
+
server's response, and a JSON string can — so the JSON is the one that has
|
|
241
|
+
to be true. (An **input** struct's `to_h` is already the wire hash it
|
|
242
|
+
sends, so there its JSON and its `to_h` agree.) The trip is exactly as
|
|
243
|
+
faithful as each scalar's own `cast:`/`serialize:` pair: a `Time` goes back
|
|
244
|
+
out with [the microseconds its registration writes](scalars.md#going-out--what-a-variable-kwarg-accepts),
|
|
245
|
+
and a `register_scalar` with a `cast:` and no `serialize:` has no wire
|
|
246
|
+
spelling at all, so its value reaches the encoder as it is — the same
|
|
247
|
+
reason an input can't send one.
|
|
171
248
|
- `OPERATION_NAME` rides along on every request as the spec's
|
|
172
249
|
`operationName`, so Apollo Studio, Hasura and your APM key traces, rate
|
|
173
250
|
limits and slow-query reports on the operation instead of lumping every
|
|
@@ -196,6 +273,12 @@ file holding two operations, since one file can't name two modules. Change a
|
|
|
196
273
|
file's `query` to `mutation` and its constant changes with it; the next
|
|
197
274
|
`generate!` prunes the old file, and `verify` fails until you regenerate.
|
|
198
275
|
|
|
276
|
+
**A graph's `namespace:` nests what it generates**, and is the answer when two
|
|
277
|
+
schemas in one app each have a `person.graphql`: `namespace: "Billing"` makes
|
|
278
|
+
that one `Billing::PersonQuery` in the same `person_query.rb`, and its shared
|
|
279
|
+
types module `Billing::GraphQLTypes`. Nothing else about the rule changes — the
|
|
280
|
+
file still names the module. See [getting started](getting_started.md#more-than-one-schema).
|
|
281
|
+
|
|
199
282
|
Parsing a raw query *string* has no file to name it after, so it uses the
|
|
200
283
|
operation name (`query GetPerson` → `GetPerson`); dynamic `parse` falls back to
|
|
201
284
|
`Query` for an anonymous one (its constants are container-scoped, so collisions
|
|
@@ -291,6 +374,14 @@ stands in its place for the arguments — stricter, and with a better message
|
|
|
291
374
|
and the `Result` it returns is a `T::Struct`, so its props are still checked one
|
|
292
375
|
by one.
|
|
293
376
|
|
|
377
|
+
`T::Configuration.default_checked_level = :never` buys nothing back here. That
|
|
378
|
+
knob governs `sig` dispatch, and the emitted sigs already opt out; the cost that
|
|
379
|
+
remains is `T::Struct`'s own prop validation, which sorbet-runtime declares
|
|
380
|
+
`.checked(:never)` in its own source and runs through a setter built at class
|
|
381
|
+
definition. `from_h` allocates and costs the same either way — measured
|
|
382
|
+
object-for-object identical — so reach for the scalar's cast, not this, when a
|
|
383
|
+
deserialization path is hot.
|
|
384
|
+
|
|
294
385
|
One kwarg per declared variable, always — so adding a variable to a query
|
|
295
386
|
adds a kwarg and leaves every existing call site alone. Two names are refused at
|
|
296
387
|
generation, `$client` and `$variables`: the generated `execute` body already
|
|
@@ -339,6 +430,40 @@ a shared type keeps one identity across modules. A query module aliases only its
|
|
|
339
430
|
diffs exactly the types it touched, and types the schema drops are pruned on
|
|
340
431
|
regeneration (`verify` flags strays). Dynamic `parse` stays self-contained.
|
|
341
432
|
|
|
433
|
+
### An input object generates its whole closure
|
|
434
|
+
|
|
435
|
+
A result type is generated per selection set, because a selection set *is* the
|
|
436
|
+
question. An input object has no selection set, so the only static answer to
|
|
437
|
+
"what can `$where` hold" is every input type it can transitively reach — and
|
|
438
|
+
codegen emits a file for each. On a hand-written schema that closure is usually
|
|
439
|
+
the one type and nothing else. On a generated one (Hasura, Gatsby), where every
|
|
440
|
+
`_bool_exp` references every other, one `$where` reaches a thousand of them.
|
|
441
|
+
|
|
442
|
+
The escape is to stop making the filter a variable. Write it as a literal in the
|
|
443
|
+
query with a variable per leaf, and codegen has ordinary scalars to generate
|
|
444
|
+
instead of the closure — on the query that emitted ~1,200 files, exactly one:
|
|
445
|
+
|
|
446
|
+
```graphql
|
|
447
|
+
query($name: String!, $minHeight: Int!) {
|
|
448
|
+
pokemon(where: { name: { _ilike: $name }, height: { _gte: $minHeight } }) {
|
|
449
|
+
name
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`srb tc` gets *more* out of that, not less. `name: String`, `min_height:
|
|
455
|
+
Integer` are types it checks at every call site, where the variable form is
|
|
456
|
+
`T.any(PokemonBoolExp, T::Hash[T.untyped, T.untyped])` — and a hash built from
|
|
457
|
+
`params`, which is how a filter is really assembled, takes the untyped branch.
|
|
458
|
+
Refusals land on the leaf too, so `path` is the form field rather than the
|
|
459
|
+
comparison operator under it.
|
|
460
|
+
|
|
461
|
+
Two shapes can't be inlined, and codegen says which when a prop collision in an
|
|
462
|
+
input type forces the question. A key chosen at runtime — the sort column in
|
|
463
|
+
`order_by: { <column>: asc }` — has no spelling, because GraphQL has no dynamic
|
|
464
|
+
object keys. And a literal list can't stand in for a length only the runtime
|
|
465
|
+
knows.
|
|
466
|
+
|
|
342
467
|
## Enums: one GraphQL enum, one Ruby type
|
|
343
468
|
|
|
344
469
|
Every schema enum a query touches — as a variable, in a result, or both —
|
|
@@ -363,6 +488,28 @@ of yours instead.
|
|
|
363
488
|
the enums into the query module itself; there's no cross-query set to share
|
|
364
489
|
against, but one enum is still one class within that module.
|
|
365
490
|
|
|
491
|
+
**The one misuse nothing catches** is comparing against the wire spelling:
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
pet.species == "CAT" # => false, always, and silently
|
|
495
|
+
pet.species == GraphQLTypes::Species::Cat
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
A generated enum is a plain `T::Enum`, so `==` against a String is `false` —
|
|
499
|
+
`srb tc` allows it (`==` takes `BasicObject`) and nothing raises. sorbet-runtime
|
|
500
|
+
owns this question and ships the switch; turn it on in dev and test and route
|
|
501
|
+
the report wherever your other soft assertions go:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
T::Configuration.enable_legacy_t_enum_migration_mode
|
|
505
|
+
T::Configuration.soft_assert_handler = ->(message, extra) { raise "#{message} #{extra}" }
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
It covers your own `T::Enum`s too, which is why it belongs there rather than in
|
|
509
|
+
the generated classes. Careful reading it: in that mode the comparison answers
|
|
510
|
+
**true** (it serializes first), so the handler, not the return value, is the
|
|
511
|
+
signal.
|
|
512
|
+
|
|
366
513
|
## Selections
|
|
367
514
|
|
|
368
515
|
- **Fragments** — inline fragments and named spreads flatten into the
|
|
@@ -380,6 +527,20 @@ mode) the struct raises a NoMethodError naming the prop that does exist —
|
|
|
380
527
|
`use 'name_with_owner'` for the exact wire name, `did you mean ...?` for
|
|
381
528
|
a near-miss typo in either casing.
|
|
382
529
|
|
|
530
|
+
A name that would shadow a method every struct answers — `class`, `hash`,
|
|
531
|
+
`display`, `to_json`, and `supplied` on an input — takes a trailing underscore
|
|
532
|
+
instead: `class` → `class_`, in results and input types alike. The generated
|
|
533
|
+
source says so on the line above the prop. Only the Ruby name moves: the wire
|
|
534
|
+
keeps the schema's spelling in both directions, so the query, the request and
|
|
535
|
+
the response are untouched, and `result.class` is still Ruby's `class`. The
|
|
536
|
+
prop is the one Ruby name for the field, so `.new`, `.coerce`, a result's
|
|
537
|
+
`#to_h` and pattern matching, and an `InputError`'s `#path` all use `class_`
|
|
538
|
+
(an input error's `#coordinate` still names the schema's `Tricky.class`). The
|
|
539
|
+
wire views are where the schema's spelling comes back: an **input** struct's
|
|
540
|
+
`#to_h` is the hash you would send — `{"class" => …}` — and a result's
|
|
541
|
+
`#as_json`/`#to_json` write `"class"` too, so `render json: result` never
|
|
542
|
+
leaks a trailing underscore. Input structs don't pattern-match at all.
|
|
543
|
+
|
|
383
544
|
### Abstract types
|
|
384
545
|
|
|
385
546
|
An abstract field emits **one struct per type condition the selection names**,
|
|
@@ -387,7 +548,10 @@ plus a catch-all `Other`, wrapped in a module with
|
|
|
387
548
|
`Type = T.type_alias { T.any(...) }` and a `from_h` that dispatches on
|
|
388
549
|
`__typename`. Generation therefore *requires* `__typename` in such a selection,
|
|
389
550
|
unaliased and unconditional — the wire response carries no type tag unless you
|
|
390
|
-
ask, and `from_h` reads it on every response.
|
|
551
|
+
ask, and `from_h` reads it on every response. One `__typename` inside each
|
|
552
|
+
`... on Type` does **not** substitute, however many of them there are: the
|
|
553
|
+
dispatch runs before any member's selection applies, and a member the query
|
|
554
|
+
never named would carry none at all.
|
|
391
555
|
|
|
392
556
|
Size follows the query, not the schema: two `... on` conditions against GitHub's
|
|
393
557
|
`Node` — an interface with a few hundred implementations — emit three structs,
|
|
@@ -473,9 +637,12 @@ pet.name # => "Shelby" — the wire value stays honest
|
|
|
473
637
|
|
|
474
638
|
The methods live on the struct, so they see its wire fields at runtime and
|
|
475
639
|
fakes/cassettes get the behavior automatically; registrations are additive
|
|
476
|
-
(repeated ones stack).
|
|
477
|
-
|
|
478
|
-
|
|
640
|
+
(repeated ones stack). The mixin is one of your own constants, so in Rails the
|
|
641
|
+
registration goes in a `to_prepare` block like `register_enum` does, and for the
|
|
642
|
+
same reason — [getting started](getting_started.md#2-run-the-generator) has the
|
|
643
|
+
rule and the boot order behind it. Editing the *mixin* in development needs a
|
|
644
|
+
restart, unlike a `.graphql` edit: a reload hands the constant a new module
|
|
645
|
+
object, and the `include` that took the old one doesn't run again.
|
|
479
646
|
For quick decoration, build the mixin inline — the block
|
|
480
647
|
is `module_eval`'d into a fresh module auto-named under
|
|
481
648
|
`GraphWeaver::TypeHelpers`:
|
|
@@ -486,12 +653,41 @@ GraphWeaver.extend_type("Pet") do
|
|
|
486
653
|
end
|
|
487
654
|
```
|
|
488
655
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
656
|
+
The name is where the block is written and what it extends:
|
|
657
|
+
`GraphWeaver::TypeHelpers::Pet` at the top level,
|
|
658
|
+
`GraphWeaver::TypeHelpers::Billing::Pet` inside `GraphWeaver.graph :billing`.
|
|
659
|
+
Generated code spells it, so it depends on your source and nothing else — two
|
|
660
|
+
graphs can extend the same type name, and the name a `generate` bakes in is the
|
|
661
|
+
one a boot creates.
|
|
662
|
+
|
|
663
|
+
**Neither form is statically checked as written**, for the same reason: `srb tc`
|
|
664
|
+
checks a mixin's method bodies in the module's own scope, not the including
|
|
665
|
+
struct's, so a helper reading a wire field (`name`, `birthday`) fails with
|
|
666
|
+
"method does not exist on the module" — and the block form has no source on disk
|
|
667
|
+
for `srb tc` to read at all (it shows up as `Unable to resolve constant` on the
|
|
668
|
+
generated `include`, which is the cost of the convenience).
|
|
669
|
+
|
|
670
|
+
A *named* module can carry real sigs, though, by declaring the fields it leans
|
|
671
|
+
on — abstract sigs are how a mixin says "whatever includes me has these", and the
|
|
672
|
+
struct's `const`s satisfy them:
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
# typed: strict
|
|
676
|
+
module PetHelpers
|
|
677
|
+
extend T::Sig
|
|
678
|
+
extend T::Helpers
|
|
679
|
+
abstract!
|
|
680
|
+
|
|
681
|
+
sig { abstract.returns(String) }
|
|
682
|
+
def name; end
|
|
683
|
+
|
|
684
|
+
sig { returns(String) }
|
|
685
|
+
def display_name = "#{name} 🐶"
|
|
686
|
+
end
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`T.unsafe(self).name` also silences it, at the cost of checking nothing. Either
|
|
690
|
+
beats `# typed: false` for a helper you want checked.
|
|
495
691
|
|
|
496
692
|
### Flat accessors with `alias:`
|
|
497
693
|
|
|
@@ -560,13 +756,23 @@ Resolution is per call (`client:`) → per module → baked constant →
|
|
|
560
756
|
[transports](transports.md#client-resolution).
|
|
561
757
|
|
|
562
758
|
Generate *without* a baked constant when you want modules to follow the
|
|
563
|
-
app default (`GraphWeaver.client =` in an initializer)
|
|
564
|
-
|
|
759
|
+
app default (`GraphWeaver.client =` in an initializer).
|
|
760
|
+
|
|
761
|
+
A baked constant is no longer a reason a module escapes
|
|
762
|
+
[testing's `graphql:` tag](testing.md): the tag is exactly the instruction
|
|
763
|
+
to replace the client generation chose, so it stands in for the baked one
|
|
764
|
+
too. What the *example* says still wins — a per-call `client:`, or
|
|
765
|
+
`MyQuery.client =` in a `before` block.
|
|
565
766
|
|
|
566
767
|
`client`/`client=` live in the gem (`GraphWeaver::QueryModule`, extended by
|
|
567
768
|
every generated module). A baked constant is emitted as a private
|
|
568
769
|
`DEFAULT_CLIENT`, resolved on first use so a module can load before the
|
|
569
|
-
initializer that builds its client.
|
|
770
|
+
initializer that builds its client. A module generated from a
|
|
771
|
+
[declared graph](getting_started.md#more-than-one-schema) also carries a private `GRAPH` naming
|
|
772
|
+
it — so with two graphs, `graphql: :fake` fabricates each module's own
|
|
773
|
+
schema instead of having to be told which one you meant, and it is the
|
|
774
|
+
`:graph` on every [instrumentation event](logging.md#the-payload) the
|
|
775
|
+
module's `execute` produces.
|
|
570
776
|
|
|
571
777
|
## Deserializing a response from another client
|
|
572
778
|
|
|
@@ -593,7 +799,7 @@ keys** (`"person"`, `"nameWithOwner"`), the top-level `"data"` / `"errors"` /
|
|
|
593
799
|
|
|
594
800
|
Which is checked, since symbolizing is the likeliest thing to go wrong at this
|
|
595
801
|
seam: a hash carrying neither `"data"` nor `"errors"` raises a
|
|
596
|
-
`GraphWeaver::
|
|
802
|
+
`GraphWeaver::CastError` naming the keys it *did* find, rather than handing back
|
|
597
803
|
an envelope that reports success with no data. `nil` and a bare String are
|
|
598
804
|
refused the same way.
|
|
599
805
|
|
|
@@ -606,6 +812,16 @@ checking matters. `GraphWeaver.run(source, query, **variables)` — or
|
|
|
606
812
|
`client.run` — is the one-shot form: parse and execute in one call, no module
|
|
607
813
|
kept.
|
|
608
814
|
|
|
815
|
+
In an app with [more than one graph](getting_started.md#more-than-one-schema), a
|
|
816
|
+
parsed module belongs to one of them — that is what a `graphql:` tag runs it
|
|
817
|
+
against, the same thing generation bakes into a file. It is read off the schema
|
|
818
|
+
you parsed against when a graph runs that class in-process; say it outright
|
|
819
|
+
otherwise:
|
|
820
|
+
|
|
821
|
+
```ruby
|
|
822
|
+
PersonQuery = GraphWeaver.parse(schema: BILLING, query: "…", graph: :billing)
|
|
823
|
+
```
|
|
824
|
+
|
|
609
825
|
Generated source is eval'd, so inputs are validated: module names must be
|
|
610
826
|
constant names, and query heredocs can't be terminated early. Still: queries
|
|
611
827
|
are code — don't feed untrusted strings to parse.
|