graph_weaver 0.4.0 → 0.4.4
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 +39 -0
- data/CLAUDE.md +1 -2
- data/Gemfile.lock +3 -3
- data/docs/federation.md +36 -58
- data/docs/generated_modules.md +33 -0
- data/docs/scalars.md +52 -9
- data/graph_weaver.gemspec +3 -1
- data/lib/graph_weaver/client.rb +4 -3
- data/lib/graph_weaver/codegen/emit.rb +10 -1
- data/lib/graph_weaver/codegen/enum_type.rb +37 -5
- data/lib/graph_weaver/codegen/nodes.rb +7 -3
- data/lib/graph_weaver/codegen.rb +63 -0
- data/lib/graph_weaver/schema_loader.rb +134 -2
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +8 -9
- metadata +3 -4
- data/lib/graph_weaver/directive_defaults_patch.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fac36591b869f046295946094078847ed33a1aa15aa8a2402e6a46f6e17ebaa0
|
|
4
|
+
data.tar.gz: c366d845da86029839f57214ba6216b78f2a870b67873851c2514d3d00aaa78d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37c814f83659a3189407b4c8e4531db4782fc4f38cd0e62df4f8e473a75c713adc3d0bb43de62a51854c1d6f0aeea1195e89848e83bd61c74018a840bfe56924
|
|
7
|
+
data.tar.gz: 787508b524cb027fb31281379f64bc566b1bb23709a79883928005ced38cf478c6c90b62a09d2de66317a6d7415eea77014667a5f543472a6f42d8ad043634de
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
### v0.4.4 (2026-07-30)
|
|
2
|
+
- Supergraph loading now derives the **API schema**: `@inaccessible` elements
|
|
3
|
+
(present in the federated graph but hidden from what the router serves) are
|
|
4
|
+
removed on load, cascading — a field/argument/union-member/interface
|
|
5
|
+
referencing a removed type goes too, and a type left empty is removed in turn.
|
|
6
|
+
So codegen validates against exactly what clients can query, with no
|
|
7
|
+
over-permitting and no Apollo JS tooling to subtract the API schema first.
|
|
8
|
+
Plain (non-federation) SDL is untouched.
|
|
9
|
+
|
|
10
|
+
### v0.4.3 (2026-07-30)
|
|
11
|
+
- Federation-aware supergraph loading: `SchemaLoader` (and `Client.new`) now
|
|
12
|
+
load a composed Apollo Federation v2 supergraph SDL directly. When the `@join__*`
|
|
13
|
+
markers are present it strips the composition machinery — the synthetic
|
|
14
|
+
`join__*`/`link__*` types and directive definitions, and every `@join__*`/`@link`
|
|
15
|
+
application — via an AST rewrite before `from_definition`, so the merged type
|
|
16
|
+
shapes load cleanly with nothing federation-internal leaking into
|
|
17
|
+
`schema.types`. Plain SDL is untouched. A query can now be typed against the
|
|
18
|
+
composed supergraph, not just per-subgraph schema objects.
|
|
19
|
+
- Removed `directive_defaults_patch.rb` (the graphql-ruby monkeypatch); the
|
|
20
|
+
preprocessor supersedes it and `graphql-ruby` fixed the underlying issue. The
|
|
21
|
+
gem now requires `graphql >= 2.6.7`.
|
|
22
|
+
|
|
23
|
+
### v0.4.2 (2026-07-30)
|
|
24
|
+
- `extend_type` accepts `alias:` — project a selected field (possibly nested)
|
|
25
|
+
onto a flat, typed accessor emitted into the struct body:
|
|
26
|
+
`extend_type("Widget", alias: { tag: "meta.tag" })` generates a sig'd
|
|
27
|
+
`def tag = meta&.tag`. Retires hand-written value objects that only flattened
|
|
28
|
+
a passthrough field. Takes a `{ name => path }` hash, a bare path string
|
|
29
|
+
(accessor named after the last segment), or an array of paths. Typed from the
|
|
30
|
+
selection — a nullable hop makes the accessor nilable and nil-safe; the leaf
|
|
31
|
+
may be a scalar, enum, or nested struct. Validated per query at generation: an
|
|
32
|
+
unselected/misspelled segment (with `did you mean`), a list hop, or a name
|
|
33
|
+
collision raises. Stacks and is client-scopable like the mixin forms.
|
|
34
|
+
|
|
35
|
+
### v0.4.1 (2026-07-29)
|
|
36
|
+
- Generated `execute!` forwards its kwargs to `execute` via hash shorthand
|
|
37
|
+
(`execute(client, name:, species:)` rather than `name: name, species: species`)
|
|
38
|
+
— cosmetic only. Regenerate to refresh (`verify` flags the drift otherwise).
|
|
39
|
+
|
|
1
40
|
### v0.4.0 (2026-07-28)
|
|
2
41
|
- Shared unions: when a named shared fragment is the whole selection on a union
|
|
3
42
|
field (`feed { ...FeedItemFields }`), its type is hoisted once into a
|
data/CLAUDE.md
CHANGED
|
@@ -26,8 +26,7 @@ where it pays off in developer experience; leave the rest at `# typed: true`.
|
|
|
26
26
|
shape without catching anything. **Don't promote these to strict.**
|
|
27
27
|
- Rule of thumb: if a sig would be mostly `T.untyped`, it isn't worth writing.
|
|
28
28
|
Concrete types = value; `T.untyped` sigs = paperwork.
|
|
29
|
-
- `railtie.rb` / `tasks.rb` are `# typed: ignore` (Rails/Rake DSL)
|
|
30
|
-
`directive_defaults_patch.rb` is `# typed: false` (a prepended monkeypatch).
|
|
29
|
+
- `railtie.rb` / `tasks.rb` are `# typed: ignore` (Rails/Rake DSL).
|
|
31
30
|
|
|
32
31
|
## Design invariants (don't "fix" these)
|
|
33
32
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
graph_weaver (0.4.
|
|
5
|
-
graphql (>= 2)
|
|
4
|
+
graph_weaver (0.4.4)
|
|
5
|
+
graphql (>= 2.6.7)
|
|
6
6
|
sorbet-runtime
|
|
7
7
|
|
|
8
8
|
GEM
|
|
@@ -176,7 +176,7 @@ CHECKSUMS
|
|
|
176
176
|
google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b
|
|
177
177
|
google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee
|
|
178
178
|
google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd
|
|
179
|
-
graph_weaver (0.4.
|
|
179
|
+
graph_weaver (0.4.4)
|
|
180
180
|
graphql (2.6.7) sha256=759755ce5819c965b6459c5039605cf2e4d876bb8ba5e03451676870ea787c45
|
|
181
181
|
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
|
182
182
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
data/docs/federation.md
CHANGED
|
@@ -20,17 +20,20 @@ URL to introspect the API schema live.
|
|
|
20
20
|
|
|
21
21
|
## Pointing weaver at a supergraph
|
|
22
22
|
|
|
23
|
-
A supergraph SDL works as-is.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
A supergraph SDL works as-is. When `SchemaLoader` sees the `@join__*` markers it
|
|
24
|
+
strips the composition machinery before building the schema — the synthetic
|
|
25
|
+
`join__*`/`link__*` types and directive definitions, and every `@join__*`/`@link`
|
|
26
|
+
application on the real types — so what codegen sees is the merged graph's
|
|
27
|
+
ordinary type shapes, with no federation plumbing leaking into `schema.types`.
|
|
28
|
+
(It's a pure AST rewrite of the SDL; no graphql-ruby monkeypatch, and plain
|
|
29
|
+
schemas pass through untouched.) Field shapes — nullability, args, enums,
|
|
30
|
+
inputs — are identical to the API schema, so your generated structs are correct;
|
|
31
|
+
and because codegen is **query-driven**, nothing federation-internal could
|
|
32
|
+
generate code anyway.
|
|
33
|
+
|
|
34
|
+
A supergraph is a **superset** of the API schema — it carries elements the
|
|
35
|
+
public API hides, marked `@inaccessible`. Weaver removes those on load (below),
|
|
36
|
+
so the schema it generates against is the API schema, not the superset.
|
|
34
37
|
|
|
35
38
|
### `@inaccessible`
|
|
36
39
|
|
|
@@ -44,57 +47,32 @@ It's a fed-v2 feature — common in mature, multi-team graphs with lots of share
|
|
|
44
47
|
types, rare in small or young ones, and targeted where present (a handful of
|
|
45
48
|
elements, not every field).
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
Weaver derives the API schema from the supergraph for you: loading strips every
|
|
51
|
+
`@inaccessible` element and cascades — a field/argument/union-member/interface
|
|
52
|
+
referencing a removed type goes too, and a type left empty is removed in turn —
|
|
53
|
+
so codegen validates against **exactly** what clients can query. There's no
|
|
54
|
+
over-permit gap, and no need for Apollo's JS tooling (`@apollo/federation-internals`)
|
|
55
|
+
to subtract the API schema first; feed weaver the raw supergraph and you get the
|
|
56
|
+
router's contract. (This is a pure SDL rewrite at load time — see
|
|
57
|
+
[`SchemaLoader`](../lib/graph_weaver/schema_loader.rb).)
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
Other federation directives hide nothing from the schema, so weaver keeps the
|
|
60
|
+
field and ignores the directive: `@requiresScopes` / `@policy` / `@authenticated`
|
|
61
|
+
enforce access at runtime; `@tag` / `@requires` / `@provides` / `@external` are
|
|
62
|
+
metadata.
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
grep -c '@inaccessible' supergraph.graphql
|
|
57
|
-
```
|
|
64
|
+
## Which schema to feed
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
directly.
|
|
66
|
+
Any of these — they all produce the same generated code:
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## The exact contract: the API schema
|
|
68
|
-
|
|
69
|
-
To make codegen match the router precisely (no over-permit), feed weaver the
|
|
70
|
-
**API schema** instead of the raw supergraph. Deriving it is a *subtraction*
|
|
71
|
-
from the composed graph — not composition — done with Apollo's tooling:
|
|
72
|
-
|
|
73
|
-
```sh
|
|
74
|
-
rover supergraph compose --config supergraph.yaml > supergraph.graphql
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
```js
|
|
78
|
-
// then, in JS, strip to the API schema:
|
|
79
|
-
import { Supergraph } from '@apollo/federation-internals';
|
|
80
|
-
import { printSchema } from 'graphql';
|
|
81
|
-
|
|
82
|
-
const api = Supergraph.fromString(supergraphSdl).apiSchema().toGraphQLJSSchema();
|
|
83
|
-
process.stdout.write(printSchema(api)); // api.graphql — feed this to weaver
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Check the API SDL in (many teams already emit it in CI) and point weaver at it:
|
|
87
|
-
codegen then validates against exactly what clients can query, with no live
|
|
88
|
-
gateway involved.
|
|
89
|
-
|
|
90
|
-
## Which to use
|
|
91
|
-
|
|
92
|
-
- **No `@inaccessible`** → point weaver at the supergraph and move on.
|
|
93
|
-
- **Uses `@inaccessible`, and you want codegen to catch hidden-field mistakes** →
|
|
94
|
-
feed the derived API schema (above), or introspect the live router.
|
|
68
|
+
- **The supergraph SDL** — weaver strips the `@join__*`/`@link` machinery and
|
|
69
|
+
derives the API schema (removing `@inaccessible`). The common case.
|
|
70
|
+
- **The API schema SDL** — already subtracted (e.g. emitted in your CI); loads
|
|
71
|
+
as an ordinary schema.
|
|
72
|
+
- **The live router** — introspect it; it already serves the API schema.
|
|
95
73
|
|
|
96
74
|
A large real supergraph carries more constructs than a toy one (interface
|
|
97
75
|
objects via `@join__type(isInterfaceObject:)`, `@join__unionMember`, enum join
|
|
98
|
-
directives). The
|
|
99
|
-
your actual composed schema plus a couple of representative
|
|
100
|
-
relying on it.
|
|
76
|
+
directives). The stripping holds across them, but the honest check is to run
|
|
77
|
+
codegen against your actual composed schema plus a couple of representative
|
|
78
|
+
queries before relying on it.
|
data/docs/generated_modules.md
CHANGED
|
@@ -270,6 +270,39 @@ mode) the struct raises a NoMethodError naming the prop that does exist —
|
|
|
270
270
|
`use 'name_with_owner'` for the exact wire name, `did you mean ...?` for
|
|
271
271
|
a near-miss typo in either casing.
|
|
272
272
|
|
|
273
|
+
### Consuming a union — dispatch on the class, not `__typename`
|
|
274
|
+
|
|
275
|
+
`from_h` already reads `__typename` off the wire and builds the right member
|
|
276
|
+
struct, so what you hold is a real `Book` or `Disc`, not a tag. Branch on the
|
|
277
|
+
class and let Sorbet do the rest:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
items.each do |item| # item : T.any(Result::Item::Book, Result::Item::Disc)
|
|
281
|
+
case item
|
|
282
|
+
when Result::Item::Book then item.title # narrowed to Book — .title is available
|
|
283
|
+
when Result::Item::Disc then item.runtime # narrowed to Disc — .runtime is available
|
|
284
|
+
else T.absurd(item)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Two things a `case item.__typename` on the string can't give you. `when Book`
|
|
290
|
+
*narrows*: inside the branch `item` is statically a `Book`, so its fields
|
|
291
|
+
typecheck (a `Disc` field would be a compile error) — a string value narrows
|
|
292
|
+
nothing. And after every member, the `T.any` is exhausted, so `T.absurd` asserts
|
|
293
|
+
the `else` is unreachable: add a member to the union, regenerate, and the
|
|
294
|
+
`T.absurd` stops compiling until you handle it. Dispatching on the string tag
|
|
295
|
+
gets you neither — mistakes and schema growth fall through to a runtime raise.
|
|
296
|
+
|
|
297
|
+
`__typename` is still there as a plain `String` if you want the raw tag, but you
|
|
298
|
+
rarely need it to dispatch. Its one real use is the case the class can't cover:
|
|
299
|
+
two *differently-selected* occurrences of the same union are distinct type
|
|
300
|
+
families (`Result::Item::Book` is not `Result::FeaturedItem::Book`), so a `case`
|
|
301
|
+
written for one won't span the other. To hold "the same union" as one type
|
|
302
|
+
across queries, select it through a shared fragment
|
|
303
|
+
([shared unions](#selections)); if all you have is the bare tag, `__typename` is
|
|
304
|
+
the common denominator (but an unchecked one).
|
|
305
|
+
|
|
273
306
|
## Naming
|
|
274
307
|
|
|
275
308
|
Module names derive from the operation name (`query GetPerson` → `GetPerson`);
|
data/docs/scalars.md
CHANGED
|
@@ -174,12 +174,18 @@ pet.display_name # => "Shelby 🦴"
|
|
|
174
174
|
pet.name # => "Shelby" — the wire value stays honest
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
The methods live on the struct, so they see its wire fields at runtime and
|
|
178
|
+
fakes/cassettes get the behavior automatically; registrations are additive
|
|
179
|
+
(global plus client-scoped stack). One caveat on *static* typing, though:
|
|
180
|
+
`srb tc` checks a mixin's method bodies in the module's own scope, not the
|
|
181
|
+
including struct's — so a helper that reads a wire field (`name`, `birthday`)
|
|
182
|
+
doesn't resolve it and fails with "method does not exist on the module." Write
|
|
183
|
+
such a helper at `# typed: false`, or reach the field through `T.unsafe(self)`
|
|
184
|
+
— either way its body isn't statically checked against the selection. (Sorbet's
|
|
185
|
+
`requires_ancestor` is the escape in principle, but it needs an experimental
|
|
186
|
+
flag and a concrete ancestor, which a per-query struct isn't.) The only place a
|
|
187
|
+
field-reading derivation type-checks natively is *inside* the struct body, where
|
|
188
|
+
the field is in scope — which is codegen's job, not a mixin's.
|
|
183
189
|
|
|
184
190
|
For quick decoration, build the mixin inline — the block is
|
|
185
191
|
`module_eval`'d into a fresh module auto-named under
|
|
@@ -191,6 +197,43 @@ api.extend_type("Pet") do
|
|
|
191
197
|
end
|
|
192
198
|
```
|
|
193
199
|
|
|
194
|
-
Same runtime behavior,
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
Same runtime behavior, less static reach: the block becomes a runtime module
|
|
201
|
+
with no source on disk, so `srb tc` can't see its methods at all — fine in
|
|
202
|
+
dynamic `parse`, but in a checked-in `# typed: strict` file it's an unresolved
|
|
203
|
+
reference. Prefer a named module (and mind the field-access caveat above) where
|
|
204
|
+
static checking matters — complexity on demand.
|
|
205
|
+
|
|
206
|
+
### Flat accessors with `alias:`
|
|
207
|
+
|
|
208
|
+
The one derivation the generator can type for you is a plain projection — a
|
|
209
|
+
selected field, possibly nested, exposed under a flat accessor. `alias:` emits a
|
|
210
|
+
sig'd delegator *into the struct body*, where the field is in scope, so it's
|
|
211
|
+
fully checked (the thing a mixin can't be):
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
GraphWeaver.extend_type("Widget", alias: { tag: "meta.tag" })
|
|
215
|
+
|
|
216
|
+
# generated on the Widget struct:
|
|
217
|
+
# sig { returns(T.nilable(String)) }
|
|
218
|
+
# def tag = meta&.tag
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
So a hand-written value object that only existed to expose `tag` flat over
|
|
222
|
+
`data.dig("meta", "tag")` drops away — the generated struct answers `.tag`
|
|
223
|
+
directly. Forms:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
alias: { tag: "meta.tag" } # explicit accessor name
|
|
227
|
+
alias: "meta.tag" # accessor named after the last segment (`tag`)
|
|
228
|
+
alias: ["meta.tag", "meta.color"] # several at once
|
|
229
|
+
alias: { label: "name", tag: "meta.tag" }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The path is the Ruby accessor chain (`meta.tag`), typed from the selection: any
|
|
233
|
+
nullable hop makes the accessor nilable and inserts `&.`; the leaf can be a
|
|
234
|
+
scalar, enum, or nested struct. It's validated against each query at generation —
|
|
235
|
+
an unselected or misspelled segment (`did you mean 'tag'?`), a path through a
|
|
236
|
+
list, or a name that collides with a real field all fail with a pointed error.
|
|
237
|
+
Registrations stack and are client-scopable, like the mixin forms. For anything
|
|
238
|
+
beyond a passthrough projection — real logic, still typed — reopen the generated
|
|
239
|
+
struct in your own file and add sig'd methods; Sorbet merges the bodies.
|
data/graph_weaver.gemspec
CHANGED
|
@@ -14,7 +14,9 @@ Gem::Specification.new do |s|
|
|
|
14
14
|
|
|
15
15
|
s.required_ruby_version = ">= 3.3"
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# 2.6.7 fills defaulted directive arguments when building from SDL
|
|
18
|
+
# (rmosolgo/graphql-ruby#5659) — needed since we dropped our own patch for it
|
|
19
|
+
s.add_dependency "graphql", ">= 2.6.7"
|
|
18
20
|
s.add_dependency "sorbet-runtime"
|
|
19
21
|
|
|
20
22
|
s.add_development_dependency "apollo-federation" # federation integration subgraphs
|
data/lib/graph_weaver/client.rb
CHANGED
|
@@ -103,10 +103,11 @@ class GraphWeaver::Client
|
|
|
103
103
|
# struct this client generates from the named GraphQL type — pass
|
|
104
104
|
# modules, or a block to build one inline. Additive with global
|
|
105
105
|
# registrations (see GraphWeaver.extend_type).
|
|
106
|
-
def extend_type(graphql_name, *mixins, requires: nil, &block)
|
|
106
|
+
def extend_type(graphql_name, *mixins, requires: nil, **kw, &block)
|
|
107
107
|
validate_registration!("type", graphql_name.to_s)
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
aliases = GraphWeaver::Codegen.take_aliases(kw)
|
|
109
|
+
entry = @types[graphql_name.to_s] ||= { mixins: [], requires: [], aliases: {} }
|
|
110
|
+
GraphWeaver::Codegen.add_type_helpers(entry, graphql_name, mixins, requires, block, aliases)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
# Parse a query (a .graphql path or raw string) into a typed module
|
|
@@ -349,6 +349,14 @@ class GraphWeaver::Codegen
|
|
|
349
349
|
out << "#{pad} rescue TypeError, ArgumentError, KeyError => e"
|
|
350
350
|
out << "#{pad} raise GraphWeaver::TypeError.new(struct: self, error: e)"
|
|
351
351
|
out << "#{pad} end"
|
|
352
|
+
|
|
353
|
+
# alias delegators (extend_type alias:) — typed accessors that project a
|
|
354
|
+
# selected field onto the struct, next to the honest wire data
|
|
355
|
+
node.aliases.each do |a|
|
|
356
|
+
out << ""
|
|
357
|
+
out << "#{pad} sig { returns(#{a.type}) }"
|
|
358
|
+
out << "#{pad} def #{a.name} = #{a.expr}"
|
|
359
|
+
end
|
|
352
360
|
out << "#{pad}end"
|
|
353
361
|
end
|
|
354
362
|
|
|
@@ -414,7 +422,8 @@ class GraphWeaver::Codegen
|
|
|
414
422
|
|
|
415
423
|
# execute returns the full envelope; execute! is the strict shortcut for
|
|
416
424
|
# `execute(...).data!` — the typed result, or a raised QueryError.
|
|
417
|
-
|
|
425
|
+
# kwargs forward via hash shorthand (key == value)
|
|
426
|
+
forward = (["client"] + params.map { |param| "#{kwarg_name(param)}:" }).join(", ")
|
|
418
427
|
|
|
419
428
|
if flatten
|
|
420
429
|
out << " # $#{variables.first.wire}'s fields, flattened into kwargs (single input-object variable)"
|
|
@@ -102,9 +102,38 @@ class GraphWeaver::Codegen
|
|
|
102
102
|
# end
|
|
103
103
|
#
|
|
104
104
|
# Additive: repeated registrations (and client-scoped ones) stack.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
#
|
|
106
|
+
# alias: projects a (possibly nested) selected field onto a flat, typed
|
|
107
|
+
# accessor on the struct — the one derivation codegen can type itself, so
|
|
108
|
+
# it's emitted into the struct body where the field is in scope:
|
|
109
|
+
#
|
|
110
|
+
# GraphWeaver.extend_type("Widget", alias: { tag: "meta.tag" })
|
|
111
|
+
# GraphWeaver.extend_type("Widget", alias: "meta.tag") # accessor named `tag`
|
|
112
|
+
# GraphWeaver.extend_type("Widget", alias: ["meta.tag", "meta.color"])
|
|
113
|
+
def extend_type(graphql_name, *mixins, requires: nil, **kw, &block)
|
|
114
|
+
aliases = take_aliases(kw)
|
|
115
|
+
entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [], aliases: {} }
|
|
116
|
+
add_type_helpers(entry, graphql_name, mixins, requires, block, aliases)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Pull alias: out of the keyword rest and normalize it; any other keyword
|
|
120
|
+
# is a typo worth flagging rather than silently dropping.
|
|
121
|
+
def take_aliases(kw)
|
|
122
|
+
aliases = normalize_aliases(kw.delete(:alias))
|
|
123
|
+
raise ArgumentError, "unknown keyword: #{kw.keys.first}" unless kw.empty?
|
|
124
|
+
aliases
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# { accessor => [path, segments] } from a path string (accessor named
|
|
128
|
+
# after the last segment), an array of such, or an { accessor => path } hash.
|
|
129
|
+
def normalize_aliases(input)
|
|
130
|
+
case input
|
|
131
|
+
when nil then {}
|
|
132
|
+
when String then { input.split(".").last => input.split(".") }
|
|
133
|
+
when Array then input.to_h { |path| [path.split(".").last, path.split(".")] }
|
|
134
|
+
when Hash then input.to_h { |name, path| [name.to_s, path.to_s.split(".")] }
|
|
135
|
+
else raise ArgumentError, "alias: expects a String, Array, or Hash, got #{input.class}"
|
|
136
|
+
end
|
|
108
137
|
end
|
|
109
138
|
|
|
110
139
|
def type_registry
|
|
@@ -113,11 +142,13 @@ class GraphWeaver::Codegen
|
|
|
113
142
|
|
|
114
143
|
# shared with Client#extend_type: build/validate the mixins and
|
|
115
144
|
# append them to a registry entry
|
|
116
|
-
def add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
145
|
+
def add_type_helpers(entry, graphql_name, mixins, requires, block, aliases = {})
|
|
117
146
|
mixins = mixins.dup
|
|
118
147
|
mixins << helper_module(graphql_name, block) if block
|
|
119
148
|
|
|
120
|
-
|
|
149
|
+
if mixins.empty? && aliases.empty?
|
|
150
|
+
raise ArgumentError, "pass one or more helper modules, a block, or alias:"
|
|
151
|
+
end
|
|
121
152
|
mixins.each do |mixin|
|
|
122
153
|
unless mixin.is_a?(Module) && mixin.name
|
|
123
154
|
raise ArgumentError, "type helpers must be named modules, got #{mixin.inspect}"
|
|
@@ -126,6 +157,7 @@ class GraphWeaver::Codegen
|
|
|
126
157
|
|
|
127
158
|
entry[:mixins].concat(mixins)
|
|
128
159
|
entry[:requires].concat(Array(requires))
|
|
160
|
+
(entry[:aliases] ||= {}).merge!(aliases)
|
|
129
161
|
entry
|
|
130
162
|
end
|
|
131
163
|
|
|
@@ -144,16 +144,20 @@ class GraphWeaver::Codegen
|
|
|
144
144
|
|
|
145
145
|
class ObjectNode < Node
|
|
146
146
|
Field = Struct.new(:prop, :key, :node)
|
|
147
|
+
# a resolved alias delegator (extend_type alias:): the accessor name, the
|
|
148
|
+
# Ruby path expression it reads (meta&.tag), and its Sorbet return type
|
|
149
|
+
Alias = Struct.new(:name, :expr, :type)
|
|
147
150
|
|
|
148
151
|
attr_reader :class_name, :fields
|
|
149
|
-
# the GraphQL type this struct was generated from,
|
|
150
|
-
#
|
|
151
|
-
attr_accessor :graphql_type, :mixins
|
|
152
|
+
# the GraphQL type this struct was generated from, any registered helper
|
|
153
|
+
# modules to include, and any resolved alias delegators (see extend_type)
|
|
154
|
+
attr_accessor :graphql_type, :mixins, :aliases
|
|
152
155
|
|
|
153
156
|
def initialize(class_name)
|
|
154
157
|
@class_name = class_name
|
|
155
158
|
@fields = []
|
|
156
159
|
@mixins = []
|
|
160
|
+
@aliases = []
|
|
157
161
|
end
|
|
158
162
|
|
|
159
163
|
def bare_type = class_name
|
data/lib/graph_weaver/codegen.rb
CHANGED
|
@@ -465,9 +465,72 @@ class GraphWeaver::Codegen
|
|
|
465
465
|
node.fields << ObjectNode::Field.new(prop, key, child)
|
|
466
466
|
end
|
|
467
467
|
|
|
468
|
+
node.aliases = resolve_aliases(node)
|
|
468
469
|
node
|
|
469
470
|
end
|
|
470
471
|
|
|
472
|
+
# Resolve each registered alias (extend_type alias:) for this struct's type
|
|
473
|
+
# against its actual selection — path -> a typed delegator emitted into the
|
|
474
|
+
# struct body. Validated here, per query, so an unselected or untraversable
|
|
475
|
+
# path fails at generation with a pointed message.
|
|
476
|
+
def resolve_aliases(node)
|
|
477
|
+
type_aliases(node.graphql_type).map do |name, segments|
|
|
478
|
+
resolve_alias(node, name, segments)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Registered aliases for a GraphQL type: global registry plus this client's
|
|
483
|
+
# overlay (client-scoped wins on a name clash).
|
|
484
|
+
def type_aliases(graphql_name)
|
|
485
|
+
global = GraphWeaver::Codegen.type_registry[graphql_name]&.dig(:aliases) || {}
|
|
486
|
+
(global.merge(@types[graphql_name]&.dig(:aliases) || {}))
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
ALIAS_RESERVED = (%w[from_h serialize to_h].to_set + RUBY_KEYWORDS).freeze
|
|
490
|
+
|
|
491
|
+
# Walk a dotted path (Ruby prop names) through this struct's selected fields,
|
|
492
|
+
# building the delegator expression (`meta&.tag`) and its return type. Any hop
|
|
493
|
+
# is nilable -> the accessor is nilable; a list hop or an unselected segment
|
|
494
|
+
# raises. The leaf may be any node (scalar, enum, nested struct).
|
|
495
|
+
def resolve_alias(node, name, segments)
|
|
496
|
+
if node.fields.any? { |f| f.prop == name } || ALIAS_RESERVED.include?(name)
|
|
497
|
+
raise GraphWeaver::Error,
|
|
498
|
+
"alias #{name.inspect} on #{node.graphql_type} collides with an existing field or method"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
current = T.let(node, T.untyped)
|
|
502
|
+
parts = []
|
|
503
|
+
nilable = T.let(false, T::Boolean)
|
|
504
|
+
segments.each_with_index do |seg, i|
|
|
505
|
+
field = current.fields.find { |f| f.prop == seg }
|
|
506
|
+
unless field
|
|
507
|
+
props = current.fields.map(&:prop)
|
|
508
|
+
suggestion = defined?(DidYouMean::SpellChecker) &&
|
|
509
|
+
DidYouMean::SpellChecker.new(dictionary: props).correct(seg).first
|
|
510
|
+
hint = suggestion ? " — did you mean '#{suggestion}'?" : " (have: #{props.join(", ")})"
|
|
511
|
+
raise GraphWeaver::Error,
|
|
512
|
+
"alias #{name.inspect} on #{node.graphql_type}: '#{seg}' is not a selected field#{hint}"
|
|
513
|
+
end
|
|
514
|
+
nilable ||= !field.node.non_null?
|
|
515
|
+
|
|
516
|
+
if i == segments.size - 1
|
|
517
|
+
leaf = field.node.bare_type
|
|
518
|
+
type = nilable && leaf != "T.untyped" ? "T.nilable(#{leaf})" : leaf
|
|
519
|
+
return ObjectNode::Alias.new(name, (parts << seg).join, type)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
inner = T.let(field.node, T.untyped)
|
|
523
|
+
inner = inner.of while inner.is_a?(NonNull)
|
|
524
|
+
raise GraphWeaver::Error, "alias #{name.inspect}: cannot traverse list-typed '#{seg}'" if inner.is_a?(List)
|
|
525
|
+
unless inner.is_a?(ObjectNode)
|
|
526
|
+
raise GraphWeaver::Error, "alias #{name.inspect}: '#{seg}' is not an object to traverse into"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
parts << seg << (field.node.non_null? ? "." : "&.")
|
|
530
|
+
current = inner
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
471
534
|
# The concrete type conditions a selection mentions (inline fragments
|
|
472
535
|
# and named spreads), minus conditions naming the abstract type itself.
|
|
473
536
|
def concrete_conditions(core, selections)
|
|
@@ -26,19 +26,151 @@ module GraphWeaver::SchemaLoader
|
|
|
26
26
|
raise ArgumentError, "unsupported schema content: #{source.lstrip[0, 80].inspect}"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
build_sdl(source)
|
|
30
30
|
else # a file path
|
|
31
31
|
case File.extname(source)
|
|
32
32
|
when ".json"
|
|
33
33
|
GraphQL::Schema.from_introspection(JSON.parse(File.read(source)))
|
|
34
34
|
when ".graphql", ".gql"
|
|
35
|
-
|
|
35
|
+
build_sdl(File.read(source))
|
|
36
36
|
else
|
|
37
37
|
raise ArgumentError, "unsupported schema format: #{source}"
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Build a schema from SDL, first stripping Apollo Federation composition
|
|
43
|
+
# machinery when the SDL is a composed supergraph — so a supergraph dump
|
|
44
|
+
# (often the only artifact for the merged graph, and what the router
|
|
45
|
+
# actually serves) loads like any schema, with the federation plumbing
|
|
46
|
+
# gone rather than leaked into schema.types. Plain SDL passes through.
|
|
47
|
+
def self.build_sdl(sdl)
|
|
48
|
+
GraphQL::Schema.from_definition(federation_sdl?(sdl) ? strip_federation(sdl) : sdl)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :build_sdl
|
|
51
|
+
|
|
52
|
+
# A composed Fed2 supergraph is marked by @join__* directives (every merged
|
|
53
|
+
# type carries them); a plain schema has none.
|
|
54
|
+
def self.federation_sdl?(sdl)
|
|
55
|
+
sdl.match?(/@join__\w/)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
FEDERATION_PREFIXES = %w[join__ link__ core__].freeze
|
|
59
|
+
FEDERATION_DIRECTIVES = %w[link core inaccessible].to_set.freeze
|
|
60
|
+
|
|
61
|
+
# Whether a type/directive name belongs to the federation composition layer.
|
|
62
|
+
def self.federation_name?(name)
|
|
63
|
+
!!name && (name.start_with?(*FEDERATION_PREFIXES) || FEDERATION_DIRECTIVES.include?(name))
|
|
64
|
+
end
|
|
65
|
+
private_class_method :federation_name?
|
|
66
|
+
|
|
67
|
+
# Drop the composition machinery from supergraph SDL: the synthetic
|
|
68
|
+
# join__*/link__* type and directive definitions, and every @join__*/@link
|
|
69
|
+
# application on the types that remain. What's left is the merged graph's
|
|
70
|
+
# ordinary type shapes — exactly what codegen reads. Parsing is lenient (it's
|
|
71
|
+
# schema *building* that rejects the join directives), so we parse, filter the
|
|
72
|
+
# AST, and reprint clean SDL for from_definition — no graphql-ruby monkeypatch
|
|
73
|
+
# and no join__* leaking into schema.types.
|
|
74
|
+
def self.strip_federation(sdl)
|
|
75
|
+
doc = GraphQL.parse(sdl)
|
|
76
|
+
defs = remove_inaccessible(doc.definitions)
|
|
77
|
+
.reject { |defn| defn.respond_to?(:name) && federation_name?(defn.name) }
|
|
78
|
+
.map { |defn| strip_federation_directives(defn) }
|
|
79
|
+
GraphQL::Language::Nodes::Document.new(definitions: defs).to_query_string
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Derive the API schema by dropping every element marked @inaccessible —
|
|
83
|
+
# present in the federated graph but hidden from the public API the router
|
|
84
|
+
# serves (its common use is safely rolling out a field on a shared type).
|
|
85
|
+
# Cascades: a field/argument/union-member/implements referencing a removed
|
|
86
|
+
# type goes too, and a type left with no fields/values/members is itself
|
|
87
|
+
# removed — repeated to a fixpoint. So codegen matches exactly what clients
|
|
88
|
+
# can query, without the over-permitting a raw supergraph would allow and
|
|
89
|
+
# without Apollo's JS tooling to subtract the API schema.
|
|
90
|
+
def self.remove_inaccessible(definitions)
|
|
91
|
+
removed = definitions.select { |d| type_definition?(d) && inaccessible?(d) }.map(&:name).to_set
|
|
92
|
+
loop do
|
|
93
|
+
survivors = definitions
|
|
94
|
+
.reject { |d| type_definition?(d) && removed.include?(d.name) }
|
|
95
|
+
.map { |d| prune_inaccessible(d, removed) }
|
|
96
|
+
newly = survivors.select { |d| type_definition?(d) && type_emptied?(d) }.map(&:name)
|
|
97
|
+
return survivors if (newly - removed.to_a).empty?
|
|
98
|
+
|
|
99
|
+
removed.merge(newly)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :remove_inaccessible
|
|
103
|
+
|
|
104
|
+
# A type-system type definition (object/interface/union/enum/input/scalar) —
|
|
105
|
+
# not a directive or schema definition.
|
|
106
|
+
def self.type_definition?(node)
|
|
107
|
+
node.respond_to?(:name) &&
|
|
108
|
+
!node.is_a?(GraphQL::Language::Nodes::DirectiveDefinition) &&
|
|
109
|
+
node.class.name.end_with?("TypeDefinition")
|
|
110
|
+
end
|
|
111
|
+
private_class_method :type_definition?
|
|
112
|
+
|
|
113
|
+
def self.inaccessible?(node)
|
|
114
|
+
node.respond_to?(:directives) && node.directives.any? { |d| d.name == "inaccessible" }
|
|
115
|
+
end
|
|
116
|
+
private_class_method :inaccessible?
|
|
117
|
+
|
|
118
|
+
# Whether pruning left the type with nothing the SDL grammar allows to be
|
|
119
|
+
# empty — a fieldless object/interface/input, a valueless enum, a memberless
|
|
120
|
+
# union — so it must be removed and its references cascaded.
|
|
121
|
+
def self.type_emptied?(node)
|
|
122
|
+
(node.respond_to?(:fields) && node.fields && node.fields.empty?) ||
|
|
123
|
+
(node.is_a?(GraphQL::Language::Nodes::EnumTypeDefinition) && node.values.empty?) ||
|
|
124
|
+
(node.is_a?(GraphQL::Language::Nodes::UnionTypeDefinition) && node.types.empty?)
|
|
125
|
+
end
|
|
126
|
+
private_class_method :type_emptied?
|
|
127
|
+
|
|
128
|
+
# the unwrapped (through NON_NULL/LIST) type name a field or argument references
|
|
129
|
+
def self.unwrapped_type_name(node)
|
|
130
|
+
type = node.type
|
|
131
|
+
type = type.of_type while type.respond_to?(:of_type)
|
|
132
|
+
type.name
|
|
133
|
+
end
|
|
134
|
+
private_class_method :unwrapped_type_name
|
|
135
|
+
|
|
136
|
+
# Remove @inaccessible children and children referencing a removed type,
|
|
137
|
+
# from a type's fields (and their arguments), enum values, union members,
|
|
138
|
+
# and implemented interfaces.
|
|
139
|
+
def self.prune_inaccessible(node, removed)
|
|
140
|
+
gone = lambda do |child|
|
|
141
|
+
inaccessible?(child) || (child.respond_to?(:type) && removed.include?(unwrapped_type_name(child)))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
changes = {}
|
|
145
|
+
if node.respond_to?(:fields) && node.fields
|
|
146
|
+
changes[:fields] = node.fields.reject(&gone).map do |field|
|
|
147
|
+
args = field.respond_to?(:arguments) && field.arguments
|
|
148
|
+
args && args.any? ? field.merge(arguments: args.reject(&gone)) : field
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
changes[:values] = node.values.reject(&gone) if node.respond_to?(:values) && node.values
|
|
152
|
+
changes[:types] = node.types.reject { |t| removed.include?(t.name) } if node.respond_to?(:types) && node.types
|
|
153
|
+
if node.respond_to?(:interfaces) && node.interfaces
|
|
154
|
+
changes[:interfaces] = node.interfaces.reject { |i| removed.include?(i.name) }
|
|
155
|
+
end
|
|
156
|
+
changes.empty? ? node : node.merge(changes)
|
|
157
|
+
end
|
|
158
|
+
private_class_method :prune_inaccessible
|
|
159
|
+
|
|
160
|
+
# Recursively remove @join__*/@link applications from a definition and its
|
|
161
|
+
# fields, arguments, and enum values.
|
|
162
|
+
def self.strip_federation_directives(node)
|
|
163
|
+
changes = {}
|
|
164
|
+
if node.respond_to?(:directives) && node.directives
|
|
165
|
+
changes[:directives] = node.directives.reject { |d| federation_name?(d.name) }
|
|
166
|
+
end
|
|
167
|
+
changes[:fields] = node.fields.map { |c| strip_federation_directives(c) } if node.respond_to?(:fields) && node.fields
|
|
168
|
+
changes[:arguments] = node.arguments.map { |c| strip_federation_directives(c) } if node.respond_to?(:arguments) && node.arguments
|
|
169
|
+
changes[:values] = node.values.map { |c| strip_federation_directives(c) } if node.respond_to?(:values) && node.values
|
|
170
|
+
changes.empty? ? node : node.merge(changes)
|
|
171
|
+
end
|
|
172
|
+
private_class_method :strip_federation_directives
|
|
173
|
+
|
|
42
174
|
# Run the standard introspection query through a transport and build a
|
|
43
175
|
# schema from the result:
|
|
44
176
|
#
|
data/lib/graph_weaver/version.rb
CHANGED
data/lib/graph_weaver.rb
CHANGED
|
@@ -17,9 +17,6 @@ require_relative "graph_weaver/railtie" if defined?(::Rails::Railtie)
|
|
|
17
17
|
|
|
18
18
|
# opt-in extras:
|
|
19
19
|
# require "graph_weaver/transport/faraday" # Faraday transport
|
|
20
|
-
# require "graph_weaver/directive_defaults_patch" # fix graphql-ruby
|
|
21
|
-
# dropping directive argument defaults when loading SDL (needed for
|
|
22
|
-
# Apollo supergraph SDL until rmosolgo/graphql-ruby#5659 ships)
|
|
23
20
|
module GraphWeaver
|
|
24
21
|
class << self
|
|
25
22
|
# A client for one GraphQL server — transport, schema, and scoped
|
|
@@ -348,13 +345,15 @@ module GraphWeaver
|
|
|
348
345
|
end
|
|
349
346
|
|
|
350
347
|
# Include app-owned helper modules into every struct generated from a
|
|
351
|
-
# GraphQL type — derived values live as methods next to the honest
|
|
352
|
-
#
|
|
348
|
+
# GraphQL type — derived values live as methods next to the honest wire
|
|
349
|
+
# data, on the struct at runtime (fakes/cassettes included):
|
|
353
350
|
#
|
|
354
351
|
# GraphWeaver.extend_type("Pet", PetHelpers)
|
|
355
352
|
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
353
|
+
# A helper that reads a wire field is checked by srb tc in the module's
|
|
354
|
+
# own scope, not the struct's, so the field won't resolve — write it
|
|
355
|
+
# # typed: false or reach it via T.unsafe(self). Or build the mixin inline
|
|
356
|
+
# with a block (module_eval'd into an auto-named module — invisible to srb):
|
|
358
357
|
#
|
|
359
358
|
# GraphWeaver.extend_type("Pet") do
|
|
360
359
|
# def display_name = "#{name} the pet"
|
|
@@ -362,8 +361,8 @@ module GraphWeaver
|
|
|
362
361
|
#
|
|
363
362
|
# Additive (repeated and client-scoped registrations stack). Global;
|
|
364
363
|
# client.extend_type scopes to one client.
|
|
365
|
-
def extend_type(graphql_name, *mixins, requires: nil, &block)
|
|
366
|
-
Codegen.extend_type(graphql_name, *mixins, requires:, &block)
|
|
364
|
+
def extend_type(graphql_name, *mixins, requires: nil, **kw, &block)
|
|
365
|
+
Codegen.extend_type(graphql_name, *mixins, requires:, **kw, &block)
|
|
367
366
|
end
|
|
368
367
|
|
|
369
368
|
# Restore the built-in scalars, dropping every custom registration —
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graph_weaver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 2.6.7
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 2.6.7
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: sorbet-runtime
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -253,7 +253,6 @@ files:
|
|
|
253
253
|
- lib/graph_weaver/codegen/enum_type.rb
|
|
254
254
|
- lib/graph_weaver/codegen/nodes.rb
|
|
255
255
|
- lib/graph_weaver/codegen/scalar_type.rb
|
|
256
|
-
- lib/graph_weaver/directive_defaults_patch.rb
|
|
257
256
|
- lib/graph_weaver/errors.rb
|
|
258
257
|
- lib/graph_weaver/hints.rb
|
|
259
258
|
- lib/graph_weaver/inflect.rb
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# typed: false — monkeypatch; `self.class` resolves against the prepended host
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "graphql"
|
|
5
|
-
|
|
6
|
-
# graphql-ruby's SDL builder (BuildFromDefinition#prepare_directives)
|
|
7
|
-
# passes only the directive arguments present at the usage site, but
|
|
8
|
-
# Directive#initialize validates ALL defined arguments — so a defaulted
|
|
9
|
-
# non-null argument (`extension: Boolean! = false`) raises
|
|
10
|
-
# InvalidArgumentError when omitted, even though the SDL spec makes it
|
|
11
|
-
# optional. Real Apollo supergraph SDL (join v0.3) hits this on every
|
|
12
|
-
# @join__type usage.
|
|
13
|
-
#
|
|
14
|
-
# Fill in the declared defaults before validation. This is what upstream
|
|
15
|
-
# should do; present in graphql 2.6.3 (latest at time of writing).
|
|
16
|
-
#
|
|
17
|
-
# TODO: delete this file (and its requires) once
|
|
18
|
-
# https://github.com/rmosolgo/graphql-ruby/pull/5659 lands in a released
|
|
19
|
-
# graphql version and the Gemfile picks it up.
|
|
20
|
-
module DirectiveDefaultsPatch
|
|
21
|
-
def initialize(owner, **arguments)
|
|
22
|
-
self.class.all_argument_definitions.each do |arg_defn|
|
|
23
|
-
if !arguments.key?(arg_defn.keyword) && arg_defn.default_value?
|
|
24
|
-
arguments[arg_defn.keyword] = arg_defn.default_value
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
super(owner, **arguments)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
GraphQL::Schema::Directive.prepend(DirectiveDefaultsPatch)
|