@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { checkConstraints } from "@nxgt/graphql-validation/codegen";
|
|
3
|
+
import {
|
|
4
|
+
isInputObjectType as isInputObjectType7,
|
|
5
|
+
isInterfaceType as isInterfaceType2,
|
|
6
|
+
isObjectType as isObjectType2
|
|
7
|
+
} from "graphql";
|
|
8
|
+
|
|
9
|
+
// src/imports.ts
|
|
10
|
+
class Imports {
|
|
11
|
+
#byModule = new Map([
|
|
12
|
+
["zod", new Map([["z", "z"]])]
|
|
13
|
+
]);
|
|
14
|
+
#taken;
|
|
15
|
+
constructor(reserved = []) {
|
|
16
|
+
this.#taken = new Set(["z", ...reserved]);
|
|
17
|
+
}
|
|
18
|
+
add(module, exported) {
|
|
19
|
+
const names = this.#byModule.get(module) ?? new Map;
|
|
20
|
+
this.#byModule.set(module, names);
|
|
21
|
+
const known = names.get(exported);
|
|
22
|
+
if (known)
|
|
23
|
+
return known;
|
|
24
|
+
let local = exported;
|
|
25
|
+
for (let n = 2;this.#taken.has(local); n++)
|
|
26
|
+
local = `${exported}${n}`;
|
|
27
|
+
this.#taken.add(local);
|
|
28
|
+
names.set(exported, local);
|
|
29
|
+
return local;
|
|
30
|
+
}
|
|
31
|
+
lines() {
|
|
32
|
+
return [...this.#byModule].map(([module, names]) => `import { ${[...names].map(([exported, local]) => exported === local ? local : `${exported} as ${local}`).join(", ")} } from ${JSON.stringify(module)};`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/naming.ts
|
|
37
|
+
import * as changeCase from "change-case";
|
|
38
|
+
|
|
39
|
+
class Naming {
|
|
40
|
+
#config;
|
|
41
|
+
#convert;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.#config = config;
|
|
44
|
+
this.#convert = converter(config.namingConvention);
|
|
45
|
+
}
|
|
46
|
+
type(name) {
|
|
47
|
+
return this.#wrap(this.#convert(name));
|
|
48
|
+
}
|
|
49
|
+
args(parent, field) {
|
|
50
|
+
const underscore = this.#config.addUnderscoreToArgsType ? "_" : "";
|
|
51
|
+
return this.#wrap(this.#convert(`${parent}${underscore}${this.#convert(field)}Args`));
|
|
52
|
+
}
|
|
53
|
+
variables(operation, kind) {
|
|
54
|
+
const type = changeCase.pascalCase(kind);
|
|
55
|
+
const { omitOperationSuffix, dedupeOperationSuffix } = this.#config;
|
|
56
|
+
const suffix = omitOperationSuffix || dedupeOperationSuffix && operation.toLowerCase().endsWith(type.toLowerCase()) ? "" : type;
|
|
57
|
+
return this.#wrap(this.#convert(`${operation}${suffix}Variables`));
|
|
58
|
+
}
|
|
59
|
+
schema(type) {
|
|
60
|
+
return `${this.#config.schemaPrefix ?? "z"}${type}`;
|
|
61
|
+
}
|
|
62
|
+
#wrap(name) {
|
|
63
|
+
return `${this.#config.typesPrefix ?? ""}${name}${this.#config.typesSuffix ?? ""}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
var lowerCaseFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
67
|
+
var upperCaseFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
68
|
+
var cases = {
|
|
69
|
+
"change-case-all": {
|
|
70
|
+
camelCase: changeCase.camelCase,
|
|
71
|
+
capitalCase: changeCase.capitalCase,
|
|
72
|
+
constantCase: changeCase.constantCase,
|
|
73
|
+
dotCase: changeCase.dotCase,
|
|
74
|
+
headerCase: changeCase.trainCase,
|
|
75
|
+
noCase: changeCase.noCase,
|
|
76
|
+
paramCase: changeCase.kebabCase,
|
|
77
|
+
pascalCase: changeCase.pascalCase,
|
|
78
|
+
pathCase: changeCase.pathCase,
|
|
79
|
+
sentenceCase: changeCase.sentenceCase,
|
|
80
|
+
snakeCase: changeCase.snakeCase,
|
|
81
|
+
lowerCase: (name) => name.toLowerCase(),
|
|
82
|
+
upperCase: (name) => name.toUpperCase(),
|
|
83
|
+
lowerCaseFirst,
|
|
84
|
+
upperCaseFirst
|
|
85
|
+
},
|
|
86
|
+
"change-case": {
|
|
87
|
+
camelCase: changeCase.camelCase,
|
|
88
|
+
capitalCase: changeCase.capitalCase,
|
|
89
|
+
constantCase: changeCase.constantCase,
|
|
90
|
+
dotCase: changeCase.dotCase,
|
|
91
|
+
kebabCase: changeCase.kebabCase,
|
|
92
|
+
noCase: changeCase.noCase,
|
|
93
|
+
pascalCase: changeCase.pascalCase,
|
|
94
|
+
pascalSnakeCase: changeCase.pascalSnakeCase,
|
|
95
|
+
pathCase: changeCase.pathCase,
|
|
96
|
+
sentenceCase: changeCase.sentenceCase,
|
|
97
|
+
snakeCase: changeCase.snakeCase,
|
|
98
|
+
trainCase: changeCase.trainCase
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var byPart = (fn, transformUnderscore) => (name) => transformUnderscore ? fn(name) : name.split("_").map(fn).join("_");
|
|
102
|
+
function converter(convention) {
|
|
103
|
+
if (convention === undefined)
|
|
104
|
+
return byPart(changeCase.pascalCase, false);
|
|
105
|
+
if (convention === "keep")
|
|
106
|
+
return (name) => name;
|
|
107
|
+
if (typeof convention === "function")
|
|
108
|
+
return byPart(convention, false);
|
|
109
|
+
if (typeof convention === "string")
|
|
110
|
+
return byPart(caseNamed(convention), false);
|
|
111
|
+
const { typeNames, transformUnderscore } = convention;
|
|
112
|
+
if (typeNames === "keep")
|
|
113
|
+
return (name) => name;
|
|
114
|
+
if (typeNames === undefined)
|
|
115
|
+
return byPart(changeCase.pascalCase, transformUnderscore ?? false);
|
|
116
|
+
const fn = typeof typeNames === "function" ? typeNames : caseNamed(typeNames);
|
|
117
|
+
return byPart(fn, true);
|
|
118
|
+
}
|
|
119
|
+
function caseNamed(spec) {
|
|
120
|
+
const [module = "", name = ""] = spec.split("#");
|
|
121
|
+
const fn = cases[module]?.[name];
|
|
122
|
+
if (fn)
|
|
123
|
+
return fn;
|
|
124
|
+
throw new Error(`@nxgt/graphql-codegen-zod: namingConvention "${spec}" is not one this plugin reads. Use 'keep', a function, or 'change-case-all#<case>' with one of ${Object.keys(cases["change-case-all"] ?? {}).join(", ")}.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/scalars.ts
|
|
128
|
+
import { createRequire } from "node:module";
|
|
129
|
+
import { dirname, resolve } from "node:path";
|
|
130
|
+
import { pathToFileURL } from "node:url";
|
|
131
|
+
|
|
132
|
+
class ScalarSources {
|
|
133
|
+
#config;
|
|
134
|
+
#imports;
|
|
135
|
+
#recordKeys;
|
|
136
|
+
constructor(config, imports, recordKeys) {
|
|
137
|
+
this.#config = config;
|
|
138
|
+
this.#imports = imports;
|
|
139
|
+
this.#recordKeys = recordKeys;
|
|
140
|
+
}
|
|
141
|
+
static async load(config, imports, outputFile) {
|
|
142
|
+
if (!config.scalarSchemas)
|
|
143
|
+
return new ScalarSources(config, imports, undefined);
|
|
144
|
+
const keys = await recordKeys(config.scalarSchemas, outputFile);
|
|
145
|
+
return new ScalarSources(config, imports, keys);
|
|
146
|
+
}
|
|
147
|
+
code(type) {
|
|
148
|
+
const own = this.#config.zodScalars?.[type.name];
|
|
149
|
+
if (own !== undefined) {
|
|
150
|
+
const at = own.lastIndexOf("#");
|
|
151
|
+
if (at <= 0 || at === own.length - 1) {
|
|
152
|
+
throw new Error(`@nxgt/graphql-codegen-zod: zodScalars.${type.name} is "${own}"; write it '<module>#<export>', e.g. './money#moneySchema'.`);
|
|
153
|
+
}
|
|
154
|
+
return this.#imports.add(own.slice(0, at), own.slice(at + 1));
|
|
155
|
+
}
|
|
156
|
+
const record = this.#config.scalarSchemas;
|
|
157
|
+
if (record && (!this.#recordKeys || this.#recordKeys.has(type.name))) {
|
|
158
|
+
return `${this.#imports.add(record, "scalarSchemas")}.${type.name}`;
|
|
159
|
+
}
|
|
160
|
+
const where = record ? `neither in zodScalars nor in ${record}'s scalarSchemas` : "not in zodScalars, and no scalarSchemas is set";
|
|
161
|
+
throw new Error(`@nxgt/graphql-codegen-zod: the scalar ${type.name} is ${where}. Map it: zodScalars: { ${type.name}: './module#export' }, or set scalarSchemas: '@nxgt/graphql-scalars'.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function recordKeys(module, outputFile) {
|
|
165
|
+
try {
|
|
166
|
+
const base = module.startsWith(".") ? resolve(dirname(resolve(outputFile ?? "index.ts")), module) : createRequire(resolve("package.json")).resolve(module);
|
|
167
|
+
const loaded = await import(pathToFileURL(base).href);
|
|
168
|
+
const record = loaded.scalarSchemas;
|
|
169
|
+
if (typeof record !== "object" || record === null) {
|
|
170
|
+
throw new Error(`@nxgt/graphql-codegen-zod: ${module} exports no scalarSchemas record.`);
|
|
171
|
+
}
|
|
172
|
+
return new Set(Object.keys(record));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof Error && error.message.startsWith("@nxgt/graphql-codegen-zod:"))
|
|
175
|
+
throw error;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/schema-output.ts
|
|
181
|
+
import { constraintsOn } from "@nxgt/graphql-validation/codegen";
|
|
182
|
+
import {
|
|
183
|
+
isEnumType as isEnumType2,
|
|
184
|
+
isInputObjectType as isInputObjectType4,
|
|
185
|
+
isInterfaceType,
|
|
186
|
+
isObjectType
|
|
187
|
+
} from "graphql";
|
|
188
|
+
|
|
189
|
+
// src/cycles.ts
|
|
190
|
+
import {
|
|
191
|
+
getNamedType,
|
|
192
|
+
isInputObjectType
|
|
193
|
+
} from "graphql";
|
|
194
|
+
function inputCycles(schema) {
|
|
195
|
+
const inputs = Object.values(schema.getTypeMap()).filter(isInputObjectType);
|
|
196
|
+
const cyclic = new Set;
|
|
197
|
+
for (const start of inputs) {
|
|
198
|
+
if (reaches(start, start.name, new Set))
|
|
199
|
+
cyclic.add(start.name);
|
|
200
|
+
}
|
|
201
|
+
return cyclic;
|
|
202
|
+
}
|
|
203
|
+
function reaches(from, target, seen) {
|
|
204
|
+
for (const field of Object.values(from.getFields())) {
|
|
205
|
+
const named = getNamedType(field.type);
|
|
206
|
+
if (!isInputObjectType(named))
|
|
207
|
+
continue;
|
|
208
|
+
if (named.name === target)
|
|
209
|
+
return true;
|
|
210
|
+
if (seen.has(named.name))
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(named.name);
|
|
213
|
+
if (reaches(named, target, seen))
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/defaults.ts
|
|
220
|
+
import * as graphql from "graphql";
|
|
221
|
+
import {
|
|
222
|
+
astFromValue,
|
|
223
|
+
GraphQLID,
|
|
224
|
+
getNamedType as getNamedType2,
|
|
225
|
+
getNullableType,
|
|
226
|
+
isInputObjectType as isInputObjectType2,
|
|
227
|
+
isListType,
|
|
228
|
+
isNonNullType,
|
|
229
|
+
isScalarType,
|
|
230
|
+
isSpecifiedScalarType,
|
|
231
|
+
Kind,
|
|
232
|
+
valueFromASTUntyped
|
|
233
|
+
} from "graphql";
|
|
234
|
+
function parsedDefault(literal, type, where) {
|
|
235
|
+
assertExact(literal, type, where);
|
|
236
|
+
return coerced(valueFromASTUntyped(literal), type);
|
|
237
|
+
}
|
|
238
|
+
function assertExact(literal, type, where) {
|
|
239
|
+
const nullable = getNullableType(type);
|
|
240
|
+
if (isListType(nullable)) {
|
|
241
|
+
const items = literal.kind === Kind.LIST ? literal.values : [literal];
|
|
242
|
+
for (const item of items)
|
|
243
|
+
assertExact(item, nullable.ofType, where);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (isInputObjectType2(nullable)) {
|
|
247
|
+
if (literal.kind !== Kind.OBJECT)
|
|
248
|
+
return;
|
|
249
|
+
const fields = nullable.getFields();
|
|
250
|
+
for (const field of literal.fields) {
|
|
251
|
+
const definition = fields[field.name.value];
|
|
252
|
+
if (definition)
|
|
253
|
+
assertExact(field.value, definition.type, where);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const keeps = nullable === GraphQLID || isScalarType(nullable) && !isSpecifiedScalarType(nullable);
|
|
258
|
+
if (keeps && literal.kind === Kind.INT && !Number.isSafeInteger(Number(literal.value)))
|
|
259
|
+
throw new Error(`@nxgt/graphql-codegen-zod: the default of ${where} holds ${literal.value}, which a JavaScript number cannot keep exact. Write it as a string, if its scalar takes one.`);
|
|
260
|
+
}
|
|
261
|
+
function defaultLiteral(input) {
|
|
262
|
+
if (input.astNode)
|
|
263
|
+
return input.astNode.defaultValue;
|
|
264
|
+
const loose = input;
|
|
265
|
+
if (loose.default?.literal)
|
|
266
|
+
return loose.default.literal;
|
|
267
|
+
if (loose.default) {
|
|
268
|
+
const toLiteral = graphql.valueToLiteral;
|
|
269
|
+
if (loose.default.value === undefined || !toLiteral)
|
|
270
|
+
return;
|
|
271
|
+
return toLiteral(loose.default.value, input.type);
|
|
272
|
+
}
|
|
273
|
+
if (loose.defaultValue === undefined)
|
|
274
|
+
return;
|
|
275
|
+
return astFromValue(loose.defaultValue, input.type) ?? undefined;
|
|
276
|
+
}
|
|
277
|
+
function coerced(value, type) {
|
|
278
|
+
const nullable = isNonNullType(type) ? type.ofType : type;
|
|
279
|
+
if (value === null || value === undefined)
|
|
280
|
+
return value;
|
|
281
|
+
if (isListType(nullable)) {
|
|
282
|
+
const items = Array.isArray(value) ? value : [value];
|
|
283
|
+
return items.map((item) => coerced(item, nullable.ofType));
|
|
284
|
+
}
|
|
285
|
+
if (isInputObjectType2(nullable) && typeof value === "object") {
|
|
286
|
+
const fields = nullable.getFields();
|
|
287
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => {
|
|
288
|
+
const definition = fields[key];
|
|
289
|
+
return [key, definition ? coerced(field, definition.type) : field];
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
if (getNamedType2(nullable) === GraphQLID && typeof value === "number")
|
|
293
|
+
return String(value);
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/source.ts
|
|
298
|
+
function docComment(description, indent = "") {
|
|
299
|
+
if (!description)
|
|
300
|
+
return "";
|
|
301
|
+
const lines = description.replaceAll(`\r
|
|
302
|
+
`, `
|
|
303
|
+
`).replaceAll("*/", "*\\/").split(`
|
|
304
|
+
`);
|
|
305
|
+
if (lines.length === 1)
|
|
306
|
+
return `${indent}/** ${lines[0]} */
|
|
307
|
+
`;
|
|
308
|
+
return `${indent}/**
|
|
309
|
+
${lines.map((line) => `${indent} * ${line}`.trimEnd()).join(`
|
|
310
|
+
`)}
|
|
311
|
+
${indent} */
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function objectMembers(members) {
|
|
315
|
+
return members.map(([name, { code, lazy, description }]) => {
|
|
316
|
+
const doc = docComment(description, "\t");
|
|
317
|
+
return lazy ? `${doc} get ${name}() {
|
|
318
|
+
return ${code};
|
|
319
|
+
},` : `${doc} ${name}: ${code},`;
|
|
320
|
+
}).join(`
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
function declare(typeName, naming, code, side, description) {
|
|
324
|
+
const schemaName = naming.schema(typeName);
|
|
325
|
+
const doc = docComment(description);
|
|
326
|
+
return `${doc}export const ${schemaName} = ${code};
|
|
327
|
+
${doc}export type ${typeName} = z.${side}<typeof ${schemaName}>;`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/type-code.ts
|
|
331
|
+
import {
|
|
332
|
+
GraphQLBoolean,
|
|
333
|
+
GraphQLFloat,
|
|
334
|
+
GraphQLID as GraphQLID2,
|
|
335
|
+
GraphQLInt,
|
|
336
|
+
GraphQLString,
|
|
337
|
+
getNamedType as getNamedType3,
|
|
338
|
+
isEnumType,
|
|
339
|
+
isInputObjectType as isInputObjectType3,
|
|
340
|
+
isListType as isListType2,
|
|
341
|
+
isNonNullType as isNonNullType2
|
|
342
|
+
} from "graphql";
|
|
343
|
+
var builtIns = {
|
|
344
|
+
[GraphQLString.name]: "string",
|
|
345
|
+
[GraphQLID2.name]: "string",
|
|
346
|
+
[GraphQLInt.name]: "number",
|
|
347
|
+
[GraphQLFloat.name]: "number",
|
|
348
|
+
[GraphQLBoolean.name]: "boolean"
|
|
349
|
+
};
|
|
350
|
+
function declareCyclic(type, code, writer, naming, cyclic) {
|
|
351
|
+
const types = new TypeCode(writer, naming, cyclic);
|
|
352
|
+
const output = naming.type(type.name);
|
|
353
|
+
const input = `${output}Input`;
|
|
354
|
+
const doc = docComment(type.description);
|
|
355
|
+
return [
|
|
356
|
+
`${doc}export type ${output} = ${types.object(type, "output")};`,
|
|
357
|
+
`${doc}export type ${input} = ${types.object(type, "input")};`,
|
|
358
|
+
`${doc}export const ${naming.schema(output)}: z.ZodType<${output}, ${input}> = ${code};`
|
|
359
|
+
].join(`
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
class TypeCode {
|
|
364
|
+
#writer;
|
|
365
|
+
#naming;
|
|
366
|
+
#cyclic;
|
|
367
|
+
constructor(writer, naming, cyclic) {
|
|
368
|
+
this.#writer = writer;
|
|
369
|
+
this.#naming = naming;
|
|
370
|
+
this.#cyclic = cyclic;
|
|
371
|
+
}
|
|
372
|
+
object(type, side) {
|
|
373
|
+
const fields = Object.values(type.getFields());
|
|
374
|
+
if (type.isOneOf)
|
|
375
|
+
return fields.map((field) => `{ ${field.name}: ${this.#required(field.type, side)} }`).join(" | ");
|
|
376
|
+
const members = fields.map((field) => `${docComment(field.description, "\t")} ${this.#member(field, side)};`);
|
|
377
|
+
return `{
|
|
378
|
+
${members.join(`
|
|
379
|
+
`)}
|
|
380
|
+
}`;
|
|
381
|
+
}
|
|
382
|
+
#member(field, side) {
|
|
383
|
+
const nullable = !isNonNullType2(field.type);
|
|
384
|
+
const filled = defaultLiteral(field) !== undefined;
|
|
385
|
+
const type = this.#required(field.type, side);
|
|
386
|
+
if (!nullable)
|
|
387
|
+
return side === "input" && filled ? `${field.name}?: ${type} | undefined` : `${field.name}: ${type}`;
|
|
388
|
+
if (side === "output" && filled)
|
|
389
|
+
return `${field.name}: ${type} | null`;
|
|
390
|
+
return `${field.name}?: ${type} | null | undefined`;
|
|
391
|
+
}
|
|
392
|
+
#required(type, side) {
|
|
393
|
+
if (isNonNullType2(type))
|
|
394
|
+
return this.#required(type.ofType, side);
|
|
395
|
+
if (!isListType2(type))
|
|
396
|
+
return this.#named(type, side);
|
|
397
|
+
const item = isNonNullType2(type.ofType) ? this.#required(type.ofType, side) : `${this.#required(type.ofType, side)} | null | undefined`;
|
|
398
|
+
const list = `Array<${item}>`;
|
|
399
|
+
return side === "input" ? `${this.#named(getNamedType3(type), side)} | ${list}` : list;
|
|
400
|
+
}
|
|
401
|
+
#named(type, side) {
|
|
402
|
+
const builtIn = builtIns[type.name];
|
|
403
|
+
if (builtIn)
|
|
404
|
+
return builtIn;
|
|
405
|
+
if (isEnumType(type))
|
|
406
|
+
return `z.output<typeof ${this.#writer.schemaName(type)}>`;
|
|
407
|
+
if (isInputObjectType3(type)) {
|
|
408
|
+
const name = this.#naming.type(type.name);
|
|
409
|
+
if (this.#cyclic.has(type.name))
|
|
410
|
+
return side === "output" ? name : `${name}Input`;
|
|
411
|
+
return `z.${side}<typeof ${this.#writer.schemaName(type)}>`;
|
|
412
|
+
}
|
|
413
|
+
return `z.${side}<typeof ${this.#writer.scalarCode(type)}>`;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/schema-output.ts
|
|
418
|
+
function schemaBlocks(schema, writer, naming) {
|
|
419
|
+
const types = Object.values(schema.getTypeMap()).filter((type) => !type.name.startsWith("__"));
|
|
420
|
+
const cyclic = inputCycles(schema);
|
|
421
|
+
return [
|
|
422
|
+
...types.filter(isEnumType2).map((type) => enumBlock(type, naming)),
|
|
423
|
+
...types.filter(isInputObjectType4).map((type) => inputBlock(type, writer, naming, cyclic)),
|
|
424
|
+
...types.flatMap((type) => {
|
|
425
|
+
if (!isObjectType(type) && !isInterfaceType(type))
|
|
426
|
+
return [];
|
|
427
|
+
return Object.values(type.getFields()).filter((field) => field.args.length > 0).map((field) => {
|
|
428
|
+
const name = naming.args(type.name, field.name);
|
|
429
|
+
const members = field.args.map((arg) => [
|
|
430
|
+
arg.name,
|
|
431
|
+
{
|
|
432
|
+
...writer.value(arg.type, constraintsOn(writer.directive, arg.astNode), `${type.name}.${field.name}(${arg.name}:)`, defaultLiteral(arg)),
|
|
433
|
+
description: arg.description
|
|
434
|
+
}
|
|
435
|
+
]);
|
|
436
|
+
return declare(name, naming, `z.object({
|
|
437
|
+
${objectMembers(members)}
|
|
438
|
+
})`, "output");
|
|
439
|
+
});
|
|
440
|
+
})
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
function enumBlock(type, naming) {
|
|
444
|
+
const values = type.getValues().map((value) => JSON.stringify(value.name));
|
|
445
|
+
return declare(naming.type(type.name), naming, `z.enum([${values.join(", ")}])`, "output", type.description);
|
|
446
|
+
}
|
|
447
|
+
function inputBlock(type, writer, naming, cyclic) {
|
|
448
|
+
const code = inputCode(type, writer);
|
|
449
|
+
return cyclic.has(type.name) ? declareCyclic(type, code, writer, naming, cyclic) : declare(naming.type(type.name), naming, code, "output", type.description);
|
|
450
|
+
}
|
|
451
|
+
function inputCode(type, writer) {
|
|
452
|
+
const fields = Object.values(type.getFields());
|
|
453
|
+
const constraints = (field) => constraintsOn(writer.directive, field.astNode);
|
|
454
|
+
const where = (field) => `${type.name}.${field.name}`;
|
|
455
|
+
if (type.isOneOf) {
|
|
456
|
+
const members = fields.map((field) => {
|
|
457
|
+
const member = {
|
|
458
|
+
...writer.required(field.type, constraints(field), where(field)),
|
|
459
|
+
description: field.description
|
|
460
|
+
};
|
|
461
|
+
return ` z.strictObject({
|
|
462
|
+
${indent(objectMembers([[field.name, member]]))}
|
|
463
|
+
})`;
|
|
464
|
+
});
|
|
465
|
+
return `z.union([
|
|
466
|
+
${members.join(`,
|
|
467
|
+
`)},
|
|
468
|
+
])`;
|
|
469
|
+
}
|
|
470
|
+
const members = fields.map((field) => [
|
|
471
|
+
field.name,
|
|
472
|
+
{
|
|
473
|
+
...writer.value(field.type, constraints(field), where(field), defaultLiteral(field)),
|
|
474
|
+
description: field.description
|
|
475
|
+
}
|
|
476
|
+
]);
|
|
477
|
+
return `z.strictObject({
|
|
478
|
+
${objectMembers(members)}
|
|
479
|
+
})`;
|
|
480
|
+
}
|
|
481
|
+
function indent(code) {
|
|
482
|
+
return code.split(`
|
|
483
|
+
`).map((line) => ` ${line}`).join(`
|
|
484
|
+
`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/variables-output.ts
|
|
488
|
+
import {
|
|
489
|
+
constraintsOn as constraintsOn2
|
|
490
|
+
} from "@nxgt/graphql-validation/codegen";
|
|
491
|
+
import {
|
|
492
|
+
getNamedType as getNamedType4,
|
|
493
|
+
isInputObjectType as isInputObjectType5,
|
|
494
|
+
isListType as isListType3,
|
|
495
|
+
isNonNullType as isNonNullType3,
|
|
496
|
+
Kind as Kind2,
|
|
497
|
+
TypeInfo,
|
|
498
|
+
typeFromAST,
|
|
499
|
+
visit,
|
|
500
|
+
visitWithTypeInfo
|
|
501
|
+
} from "graphql";
|
|
502
|
+
function variablesBlocks(schema, documents, writer, naming) {
|
|
503
|
+
const definitions = documents.flatMap((file) => file.document?.definitions ?? []);
|
|
504
|
+
const fragments = new Map(definitions.filter((node) => node.kind === Kind2.FRAGMENT_DEFINITION).map((node) => [node.name.value, node]));
|
|
505
|
+
return definitions.filter((node) => node.kind === Kind2.OPERATION_DEFINITION).flatMap((operation) => {
|
|
506
|
+
const operationName = operation.name?.value;
|
|
507
|
+
if (!operationName)
|
|
508
|
+
return [];
|
|
509
|
+
const used = usages(schema, writer.directive, operation, fragments);
|
|
510
|
+
const members = (operation.variableDefinitions ?? []).map((definition) => {
|
|
511
|
+
const variable = definition.variable.name.value;
|
|
512
|
+
const type = typeFromAST(schema, definition.type);
|
|
513
|
+
if (!type) {
|
|
514
|
+
throw new Error(`@nxgt/graphql-codegen-zod: ${operationName}'s $${variable} has a type the schema does not define.`);
|
|
515
|
+
}
|
|
516
|
+
const where = `${operationName}($${variable}:)`;
|
|
517
|
+
const constraints = merged(used.get(variable) ?? [], where);
|
|
518
|
+
return [
|
|
519
|
+
variable,
|
|
520
|
+
writer.value(type, constraints, where, definition.defaultValue)
|
|
521
|
+
];
|
|
522
|
+
});
|
|
523
|
+
return [
|
|
524
|
+
declare(naming.variables(operationName, operation.operation), naming, `z.object({
|
|
525
|
+
${objectMembers(members)}
|
|
526
|
+
})`, "input")
|
|
527
|
+
];
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function usages(schema, directive, operation, fragments) {
|
|
531
|
+
const walk = {
|
|
532
|
+
found: new Map,
|
|
533
|
+
variables: new Set((operation.variableDefinitions ?? []).map((d) => d.variable.name.value)),
|
|
534
|
+
directive
|
|
535
|
+
};
|
|
536
|
+
const seen = new Set;
|
|
537
|
+
const visitNode = (node) => {
|
|
538
|
+
const typeInfo = new TypeInfo(schema);
|
|
539
|
+
visit(node, visitWithTypeInfo(typeInfo, {
|
|
540
|
+
Argument(argument) {
|
|
541
|
+
const definition = typeInfo.getArgument();
|
|
542
|
+
const field = typeInfo.getFieldDef();
|
|
543
|
+
if (!definition || !field || typeInfo.getDirective() !== null)
|
|
544
|
+
return;
|
|
545
|
+
const where = `${typeInfo.getParentType()?.name}.${field.name}(${definition.name}:)`;
|
|
546
|
+
walkValue(walk, argument.value, definition.type, constraintsOn2(directive, definition.astNode), where);
|
|
547
|
+
},
|
|
548
|
+
FragmentSpread(spread) {
|
|
549
|
+
const fragment = fragments.get(spread.name.value);
|
|
550
|
+
if (fragment && !seen.has(fragment.name.value)) {
|
|
551
|
+
seen.add(fragment.name.value);
|
|
552
|
+
visitNode(fragment);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}));
|
|
556
|
+
};
|
|
557
|
+
visitNode(operation);
|
|
558
|
+
return walk.found;
|
|
559
|
+
}
|
|
560
|
+
function walkValue(walk, value, type, constraints, where) {
|
|
561
|
+
if (value.kind === Kind2.VARIABLE) {
|
|
562
|
+
if (!walk.variables.has(value.name.value))
|
|
563
|
+
return;
|
|
564
|
+
const list = walk.found.get(value.name.value) ?? [];
|
|
565
|
+
list.push({ where, constraints });
|
|
566
|
+
walk.found.set(value.name.value, list);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const nullable = isNonNullType3(type) ? type.ofType : type;
|
|
570
|
+
if (value.kind === Kind2.LIST && isListType3(nullable)) {
|
|
571
|
+
const items = constraints.filter(({ rule }) => rule.target !== "list");
|
|
572
|
+
for (const item of value.values)
|
|
573
|
+
walkValue(walk, item, nullable.ofType, items, where);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const named = getNamedType4(type);
|
|
577
|
+
if (value.kind === Kind2.OBJECT && isInputObjectType5(named)) {
|
|
578
|
+
for (const field of value.fields) {
|
|
579
|
+
const definition = named.getFields()[field.name.value];
|
|
580
|
+
if (definition) {
|
|
581
|
+
walkValue(walk, field.value, definition.type, constraintsOn2(walk.directive, definition.astNode), `${named.name}.${definition.name}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function merged(usages, where) {
|
|
587
|
+
const all = [];
|
|
588
|
+
for (const usage of usages) {
|
|
589
|
+
for (const constraint of usage.constraints) {
|
|
590
|
+
const same = all.some((known) => known.rule === constraint.rule && known.value === constraint.value);
|
|
591
|
+
if (!same)
|
|
592
|
+
all.push({ ...constraint, at: usage.where });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const bases = all.filter(({ rule }) => rule.base);
|
|
596
|
+
const [first, second] = bases;
|
|
597
|
+
if (first && second) {
|
|
598
|
+
throw new Error(`@nxgt/graphql-codegen-zod: ${where} is passed to ${first.at} (${first.rule.argument}: ${JSON.stringify(first.value)}) and to ${second.at} (${second.rule.argument}: ${JSON.stringify(second.value)}), whose schemas cannot both apply. Use one variable for each.`);
|
|
599
|
+
}
|
|
600
|
+
return [...bases, ...all.filter(({ rule }) => !rule.base)].map(({ rule, value }) => ({ rule, value }));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/writer.ts
|
|
604
|
+
import {
|
|
605
|
+
inputCode as inputCode2
|
|
606
|
+
} from "@nxgt/graphql-validation/codegen";
|
|
607
|
+
import {
|
|
608
|
+
GraphQLNonNull,
|
|
609
|
+
getNamedType as getNamedType5,
|
|
610
|
+
isEnumType as isEnumType3,
|
|
611
|
+
isInputObjectType as isInputObjectType6,
|
|
612
|
+
isNonNullType as isNonNullType4,
|
|
613
|
+
isScalarType as isScalarType2,
|
|
614
|
+
isSpecifiedScalarType as isSpecifiedScalarType2
|
|
615
|
+
} from "graphql";
|
|
616
|
+
class Writer {
|
|
617
|
+
directive;
|
|
618
|
+
#naming;
|
|
619
|
+
#scalars;
|
|
620
|
+
constructor(directive, naming, scalars) {
|
|
621
|
+
this.directive = directive;
|
|
622
|
+
this.#naming = naming;
|
|
623
|
+
this.#scalars = scalars;
|
|
624
|
+
}
|
|
625
|
+
scalarCode(type) {
|
|
626
|
+
return this.#scalars.code(type);
|
|
627
|
+
}
|
|
628
|
+
schemaName(type) {
|
|
629
|
+
return this.#naming.schema(this.#naming.type(type.name));
|
|
630
|
+
}
|
|
631
|
+
value(type, constraints, where, defaultValue) {
|
|
632
|
+
let lazy = false;
|
|
633
|
+
const named = (named) => {
|
|
634
|
+
if (isInputObjectType6(named)) {
|
|
635
|
+
lazy = true;
|
|
636
|
+
return this.schemaName(named);
|
|
637
|
+
}
|
|
638
|
+
if (isEnumType3(named))
|
|
639
|
+
return this.schemaName(named);
|
|
640
|
+
return this.#scalars.code(named);
|
|
641
|
+
};
|
|
642
|
+
if (!defaultValue)
|
|
643
|
+
return {
|
|
644
|
+
code: inputCode2(type, constraints, where, named, {
|
|
645
|
+
list: singleOrList
|
|
646
|
+
}),
|
|
647
|
+
lazy
|
|
648
|
+
};
|
|
649
|
+
const nullable = !isNonNullType4(type);
|
|
650
|
+
const required = inputCode2(nullable ? new GraphQLNonNull(type) : type, constraints, where, named, { list: singleOrList });
|
|
651
|
+
const value = JSON.stringify(parsedDefault(defaultValue, type, where));
|
|
652
|
+
return {
|
|
653
|
+
code: `${required}.prefault(${value})${nullable ? ".nullable()" : ""}`,
|
|
654
|
+
lazy
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
required(type, constraints, where) {
|
|
658
|
+
return this.value(isNonNullType4(type) ? type : new GraphQLNonNull(type), constraints, where);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
var singleOrList = ({
|
|
662
|
+
type,
|
|
663
|
+
code,
|
|
664
|
+
single
|
|
665
|
+
}) => {
|
|
666
|
+
const named = getNamedType5(type);
|
|
667
|
+
const transforms = isInputObjectType6(named) || isScalarType2(named) && !isSpecifiedScalarType2(named);
|
|
668
|
+
const first = transforms ? `z.custom<z.input<typeof ${single}>>((value) => value != null && !Array.isArray(value))` : single;
|
|
669
|
+
return `z.union([${code}, ${first}.transform((value): unknown[] => [value]).pipe(${code})])`;
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/index.ts
|
|
673
|
+
async function plugin(schema, documents, config = {}, info) {
|
|
674
|
+
const directive = checkConstraints(schema);
|
|
675
|
+
if (directive)
|
|
676
|
+
assertSdl(schema);
|
|
677
|
+
const naming = new Naming(config);
|
|
678
|
+
const write = async (imports) => {
|
|
679
|
+
const scalars = await ScalarSources.load(config, imports, info?.outputFile);
|
|
680
|
+
const writer = new Writer(directive, naming, scalars);
|
|
681
|
+
return [
|
|
682
|
+
...schemaBlocks(schema, writer, naming),
|
|
683
|
+
...variablesBlocks(schema, documents, writer, naming)
|
|
684
|
+
];
|
|
685
|
+
};
|
|
686
|
+
const declarations = (await write(new Imports)).flatMap((block) => [...block.matchAll(/^export (const|type) (\w+)/gm)].map((m) => `${m[1]} ${m[2]}`));
|
|
687
|
+
const twice = declarations.find((declaration, index) => declarations.indexOf(declaration) !== index);
|
|
688
|
+
if (twice)
|
|
689
|
+
throw new Error(`@nxgt/graphql-codegen-zod: the file would declare ${twice.split(" ")[1]} twice: two GraphQL names, or an input type in a cycle and its Input type, give the same name. Rename one of the GraphQL types, or set typesSuffix.`);
|
|
690
|
+
const declared = declarations.map((declaration) => declaration.split(" ")[1] ?? "");
|
|
691
|
+
const imports = new Imports(declared);
|
|
692
|
+
const blocks = await write(imports);
|
|
693
|
+
return `${imports.lines().join(`
|
|
694
|
+
`)}
|
|
695
|
+
|
|
696
|
+
${blocks.join(`
|
|
697
|
+
|
|
698
|
+
`)}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
function assertSdl(schema) {
|
|
702
|
+
for (const type of Object.values(schema.getTypeMap())) {
|
|
703
|
+
if (type.name.startsWith("__"))
|
|
704
|
+
continue;
|
|
705
|
+
const inputs = isInputObjectType7(type) ? Object.values(type.getFields()) : isObjectType2(type) || isInterfaceType2(type) ? Object.values(type.getFields()).flatMap((field) => field.args) : [];
|
|
706
|
+
const bare = inputs.find((input) => !input.astNode);
|
|
707
|
+
if (bare) {
|
|
708
|
+
throw new Error(`@nxgt/graphql-codegen-zod: the schema declares @constraint, but ${type.name} has no SDL to read it from (an introspected schema?). Point codegen's schema at the SDL files.`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
export {
|
|
713
|
+
plugin
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
//# debugId=AA4EFD5F1FAF8A9864756E2164756E21
|
|
717
|
+
//# sourceMappingURL=index.js.map
|