graph_weaver 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1314 -0
- data/CLAUDE.md +100 -8
- data/DECISIONS.md +309 -0
- data/Gemfile.lock +23 -23
- data/NOTES.md +5 -5
- data/PLAN.md +106 -135
- data/README.md +115 -96
- data/REVIEW.md +946 -0
- data/docs/cassettes.md +75 -48
- data/docs/editors.md +82 -0
- data/docs/errors.md +32 -30
- data/docs/federation.md +520 -48
- data/docs/generated_modules.md +352 -137
- data/docs/getting_started.md +237 -67
- data/docs/logging.md +35 -6
- data/docs/real_world.md +21 -15
- data/docs/scalars.md +49 -154
- data/docs/testing.md +299 -52
- data/docs/transports.md +129 -30
- data/docs/upgrading.md +112 -0
- data/graph_weaver.gemspec +3 -1
- data/lib/generators/graph_weaver/install_generator.rb +259 -0
- data/lib/graph_weaver/client.rb +114 -111
- data/lib/graph_weaver/codegen/aliases.rb +217 -0
- data/lib/graph_weaver/codegen/emit.rb +272 -258
- data/lib/graph_weaver/codegen/enum_type.rb +27 -124
- data/lib/graph_weaver/codegen/nodes.rb +72 -13
- data/lib/graph_weaver/codegen/scalar_type.rb +68 -66
- data/lib/graph_weaver/codegen/type_helpers.rb +142 -0
- data/lib/graph_weaver/codegen.rb +593 -334
- data/lib/graph_weaver/errors.rb +127 -10
- data/lib/graph_weaver/federation.rb +272 -0
- data/lib/graph_weaver/hints.rb +9 -1
- data/lib/graph_weaver/in_process.rb +90 -0
- data/lib/graph_weaver/input_struct.rb +14 -2
- data/lib/graph_weaver/logging.rb +29 -0
- data/lib/graph_weaver/parsing.rb +67 -0
- data/lib/graph_weaver/query_module.rb +55 -0
- data/lib/graph_weaver/railtie.rb +23 -1
- data/lib/graph_weaver/representation.rb +74 -0
- data/lib/graph_weaver/response.rb +7 -0
- data/lib/graph_weaver/retry.rb +29 -8
- data/lib/graph_weaver/rspec.rb +214 -16
- data/lib/graph_weaver/schema_loader.rb +794 -59
- data/lib/graph_weaver/schemas.rb +46 -0
- data/lib/graph_weaver/selection.rb +43 -8
- data/lib/graph_weaver/tasks.rb +216 -21
- data/lib/graph_weaver/testing/cassette.rb +160 -61
- data/lib/graph_weaver/testing/coverage.rb +165 -0
- data/lib/graph_weaver/testing/failure.rb +10 -23
- data/lib/graph_weaver/testing/fake_client.rb +181 -21
- data/lib/graph_weaver/testing/fake_subgraph.rb +85 -0
- data/lib/graph_weaver/testing/router.rb +1431 -0
- data/lib/graph_weaver/testing/subgraphs.rb +130 -0
- data/lib/graph_weaver/testing.rb +204 -14
- data/lib/graph_weaver/transport/faraday.rb +28 -10
- data/lib/graph_weaver/transport/http.rb +99 -36
- data/lib/graph_weaver/transport.rb +67 -14
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +389 -170
- metadata +20 -3
data/PLAN.md
CHANGED
|
@@ -1,144 +1,115 @@
|
|
|
1
1
|
# Project Plan — GraphWeaver, typed GraphQL client for Ruby/Sorbet
|
|
2
2
|
|
|
3
|
-
_Resume-from-here notes
|
|
4
|
-
|
|
3
|
+
_Resume-from-here notes: where the project stands and what's next. The README
|
|
4
|
+
documents the product, CHANGELOG records what changed, DECISIONS.md records the
|
|
5
|
+
roads not taken, NOTES.md is the research notebook this grew out of. Update on
|
|
6
|
+
change._
|
|
5
7
|
|
|
6
8
|
## Vision
|
|
7
9
|
|
|
8
10
|
A "graphql-codegen for Ruby": `.graphql` queries + a schema (live class,
|
|
9
|
-
introspection JSON, or
|
|
10
|
-
T::
|
|
11
|
-
shape of every query result. Dynamic
|
|
12
|
-
step for CI
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Testing
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
11
|
+
introspection JSON, SDL, or an Apollo supergraph) → checked-in `# typed: strict`
|
|
12
|
+
Ruby — nested `T::Struct`s, generated casting, a typed `execute` — so `srb tc`
|
|
13
|
+
sees the exact shape of every query result. Dynamic mode for consoles, a build
|
|
14
|
+
step for CI. Runtime deps: `graphql` + `sorbet-runtime`, nothing else.
|
|
15
|
+
|
|
16
|
+
## State
|
|
17
|
+
|
|
18
|
+
`0.4.6` on RubyGems. `main` carries a large unreleased body of work headed for
|
|
19
|
+
**0.5.0** — see `## Unreleased` in the CHANGELOG, which is long and has a real
|
|
20
|
+
upgrade story to tell.
|
|
21
|
+
|
|
22
|
+
Green gate is in `CLAUDE.md`; `make check` runs the core of it.
|
|
23
|
+
|
|
24
|
+
**What's built**, in brief — the CHANGELOG has the detail:
|
|
25
|
+
|
|
26
|
+
- **Codegen.** Queries and mutations, typed variable kwargs, fragments (inline,
|
|
27
|
+
named, shared across queries), unions and interfaces (a struct per named
|
|
28
|
+
condition plus a forward-compatible `Other`), enums as `T::Enum`, custom
|
|
29
|
+
scalars, `@skip`/`@include` nullability. Generated class names derive from the
|
|
30
|
+
response key, so they're stable under unrelated edits. Shared types live once
|
|
31
|
+
per schema in `GraphQLTypes`.
|
|
32
|
+
- **Sources.** Live schema class, introspection JSON, SDL, Apollo supergraph
|
|
33
|
+
(composition machinery stripped, `@inaccessible` subtracted to the API schema —
|
|
34
|
+
verified identical to Apollo's own `toAPISchema`), and raw subgraph SDL.
|
|
35
|
+
- **Transports.** `Transport::HTTP` (zero-dep, pooled, keep-alive) by default;
|
|
36
|
+
Faraday on explicit opt-in. `InProcess` wraps a live schema class with
|
|
37
|
+
`context:`, logging and branded errors. Composable `Retry` honouring
|
|
38
|
+
`Retry-After`. One instrumentation seam covering both paths.
|
|
39
|
+
- **Errors.** A typed `Response` envelope, an error hierarchy split by failure
|
|
40
|
+
site, field-level reporting with entity ids, `schema_stale?`, `#to_h`
|
|
41
|
+
throughout.
|
|
42
|
+
- **Testing.** Schema-correct fakes, failure simulation, anonymizing cassettes,
|
|
43
|
+
and an in-process federation router that runs real subgraph resolvers —
|
|
44
|
+
verified against a real `@apollo/gateway` (42 identical, 1 refused, 0 wrong),
|
|
45
|
+
refusing at plan time anything it can't answer faithfully. One rspec tag picks
|
|
46
|
+
the mode: `graphql: :fake | :in_process | :router`.
|
|
47
|
+
- **Lifecycle.** `generate` / `verify` / `schema:refresh` / `schema:diff` /
|
|
48
|
+
`queries:check` / `federation:diff`, plus `rails g graph_weaver:install`.
|
|
49
|
+
|
|
50
|
+
## Next
|
|
51
|
+
|
|
52
|
+
1. **Cut 0.5.0.** Needs an upgrade guide rather than a changelog dump — the
|
|
53
|
+
breaking list is long, but most of it is caught mechanically, so the guide is
|
|
54
|
+
largely *"regenerate, then follow `srb tc` and `verify_generated!`"*.
|
|
55
|
+
`gem push` needs an OTP.
|
|
56
|
+
2. **`extend_type`'s mixin forms can't be statically checked.** A mixin's method
|
|
57
|
+
bodies are checked in the module's scope, not the struct's, so the docs have
|
|
58
|
+
to recommend `# typed: false` or `T.unsafe(self)`. In a library whose pitch is
|
|
59
|
+
static checking, that's a seam worth a design pass. Note `alias:` — which
|
|
60
|
+
emits into the struct body — *is* checked, which suggests the mixin forms are
|
|
61
|
+
the ones carrying the cost.
|
|
62
|
+
3. **Nice-to-haves, unclaimed.** `write_timeout` on `Transport::HTTP` (and
|
|
63
|
+
possibly a `net_http:` passthrough rather than more kwargs); a Tapioca DSL
|
|
64
|
+
compiler so dynamic `parse` modules get static types without the build step.
|
|
65
|
+
|
|
66
|
+
## Federation router: what it still refuses
|
|
67
|
+
|
|
68
|
+
Each refuses at plan time with the type, field, subgraphs and next action. The
|
|
69
|
+
cost of moving each boundary, if a real query mix ever demands it:
|
|
70
|
+
|
|
71
|
+
- **`@requires` needing a chain** — the prefetch's own key must come from the
|
|
72
|
+
subgraph in hand; needs a real dependency DAG.
|
|
73
|
+
- **An abstract type the supergraph doesn't break down** — a union or interface
|
|
74
|
+
at a boundary now plans, one branch per concrete type, bucketed on
|
|
75
|
+
`__typename` at execution. What is left is the supergraph that doesn't say
|
|
76
|
+
which concrete types a subgraph answers it with — no
|
|
77
|
+
`@join__unionMember`/`@join__implements`, and the type in more than one
|
|
78
|
+
subgraph. Closing it means reading a join version that predates those
|
|
79
|
+
directives; a modern composition always carries them.
|
|
80
|
+
- **A nested field set no one fetch can build** — a nested field set now
|
|
81
|
+
crosses as the object it is, to any depth. What is left is the one whose
|
|
82
|
+
fields are split across subgraphs (`origin` in one and `origin.lat` in
|
|
83
|
+
another, or a `@key`'s object a `@requires` would half-fill from
|
|
84
|
+
elsewhere): a representation comes from one fetch, so the object would
|
|
85
|
+
arrive in pieces. Closing it means merging the pieces, which
|
|
86
|
+
`DECISIONS.md` argues against — the shapes that produce a split are the
|
|
87
|
+
ones where a real gateway stops being an oracle.
|
|
88
|
+
- **Mutation root fields spanning subgraphs** — root mutation fields run in
|
|
89
|
+
series, so grouping them would run them in plan order.
|
|
90
|
+
- **An alias shadowing an injected `@key`** — Apollo resolves the collision in
|
|
91
|
+
favour of its own key and a spec-conformant server doesn't, so there is no one
|
|
92
|
+
answer to agree with. Unfixable by design.
|
|
93
|
+
|
|
94
|
+
`rake graph_weaver:federation:coverage` reports the refusal rate against a real
|
|
95
|
+
supergraph and query set. That number decides whether any of the above is worth
|
|
96
|
+
building — on the demo corpus it is 17/17.
|
|
97
|
+
|
|
98
|
+
## Stated non-goals
|
|
99
|
+
|
|
100
|
+
Recorded so they read as decisions rather than omissions, with the reasoning in
|
|
101
|
+
`REVIEW.md` §7: subscriptions, `@defer`/`@stream`, file uploads, normalized
|
|
102
|
+
caching, fragment masking, request batching, and a watch mode.
|
|
96
103
|
|
|
97
104
|
## Gotchas worth remembering
|
|
98
105
|
|
|
99
|
-
- graphql-ruby to_definition
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
table-driven (InputStruct runtime + FIELDS) — the PokeAPI filtered-query
|
|
110
|
-
module dropped 29,233 -> 11,562 lines. The floor is ~2 lines/field
|
|
111
|
-
(typed const + FIELDS entry). Remaining lever if it matters again:
|
|
112
|
-
shared input structs emitted once per schema instead of per module.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
A junior + senior agent pair exercised the repo cold (clone, examples,
|
|
116
|
-
extensions, a Pokedex app against Hasura's 4,441-type PokeAPI schema).
|
|
117
|
-
Fixed same-day: snake_case type names generated invalid constants
|
|
118
|
-
(camelize), GraphQL::ParseError/NotImplementedError escaping the Error
|
|
119
|
-
umbrella, HttpExecutor timeouts, GitHub's top-level "type" error codes,
|
|
120
|
-
typo'd client registrations silently no-oping. Still open:
|
|
121
|
-
|
|
122
|
-
~~Recursive input types~~ DONE 2026-07-12: register-before-walk breaks
|
|
123
|
-
the recursion; emission dependency-orders the structs and, for cycles,
|
|
124
|
-
forward-declares the classes in an eval (srb rejects reopening a
|
|
125
|
-
T::Struct statically, but adding props at runtime works — the full
|
|
126
|
-
bodies below the eval are all srb sees). Verified live against Hasura
|
|
127
|
-
bool_exp.
|
|
128
|
-
|
|
129
|
-
~~Connection reuse in Transport::HTTP~~ DONE 2026-07-12: persistent
|
|
130
|
-
keep-alive connection behind a mutex, dropped on any failure; net/http's
|
|
131
|
-
keep_alive_timeout handles idle expiry. Faraday remains the pooling
|
|
132
|
-
answer.
|
|
133
|
-
|
|
134
|
-
- Subscriptions; @defer/@stream (routers send multipart responses — the
|
|
135
|
-
transport classifies them as ServerError today, no incremental support).
|
|
136
|
-
- Shared input-type structs across generated modules: one Hasura
|
|
137
|
-
bool_exp variable pulls its whole recursive closure into EVERY module
|
|
138
|
-
(~28k lines each) — correct but heavy in PRs; needs cross-module
|
|
139
|
-
sharing or selection-based pruning (field-test round 2).
|
|
140
|
-
- Structured logging: log_tag pairs lines and names operations now, but
|
|
141
|
-
events are prose — an optional {event:, url:, ms:} payload contract, a
|
|
142
|
-
scrub_variables hook, and cache-age on hit lines (field-test round 2).
|
|
143
|
-
- Cut 0.1.1 — RubyGems 0.1.0 is materially behind main (snake_case fix,
|
|
144
|
-
recursive inputs, keep-alive, logging, error umbrella).
|
|
106
|
+
- graphql-ruby's `to_definition`/`from_introspection` reorder enum values and
|
|
107
|
+
possible types — codegen sorts both; keep any new emission deterministic.
|
|
108
|
+
- Schemas built from introspection or SDL have no scalar coercion or resolvers,
|
|
109
|
+
so codegen stays name-keyed and never calls schema runtime hooks.
|
|
110
|
+
- A `SchemaDefinition` node reprints without its body when the root type names
|
|
111
|
+
are the GraphQL defaults, so directives on `schema` must be stripped before
|
|
112
|
+
reprinting a supergraph.
|
|
113
|
+
- Code the build doesn't exercise rots silently — integration specs excluded from
|
|
114
|
+
the default run, examples the generator skips, doc samples nobody executes.
|
|
115
|
+
Six fabricated doc samples were found in one session by running them.
|
data/README.md
CHANGED
|
@@ -3,14 +3,19 @@ GraphWeaver
|
|
|
3
3
|

|
|
4
4
|
[](https://codecov.io/gh/dpep/graph_weaver)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
**Your `.graphql` files, compiled into Sorbet types — and the fakes to test them.**
|
|
7
7
|
|
|
8
|
-
GraphWeaver
|
|
8
|
+
GraphWeaver is graphql-codegen for Ruby. Write a query as a `.graphql` file and it
|
|
9
|
+
generates checked-in `# typed: strict` Ruby — nested `T::Struct`s, casting, a typed
|
|
10
|
+
`execute` — so `srb tc` knows the exact shape of every result. The schema can be a
|
|
11
|
+
live graphql-ruby class, an introspection dump, SDL, or an Apollo supergraph;
|
|
12
|
+
at runtime the only dependencies are `graphql` and `sorbet-runtime`.
|
|
9
13
|
|
|
10
14
|
```graphql
|
|
11
|
-
# queries/person.graphql
|
|
15
|
+
# app/graphql/queries/person.graphql
|
|
12
16
|
query($id: ID!) {
|
|
13
17
|
person(id: $id) {
|
|
18
|
+
id
|
|
14
19
|
name
|
|
15
20
|
birthday
|
|
16
21
|
pets { name }
|
|
@@ -18,132 +23,146 @@ query($id: ID!) {
|
|
|
18
23
|
}
|
|
19
24
|
```
|
|
20
25
|
|
|
26
|
+
`rake graph_weaver:generate` turns that file into a `PersonQuery` module, and what
|
|
27
|
+
comes back is a struct rather than a Hash you have to trust:
|
|
28
|
+
|
|
21
29
|
```ruby
|
|
22
|
-
result = PersonQuery.execute!(id: "1") #
|
|
30
|
+
result = PersonQuery.execute!(id: "1") # or #execute, for the Response envelope
|
|
23
31
|
|
|
24
|
-
result.person&.name
|
|
25
|
-
result.person&.birthday
|
|
26
|
-
result.person&.
|
|
27
|
-
```
|
|
32
|
+
result.person&.name # => "Daniel"
|
|
33
|
+
result.person&.birthday # => #<Date: 1990-06-15> custom scalars deserialize
|
|
34
|
+
result.person&.pets&.map(&:name) # => ["Shelby", "Brownie"]
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pagination, a block-built type helper), and `examples/github/run.rb`
|
|
34
|
-
(auth + checked-in generated modules; it stars this repo ⭐ and introduces
|
|
35
|
-
you to your fellow stargazers).
|
|
36
|
+
result.person&.nmae
|
|
37
|
+
# srb tc: Method `nmae` does not exist on `PersonQuery::Result::Person`
|
|
38
|
+
# Did you mean `name`?
|
|
39
|
+
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
`person` is `T.nilable` because the schema says the field is nullable — the `&.`
|
|
42
|
+
isn't defensive, it's the schema talking. A field you misspelled, or never
|
|
43
|
+
selected, is a typecheck error rather than a `NoMethodError` in production.
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **Any schema source**: live schema class, introspection JSON, or SDL — including Apollo Federation supergraph SDL; introspect live endpoints with caching
|
|
42
|
-
- **Any transport**: in-process schema execution, the zero-dependency HTTP executor, or Faraday with your own middleware — plus a composable `Retry` (exponential/linear/custom backoff, jitter, retry-by-error-class or GraphQL code) — swap per call with `executor:`
|
|
43
|
-
- **Structured errors**: a typed response envelope (partial data + extensions survive), an error hierarchy split by failure site, field-level reports with entity ids, and `schema_stale?` detection — every error dual-surfaced as a human message plus JSON-ready `#to_h`
|
|
44
|
-
- **Testing built in**: schema-correct fakes, failure simulation, record/replay cassettes with anonymization, rspec integration
|
|
45
|
-
- **Dynamic mode** for development: `GraphWeaver.parse(...)` generates and evals on the fly, no build step
|
|
45
|
+
Typed structs are the part every generator gets right. What decides whether you're
|
|
46
|
+
still happy six months in is everything around them.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
## Precise types are expensive to fake, so it fakes them for you
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
Generation makes result types exact, which makes them tedious to build by hand —
|
|
51
|
+
and most generators stop there and leave you the fixtures. GraphWeaver ships the
|
|
52
|
+
fabricator. One line in the spec helper:
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| Development, consoles | `client.parse` / `client.load_queries!` — no build step |
|
|
55
|
-
| Scripts, one-offs | `client.execute!` — no module at all |
|
|
54
|
+
```ruby
|
|
55
|
+
require "graph_weaver/rspec"
|
|
56
|
+
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
the pieces:
|
|
58
|
+
then one tag says what an example runs against:
|
|
59
59
|
|
|
60
60
|
```ruby
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# make it the app default — generated modules execute through it
|
|
69
|
-
GraphWeaver.client = api
|
|
70
|
-
|
|
71
|
-
# generate checked-in typed modules (rake graph_weaver:generate, or directly)
|
|
72
|
-
source = GraphWeaver::Codegen.generate(
|
|
73
|
-
schema: api.schema,
|
|
74
|
-
query: File.read("queries/person.graphql"),
|
|
75
|
-
module_name: "PersonQuery",
|
|
76
|
-
)
|
|
77
|
-
File.write("app/queries/person_query.rb", source)
|
|
78
|
-
|
|
79
|
-
# at runtime
|
|
80
|
-
PersonQuery.execute(id: "1") # via GraphWeaver.client
|
|
81
|
-
PersonQuery.execute(other_client, id: "1") # or per call
|
|
61
|
+
it "shows the profile", graphql: :fake do
|
|
62
|
+
person = PersonQuery.execute!(id: "1").person
|
|
63
|
+
|
|
64
|
+
person.name # => "Shakita Stark" fabricated from your schema
|
|
65
|
+
person.birthday # => #<Date: 2024-12-16> custom scalars included
|
|
66
|
+
person.pets.size # => 2
|
|
67
|
+
end
|
|
82
68
|
```
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
middleware blocks and ready connections in [transports](docs/transports.md).
|
|
70
|
+
No fixture, no stub, no HTTP — and the values are seeded from rspec's own seed, so
|
|
71
|
+
`--seed 4242` hands back that same person and a failure reproduces. The tag also
|
|
72
|
+
picks a *real* client when you want one: `:in_process` runs your resolvers,
|
|
73
|
+
`:router` runs them across a federated graph. Field-level failure simulation and
|
|
74
|
+
record/replay cassettes with anonymization are in [testing](docs/testing.md).
|
|
90
75
|
|
|
91
|
-
|
|
92
|
-
carry the client's transport, no global wiring needed:
|
|
76
|
+
## Federation without a gateway
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
When your app is both a GraphQL client and a subgraph, the local router plans a
|
|
79
|
+
query across the composed supergraph and runs your **real resolvers** over the
|
|
80
|
+
boundary — no gateway process, no node, no sockets. That's
|
|
81
|
+
[`examples/federation.rb`](examples/federation.rb), the example that needs no network:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
$ bundle exec examples/federation.rb
|
|
85
|
+
#<GraphWeaver::Testing::Router subgraphs=["accounts", "products", "reviews"]>
|
|
98
86
|
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
dpep reviewed 2 products:
|
|
88
|
+
Table ($899) — Love it
|
|
89
|
+
Couch ($1299) — Too expensive
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
fetches:
|
|
92
|
+
→ accounts root fields
|
|
93
|
+
→ reviews _entities × 1 User
|
|
94
|
+
→ products _entities × 2 Product
|
|
104
95
|
```
|
|
105
96
|
|
|
97
|
+
The trace is the query plan: every node at a level in one `_entities` call, so two
|
|
98
|
+
products cost one fetch. Anything it can't answer *faithfully* it refuses at plan
|
|
99
|
+
time rather than guessing — and it's diffed against a real `@apollo/gateway` over
|
|
100
|
+
the same supergraph, currently 42 queries identical, 1 refused, 0 wrong
|
|
101
|
+
([`spec/integration/router_parity_spec.rb`](spec/integration/router_parity_spec.rb)).
|
|
102
|
+
See [federation](docs/federation.md).
|
|
106
103
|
|
|
107
|
-
|
|
104
|
+
## The schema keeps itself honest
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
introspect a live endpoint (GitHub end to end), dynamic mode, schema caching
|
|
116
|
-
- **[Federation](docs/federation.md)** — Apollo Federation: supergraph vs API
|
|
117
|
-
schema, feeding weaver a composed graph, the `@inaccessible` caveat
|
|
118
|
-
- **[Transports](docs/transports.md)** — clients, the execute contract,
|
|
119
|
-
Faraday, retries and backoff
|
|
120
|
-
- **[Custom scalars](docs/scalars.md)** — the registry: codec inference,
|
|
121
|
-
requires, input coercion
|
|
122
|
-
- **[Errors](docs/errors.md)** — the Response envelope, the error hierarchy,
|
|
123
|
-
field-level reports with entity ids, stale-schema detection
|
|
124
|
-
- **[Logging](docs/logging.md)** — point `GraphWeaver.logger` at any Logger:
|
|
125
|
-
wire traffic at debug, introspection/cache/codegen at info, errors at warn
|
|
126
|
-
- **[Testing](docs/testing.md)** — schema-correct fakes, failure simulation,
|
|
127
|
-
rspec integration
|
|
128
|
-
- **[Cassettes](docs/cassettes.md)** — capture and replay real API
|
|
129
|
-
responses; anonymized recording (`GRAPHWEAVER_RECORD=1`, rake tasks)
|
|
106
|
+
The lifecycle is rake tasks, not a CI pipeline you assemble yourself:
|
|
107
|
+
`schema:refresh` re-introspects the committed dump, `schema:diff` fails when the
|
|
108
|
+
server has drifted, `queries:check` names the queries that drift broke and where,
|
|
109
|
+
and `verify` fails when the checked-in Ruby is stale. Generation is deterministic
|
|
110
|
+
— same schema and queries, byte-identical files — so regenerating never shows a
|
|
111
|
+
diff you didn't earn. See [getting started](docs/getting_started.md#5-verify-in-ci).
|
|
130
112
|
|
|
131
|
-
|
|
132
|
-
## Installation
|
|
113
|
+
## Start here
|
|
133
114
|
|
|
134
115
|
```ruby
|
|
135
116
|
# Gemfile
|
|
136
117
|
gem "graph_weaver"
|
|
137
118
|
```
|
|
138
119
|
|
|
139
|
-
|
|
120
|
+
In Rails, setup is then one command:
|
|
140
121
|
|
|
141
122
|
```sh
|
|
142
|
-
|
|
123
|
+
rails g graph_weaver:install https://api.example.com/graphql
|
|
143
124
|
```
|
|
144
125
|
|
|
126
|
+
which writes the initializer, the `app/graphql` layout, the editor config and the
|
|
127
|
+
schema dump. **[Getting started](docs/getting_started.md)** walks the production
|
|
128
|
+
setup end to end. Or skip the build step entirely and poke at an API from a
|
|
129
|
+
console — anything holding a schema parses, and the module runs on what parsed it:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
api = GraphWeaver.new("https://countries.trevorblades.com/")
|
|
133
|
+
CountryQuery = api.parse("queries/country.graphql") # a path or a raw string
|
|
134
|
+
CountryQuery.execute!(code: "JP").country&.capital # => "Tokyo"
|
|
135
|
+
|
|
136
|
+
api.run!("query { continents { name } }").continents # or no module at all
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The **[examples](examples/)** run that path for real, smallest first: a public API
|
|
140
|
+
in 30 lines, a paginated search, the production path against GitHub, and the
|
|
141
|
+
federated graph above.
|
|
142
|
+
|
|
143
|
+
#### Also in the box
|
|
144
|
+
|
|
145
|
+
- **Queries and mutations** with typed variable kwargs — enums as `T::Enum`s, input objects as `T::Struct`s, required vs optional falling out of nullability and defaults
|
|
146
|
+
- **Fragments** (inline, named, type conditions), **unions and interfaces** (member structs, `__typename` dispatch), `@skip`/`@include` nullability
|
|
147
|
+
- **Any transport**: in-process execution, a zero-dependency HTTP client, or Faraday with your own middleware — plus a composable `Retry` with backoff and jitter
|
|
148
|
+
- **Structured errors**: a typed envelope that keeps partial data and extensions, an error hierarchy split by failure site, field-level reports with entity ids, and stale-schema detection
|
|
149
|
+
|
|
150
|
+
#### Dig deeper
|
|
151
|
+
|
|
152
|
+
- **[Getting started](docs/getting_started.md)** — the production path in Rails, step by step
|
|
153
|
+
- **[Generated modules](docs/generated_modules.md)** — module anatomy, typed variables, fragments/unions/interfaces, naming, clients, dynamic mode
|
|
154
|
+
- **[Testing](docs/testing.md)** — fakes, failure simulation, the rspec tags
|
|
155
|
+
- **[Federation](docs/federation.md)** — supergraph vs API schema, the local router, what it refuses
|
|
156
|
+
- **[Transports](docs/transports.md)** — the execute contract, Faraday, retries and backoff
|
|
157
|
+
- **[Errors](docs/errors.md)** — the Response envelope, the error hierarchy, field-level reports
|
|
158
|
+
- **[Custom scalars](docs/scalars.md)** — the registry: codec inference, requires, input coercion
|
|
159
|
+
- **[Cassettes](docs/cassettes.md)** — capture and replay real responses, anonymized
|
|
160
|
+
- **[Editor support](docs/editors.md)** — five lines of YAML for schema autocomplete in `.graphql` files, no JS project
|
|
161
|
+
- **[Against a real API](docs/real_world.md)** — introspecting a live endpoint, GitHub end to end
|
|
162
|
+
- **[Logging](docs/logging.md)** — point `GraphWeaver.logger` at any Logger
|
|
163
|
+
|
|
145
164
|
----
|
|
146
165
|
## Development
|
|
147
166
|
|
|
148
167
|
- `make check` — regenerate spec fixtures, run specs, typecheck
|
|
149
|
-
- `make integration` — one-off checks against
|
|
168
|
+
- `make integration` — one-off checks against live APIs (GitHub needs a token) and a federation gateway (needs node)
|