@nxgt/graphql-codegen-zod 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +144 -2
- package/dist/config.d.ts +43 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/cycles.d.ts +9 -0
- package/dist/cycles.d.ts.map +1 -0
- package/dist/defaults.d.ts +21 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/imports.d.ts +14 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +717 -0
- package/dist/index.js.map +20 -0
- package/dist/naming.d.ts +20 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/scalars.d.ts +21 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/schema-output.d.ts +10 -0
- package/dist/schema-output.d.ts.map +1 -0
- package/dist/source.d.ts +9 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/type-code.d.ts +12 -0
- package/dist/type-code.d.ts.map +1 -0
- package/dist/variables-output.d.ts +13 -0
- package/dist/variables-output.d.ts.map +1 -0
- package/dist/writer.d.ts +36 -0
- package/dist/writer.d.ts.map +1 -0
- package/docs/README.md +14 -0
- package/docs/guide/output.md +460 -0
- package/docs/roadmap.md +35 -0
- package/docs/troubleshooting.md +220 -0
- package/package.json +61 -5
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Output
|
|
2
|
+
|
|
3
|
+
What the plugin writes from your schema and operations: the schemas, the
|
|
4
|
+
types, their names, and how defaults, recursion, `@oneOf` and custom scalars
|
|
5
|
+
come out.
|
|
6
|
+
|
|
7
|
+
## The smallest example
|
|
8
|
+
|
|
9
|
+
```graphql
|
|
10
|
+
# schema
|
|
11
|
+
input SignUpInput {
|
|
12
|
+
email: String! @constraint(format: "email")
|
|
13
|
+
name: String! @constraint(minLength: 2)
|
|
14
|
+
age: Int @constraint(min: 13, max: 120)
|
|
15
|
+
role: Role = USER
|
|
16
|
+
}
|
|
17
|
+
enum Role { ADMIN USER }
|
|
18
|
+
type Mutation { signUp(input: SignUpInput!): Boolean }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```graphql
|
|
22
|
+
# a document
|
|
23
|
+
mutation SignUp($input: SignUpInput!) { signUp(input: $input) }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// codegen.ts
|
|
28
|
+
import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
29
|
+
|
|
30
|
+
const config: CodegenConfig = {
|
|
31
|
+
schema: 'src/schema/**/*.graphql',
|
|
32
|
+
documents: 'src/operations/**/*.graphql',
|
|
33
|
+
generates: {
|
|
34
|
+
'src/generated/zod.ts': {
|
|
35
|
+
plugins: ['@nxgt/graphql-codegen-zod'],
|
|
36
|
+
config: { scalarSchemas: '@nxgt/graphql-scalars' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default config;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The plugin writes, in this order: every enum, every input type, the
|
|
44
|
+
arguments of every object or interface field that takes any, then the
|
|
45
|
+
variables of every named operation. Each is a schema and its type.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
|
|
50
|
+
export const zRole = z.enum(["ADMIN", "USER"]);
|
|
51
|
+
export type Role = z.output<typeof zRole>;
|
|
52
|
+
|
|
53
|
+
export const zSignUpInput = z.strictObject({
|
|
54
|
+
email: z.email(),
|
|
55
|
+
name: z.string().min(2),
|
|
56
|
+
age: z.int32().lte(120).gte(13).nullish(),
|
|
57
|
+
role: zRole.prefault("USER").nullable(),
|
|
58
|
+
});
|
|
59
|
+
export type SignUpInput = z.output<typeof zSignUpInput>;
|
|
60
|
+
|
|
61
|
+
export const zMutationSignUpArgs = z.object({
|
|
62
|
+
get input() {
|
|
63
|
+
return zSignUpInput;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
export type MutationSignUpArgs = z.output<typeof zMutationSignUpArgs>;
|
|
67
|
+
|
|
68
|
+
export const zSignUpMutationVariables = z.object({
|
|
69
|
+
get input() {
|
|
70
|
+
return zSignUpInput;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
export type SignUpMutationVariables = z.input<typeof zSignUpMutationVariables>;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The file imports only `zod` and the modules of your scalar schemas. Every
|
|
77
|
+
string in it is double-quoted. An input type is a `z.strictObject`: a field
|
|
78
|
+
the schema does not declare is refused, as `graphql` refuses it. The
|
|
79
|
+
arguments and variables objects stay `z.object`, because `graphql` ignores an
|
|
80
|
+
extra argument or variable.
|
|
81
|
+
|
|
82
|
+
A GraphQL description becomes a doc comment on the schema and its type, and on
|
|
83
|
+
each argument and input field that has one (line endings are normalised, so a
|
|
84
|
+
CRLF description gives the same comment). Every other import takes a name the
|
|
85
|
+
file does not declare: a clash is aliased, `money as money2`.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
export const zQueryUserArgs = z.object({
|
|
89
|
+
/** At least three characters. */
|
|
90
|
+
id: z.string().min(3),
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What each part is for
|
|
95
|
+
|
|
96
|
+
| Generated | From | Typed | Use it for |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `z<Enum>` | an `enum` | `z.output` | `z.enum([...])` |
|
|
99
|
+
| `z<Input>` | an `input` | `z.output` | the value a resolver receives |
|
|
100
|
+
| `z<Parent><Field>Args` | an object or interface field with arguments | `z.output` | typing the resolver's arguments |
|
|
101
|
+
| `z<Operation><Kind>Variables` | a named operation in `documents` | `z.input` | checking a form before sending it |
|
|
102
|
+
|
|
103
|
+
`z.output` is what a resolver receives after `withValidation` parsed the
|
|
104
|
+
arguments; `z.input` is what a client sends. They differ where a default
|
|
105
|
+
applies, see [Defaults](#defaults).
|
|
106
|
+
|
|
107
|
+
### On the server
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import type { MutationSignUpArgs } from './generated/zod';
|
|
111
|
+
|
|
112
|
+
export const signUp = (_: unknown, { input }: MutationSignUpArgs) =>
|
|
113
|
+
input.role === 'ADMIN'; // typed: 'ADMIN' | 'USER' | null
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### On the client
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { zSignUpMutationVariables } from './generated/zod';
|
|
120
|
+
|
|
121
|
+
const parsed = zSignUpMutationVariables.safeParse({
|
|
122
|
+
input: { email: 'nope', name: 'A' },
|
|
123
|
+
});
|
|
124
|
+
// parsed.success === false; parsed.error.issues lists the email and the name
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The schema is the same rule the server applies: a spec in this package
|
|
128
|
+
checks, operation by operation, that the client schema accepts exactly what
|
|
129
|
+
the server accepts.
|
|
130
|
+
|
|
131
|
+
## Naming
|
|
132
|
+
|
|
133
|
+
Schemas are prefixed `z`; types keep the names `@graphql-codegen/typescript`
|
|
134
|
+
and `typescript-operations` give them.
|
|
135
|
+
|
|
136
|
+
| What | Schema | Type |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| input | `zSignUpInput` | `SignUpInput` |
|
|
139
|
+
| enum | `zRole` | `Role` |
|
|
140
|
+
| field arguments | `zMutationSignUpArgs` | `MutationSignUpArgs` |
|
|
141
|
+
| operation variables | `zSignUpMutationVariables` | `SignUpMutationVariables` |
|
|
142
|
+
|
|
143
|
+
| Option | Type | Default | Effect |
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
|
+
| `schemaPrefix` | `string` | `'z'` | before each schema's name: `schemaPrefix: 'schema'` gives `schemaSignUpInput` |
|
|
146
|
+
| `namingConvention` | `'keep' \| 'change-case-all#<case>' \| (name: string) => string \| { typeNames, transformUnderscore, enumValues }` | PascalCase each part between underscores | how type names are cased, see below |
|
|
147
|
+
| `typesPrefix` | `string` | `''` | before each type's name: `ISignUpInput`; the schema's name follows: `zISignUpInput` |
|
|
148
|
+
| `typesSuffix` | `string` | `''` | after each type's name |
|
|
149
|
+
| `addUnderscoreToArgsType` | `boolean` | `false` | `Query_FindUserArgs` instead of `QueryFindUserArgs` |
|
|
150
|
+
| `dedupeOperationSuffix` | `boolean` | `false` | `findUserQuery` gives `FindUserQueryVariables`, not `FindUserQueryQueryVariables` |
|
|
151
|
+
| `omitOperationSuffix` | `boolean` | `false` | `FindUserVariables`: no `Query`, `Mutation` or `Subscription` |
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
config: {
|
|
155
|
+
namingConvention: 'keep',
|
|
156
|
+
typesPrefix: 'I',
|
|
157
|
+
addUnderscoreToArgsType: true,
|
|
158
|
+
}
|
|
159
|
+
// type IQuery_find_userArgs, schema zIQuery_find_userArgs
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`namingConvention` takes every form the typescript plugins take for type
|
|
163
|
+
names:
|
|
164
|
+
|
|
165
|
+
| Form | Effect |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `'keep'` | names as written |
|
|
168
|
+
| `'change-case-all#<case>'` or `'change-case#<case>'` | the case applied to each part between underscores. change-case-all's `<case>`: `camelCase`, `capitalCase`, `constantCase`, `dotCase`, `headerCase`, `noCase`, `paramCase`, `pascalCase`, `pathCase`, `sentenceCase`, `snakeCase`, `lowerCase`, `upperCase`, `lowerCaseFirst`, `upperCaseFirst`; change-case 5's names (`kebabCase`, `trainCase`, `pascalSnakeCase`, …) after `change-case#` |
|
|
169
|
+
| `(name) => string` | the function applied to each part between underscores |
|
|
170
|
+
| `{ typeNames, transformUnderscore, enumValues }` | with `typeNames` (a case or a function) the case applies to the whole name, underscores included, whatever `transformUnderscore` says, as in the typescript plugins; `'keep'` keeps it. Without `typeNames`: PascalCase each part, or the whole name with `transformUnderscore: true`. `enumValues` is ignored: enum values are written as they are |
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
config: { namingConvention: 'change-case-all#camelCase' }
|
|
174
|
+
config: { namingConvention: { transformUnderscore: true } } // sign_upInput → SignUpInput
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Any other module string (`'my-module#myCase'`) throws, see
|
|
178
|
+
[Troubleshooting](../troubleshooting.md).
|
|
179
|
+
|
|
180
|
+
### One file of its own
|
|
181
|
+
|
|
182
|
+
Generate into a file the typescript plugins do not write to: the names would
|
|
183
|
+
collide. This plugin's types can replace the typescript plugin's `*Args` and
|
|
184
|
+
`*Variables`, so the other plugins can be limited to the output types.
|
|
185
|
+
|
|
186
|
+
## Defaults
|
|
187
|
+
|
|
188
|
+
A default value becomes `.prefault(<default>)`, then `.nullable()` when the
|
|
189
|
+
type is nullable. The key is optional on the way in and always present on the
|
|
190
|
+
way out, as `graphql` fills it.
|
|
191
|
+
|
|
192
|
+
```graphql
|
|
193
|
+
input SignUpInput {
|
|
194
|
+
role: Role = USER
|
|
195
|
+
tags: [String!] = "new" @constraint(minItems: 1)
|
|
196
|
+
}
|
|
197
|
+
type Query { users(first: Int = 10 @constraint(min: 1, max: 50)): [String!]! }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
export const zSignUpInput = z.strictObject({
|
|
202
|
+
role: zRole.prefault("USER").nullable(),
|
|
203
|
+
tags: z.union([z.array(z.string()).min(1), z.string().transform((value): unknown[] => [value]).pipe(z.array(z.string()).min(1))]).prefault(["new"]).nullable(),
|
|
204
|
+
});
|
|
205
|
+
export const zQueryUsersArgs = z.object({
|
|
206
|
+
first: z.int32().lte(50).gte(1).prefault(10).nullable(),
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { zQueryUsersArgs, type QueryUsersArgs, type SignUpMutationVariables } from './generated/zod';
|
|
212
|
+
|
|
213
|
+
zQueryUsersArgs.parse({}); // { first: 10 }
|
|
214
|
+
|
|
215
|
+
const first: QueryUsersArgs['first'] = 10; // number | null: always present
|
|
216
|
+
const sent: SignUpMutationVariables = { input: { email: 'a@b.co', name: 'Al' } }; // role may be left out
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
A default is coerced as `graphql` coerces it, at any depth. A single value
|
|
220
|
+
given for a list is wrapped, once per level of nesting: `tags: [String!] = "new"`
|
|
221
|
+
is `.prefault(["new"])`, and `[[Int]] = 1` is `[[1]]`. This holds inside the
|
|
222
|
+
default literal of an input object:
|
|
223
|
+
|
|
224
|
+
```graphql
|
|
225
|
+
input Prefs { tags: [String!], grid: [[Int]] }
|
|
226
|
+
input SignUpInput { prefs: Prefs = { tags: "x", grid: 1 } }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
get prefs() {
|
|
231
|
+
return zPrefs.prefault({"tags":["x"],"grid":[[1]]}).nullable();
|
|
232
|
+
},
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
An integer default past `Number.MAX_SAFE_INTEGER` given to an `ID` or a
|
|
236
|
+
custom scalar fails generation, since the server keeps every digit: write
|
|
237
|
+
it as a string, `n: Long = "9007199254740993"`.
|
|
238
|
+
|
|
239
|
+
An `Int` given for an `ID` is its string, as `graphql` makes it: `owner: ID = 7`
|
|
240
|
+
is `.prefault("7")`, `ids: [ID!] = 1` is `.prefault(["1"])`.
|
|
241
|
+
|
|
242
|
+
A schema built without SDL and without `@constraint` keeps its defaults:
|
|
243
|
+
introspected ones as their literal, a graphql 17 code-first one through
|
|
244
|
+
`valueToLiteral`, a graphql 16 one through `astFromValue`.
|
|
245
|
+
|
|
246
|
+
## Recursion
|
|
247
|
+
|
|
248
|
+
An input type that refers to a type declared later is written with a Zod 4
|
|
249
|
+
getter, so the order of declaration does not matter. Only a member that
|
|
250
|
+
names an input type is a getter; the rest are plain properties.
|
|
251
|
+
|
|
252
|
+
An input type in a cycle, one that reaches itself through its fields
|
|
253
|
+
(`Filter.and: [Filter]`, or `Group` → `Member` → `Group`), cannot have its
|
|
254
|
+
type inferred once a field takes a single value for a list. Its types are
|
|
255
|
+
written out instead: `Filter`, what the resolver receives, and `FilterInput`,
|
|
256
|
+
what a client sends, and the schema is typed by both. The file's typecheck
|
|
257
|
+
proves the schema's types fit inside them; the package's type specs pin
|
|
258
|
+
them field by field.
|
|
259
|
+
|
|
260
|
+
```graphql
|
|
261
|
+
"""A filter that nests."""
|
|
262
|
+
input Filter {
|
|
263
|
+
and: [Filter!] @constraint(maxItems: 3)
|
|
264
|
+
name: String @constraint(startsWith: "n")
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
/** A filter that nests. */
|
|
270
|
+
export type Filter = {
|
|
271
|
+
and?: Array<Filter> | null | undefined;
|
|
272
|
+
name?: string | null | undefined;
|
|
273
|
+
};
|
|
274
|
+
/** A filter that nests. */
|
|
275
|
+
export type FilterInput = {
|
|
276
|
+
and?: FilterInput | Array<FilterInput> | null | undefined;
|
|
277
|
+
name?: string | null | undefined;
|
|
278
|
+
};
|
|
279
|
+
/** A filter that nests. */
|
|
280
|
+
export const zFilter: z.ZodType<Filter, FilterInput> = z.strictObject({
|
|
281
|
+
get and() {
|
|
282
|
+
return z.union([z.array(zFilter).max(3), z.custom<z.input<typeof zFilter>>((value) => !Array.isArray(value)).transform((value): unknown[] => [value]).pipe(z.array(zFilter).max(3))]).nullish();
|
|
283
|
+
},
|
|
284
|
+
name: z.string().startsWith("n").nullish(),
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Trap:** typed `z.ZodType`, a schema in a cycle has no `.shape`,
|
|
289
|
+
`.extend` or `.pick`. Use the types, or rebuild the object you need. A type
|
|
290
|
+
outside a cycle keeps its `ZodObject`.
|
|
291
|
+
|
|
292
|
+
## @oneOf
|
|
293
|
+
|
|
294
|
+
An input type marked `@oneOf` becomes a `z.union` of `z.strictObject`s, each
|
|
295
|
+
holding one member, set.
|
|
296
|
+
|
|
297
|
+
```graphql
|
|
298
|
+
input Contact @oneOf {
|
|
299
|
+
email: String @constraint(format: "email")
|
|
300
|
+
phone: String @constraint(minLength: 6)
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
export const zContact = z.union([
|
|
306
|
+
z.strictObject({
|
|
307
|
+
email: z.email(),
|
|
308
|
+
}),
|
|
309
|
+
z.strictObject({
|
|
310
|
+
phone: z.string().min(6),
|
|
311
|
+
}),
|
|
312
|
+
]);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
zContact.safeParse({ email: 'a@b.co' }).success; // true
|
|
317
|
+
zContact.safeParse({ email: 'a@b.co', phone: '123456' }).success; // false
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Variables
|
|
321
|
+
|
|
322
|
+
A variable takes the constraints of every argument or input field it is
|
|
323
|
+
passed to, including inside list and object literals and in the fragments the
|
|
324
|
+
operation spreads.
|
|
325
|
+
|
|
326
|
+
```graphql
|
|
327
|
+
query Users($name: String, $first: Int = 5) {
|
|
328
|
+
users(filter: { and: [{ name: $name }] }, first: $first) { id }
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`$name` is passed to `Filter.name`, which `startsWith: "n"`; `$first` to
|
|
333
|
+
`first`, which is `min: 1, max: 50`:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
export const zUsersQueryVariables = z.object({
|
|
337
|
+
name: z.string().startsWith("n").nullish(),
|
|
338
|
+
first: z.int32().lte(50).gte(1).prefault(5).nullable(),
|
|
339
|
+
});
|
|
340
|
+
export type UsersQueryVariables = z.input<typeof zUsersQueryVariables>;
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- Constraints come only from a variable's direct usages and from list or
|
|
344
|
+
object literals: `rate(ids: [$id])` with `ids: [ID!]! @constraint(minItems: 1, minLength: 3)`
|
|
345
|
+
gives `id: z.string().min(3)`. A variable of a non-list type is never
|
|
346
|
+
passed where a list is expected, so it takes no list rule.
|
|
347
|
+
- A variable passed to several places takes the constraints of all of them.
|
|
348
|
+
Two different `format`s cannot both apply, and fail generation, see
|
|
349
|
+
[Troubleshooting](../troubleshooting.md).
|
|
350
|
+
- An **anonymous operation gets no schema**: it has no name to give one.
|
|
351
|
+
Name it.
|
|
352
|
+
- The variable of a directive's argument (`@include(if: $x)`) carries no
|
|
353
|
+
constraint.
|
|
354
|
+
|
|
355
|
+
**Coercions.** A list also takes a single value, as `graphql` does, whatever
|
|
356
|
+
its items and at any depth (`"a"` for `[[String]]` is `[["a"]]`): the value
|
|
357
|
+
is wrapped, then checked as the list. From `Prefs.tags`:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
tags: z.union([z.array(z.string()), z.string().transform((value): unknown[] => [value]).pipe(z.array(z.string()))]).nullish(),
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`zPrefs.parse({ tags: "ok" })` gives `{ tags: ["ok"] }`, what the resolver
|
|
364
|
+
receives. A rule refuses a single value as the server does, at the same path:
|
|
365
|
+
`SignUpInput.tags` is `[String!] @constraint(minItems: 1, maxLength: 8)`, and
|
|
366
|
+
`tags: "toolongtag"` fails at `["input", "tags", 0]` with the server's
|
|
367
|
+
message.
|
|
368
|
+
|
|
369
|
+
A custom scalar's schema may decode (`DateTime`, a string to a `Date`), and
|
|
370
|
+
an input object's holds such scalars. For both, the single value reaches the
|
|
371
|
+
list's schema as it was sent, and is parsed once:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
dates: z.union([z.array(scalarSchemas.DateTime), z.custom<z.input<typeof scalarSchemas.DateTime>>((value) => !Array.isArray(value)).transform((value): unknown[] => [value]).pipe(z.array(scalarSchemas.DateTime))]).nullish(),
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
get contacts() {
|
|
379
|
+
return z.union([z.array(zContact).max(2), z.custom<z.input<typeof zContact>>((value) => !Array.isArray(value)).transform((value): unknown[] => [value]).pipe(z.array(zContact).max(2))]).nullish();
|
|
380
|
+
},
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
One exception, refused: an `Int` for an `ID`. `graphql` takes it, the schema
|
|
384
|
+
is `z.string()`. Send ids as strings.
|
|
385
|
+
|
|
386
|
+
## Scalars
|
|
387
|
+
|
|
388
|
+
Built-in scalars are written in full: `String` and `ID` as `z.string()`,
|
|
389
|
+
`Int` as `z.int32()` (32-bit, as graphql reads it), `Float` as `z.number()`, `Boolean` as
|
|
390
|
+
`z.boolean()`, each with its constraints.
|
|
391
|
+
|
|
392
|
+
A custom scalar needs a Zod schema from you, mapped in one of two ways:
|
|
393
|
+
|
|
394
|
+
| Option | Type | Effect |
|
|
395
|
+
| --- | --- | --- |
|
|
396
|
+
| `scalarSchemas` | `string` | a module exporting a `scalarSchemas` record keyed by GraphQL name; `@nxgt/graphql-scalars` exports one from 0.4.0 |
|
|
397
|
+
| `zodScalars` | `Record<string, string>` | `'<module>#<export>'` for one scalar; wins over the record |
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
config: {
|
|
401
|
+
scalarSchemas: '@nxgt/graphql-scalars',
|
|
402
|
+
zodScalars: { Money: './money#moneySchema' },
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// what the generated file imports
|
|
408
|
+
import { z } from "zod";
|
|
409
|
+
import { scalarSchemas } from "@nxgt/graphql-scalars";
|
|
410
|
+
import { moneySchema } from "./money";
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
birth: scalarSchemas.DateTime.nullish(),
|
|
415
|
+
price: moneySchema,
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- Two exports with the same name from different modules are aliased:
|
|
419
|
+
`import { schema } from "./money"; import { schema as schema2 } from "./cost";`.
|
|
420
|
+
- Module paths are written as they are in the generated file, so a relative
|
|
421
|
+
one (`./money`) is relative to **it**, not to `codegen.ts`.
|
|
422
|
+
- A custom scalar that is in neither fails generation, naming it. See
|
|
423
|
+
[Troubleshooting](../troubleshooting.md).
|
|
424
|
+
- The option is `zodScalars`, not `scalars`: a root `config: { scalars: {
|
|
425
|
+
DateTime: 'Date' } }` written for the typescript plugins reaches this
|
|
426
|
+
plugin too, and is ignored.
|
|
427
|
+
- The plugin loads the `scalarSchemas` module to check its keys when it can.
|
|
428
|
+
When it cannot (a `.ts` file under Node), it trusts the record, and the
|
|
429
|
+
generated file then fails to typecheck on a missing key.
|
|
430
|
+
- A rule on a custom scalar (`@constraint` on a `DateTime` field) is refused,
|
|
431
|
+
as `withValidation` refuses it; check the value in the scalar's own schema.
|
|
432
|
+
|
|
433
|
+
## Errors
|
|
434
|
+
|
|
435
|
+
A schema that declares `@constraint` must be read from its SDL files: an
|
|
436
|
+
introspected schema, or one loaded from a URL, has no directives to read and
|
|
437
|
+
fails generation. Without `@constraint` it works.
|
|
438
|
+
|
|
439
|
+
The plugin refuses a schema `withValidation` would refuse, with the same
|
|
440
|
+
message: a rule on the wrong type, `@constraint` on an output field, two
|
|
441
|
+
declarations of the directive. Those messages are in
|
|
442
|
+
[`@nxgt/graphql-validation`'s troubleshooting](https://github.com/softistx/nxgt-graphql/blob/develop/packages/graphql-validation/docs/troubleshooting.md).
|
|
443
|
+
The messages of this plugin are in [Troubleshooting](../troubleshooting.md).
|
|
444
|
+
|
|
445
|
+
## The plugin function
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { plugin, type CodegenZodConfig, type DocumentFile } from '@nxgt/graphql-codegen-zod';
|
|
449
|
+
|
|
450
|
+
declare function plugin(
|
|
451
|
+
schema: GraphQLSchema,
|
|
452
|
+
documents: readonly DocumentFile[], // { document?: DocumentNode; location?: string }
|
|
453
|
+
config?: CodegenZodConfig,
|
|
454
|
+
info?: { outputFile?: string },
|
|
455
|
+
): Promise<string>;
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
graphql-codegen calls it; call it yourself to generate in a script or a test.
|
|
459
|
+
`info.outputFile` lets a relative `scalarSchemas` path be resolved to check
|
|
460
|
+
its keys.
|
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
What `@nxgt/graphql-codegen-zod` is heading for, phrased as what you get.
|
|
4
|
+
|
|
5
|
+
## Now
|
|
6
|
+
|
|
7
|
+
- **The first release, 0.1.0.** Zod 4 schemas and types for enums, input
|
|
8
|
+
types, field arguments and operation variables, carrying every
|
|
9
|
+
`@constraint` of `@nxgt/graphql-validation`, so the client refuses what the
|
|
10
|
+
server refuses.
|
|
11
|
+
|
|
12
|
+
## Next
|
|
13
|
+
|
|
14
|
+
Candidates, not commitments.
|
|
15
|
+
|
|
16
|
+
- **Emit only some parts**, an option to write just the arguments, or just
|
|
17
|
+
the variables, of a file.
|
|
18
|
+
- **Your own formats**, once `@nxgt/graphql-validation` supports custom
|
|
19
|
+
`@constraint(format: "...")` values; the plugin will carry them.
|
|
20
|
+
|
|
21
|
+
## Later
|
|
22
|
+
|
|
23
|
+
Nothing yet.
|
|
24
|
+
|
|
25
|
+
## Not planned
|
|
26
|
+
|
|
27
|
+
- **An `Int` for an `ID` on the client.** An id is a string in what the
|
|
28
|
+
client sends, as in what the resolver receives.
|
|
29
|
+
|
|
30
|
+
- **Zod 3.** The schemas use Zod 4's getters, `prefault`, `z.strictObject` and
|
|
31
|
+
`z.email()`; they have no Zod 3 form.
|
|
32
|
+
|
|
33
|
+
## Shipped
|
|
34
|
+
|
|
35
|
+
Nothing yet.
|