@nxgt/graphql-codegen-zod 0.0.0-stage → 0.2.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 +232 -2
- package/dist/collect-fields.d.ts +24 -0
- package/dist/collect-fields.d.ts.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/cycles.d.ts +16 -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 +1114 -0
- package/dist/index.js.map +25 -0
- package/dist/naming.d.ts +24 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/objects-output.d.ts +16 -0
- package/dist/objects-output.d.ts.map +1 -0
- package/dist/output-types.d.ts +15 -0
- package/dist/output-types.d.ts.map +1 -0
- package/dist/results-output.d.ts +16 -0
- package/dist/results-output.d.ts.map +1 -0
- package/dist/results-selections.d.ts +21 -0
- package/dist/results-selections.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 +732 -0
- package/docs/roadmap.md +38 -0
- package/docs/troubleshooting.md +267 -0
- package/package.json +62 -5
|
@@ -0,0 +1,732 @@
|
|
|
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`, custom scalars,
|
|
5
|
+
output types and operation results come out.
|
|
6
|
+
|
|
7
|
+
## The smallest example
|
|
8
|
+
|
|
9
|
+
The SDL files for `@constraint` and the scalars, and how to run codegen, are in
|
|
10
|
+
the README's [Setup](../../README.md#setup).
|
|
11
|
+
|
|
12
|
+
```graphql
|
|
13
|
+
# schema
|
|
14
|
+
input SignUpInput {
|
|
15
|
+
email: String! @constraint(format: "email")
|
|
16
|
+
name: String! @constraint(minLength: 2)
|
|
17
|
+
age: Int @constraint(min: 13, max: 120)
|
|
18
|
+
role: Role = USER
|
|
19
|
+
}
|
|
20
|
+
enum Role { ADMIN USER }
|
|
21
|
+
type Mutation { signUp(input: SignUpInput!): Boolean }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```graphql
|
|
25
|
+
# a document
|
|
26
|
+
mutation SignUp($input: SignUpInput!) { signUp(input: $input) }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// codegen.ts
|
|
31
|
+
import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
32
|
+
|
|
33
|
+
const config: CodegenConfig = {
|
|
34
|
+
schema: 'src/schema/**/*.graphql',
|
|
35
|
+
documents: 'src/operations/**/*.graphql',
|
|
36
|
+
generates: {
|
|
37
|
+
'src/generated/zod.ts': {
|
|
38
|
+
plugins: ['@nxgt/graphql-codegen-zod'],
|
|
39
|
+
config: { scalarSchemas: '@nxgt/graphql-scalars' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export default config;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The plugin writes, in this order: every enum, every input type, the
|
|
47
|
+
arguments of every object or interface field that takes any, every object
|
|
48
|
+
type, then every interface and union (see [Output types](#output-types)),
|
|
49
|
+
then the variables of every named operation. The results come last: every
|
|
50
|
+
fragment's, then every named operation's (see
|
|
51
|
+
[Operation results](#operation-results)). Each is a schema and its type.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { z } from "zod";
|
|
55
|
+
|
|
56
|
+
export const zRole = z.enum(["ADMIN", "USER"]);
|
|
57
|
+
export type Role = z.output<typeof zRole>;
|
|
58
|
+
|
|
59
|
+
export const zSignUpInput = z.strictObject({
|
|
60
|
+
email: z.email(),
|
|
61
|
+
name: z.string().min(2),
|
|
62
|
+
age: z.int32().lte(120).gte(13).nullish(),
|
|
63
|
+
role: zRole.prefault("USER").nullable(),
|
|
64
|
+
});
|
|
65
|
+
export type SignUpInput = z.output<typeof zSignUpInput>;
|
|
66
|
+
|
|
67
|
+
export const zMutationSignUpArgs = z.object({
|
|
68
|
+
get input() {
|
|
69
|
+
return zSignUpInput;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
export type MutationSignUpArgs = z.output<typeof zMutationSignUpArgs>;
|
|
73
|
+
|
|
74
|
+
export const zMutation = z.object({
|
|
75
|
+
__typename: z.literal("Mutation").optional(),
|
|
76
|
+
signUp: z.boolean().nullish(),
|
|
77
|
+
});
|
|
78
|
+
export type Mutation = z.output<typeof zMutation>;
|
|
79
|
+
|
|
80
|
+
export const zSignUpMutationVariables = z.object({
|
|
81
|
+
get input() {
|
|
82
|
+
return zSignUpInput;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
export type SignUpMutationVariables = z.input<typeof zSignUpMutationVariables>;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The file imports only `zod` and the modules of your scalar schemas. Every
|
|
89
|
+
string in it is double-quoted. An input type is a `z.strictObject`: a field
|
|
90
|
+
the schema does not declare is refused, as `graphql` refuses it. The
|
|
91
|
+
arguments and variables objects stay `z.object`, because `graphql` ignores an
|
|
92
|
+
extra argument or variable.
|
|
93
|
+
|
|
94
|
+
A GraphQL description becomes a doc comment on the schema and its type, and on
|
|
95
|
+
each argument and input field that has one (line endings are normalised, so a
|
|
96
|
+
CRLF description gives the same comment). Every other import takes a name the
|
|
97
|
+
file does not declare: a clash is aliased, `money as money2`.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export const zQueryUserArgs = z.object({
|
|
101
|
+
/** At least three characters. */
|
|
102
|
+
id: z.string().min(3),
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## What each part is for
|
|
107
|
+
|
|
108
|
+
| Generated | From | Typed | Use it for |
|
|
109
|
+
| --- | --- | --- | --- |
|
|
110
|
+
| `z<Enum>` | an `enum` | `z.output` | `z.enum([...])` |
|
|
111
|
+
| `z<Input>` | an `input` | `z.output` | the value a resolver receives |
|
|
112
|
+
| `z<Parent><Field>Args` | an object or interface field with arguments | `z.output` | typing the resolver's arguments |
|
|
113
|
+
| `z<Operation><Kind>Variables` | a named operation in `documents` | `z.input` | checking a form before sending it |
|
|
114
|
+
| `z<Type>` | an object type, interface or union | `z.output` | typing what a resolver returns; `z.encode` checks it |
|
|
115
|
+
| `z<Operation><Kind>` | a named operation in `documents` | `z.output` | parsing the response on the client |
|
|
116
|
+
| `z<Fragment>Fragment` | a fragment in `documents` | `z.output` | parsing the part of a response a fragment selects |
|
|
117
|
+
|
|
118
|
+
`z.output` is what a resolver receives after `withValidation` parsed the
|
|
119
|
+
arguments; `z.input` is what a client sends. They differ where a default
|
|
120
|
+
applies, see [Defaults](#defaults).
|
|
121
|
+
|
|
122
|
+
### On the server
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import type { MutationSignUpArgs } from './generated/zod';
|
|
126
|
+
|
|
127
|
+
export const signUp = (_: unknown, { input }: MutationSignUpArgs) =>
|
|
128
|
+
input.role === 'ADMIN'; // typed: 'ADMIN' | 'USER' | null
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### On the client
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { zSignUpMutationVariables } from './generated/zod';
|
|
135
|
+
|
|
136
|
+
const parsed = zSignUpMutationVariables.safeParse({
|
|
137
|
+
input: { email: 'nope', name: 'A' },
|
|
138
|
+
});
|
|
139
|
+
// parsed.success === false; parsed.error.issues lists the email and the name
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The schema is the same rule the server applies: a spec in this package
|
|
143
|
+
checks, operation by operation, that the client schema accepts exactly what
|
|
144
|
+
the server accepts.
|
|
145
|
+
|
|
146
|
+
## Naming
|
|
147
|
+
|
|
148
|
+
Schemas are prefixed `z`; types keep the names `@graphql-codegen/typescript`
|
|
149
|
+
and `typescript-operations` give them.
|
|
150
|
+
|
|
151
|
+
| What | Schema | Type |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| input | `zSignUpInput` | `SignUpInput` |
|
|
154
|
+
| enum | `zRole` | `Role` |
|
|
155
|
+
| field arguments | `zMutationSignUpArgs` | `MutationSignUpArgs` |
|
|
156
|
+
| operation variables | `zSignUpMutationVariables` | `SignUpMutationVariables` |
|
|
157
|
+
| object type, interface, union | `zUser` | `User` |
|
|
158
|
+
| operation result | `zSearchQuery` | `SearchQuery` |
|
|
159
|
+
| fragment | `zUserFieldsFragment` | `UserFieldsFragment` |
|
|
160
|
+
|
|
161
|
+
| Option | Type | Default | Effect |
|
|
162
|
+
| --- | --- | --- | --- |
|
|
163
|
+
| `schemaPrefix` | `string` | `'z'` | before each schema's name: `schemaPrefix: 'schema'` gives `schemaSignUpInput` |
|
|
164
|
+
| `namingConvention` | `'keep' \| 'change-case-all#<case>' \| (name: string) => string \| { typeNames, transformUnderscore, enumValues }` | PascalCase each part between underscores | how type names are cased, see below |
|
|
165
|
+
| `typesPrefix` | `string` | `''` | before each type's name: `ISignUpInput`; the schema's name follows: `zISignUpInput` |
|
|
166
|
+
| `typesSuffix` | `string` | `''` | after each type's name |
|
|
167
|
+
| `addUnderscoreToArgsType` | `boolean` | `false` | `Query_FindUserArgs` instead of `QueryFindUserArgs` |
|
|
168
|
+
| `dedupeOperationSuffix` | `boolean` | `false` | `findUserQuery` gives `FindUserQueryVariables` and `FindUserQuery`, not `FindUserQueryQueryVariables` and `FindUserQueryQuery`; a fragment `UFFragment` gives `UfFragment` |
|
|
169
|
+
| `omitOperationSuffix` | `boolean` | `false` | `FindUserVariables`, `FindUser`: no `Query`, `Mutation`, `Subscription` or `Fragment` |
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
config: {
|
|
173
|
+
namingConvention: 'keep',
|
|
174
|
+
typesPrefix: 'I',
|
|
175
|
+
addUnderscoreToArgsType: true,
|
|
176
|
+
}
|
|
177
|
+
// type IQuery_find_userArgs, schema zIQuery_find_userArgs
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`namingConvention` takes every form the typescript plugins take for type
|
|
181
|
+
names:
|
|
182
|
+
|
|
183
|
+
| Form | Effect |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `'keep'` | names as written |
|
|
186
|
+
| `'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#` |
|
|
187
|
+
| `(name) => string` | the function applied to each part between underscores |
|
|
188
|
+
| `{ 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 |
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
config: { namingConvention: 'change-case-all#camelCase' }
|
|
192
|
+
config: { namingConvention: { transformUnderscore: true } } // sign_upInput → SignUpInput
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Any other module string (`'my-module#myCase'`) throws, see
|
|
196
|
+
[Troubleshooting](../troubleshooting.md).
|
|
197
|
+
|
|
198
|
+
### One file of its own
|
|
199
|
+
|
|
200
|
+
Generate into a file the typescript plugins do not write to: the names would
|
|
201
|
+
collide. This plugin's types can replace the typescript plugin's `*Args` and
|
|
202
|
+
`*Variables`, and typescript-operations' `*Query`, `*Mutation` and `*Fragment`,
|
|
203
|
+
so the other plugins can be limited to the output types. Or set
|
|
204
|
+
`operations: false`, and keep those from typescript-operations.
|
|
205
|
+
|
|
206
|
+
## Defaults
|
|
207
|
+
|
|
208
|
+
A default value becomes `.prefault(<default>)`, then `.nullable()` when the
|
|
209
|
+
type is nullable. The key is optional on the way in and always present on the
|
|
210
|
+
way out, as `graphql` fills it.
|
|
211
|
+
|
|
212
|
+
```graphql
|
|
213
|
+
input SignUpInput {
|
|
214
|
+
role: Role = USER
|
|
215
|
+
tags: [String!] = "new" @constraint(minItems: 1)
|
|
216
|
+
}
|
|
217
|
+
type Query { users(first: Int = 10 @constraint(min: 1, max: 50)): [String!]! }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
export const zSignUpInput = z.strictObject({
|
|
222
|
+
role: zRole.prefault("USER").nullable(),
|
|
223
|
+
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(),
|
|
224
|
+
});
|
|
225
|
+
export const zQueryUsersArgs = z.object({
|
|
226
|
+
first: z.int32().lte(50).gte(1).prefault(10).nullable(),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { zQueryUsersArgs, type QueryUsersArgs, type SignUpMutationVariables } from './generated/zod';
|
|
232
|
+
|
|
233
|
+
zQueryUsersArgs.parse({}); // { first: 10 }
|
|
234
|
+
|
|
235
|
+
const first: QueryUsersArgs['first'] = 10; // number | null: always present
|
|
236
|
+
const sent: SignUpMutationVariables = { input: { email: 'a@b.co', name: 'Al' } }; // role may be left out
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A default is coerced as `graphql` coerces it, at any depth. A single value
|
|
240
|
+
given for a list is wrapped, once per level of nesting: `tags: [String!] = "new"`
|
|
241
|
+
is `.prefault(["new"])`, and `[[Int]] = 1` is `[[1]]`. This holds inside the
|
|
242
|
+
default literal of an input object:
|
|
243
|
+
|
|
244
|
+
```graphql
|
|
245
|
+
input Prefs { tags: [String!], grid: [[Int]] }
|
|
246
|
+
input SignUpInput { prefs: Prefs = { tags: "x", grid: 1 } }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
get prefs() {
|
|
251
|
+
return zPrefs.prefault({"tags":["x"],"grid":[[1]]}).nullable();
|
|
252
|
+
},
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
An integer default past `Number.MAX_SAFE_INTEGER` given to an `ID` or a
|
|
256
|
+
custom scalar fails generation, since the server keeps every digit: write
|
|
257
|
+
it as a string, `n: Long = "9007199254740993"`.
|
|
258
|
+
|
|
259
|
+
An `Int` given for an `ID` is its string, as `graphql` makes it: `owner: ID = 7`
|
|
260
|
+
is `.prefault("7")`, `ids: [ID!] = 1` is `.prefault(["1"])`.
|
|
261
|
+
|
|
262
|
+
A schema built without SDL and without `@constraint` keeps its defaults:
|
|
263
|
+
introspected ones as their literal, a graphql 17 code-first one through
|
|
264
|
+
`valueToLiteral`, a graphql 16 one through `astFromValue`.
|
|
265
|
+
|
|
266
|
+
## Recursion
|
|
267
|
+
|
|
268
|
+
An input type that refers to a type declared later is written with a Zod 4
|
|
269
|
+
getter, so the order of declaration does not matter. Only a member that
|
|
270
|
+
names an input type is a getter; the rest are plain properties.
|
|
271
|
+
|
|
272
|
+
An input type in a cycle, one that reaches itself through its fields
|
|
273
|
+
(`Filter.and: [Filter]`, or `Group` → `Member` → `Group`), cannot have its
|
|
274
|
+
type inferred once a field takes a single value for a list. Its types are
|
|
275
|
+
written out instead: `Filter`, what the resolver receives, and `FilterInput`,
|
|
276
|
+
what a client sends, and the schema is typed by both. The file's typecheck
|
|
277
|
+
proves the schema's types fit inside them; the package's type specs pin
|
|
278
|
+
them field by field.
|
|
279
|
+
|
|
280
|
+
```graphql
|
|
281
|
+
"""A filter that nests."""
|
|
282
|
+
input Filter {
|
|
283
|
+
and: [Filter!] @constraint(maxItems: 3)
|
|
284
|
+
name: String @constraint(startsWith: "n")
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
/** A filter that nests. */
|
|
290
|
+
export type Filter = {
|
|
291
|
+
and?: Array<Filter> | null | undefined;
|
|
292
|
+
name?: string | null | undefined;
|
|
293
|
+
};
|
|
294
|
+
/** A filter that nests. */
|
|
295
|
+
export type FilterInput = {
|
|
296
|
+
and?: FilterInput | Array<FilterInput> | null | undefined;
|
|
297
|
+
name?: string | null | undefined;
|
|
298
|
+
};
|
|
299
|
+
/** A filter that nests. */
|
|
300
|
+
export const zFilter: z.ZodType<Filter, FilterInput> = z.strictObject({
|
|
301
|
+
get and() {
|
|
302
|
+
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();
|
|
303
|
+
},
|
|
304
|
+
name: z.string().startsWith("n").nullish(),
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Trap:** typed `z.ZodType`, a schema in a cycle has no `.shape`,
|
|
309
|
+
`.extend` or `.pick`. Use the types, or rebuild the object you need. A type
|
|
310
|
+
outside a cycle keeps its `ZodObject`.
|
|
311
|
+
|
|
312
|
+
## @oneOf
|
|
313
|
+
|
|
314
|
+
An input type marked `@oneOf` becomes a `z.union` of `z.strictObject`s, each
|
|
315
|
+
holding one member, set.
|
|
316
|
+
|
|
317
|
+
```graphql
|
|
318
|
+
input Contact @oneOf {
|
|
319
|
+
email: String @constraint(format: "email")
|
|
320
|
+
phone: String @constraint(minLength: 6)
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
export const zContact = z.union([
|
|
326
|
+
z.strictObject({
|
|
327
|
+
email: z.email(),
|
|
328
|
+
}),
|
|
329
|
+
z.strictObject({
|
|
330
|
+
phone: z.string().min(6),
|
|
331
|
+
}),
|
|
332
|
+
]);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
zContact.safeParse({ email: 'a@b.co' }).success; // true
|
|
337
|
+
zContact.safeParse({ email: 'a@b.co', phone: '123456' }).success; // false
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Variables
|
|
341
|
+
|
|
342
|
+
A variable takes the constraints of every argument or input field it is
|
|
343
|
+
passed to, including inside list and object literals and in the fragments the
|
|
344
|
+
operation spreads.
|
|
345
|
+
|
|
346
|
+
```graphql
|
|
347
|
+
query Users($name: String, $first: Int = 5) {
|
|
348
|
+
users(filter: { and: [{ name: $name }] }, first: $first) { id }
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`$name` is passed to `Filter.name`, which `startsWith: "n"`; `$first` to
|
|
353
|
+
`first`, which is `min: 1, max: 50`:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
export const zUsersQueryVariables = z.object({
|
|
357
|
+
name: z.string().startsWith("n").nullish(),
|
|
358
|
+
first: z.int32().lte(50).gte(1).prefault(5).nullable(),
|
|
359
|
+
});
|
|
360
|
+
export type UsersQueryVariables = z.input<typeof zUsersQueryVariables>;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- Constraints come only from a variable's direct usages and from list or
|
|
364
|
+
object literals: `rate(ids: [$id])` with `ids: [ID!]! @constraint(minItems: 1, minLength: 3)`
|
|
365
|
+
gives `id: z.string().min(3)`. A variable of a non-list type is never
|
|
366
|
+
passed where a list is expected, so it takes no list rule.
|
|
367
|
+
- A variable passed to several places takes the constraints of all of them.
|
|
368
|
+
Two different `format`s cannot both apply, and fail generation, see
|
|
369
|
+
[Troubleshooting](../troubleshooting.md).
|
|
370
|
+
- An **anonymous operation gets no schema**: it has no name to give one.
|
|
371
|
+
Name it.
|
|
372
|
+
- The variable of a directive's argument (`@include(if: $x)`) carries no
|
|
373
|
+
constraint.
|
|
374
|
+
|
|
375
|
+
**Coercions.** A list also takes a single value, as `graphql` does, whatever
|
|
376
|
+
its items and at any depth (`"a"` for `[[String]]` is `[["a"]]`): the value
|
|
377
|
+
is wrapped, then checked as the list. From `Prefs.tags`:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
tags: z.union([z.array(z.string()), z.string().transform((value): unknown[] => [value]).pipe(z.array(z.string()))]).nullish(),
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`zPrefs.parse({ tags: "ok" })` gives `{ tags: ["ok"] }`, what the resolver
|
|
384
|
+
receives. A rule refuses a single value as the server does, at the same path:
|
|
385
|
+
`SignUpInput.tags` is `[String!] @constraint(minItems: 1, maxLength: 8)`, and
|
|
386
|
+
`tags: "toolongtag"` fails at `["input", "tags", 0]` with the server's
|
|
387
|
+
message.
|
|
388
|
+
|
|
389
|
+
A custom scalar's schema may decode (`DateTime`, a string to a `Date`), and
|
|
390
|
+
an input object's holds such scalars. For both, the single value reaches the
|
|
391
|
+
list's schema as it was sent, and is parsed once:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
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(),
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
get contacts() {
|
|
399
|
+
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();
|
|
400
|
+
},
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
One exception, refused: an `Int` for an `ID`. `graphql` takes it, the schema
|
|
404
|
+
is `z.string()`. Send ids as strings.
|
|
405
|
+
|
|
406
|
+
## Scalars
|
|
407
|
+
|
|
408
|
+
Built-in scalars are written in full: `String` and `ID` as `z.string()`,
|
|
409
|
+
`Int` as `z.int32()` (32-bit, as graphql reads it), `Float` as `z.number()`, `Boolean` as
|
|
410
|
+
`z.boolean()`, each with its constraints.
|
|
411
|
+
|
|
412
|
+
A custom scalar needs a Zod schema from you, mapped in one of two ways:
|
|
413
|
+
|
|
414
|
+
| Option | Type | Effect |
|
|
415
|
+
| --- | --- | --- |
|
|
416
|
+
| `scalarSchemas` | `string` | a module exporting a `scalarSchemas` record keyed by GraphQL name; `@nxgt/graphql-scalars` exports one from 0.4.0 |
|
|
417
|
+
| `zodScalars` | `Record<string, string>` | `'<module>#<export>'` for one scalar; wins over the record |
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
config: {
|
|
421
|
+
scalarSchemas: '@nxgt/graphql-scalars',
|
|
422
|
+
zodScalars: { Money: './money#moneySchema' },
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
// what the generated file imports
|
|
428
|
+
import { z } from "zod";
|
|
429
|
+
import { scalarSchemas } from "@nxgt/graphql-scalars";
|
|
430
|
+
import { moneySchema } from "./money";
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
birth: scalarSchemas.DateTime.nullish(),
|
|
435
|
+
price: moneySchema,
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
- Two exports with the same name from different modules are aliased:
|
|
439
|
+
`import { schema } from "./money"; import { schema as schema2 } from "./cost";`.
|
|
440
|
+
- Module paths are written as they are in the generated file, so a relative
|
|
441
|
+
one (`./money`) is relative to **it**, not to `codegen.ts`.
|
|
442
|
+
- A custom scalar that is in neither fails generation, naming it. See
|
|
443
|
+
[Troubleshooting](../troubleshooting.md).
|
|
444
|
+
- The option is `zodScalars`, not `scalars`: a root `config: { scalars: {
|
|
445
|
+
DateTime: 'Date' } }` written for the typescript plugins reaches this
|
|
446
|
+
plugin too, and is ignored.
|
|
447
|
+
- The plugin loads the `scalarSchemas` module to check its keys when it can.
|
|
448
|
+
When it cannot (a `.ts` file under Node), it trusts the record, and the
|
|
449
|
+
generated file then fails to typecheck on a missing key.
|
|
450
|
+
- A rule on a custom scalar (`@constraint` on a `DateTime` field) is refused,
|
|
451
|
+
as `withValidation` refuses it; check the value in the scalar's own schema.
|
|
452
|
+
|
|
453
|
+
## Output types
|
|
454
|
+
|
|
455
|
+
Every object type gets a schema and a type, typed `z.output`: what a
|
|
456
|
+
resolver returns. The root types are written too: a field may return
|
|
457
|
+
`Query` (a mutation payload's `query: Query!`). Then every interface and union, as a `z.union` of its
|
|
458
|
+
object types. `objects: false` writes none of them.
|
|
459
|
+
|
|
460
|
+
```graphql
|
|
461
|
+
"""Someone with an account."""
|
|
462
|
+
type User implements Node {
|
|
463
|
+
id: ID!
|
|
464
|
+
name: String!
|
|
465
|
+
role: Role
|
|
466
|
+
"Who they follow."
|
|
467
|
+
friends: [User!]
|
|
468
|
+
pinned: SearchResult
|
|
469
|
+
joined: DateTime!
|
|
470
|
+
}
|
|
471
|
+
union SearchResult = User | Post
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
A type outside any cycle is a `z.object`, its type inferred:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
export const zMutation = z.object({
|
|
478
|
+
__typename: z.literal("Mutation").optional(),
|
|
479
|
+
signUp: z.boolean().nullish(),
|
|
480
|
+
});
|
|
481
|
+
export type Mutation = z.output<typeof zMutation>;
|
|
482
|
+
|
|
483
|
+
export const zSearchResult = z.union([zUser, zPost]);
|
|
484
|
+
export type SearchResult = z.output<typeof zSearchResult>;
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
An interface no object type implements is `z.never()`; a union of one
|
|
488
|
+
member is that member's schema.
|
|
489
|
+
|
|
490
|
+
A type in a cycle (`User.friends: [User!]`, or `User` → `Org` → `User`) has
|
|
491
|
+
its types written out, as an input type in a cycle has: TypeScript infers a
|
|
492
|
+
looping schema only so deep, and fails with TS2589 on a loop of ten types.
|
|
493
|
+
`User` is what a resolver returns, `UserWire` the wire value (not
|
|
494
|
+
`UserInput`, which an SDL `input UserInput` often already takes), and `zUser`
|
|
495
|
+
is typed by both:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
/** Someone with an account. */
|
|
499
|
+
export type User = {
|
|
500
|
+
__typename?: "User" | undefined;
|
|
501
|
+
id: string;
|
|
502
|
+
name: string;
|
|
503
|
+
role?: z.output<typeof zRole> | null | undefined;
|
|
504
|
+
/** Who they follow. */
|
|
505
|
+
friends?: Array<User> | null | undefined;
|
|
506
|
+
pinned?: z.output<typeof zSearchResult> | null | undefined;
|
|
507
|
+
joined: z.output<typeof scalarSchemas.DateTime>;
|
|
508
|
+
};
|
|
509
|
+
/** Someone with an account. */
|
|
510
|
+
export type UserWire = {
|
|
511
|
+
/* the same, with z.input for scalars, UserWire, z.input<typeof zSearchResult> */
|
|
512
|
+
};
|
|
513
|
+
/** Someone with an account. */
|
|
514
|
+
export const zUser: z.ZodType<User, UserWire> = z.object({
|
|
515
|
+
__typename: z.literal("User").optional(),
|
|
516
|
+
id: z.string(),
|
|
517
|
+
name: z.string(),
|
|
518
|
+
role: zRole.nullish(),
|
|
519
|
+
/** Who they follow. */
|
|
520
|
+
get friends() {
|
|
521
|
+
return z.array(zUser).nullish();
|
|
522
|
+
},
|
|
523
|
+
get pinned() {
|
|
524
|
+
return zSearchResult.nullish();
|
|
525
|
+
},
|
|
526
|
+
joined: scalarSchemas.DateTime,
|
|
527
|
+
});
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`zUser` is then a `z.ZodType`, not a `z.ZodObject`: `.shape`, `.pick` and
|
|
531
|
+
`.extend` are not offered on a type in a cycle.
|
|
532
|
+
|
|
533
|
+
- **`__typename` is optional:** a resolver rarely sets it. When it is set, it
|
|
534
|
+
must be the type's name.
|
|
535
|
+
- **A field the type does not declare is dropped** (`z.object`): a newer
|
|
536
|
+
server's extra field does not fail a parse.
|
|
537
|
+
- **Custom scalars are decoded:** `User['joined']` is a `Date` with
|
|
538
|
+
`@nxgt/graphql-scalars`; `z.input<typeof zUser>` has the wire `string`.
|
|
539
|
+
- **Every field is written**, arguments ignored: `User` is the whole type, as
|
|
540
|
+
`@graphql-codegen/typescript` writes it.
|
|
541
|
+
- **Nesting goes through getters**, so types refer to each other in any
|
|
542
|
+
order. Interfaces and unions are declared after every object type.
|
|
543
|
+
- `Int` is `z.int32()` and `ID` a `z.string()`, as for inputs. A nullable
|
|
544
|
+
field is `.nullish()`.
|
|
545
|
+
- **Every custom scalar needs a schema**, an output-only one included
|
|
546
|
+
(`createdAt: DateTime` on an object type): generation fails naming it,
|
|
547
|
+
as for inputs. 0.1.0 did not read output fields, and nor operations'
|
|
548
|
+
results; 0.1.0's output needs both `objects: false` and `operations: false`.
|
|
549
|
+
- **A name the plugin already writes clashes:** an object type named
|
|
550
|
+
`QueryFindArgs`, or `FilterInput` beside a cyclic input `Filter`, fails
|
|
551
|
+
generation with "would declare … twice". Rename it, set `typesSuffix`, or
|
|
552
|
+
set `objects: false` (or `operations: false`, for a clash with a result
|
|
553
|
+
such as `SearchQuery`).
|
|
554
|
+
|
|
555
|
+
## Operation results
|
|
556
|
+
|
|
557
|
+
Each fragment and each named operation gets a schema and a type, typed
|
|
558
|
+
`z.output`: the response as it arrives, under the aliases the document gave.
|
|
559
|
+
`operations: false` writes none of them; the variables stay. An anonymous
|
|
560
|
+
operation gets none, as for variables.
|
|
561
|
+
|
|
562
|
+
```graphql
|
|
563
|
+
query Search($text: String!, $id: ID!, $withTags: Boolean!) {
|
|
564
|
+
search(text: $text) {
|
|
565
|
+
__typename
|
|
566
|
+
... on User { id who: name joined role }
|
|
567
|
+
... on Post { id title tags @include(if: $withTags) author { ...UserFields } }
|
|
568
|
+
}
|
|
569
|
+
node(id: $id) { __typename id kind: __typename }
|
|
570
|
+
}
|
|
571
|
+
fragment UserFields on User { id name }
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
export const zSearchQuery = z.object({
|
|
576
|
+
search: z.array(z.discriminatedUnion("__typename", [
|
|
577
|
+
z.object({
|
|
578
|
+
__typename: z.literal("User"),
|
|
579
|
+
id: z.string(),
|
|
580
|
+
who: z.string(),
|
|
581
|
+
joined: scalarSchemas.DateTime,
|
|
582
|
+
role: zRole.nullable(),
|
|
583
|
+
}),
|
|
584
|
+
z.object({
|
|
585
|
+
__typename: z.literal("Post"),
|
|
586
|
+
id: z.string(),
|
|
587
|
+
title: z.string(),
|
|
588
|
+
tags: z.array(z.array(z.string().nullable()).nullable()).nullable().optional(),
|
|
589
|
+
author: z.object({
|
|
590
|
+
id: z.string(),
|
|
591
|
+
name: z.string(),
|
|
592
|
+
}),
|
|
593
|
+
}),
|
|
594
|
+
])),
|
|
595
|
+
node: z.object({
|
|
596
|
+
__typename: z.enum(["User", "Post"]),
|
|
597
|
+
id: z.string(),
|
|
598
|
+
kind: z.enum(["User", "Post"]),
|
|
599
|
+
}).nullable(),
|
|
600
|
+
});
|
|
601
|
+
export type SearchQuery = z.output<typeof zSearchQuery>;
|
|
602
|
+
|
|
603
|
+
export const zUserFieldsFragment = z.object({
|
|
604
|
+
id: z.string(),
|
|
605
|
+
name: z.string(),
|
|
606
|
+
});
|
|
607
|
+
export type UserFieldsFragment = z.output<typeof zUserFieldsFragment>;
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
- **Custom scalars are decoded:** `joined` is a `Date` with
|
|
611
|
+
`@nxgt/graphql-scalars`.
|
|
612
|
+
- **`z.object`:** a field the selection does not name is dropped, so a newer
|
|
613
|
+
server's extra field does not fail a parse.
|
|
614
|
+
- **A fragment spread is inlined** into the operation: `author` holds
|
|
615
|
+
`UserFields`' `id` and `name`. A fragment has its own schema too, and the
|
|
616
|
+
inlined one is not a reference to it.
|
|
617
|
+
- **An abstract type's selection is a `z.discriminatedUnion` on
|
|
618
|
+
`__typename`** when its possible types select different fields, and then
|
|
619
|
+
`__typename` must be selected for every member. Without it generation
|
|
620
|
+
fails, see [Troubleshooting](../troubleshooting.md). A `__typename` under
|
|
621
|
+
`@skip` or `@include` does not count.
|
|
622
|
+
- **Possible types that select the same fields share one member**, with
|
|
623
|
+
`__typename: z.enum([...])`: `node` selects only `id` and `kind`, the same
|
|
624
|
+
for `User` and `Post`. A single group is a plain object, not a union.
|
|
625
|
+
- **When every possible type selects the same fields, `__typename` is not
|
|
626
|
+
needed:** the result is one plain object. `plain: node(id: $id) { id }`
|
|
627
|
+
gives `plain: z.object({ id: z.string() }).nullable()`. Selected there,
|
|
628
|
+
`__typename` is `z.enum([...])`.
|
|
629
|
+
- **An aliased `__typename` is the discriminator under its alias:** the
|
|
630
|
+
union is discriminated on `kind` if the document selects only
|
|
631
|
+
`kind: __typename`. The members must select it under one key; when several
|
|
632
|
+
keys select it, `__typename` is preferred. Otherwise generation fails.
|
|
633
|
+
- **A field under `@skip`, `@include` or `@defer` is `.optional()`:** `tags`
|
|
634
|
+
may be absent. A field selected both with and without the directive is
|
|
635
|
+
required, but a sub-field that only the conditional node selects stays
|
|
636
|
+
optional: with `friends { id } friends @include(if: $v) { name }`, `name`
|
|
637
|
+
is optional. A field selected only under the directive is optional, and
|
|
638
|
+
its own fields are not: `maybe: user(id: $id) @include(if: $v) { id }`
|
|
639
|
+
gives `{ id: string } | null | undefined`.
|
|
640
|
+
- **A nullable field is `.nullable()`**, not `.nullish()`: a response holds
|
|
641
|
+
null, not undefined.
|
|
642
|
+
- `Int` is `z.int32()` and `ID` a `z.string()`, as elsewhere. An enum is its
|
|
643
|
+
schema (`zRole`).
|
|
644
|
+
- **An introspection field** (`__schema`, `__type`) is `z.unknown()`.
|
|
645
|
+
|
|
646
|
+
**Deep selections.** Past five nested levels, a selection is declared apart,
|
|
647
|
+
so TypeScript infers an operation of any depth (it failed with TS2589 at
|
|
648
|
+
fourteen levels). The helpers are not exported; identical deep selections
|
|
649
|
+
share one:
|
|
650
|
+
|
|
651
|
+
```ts
|
|
652
|
+
const zDeepQuery$1$ = z.object({ /* the selection */ });
|
|
653
|
+
const zDeepQuery$1: z.ZodType<z.output<typeof zDeepQuery$1$>, z.input<typeof zDeepQuery$1$>> = zDeepQuery$1$;
|
|
654
|
+
// zDeepQuery$2 and zDeepQuery$3 the same way, each using the one before.
|
|
655
|
+
export const zDeepQuery = z.object({
|
|
656
|
+
user: z.object({
|
|
657
|
+
friends: z.array(z.object({
|
|
658
|
+
friends: z.array(z.object({
|
|
659
|
+
friends: z.array(z.object({
|
|
660
|
+
friends: z.array(zDeepQuery$3).nullable(),
|
|
661
|
+
})).nullable(),
|
|
662
|
+
})).nullable(),
|
|
663
|
+
})).nullable(),
|
|
664
|
+
}).nullable(),
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
A fragment's name follows `omitOperationSuffix` and `dedupeOperationSuffix`,
|
|
669
|
+
as typescript-operations names it: `fragment UserFields` is
|
|
670
|
+
`UserFieldsFragment`, and `UserFields` under `omitOperationSuffix`;
|
|
671
|
+
`fragment UFFragment` is `UfFragmentFragment`, and `UfFragment` under either
|
|
672
|
+
option.
|
|
673
|
+
|
|
674
|
+
A fragment on an abstract type, or one that spreads into several places,
|
|
675
|
+
follows the same rules. Here `Pinned` selects `pinned`, a union, whose `User`
|
|
676
|
+
member holds only `__typename`:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
export const zPinnedFragment = z.object({
|
|
680
|
+
pinned: z.discriminatedUnion("__typename", [
|
|
681
|
+
z.object({
|
|
682
|
+
__typename: z.literal("User"),
|
|
683
|
+
}),
|
|
684
|
+
z.object({
|
|
685
|
+
__typename: z.literal("Post"),
|
|
686
|
+
title: z.string(),
|
|
687
|
+
}),
|
|
688
|
+
]).nullable(),
|
|
689
|
+
});
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
On the client, parse the response, then narrow on the discriminator:
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import { zSearchQuery, type SearchQuery } from './generated/zod';
|
|
696
|
+
|
|
697
|
+
export function names(data: unknown): string[] {
|
|
698
|
+
const { search }: SearchQuery = zSearchQuery.parse(data);
|
|
699
|
+
return search.map((hit) =>
|
|
700
|
+
hit.__typename === 'User' ? hit.who : hit.title,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Errors
|
|
706
|
+
|
|
707
|
+
A schema that declares `@constraint` must be read from its SDL files: an
|
|
708
|
+
introspected schema, or one loaded from a URL, has no directives to read and
|
|
709
|
+
fails generation. Without `@constraint` it works.
|
|
710
|
+
|
|
711
|
+
The plugin refuses a schema `withValidation` would refuse, with the same
|
|
712
|
+
message: a rule on the wrong type, `@constraint` on an output field, two
|
|
713
|
+
declarations of the directive. Those messages are in
|
|
714
|
+
[`@nxgt/graphql-validation`'s troubleshooting](https://github.com/softistx/nxgt-graphql/blob/develop/packages/graphql-validation/docs/troubleshooting.md).
|
|
715
|
+
The messages of this plugin are in [Troubleshooting](../troubleshooting.md).
|
|
716
|
+
|
|
717
|
+
## The plugin function
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
import { plugin, type CodegenZodConfig, type DocumentFile } from '@nxgt/graphql-codegen-zod';
|
|
721
|
+
|
|
722
|
+
declare function plugin(
|
|
723
|
+
schema: GraphQLSchema,
|
|
724
|
+
documents: readonly DocumentFile[], // { document?: DocumentNode; location?: string }
|
|
725
|
+
config?: CodegenZodConfig,
|
|
726
|
+
info?: { outputFile?: string },
|
|
727
|
+
): Promise<string>;
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
graphql-codegen calls it; call it yourself to generate in a script or a test.
|
|
731
|
+
`info.outputFile` lets a relative `scalarSchemas` path be resolved to check
|
|
732
|
+
its keys.
|