@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -2
  3. package/dist/collect-fields.d.ts +24 -0
  4. package/dist/collect-fields.d.ts.map +1 -0
  5. package/dist/config.d.ts +53 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/cycles.d.ts +16 -0
  8. package/dist/cycles.d.ts.map +1 -0
  9. package/dist/defaults.d.ts +21 -0
  10. package/dist/defaults.d.ts.map +1 -0
  11. package/dist/imports.d.ts +14 -0
  12. package/dist/imports.d.ts.map +1 -0
  13. package/dist/index.d.ts +14 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1114 -0
  16. package/dist/index.js.map +25 -0
  17. package/dist/naming.d.ts +24 -0
  18. package/dist/naming.d.ts.map +1 -0
  19. package/dist/objects-output.d.ts +16 -0
  20. package/dist/objects-output.d.ts.map +1 -0
  21. package/dist/output-types.d.ts +15 -0
  22. package/dist/output-types.d.ts.map +1 -0
  23. package/dist/results-output.d.ts +16 -0
  24. package/dist/results-output.d.ts.map +1 -0
  25. package/dist/results-selections.d.ts +21 -0
  26. package/dist/results-selections.d.ts.map +1 -0
  27. package/dist/scalars.d.ts +21 -0
  28. package/dist/scalars.d.ts.map +1 -0
  29. package/dist/schema-output.d.ts +10 -0
  30. package/dist/schema-output.d.ts.map +1 -0
  31. package/dist/source.d.ts +9 -0
  32. package/dist/source.d.ts.map +1 -0
  33. package/dist/type-code.d.ts +12 -0
  34. package/dist/type-code.d.ts.map +1 -0
  35. package/dist/variables-output.d.ts +13 -0
  36. package/dist/variables-output.d.ts.map +1 -0
  37. package/dist/writer.d.ts +36 -0
  38. package/dist/writer.d.ts.map +1 -0
  39. package/docs/README.md +14 -0
  40. package/docs/guide/output.md +732 -0
  41. package/docs/roadmap.md +38 -0
  42. package/docs/troubleshooting.md +267 -0
  43. 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.