@dudousxd/nestjs-codegen 0.2.1 → 0.4.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/CHANGELOG.md +57 -0
- package/dist/cli/main.cjs +1229 -1612
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +1211 -1594
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.cjs +12 -2
- package/dist/extension/index.cjs.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +10 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-BwIRjOQA.d.cts → index-DA4uySjo.d.cts} +84 -41
- package/dist/{index-BwIRjOQA.d.ts → index-DA4uySjo.d.ts} +84 -41
- package/dist/index.cjs +1073 -1460
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -15
- package/dist/index.d.ts +56 -15
- package/dist/index.js +1043 -1430
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +934 -1365
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +9 -2
- package/dist/nest/index.d.ts +9 -2
- package/dist/nest/index.js +921 -1352
- package/dist/nest/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -22,112 +22,9 @@ var CodegenError = class extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
// src/adapters/zod.ts
|
|
26
|
-
function toObjectKey(name) {
|
|
27
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
28
|
-
}
|
|
29
|
-
function messageSuffix(messageRaw2) {
|
|
30
|
-
return messageRaw2 ? `{ message: ${messageRaw2} }` : "";
|
|
31
|
-
}
|
|
32
|
-
function renderStringChecks(checks) {
|
|
33
|
-
let out = "";
|
|
34
|
-
for (const c of checks) {
|
|
35
|
-
switch (c.check) {
|
|
36
|
-
case "email":
|
|
37
|
-
out += `.email(${messageSuffix(c.messageRaw)})`;
|
|
38
|
-
break;
|
|
39
|
-
case "url":
|
|
40
|
-
out += `.url(${messageSuffix(c.messageRaw)})`;
|
|
41
|
-
break;
|
|
42
|
-
case "uuid":
|
|
43
|
-
out += `.uuid(${messageSuffix(c.messageRaw)})`;
|
|
44
|
-
break;
|
|
45
|
-
case "regex":
|
|
46
|
-
out += `.regex(${c.pattern})`;
|
|
47
|
-
break;
|
|
48
|
-
case "min":
|
|
49
|
-
out += `.min(${c.value})`;
|
|
50
|
-
break;
|
|
51
|
-
case "max":
|
|
52
|
-
out += `.max(${c.value})`;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
function render(node, ctx) {
|
|
59
|
-
switch (node.kind) {
|
|
60
|
-
case "string":
|
|
61
|
-
return `z.string()${renderStringChecks(node.checks)}`;
|
|
62
|
-
case "number": {
|
|
63
|
-
let out = "z.number()";
|
|
64
|
-
for (const c of node.checks) {
|
|
65
|
-
if (c.check === "int") out += ".int()";
|
|
66
|
-
else if (c.check === "positive") out += ".positive()";
|
|
67
|
-
else if (c.check === "negative") out += ".negative()";
|
|
68
|
-
else if (c.check === "min") out += `.min(${c.value})`;
|
|
69
|
-
else if (c.check === "max") out += `.max(${c.value})`;
|
|
70
|
-
}
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
case "boolean":
|
|
74
|
-
return "z.boolean()";
|
|
75
|
-
case "date":
|
|
76
|
-
return "z.coerce.date()";
|
|
77
|
-
case "unknown":
|
|
78
|
-
return node.note ? `z.unknown() /* ${node.note} */` : "z.unknown()";
|
|
79
|
-
case "instanceof":
|
|
80
|
-
return `z.instanceof(${node.ctor})`;
|
|
81
|
-
case "enum":
|
|
82
|
-
return `z.enum([${node.literals.join(", ")}])`;
|
|
83
|
-
case "literal":
|
|
84
|
-
return `z.literal(${node.raw})`;
|
|
85
|
-
case "union":
|
|
86
|
-
return `z.union([${node.options.map((o) => render(o, ctx)).join(", ")}])`;
|
|
87
|
-
case "object": {
|
|
88
|
-
if (node.fields.length === 0) {
|
|
89
|
-
return node.passthrough ? "z.object({}).passthrough()" : "z.object({})";
|
|
90
|
-
}
|
|
91
|
-
const inner = node.fields.map((f) => `${toObjectKey(f.key)}: ${render(f.value, ctx)}`).join(", ");
|
|
92
|
-
return `z.object({ ${inner} })${node.passthrough ? ".passthrough()" : ""}`;
|
|
93
|
-
}
|
|
94
|
-
case "array":
|
|
95
|
-
return `z.array(${render(node.element, ctx)})`;
|
|
96
|
-
case "optional":
|
|
97
|
-
return `${render(node.inner, ctx)}.optional()`;
|
|
98
|
-
case "ref":
|
|
99
|
-
return node.name;
|
|
100
|
-
case "lazyRef":
|
|
101
|
-
return `z.lazy(() => ${node.name})`;
|
|
102
|
-
case "annotated": {
|
|
103
|
-
const comments = node.unmappable.map((n) => `/* @${n}: not translatable to zod (server-only) */`).join(" ");
|
|
104
|
-
return `${render(node.inner, ctx)} ${comments}`;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
var zodAdapter = {
|
|
109
|
-
name: "zod",
|
|
110
|
-
importStatements(usage) {
|
|
111
|
-
return usage.used ? ["import { z } from 'zod';"] : [];
|
|
112
|
-
},
|
|
113
|
-
render,
|
|
114
|
-
inferType(schemaConst) {
|
|
115
|
-
return `z.infer<typeof ${schemaConst}>`;
|
|
116
|
-
},
|
|
117
|
-
renderModule(mod) {
|
|
118
|
-
const ctx = { named: mod.named };
|
|
119
|
-
const namedNestedSchemas = /* @__PURE__ */ new Map();
|
|
120
|
-
for (const [name, node] of mod.named) {
|
|
121
|
-
namedNestedSchemas.set(name, render(node, ctx));
|
|
122
|
-
}
|
|
123
|
-
return { schemaText: render(mod.root, ctx), namedNestedSchemas, warnings: mod.warnings };
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
25
|
// src/adapters/registry.ts
|
|
128
26
|
function resolveAdapter(option) {
|
|
129
27
|
if (typeof option !== "string") return option;
|
|
130
|
-
if (option === "zod") return zodAdapter;
|
|
131
28
|
const pkg = `@dudousxd/nestjs-codegen-${option}`;
|
|
132
29
|
const named = `${option}Adapter`;
|
|
133
30
|
throw new ConfigError(
|
|
@@ -180,8 +77,21 @@ If this is intentional, move the file inside your project directory.`
|
|
|
180
77
|
function resolveConfig(userConfig, cwd) {
|
|
181
78
|
return applyDefaults(userConfig, cwd ?? process.cwd());
|
|
182
79
|
}
|
|
80
|
+
function validateUserConfig(userConfig) {
|
|
81
|
+
if (userConfig.validation == null) {
|
|
82
|
+
throw new ConfigError(
|
|
83
|
+
"validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
87
|
+
throw new ConfigError(
|
|
88
|
+
"Config validation failed: `pages.glob` must be a string when `pages` is set"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
183
92
|
function applyDefaults(userConfig, cwd) {
|
|
184
|
-
|
|
93
|
+
validateUserConfig(userConfig);
|
|
94
|
+
const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
|
|
185
95
|
const resolvedCwd = userConfig.codegen?.cwd ? resolveAbsolute(cwd, userConfig.codegen.cwd) : cwd;
|
|
186
96
|
let app = null;
|
|
187
97
|
if (userConfig.app) {
|
|
@@ -199,7 +109,8 @@ function applyDefaults(userConfig, cwd) {
|
|
|
199
109
|
}
|
|
200
110
|
return {
|
|
201
111
|
extensions: userConfig.extensions ?? [],
|
|
202
|
-
|
|
112
|
+
// Non-null: validateUserConfig() above throws when `validation` is absent.
|
|
113
|
+
validation: resolveAdapter(userConfig.validation),
|
|
203
114
|
pages: userConfig.pages ? {
|
|
204
115
|
glob: userConfig.pages.glob,
|
|
205
116
|
propsExport: userConfig.pages.propsExport ?? "ComponentProps",
|
|
@@ -253,18 +164,12 @@ Run \`nestjs-codegen init\` to create a starter config.`
|
|
|
253
164
|
`Config file must have a default export. Did you forget \`export default defineConfig({...})\`? (${configPath})`
|
|
254
165
|
);
|
|
255
166
|
}
|
|
256
|
-
if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
|
|
257
|
-
throw new ConfigError(
|
|
258
|
-
`Config validation failed: \`pages.glob\` must be a string when \`pages\` is set (${configPath})`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
167
|
return applyDefaults(userConfig, resolvedCwd);
|
|
262
168
|
}
|
|
263
169
|
|
|
264
170
|
// src/generate.ts
|
|
265
171
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
266
|
-
import { dirname as dirname2, join as
|
|
267
|
-
import { Project as Project2 } from "ts-morph";
|
|
172
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
268
173
|
|
|
269
174
|
// src/discovery/pages.ts
|
|
270
175
|
import { readFile } from "fs/promises";
|
|
@@ -319,7 +224,8 @@ function extractPropsSource(source, exportName) {
|
|
|
319
224
|
}
|
|
320
225
|
|
|
321
226
|
// src/discovery/shared-props.ts
|
|
322
|
-
import {
|
|
227
|
+
import { join as join3 } from "path";
|
|
228
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
323
229
|
function discoverSharedProps(project, moduleEntry) {
|
|
324
230
|
try {
|
|
325
231
|
let sourceFile = project.getSourceFile(moduleEntry);
|
|
@@ -339,6 +245,31 @@ function discoverSharedProps(project, moduleEntry) {
|
|
|
339
245
|
return null;
|
|
340
246
|
}
|
|
341
247
|
}
|
|
248
|
+
function discoverSharedPropsFromConfig(config) {
|
|
249
|
+
if (!config.app?.moduleEntry) return null;
|
|
250
|
+
try {
|
|
251
|
+
const tsconfigPath = config.app.tsconfig ?? join3(config.codegen.cwd, "tsconfig.json");
|
|
252
|
+
let project;
|
|
253
|
+
try {
|
|
254
|
+
project = new Project({
|
|
255
|
+
tsConfigFilePath: tsconfigPath,
|
|
256
|
+
skipAddingFilesFromTsConfig: true,
|
|
257
|
+
skipLoadingLibFiles: true,
|
|
258
|
+
skipFileDependencyResolution: true
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
project = new Project({
|
|
262
|
+
skipAddingFilesFromTsConfig: true,
|
|
263
|
+
skipLoadingLibFiles: true,
|
|
264
|
+
skipFileDependencyResolution: true,
|
|
265
|
+
compilerOptions: { allowJs: true, strict: false }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return discoverSharedProps(project, config.app.moduleEntry);
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
342
273
|
function findForRootCall(sourceFile) {
|
|
343
274
|
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
344
275
|
for (const call of callExpressions) {
|
|
@@ -358,9 +289,9 @@ function findForRootCall(sourceFile) {
|
|
|
358
289
|
function findShareInitializer(forRootCall) {
|
|
359
290
|
if (!Node.isCallExpression(forRootCall)) return null;
|
|
360
291
|
const args = forRootCall.getArguments();
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
363
|
-
for (const prop of
|
|
292
|
+
const firstArg2 = args[0];
|
|
293
|
+
if (!firstArg2 || !Node.isObjectLiteralExpression(firstArg2)) return null;
|
|
294
|
+
for (const prop of firstArg2.getProperties()) {
|
|
364
295
|
if (Node.isPropertyAssignment(prop) && prop.getName() === "share") {
|
|
365
296
|
return prop.getInitializer() ?? null;
|
|
366
297
|
}
|
|
@@ -459,9 +390,9 @@ function extractFromReturnTypeAnnotation(typeNode) {
|
|
|
459
390
|
const typeName = typeNode.getTypeName();
|
|
460
391
|
if (Node.isIdentifier(typeName) && typeName.getText() === "Promise") {
|
|
461
392
|
const typeArgs = typeNode.getTypeArguments();
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
return extractFromReturnTypeAnnotation(
|
|
393
|
+
const firstArg2 = typeArgs[0];
|
|
394
|
+
if (firstArg2) {
|
|
395
|
+
return extractFromReturnTypeAnnotation(firstArg2);
|
|
465
396
|
}
|
|
466
397
|
return null;
|
|
467
398
|
}
|
|
@@ -567,25 +498,14 @@ function inferExpressionType(node) {
|
|
|
567
498
|
|
|
568
499
|
// src/emit/emit-api.ts
|
|
569
500
|
import { mkdir, writeFile } from "fs/promises";
|
|
570
|
-
import { isAbsolute as isAbsolute2, join as
|
|
501
|
+
import { isAbsolute as isAbsolute2, join as join4, relative as relative3 } from "path";
|
|
571
502
|
|
|
572
503
|
// src/extension/registry.ts
|
|
573
|
-
import { Project } from "ts-morph";
|
|
504
|
+
import { Project as Project2 } from "ts-morph";
|
|
574
505
|
function resolveApiSlots(extensions) {
|
|
575
|
-
let transport;
|
|
576
|
-
let transportOwner;
|
|
577
506
|
let layer;
|
|
578
507
|
let layerOwner;
|
|
579
508
|
for (const ext of extensions) {
|
|
580
|
-
if (ext.apiTransport) {
|
|
581
|
-
if (transport) {
|
|
582
|
-
throw new CodegenError(
|
|
583
|
-
`api transport claimed by both "${transportOwner}" and "${ext.name}" \u2014 only one extension may set apiTransport.`
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
transport = ext.apiTransport;
|
|
587
|
-
transportOwner = ext.name;
|
|
588
|
-
}
|
|
589
509
|
if (ext.apiClientLayer) {
|
|
590
510
|
if (layer) {
|
|
591
511
|
throw new CodegenError(
|
|
@@ -597,11 +517,22 @@ function resolveApiSlots(extensions) {
|
|
|
597
517
|
}
|
|
598
518
|
}
|
|
599
519
|
return {
|
|
600
|
-
...transport ? { transport } : {},
|
|
601
520
|
...layer ? { layer } : {}
|
|
602
521
|
};
|
|
603
522
|
}
|
|
604
523
|
var CORE_FILES = /* @__PURE__ */ new Set(["routes.ts", "api.ts", "forms.ts", "index.d.ts", "pages.d.ts"]);
|
|
524
|
+
function mergeExclusive(target, incoming, {
|
|
525
|
+
owner,
|
|
526
|
+
describe
|
|
527
|
+
}) {
|
|
528
|
+
for (const [key, value] of incoming) {
|
|
529
|
+
const prev = target.get(key);
|
|
530
|
+
if (prev !== void 0) {
|
|
531
|
+
throw new CodegenError(describe(key, prev.owner, owner));
|
|
532
|
+
}
|
|
533
|
+
target.set(key, { value, owner });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
605
536
|
function createExtensionContext(config, getRoutes) {
|
|
606
537
|
let project;
|
|
607
538
|
return {
|
|
@@ -613,7 +544,7 @@ function createExtensionContext(config, getRoutes) {
|
|
|
613
544
|
},
|
|
614
545
|
project() {
|
|
615
546
|
if (!project) {
|
|
616
|
-
project = new
|
|
547
|
+
project = new Project2({
|
|
617
548
|
skipAddingFilesFromTsConfig: true,
|
|
618
549
|
skipLoadingLibFiles: true,
|
|
619
550
|
skipFileDependencyResolution: true,
|
|
@@ -646,29 +577,36 @@ async function collectEmittedFiles(extensions, ctx) {
|
|
|
646
577
|
`Extension "${ext.name}" tried to emit the core-owned file "${file.path}". Core files (${[...CORE_FILES].join(", ")}) cannot be produced by extensions.`
|
|
647
578
|
);
|
|
648
579
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
owners.set(key, ext.name);
|
|
580
|
+
mergeExclusive(owners, [[key, file]], {
|
|
581
|
+
owner: ext.name,
|
|
582
|
+
describe: (_key, prevOwner, owner) => `Output file "${file.path}" is emitted by both "${prevOwner}" and "${owner}". Two extensions cannot write the same file.`
|
|
583
|
+
});
|
|
656
584
|
files.push(file);
|
|
657
585
|
}
|
|
658
586
|
}
|
|
659
587
|
return files;
|
|
660
588
|
}
|
|
661
589
|
|
|
590
|
+
// src/extension/types.ts
|
|
591
|
+
function requestShape(route) {
|
|
592
|
+
const cs = route.contract?.contractSource;
|
|
593
|
+
const isGet = route.method.toUpperCase() === "GET";
|
|
594
|
+
const isQuery = isGet || !!cs?.filterFields?.length;
|
|
595
|
+
const hasBody = !!cs?.bodyRef || cs?.body != null && cs.body !== "never";
|
|
596
|
+
const hasQuery = isGet || !!cs?.queryRef || cs?.query != null && cs.query !== "never";
|
|
597
|
+
return { isGet, isQuery, hasBody, hasQuery };
|
|
598
|
+
}
|
|
599
|
+
|
|
662
600
|
// src/emit/emit-api.ts
|
|
663
601
|
async function emitApi(routes, outDir, opts = {}) {
|
|
664
602
|
await mkdir(outDir, { recursive: true });
|
|
665
603
|
const content = buildApiFile(routes, outDir, opts);
|
|
666
|
-
await writeFile(
|
|
604
|
+
await writeFile(join4(outDir, "api.ts"), content, "utf8");
|
|
667
605
|
}
|
|
668
606
|
function splitName(name) {
|
|
669
607
|
return name.split(".");
|
|
670
608
|
}
|
|
671
|
-
function
|
|
609
|
+
function toObjectKey(segment) {
|
|
672
610
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment)) {
|
|
673
611
|
return segment;
|
|
674
612
|
}
|
|
@@ -775,7 +713,7 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
775
713
|
const pad = " ".repeat(indent);
|
|
776
714
|
const lines = [];
|
|
777
715
|
for (const [key, node] of tree) {
|
|
778
|
-
const objKey =
|
|
716
|
+
const objKey = toObjectKey(key);
|
|
779
717
|
if (node.kind === "leaf") {
|
|
780
718
|
const c = node;
|
|
781
719
|
const method = c.method.toUpperCase();
|
|
@@ -801,15 +739,12 @@ function emitRouterTypeBlock(tree, indent, outDir) {
|
|
|
801
739
|
return lines;
|
|
802
740
|
}
|
|
803
741
|
function buildRequestModel(c) {
|
|
804
|
-
const isGet = c.method.toUpperCase() === "GET";
|
|
805
742
|
const m = c.method.toLowerCase();
|
|
806
743
|
const flat = JSON.stringify(c.name);
|
|
807
744
|
const path = JSON.stringify(c.path);
|
|
808
745
|
const TA = buildRouterTypeAccess(c.name);
|
|
809
746
|
const withParams = hasPathParams(c.params);
|
|
810
|
-
const
|
|
811
|
-
const isQuery = isGet || !!c.contractSource.filterFields?.length;
|
|
812
|
-
const hasQuery = isGet || !!c.contractSource.queryRef || c.contractSource.query != null && c.contractSource.query !== "never";
|
|
747
|
+
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
813
748
|
const fields = [];
|
|
814
749
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
815
750
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -831,7 +766,6 @@ function buildRequestModel(c) {
|
|
|
831
766
|
urlExpr,
|
|
832
767
|
optsExpr,
|
|
833
768
|
responseType: `${TA}['response']`,
|
|
834
|
-
bodyType: `${TA}['body']`,
|
|
835
769
|
queryKeyExpr: `[${flat}, input] as const`
|
|
836
770
|
};
|
|
837
771
|
}
|
|
@@ -879,7 +813,7 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
879
813
|
const pad = " ".repeat(indent);
|
|
880
814
|
const lines = [];
|
|
881
815
|
for (const [key, node] of tree) {
|
|
882
|
-
const objKey =
|
|
816
|
+
const objKey = toObjectKey(key);
|
|
883
817
|
if (node.kind === "branch") {
|
|
884
818
|
lines.push(`${pad}${objKey}: {`);
|
|
885
819
|
lines.push(...emitApiObjectBlock(node.children, indent + 2, p));
|
|
@@ -887,30 +821,28 @@ function emitApiObjectBlock(tree, indent, p) {
|
|
|
887
821
|
continue;
|
|
888
822
|
}
|
|
889
823
|
const req = buildRequestModel(node);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
params: node.params,
|
|
895
|
-
contract: { contractSource: node.contractSource },
|
|
896
|
-
...node.controllerRef ? { controllerRef: node.controllerRef } : {}
|
|
824
|
+
const leaf = {
|
|
825
|
+
route: node.route,
|
|
826
|
+
request: req,
|
|
827
|
+
requestExpr: renderFetcherRequest(req)
|
|
897
828
|
};
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
829
|
+
const owned = /* @__PURE__ */ new Map();
|
|
830
|
+
if (p.layer) {
|
|
831
|
+
mergeExclusive(owned, Object.entries(p.layer.buildMembers(leaf.requestExpr, leaf, p.ctx)), {
|
|
832
|
+
owner: p.layer.name,
|
|
833
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
834
|
+
});
|
|
835
|
+
}
|
|
902
836
|
for (const ext of p.memberExts) {
|
|
903
837
|
const extra = ext.apiMembers?.(leaf, p.ctx);
|
|
904
838
|
if (!extra) continue;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
members[name] = value;
|
|
912
|
-
}
|
|
839
|
+
mergeExclusive(owned, Object.entries(extra), {
|
|
840
|
+
owner: ext.name,
|
|
841
|
+
describe: (name, prevOwner, owner) => `api member "${name}" on route "${req.routeName}" is contributed by more than one extension (conflict between "${prevOwner}" and "${owner}").`
|
|
842
|
+
});
|
|
913
843
|
}
|
|
844
|
+
const members = {};
|
|
845
|
+
for (const [name, { value }] of owned) members[name] = value;
|
|
914
846
|
lines.push(...renderLeaf(pad, objKey, req, leaf.requestExpr, members));
|
|
915
847
|
}
|
|
916
848
|
return lines;
|
|
@@ -919,10 +851,82 @@ function buildRouterTypeAccess(name) {
|
|
|
919
851
|
const segments = splitName(name);
|
|
920
852
|
return `ApiRouter${segments.map((s) => `[${JSON.stringify(s)}]`).join("")}`;
|
|
921
853
|
}
|
|
854
|
+
var RESOLVER_HELPERS = [
|
|
855
|
+
// --- Recursive helper type _RouterAt: walks nested ApiRouter by dot-path ---
|
|
856
|
+
"type _RouterAt<R, P extends string> = P extends `${infer Head}.${infer Tail}`",
|
|
857
|
+
" ? Head extends keyof R ? _RouterAt<R[Head], Tail> : never",
|
|
858
|
+
" : P extends keyof R ? R[P] : never;",
|
|
859
|
+
"",
|
|
860
|
+
// --- ResolveByName: resolve a field from a dot-path name ---
|
|
861
|
+
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;",
|
|
862
|
+
"",
|
|
863
|
+
// --- ResolveByPath: scan all leaves for matching method + url ---
|
|
864
|
+
// Flattens ApiRouter recursively and finds the entry whose method === M and url === U.
|
|
865
|
+
"type _LeafValues<T> = T extends { method: string; url: string }",
|
|
866
|
+
" ? T",
|
|
867
|
+
" : T extends object ? _LeafValues<T[keyof T]> : never;",
|
|
868
|
+
"",
|
|
869
|
+
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L",
|
|
870
|
+
" ? L extends { method: M; url: U }",
|
|
871
|
+
" ? Field extends keyof L ? L[Field] : never",
|
|
872
|
+
" : never",
|
|
873
|
+
" : never;",
|
|
874
|
+
""
|
|
875
|
+
];
|
|
876
|
+
var ROUTE_NAMESPACE = [
|
|
877
|
+
"export namespace Route {",
|
|
878
|
+
' export type Response<K extends string> = ResolveByName<K, "response">;',
|
|
879
|
+
' export type Body<K extends string> = ResolveByName<K, "body">;',
|
|
880
|
+
' export type Query<K extends string> = ResolveByName<K, "query">;',
|
|
881
|
+
' export type Params<K extends string> = ResolveByName<K, "params">;',
|
|
882
|
+
' export type Error<K extends string> = ResolveByName<K, "error">;',
|
|
883
|
+
' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;',
|
|
884
|
+
" export type Request<K extends string> = {",
|
|
885
|
+
" body: Body<K>;",
|
|
886
|
+
" query: Query<K>;",
|
|
887
|
+
" params: Params<K>;",
|
|
888
|
+
" };",
|
|
889
|
+
"}",
|
|
890
|
+
""
|
|
891
|
+
];
|
|
892
|
+
var PATH_NAMESPACE = [
|
|
893
|
+
"export namespace Path {",
|
|
894
|
+
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;',
|
|
895
|
+
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;',
|
|
896
|
+
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;',
|
|
897
|
+
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;',
|
|
898
|
+
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;',
|
|
899
|
+
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;',
|
|
900
|
+
"}",
|
|
901
|
+
""
|
|
902
|
+
];
|
|
903
|
+
var EMPTY_ROUTE_NAMESPACE = [
|
|
904
|
+
"export namespace Route {",
|
|
905
|
+
" export type Response<K extends string> = never;",
|
|
906
|
+
" export type Body<K extends string> = never;",
|
|
907
|
+
" export type Query<K extends string> = never;",
|
|
908
|
+
" export type Params<K extends string> = never;",
|
|
909
|
+
" export type Error<K extends string> = never;",
|
|
910
|
+
" export type FilterFields<K extends string> = never;",
|
|
911
|
+
" export type Request<K extends string> = { body: never; query: never; params: never };",
|
|
912
|
+
"}",
|
|
913
|
+
""
|
|
914
|
+
];
|
|
915
|
+
var EMPTY_PATH_NAMESPACE = [
|
|
916
|
+
"export namespace Path {",
|
|
917
|
+
" export type Response<M extends string, U extends string> = never;",
|
|
918
|
+
" export type Body<M extends string, U extends string> = never;",
|
|
919
|
+
" export type Query<M extends string, U extends string> = never;",
|
|
920
|
+
" export type Params<M extends string, U extends string> = never;",
|
|
921
|
+
" export type Error<M extends string, U extends string> = never;",
|
|
922
|
+
" export type FilterFields<M extends string, U extends string> = never;",
|
|
923
|
+
"}",
|
|
924
|
+
""
|
|
925
|
+
];
|
|
922
926
|
function buildApiFile(routes, outDir, opts = {}) {
|
|
923
927
|
const fetcherImportPath = opts.fetcherImportPath;
|
|
924
928
|
const extensions = opts.extensions ?? [];
|
|
925
|
-
const {
|
|
929
|
+
const { layer } = resolveApiSlots(extensions);
|
|
926
930
|
const memberExts = extensions.filter((e) => e.apiMembers);
|
|
927
931
|
const headerExts = extensions.filter((e) => e.apiHeader);
|
|
928
932
|
const contracted = routes.filter((r) => r.contract);
|
|
@@ -967,7 +971,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
967
971
|
seenImports.add(imp);
|
|
968
972
|
extImports.push(imp);
|
|
969
973
|
};
|
|
970
|
-
for (const imp of transport?.imports?.(ctx) ?? []) pushImport(imp);
|
|
971
974
|
for (const imp of layer?.imports?.(ctx) ?? []) pushImport(imp);
|
|
972
975
|
for (const ext of headerExts) {
|
|
973
976
|
for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
|
|
@@ -1012,27 +1015,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1012
1015
|
lines.push("}");
|
|
1013
1016
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1014
1017
|
lines.push("");
|
|
1015
|
-
lines.push(
|
|
1016
|
-
lines.push(
|
|
1017
|
-
lines.push(" export type Body<K extends string> = never;");
|
|
1018
|
-
lines.push(" export type Query<K extends string> = never;");
|
|
1019
|
-
lines.push(" export type Params<K extends string> = never;");
|
|
1020
|
-
lines.push(" export type Error<K extends string> = never;");
|
|
1021
|
-
lines.push(" export type FilterFields<K extends string> = never;");
|
|
1022
|
-
lines.push(
|
|
1023
|
-
" export type Request<K extends string> = { body: never; query: never; params: never };"
|
|
1024
|
-
);
|
|
1025
|
-
lines.push("}");
|
|
1026
|
-
lines.push("");
|
|
1027
|
-
lines.push("export namespace Path {");
|
|
1028
|
-
lines.push(" export type Response<M extends string, U extends string> = never;");
|
|
1029
|
-
lines.push(" export type Body<M extends string, U extends string> = never;");
|
|
1030
|
-
lines.push(" export type Query<M extends string, U extends string> = never;");
|
|
1031
|
-
lines.push(" export type Params<M extends string, U extends string> = never;");
|
|
1032
|
-
lines.push(" export type Error<M extends string, U extends string> = never;");
|
|
1033
|
-
lines.push(" export type FilterFields<M extends string, U extends string> = never;");
|
|
1034
|
-
lines.push("}");
|
|
1035
|
-
lines.push("");
|
|
1018
|
+
lines.push(...EMPTY_ROUTE_NAMESPACE);
|
|
1019
|
+
lines.push(...EMPTY_PATH_NAMESPACE);
|
|
1036
1020
|
return lines.join("\n");
|
|
1037
1021
|
}
|
|
1038
1022
|
const tree = /* @__PURE__ */ new Map();
|
|
@@ -1050,7 +1034,8 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1050
1034
|
path: r.path,
|
|
1051
1035
|
params: r.params,
|
|
1052
1036
|
controllerRef: r.controllerRef,
|
|
1053
|
-
contractSource: c.contractSource
|
|
1037
|
+
contractSource: c.contractSource,
|
|
1038
|
+
route: r
|
|
1054
1039
|
};
|
|
1055
1040
|
insertIntoTree(tree, segments, leaf, name);
|
|
1056
1041
|
}
|
|
@@ -1063,7 +1048,6 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1063
1048
|
lines.push(" return {");
|
|
1064
1049
|
lines.push(
|
|
1065
1050
|
...emitApiObjectBlock(tree, 4, {
|
|
1066
|
-
...transport ? { transport } : {},
|
|
1067
1051
|
...layer ? { layer } : {},
|
|
1068
1052
|
memberExts,
|
|
1069
1053
|
ctx
|
|
@@ -1074,61 +1058,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1074
1058
|
lines.push("");
|
|
1075
1059
|
lines.push("export type Api = ReturnType<typeof createApi>;");
|
|
1076
1060
|
lines.push("");
|
|
1077
|
-
lines.push(
|
|
1078
|
-
lines.push(
|
|
1079
|
-
lines.push(
|
|
1080
|
-
lines.push("");
|
|
1081
|
-
lines.push(
|
|
1082
|
-
"type ResolveByName<K extends string, Field extends string> = _RouterAt<ApiRouter, K> extends infer R ? Field extends keyof R ? R[Field] : never : never;"
|
|
1083
|
-
);
|
|
1084
|
-
lines.push("");
|
|
1085
|
-
lines.push("type _LeafValues<T> = T extends { method: string; url: string }");
|
|
1086
|
-
lines.push(" ? T");
|
|
1087
|
-
lines.push(" : T extends object ? _LeafValues<T[keyof T]> : never;");
|
|
1088
|
-
lines.push("");
|
|
1089
|
-
lines.push(
|
|
1090
|
-
"type ResolveByPath<M extends string, U extends string, Field extends string> = _LeafValues<ApiRouter> extends infer L"
|
|
1091
|
-
);
|
|
1092
|
-
lines.push(" ? L extends { method: M; url: U }");
|
|
1093
|
-
lines.push(" ? Field extends keyof L ? L[Field] : never");
|
|
1094
|
-
lines.push(" : never");
|
|
1095
|
-
lines.push(" : never;");
|
|
1096
|
-
lines.push("");
|
|
1097
|
-
lines.push("export namespace Route {");
|
|
1098
|
-
lines.push(' export type Response<K extends string> = ResolveByName<K, "response">;');
|
|
1099
|
-
lines.push(' export type Body<K extends string> = ResolveByName<K, "body">;');
|
|
1100
|
-
lines.push(' export type Query<K extends string> = ResolveByName<K, "query">;');
|
|
1101
|
-
lines.push(' export type Params<K extends string> = ResolveByName<K, "params">;');
|
|
1102
|
-
lines.push(' export type Error<K extends string> = ResolveByName<K, "error">;');
|
|
1103
|
-
lines.push(' export type FilterFields<K extends string> = ResolveByName<K, "filterFields">;');
|
|
1104
|
-
lines.push(" export type Request<K extends string> = {");
|
|
1105
|
-
lines.push(" body: Body<K>;");
|
|
1106
|
-
lines.push(" query: Query<K>;");
|
|
1107
|
-
lines.push(" params: Params<K>;");
|
|
1108
|
-
lines.push(" };");
|
|
1109
|
-
lines.push("}");
|
|
1110
|
-
lines.push("");
|
|
1111
|
-
lines.push("export namespace Path {");
|
|
1112
|
-
lines.push(
|
|
1113
|
-
' export type Response<M extends string, U extends string> = ResolveByPath<M, U, "response">;'
|
|
1114
|
-
);
|
|
1115
|
-
lines.push(
|
|
1116
|
-
' export type Body<M extends string, U extends string> = ResolveByPath<M, U, "body">;'
|
|
1117
|
-
);
|
|
1118
|
-
lines.push(
|
|
1119
|
-
' export type Query<M extends string, U extends string> = ResolveByPath<M, U, "query">;'
|
|
1120
|
-
);
|
|
1121
|
-
lines.push(
|
|
1122
|
-
' export type Params<M extends string, U extends string> = ResolveByPath<M, U, "params">;'
|
|
1123
|
-
);
|
|
1124
|
-
lines.push(
|
|
1125
|
-
' export type Error<M extends string, U extends string> = ResolveByPath<M, U, "error">;'
|
|
1126
|
-
);
|
|
1127
|
-
lines.push(
|
|
1128
|
-
' export type FilterFields<M extends string, U extends string> = ResolveByPath<M, U, "filterFields">;'
|
|
1129
|
-
);
|
|
1130
|
-
lines.push("}");
|
|
1131
|
-
lines.push("");
|
|
1061
|
+
lines.push(...RESOLVER_HELPERS);
|
|
1062
|
+
lines.push(...ROUTE_NAMESPACE);
|
|
1063
|
+
lines.push(...PATH_NAMESPACE);
|
|
1132
1064
|
for (const ext of headerExts) {
|
|
1133
1065
|
const statements = ext.apiHeader?.(ctx)?.statements;
|
|
1134
1066
|
if (statements?.length) {
|
|
@@ -1140,7 +1072,7 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1140
1072
|
|
|
1141
1073
|
// src/emit/emit-cache.ts
|
|
1142
1074
|
import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1143
|
-
import { join as
|
|
1075
|
+
import { join as join5 } from "path";
|
|
1144
1076
|
async function emitCache(pages, outDir) {
|
|
1145
1077
|
await mkdir2(outDir, { recursive: true });
|
|
1146
1078
|
const entries = await Promise.all(
|
|
@@ -1154,95 +1086,21 @@ async function emitCache(pages, outDir) {
|
|
|
1154
1086
|
})
|
|
1155
1087
|
);
|
|
1156
1088
|
const cache = { pages: entries };
|
|
1157
|
-
await writeFile2(
|
|
1089
|
+
await writeFile2(join5(outDir, "components.json"), `${JSON.stringify(cache, null, 2)}
|
|
1158
1090
|
`, "utf8");
|
|
1159
1091
|
}
|
|
1160
1092
|
|
|
1161
1093
|
// src/emit/emit-forms.ts
|
|
1162
1094
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1163
|
-
import { join as
|
|
1095
|
+
import { join as join6, relative as relative4 } from "path";
|
|
1164
1096
|
async function emitForms(routes, outDir, config, adapter) {
|
|
1165
1097
|
if (config && config.enabled === false) return false;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (content2 === null) return false;
|
|
1169
|
-
await mkdir3(outDir, { recursive: true });
|
|
1170
|
-
await writeFile3(join5(outDir, "forms.ts"), content2, "utf8");
|
|
1171
|
-
return true;
|
|
1172
|
-
}
|
|
1173
|
-
const entries = collectFormEntries(routes);
|
|
1174
|
-
if (entries.length === 0) return false;
|
|
1098
|
+
const content = buildFormsFileWithAdapter(routes, outDir, adapter, config);
|
|
1099
|
+
if (content === null) return false;
|
|
1175
1100
|
await mkdir3(outDir, { recursive: true });
|
|
1176
|
-
|
|
1177
|
-
await writeFile3(join5(outDir, "forms.ts"), content, "utf8");
|
|
1101
|
+
await writeFile3(join6(outDir, "forms.ts"), content, "utf8");
|
|
1178
1102
|
return true;
|
|
1179
1103
|
}
|
|
1180
|
-
function buildFormsFileWithAdapter(routes, adapter) {
|
|
1181
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1182
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1183
|
-
for (const route of sorted) {
|
|
1184
|
-
const cs = route.contract.contractSource;
|
|
1185
|
-
if (!cs.bodySchema && !cs.querySchema) continue;
|
|
1186
|
-
methodNameCounts.set(
|
|
1187
|
-
deriveBaseName(route.name).method,
|
|
1188
|
-
(methodNameCounts.get(deriveBaseName(route.name).method) ?? 0) + 1
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
const named = /* @__PURE__ */ new Map();
|
|
1192
|
-
const decls = [];
|
|
1193
|
-
const mapEntries = [];
|
|
1194
|
-
let used = false;
|
|
1195
|
-
for (const route of sorted) {
|
|
1196
|
-
const cs = route.contract.contractSource;
|
|
1197
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1198
|
-
const base = (methodNameCounts.get(method) ?? 0) > 1 ? full : method;
|
|
1199
|
-
const block = [];
|
|
1200
|
-
if (cs.bodyZodText && !cs.bodySchema) {
|
|
1201
|
-
block.push(
|
|
1202
|
-
`// warning: ${route.name} body is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
let bodyConst;
|
|
1206
|
-
if (cs.bodySchema) {
|
|
1207
|
-
used = true;
|
|
1208
|
-
const r = adapter.renderModule(cs.bodySchema);
|
|
1209
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1210
|
-
bodyConst = `${base}BodySchema`;
|
|
1211
|
-
block.push(`export const ${bodyConst} = ${r.schemaText};`);
|
|
1212
|
-
block.push(`export type ${base}Body = ${adapter.inferType(bodyConst)};`);
|
|
1213
|
-
}
|
|
1214
|
-
if (cs.querySchema) {
|
|
1215
|
-
used = true;
|
|
1216
|
-
const r = adapter.renderModule(cs.querySchema);
|
|
1217
|
-
for (const [n, t] of r.namedNestedSchemas) named.set(n, t);
|
|
1218
|
-
const queryConst = `${base}QuerySchema`;
|
|
1219
|
-
block.push(`export const ${queryConst} = ${r.schemaText};`);
|
|
1220
|
-
block.push(`export type ${base}Query = ${adapter.inferType(queryConst)};`);
|
|
1221
|
-
}
|
|
1222
|
-
if (block.length === 0) continue;
|
|
1223
|
-
decls.push(`// ${route.name}`, ...block, "");
|
|
1224
|
-
if (bodyConst) mapEntries.push(` ${JSON.stringify(route.name)}: ${bodyConst},`);
|
|
1225
|
-
}
|
|
1226
|
-
if (!used) return null;
|
|
1227
|
-
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1228
|
-
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1229
|
-
lines.push("");
|
|
1230
|
-
if (named.size > 0) {
|
|
1231
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1232
|
-
for (const [n, t] of named) lines.push(`const ${n} = ${t};`);
|
|
1233
|
-
lines.push("");
|
|
1234
|
-
}
|
|
1235
|
-
lines.push(...decls);
|
|
1236
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1237
|
-
lines.push("export const formSchemas = {");
|
|
1238
|
-
lines.push(...mapEntries);
|
|
1239
|
-
lines.push("} as const;");
|
|
1240
|
-
lines.push("");
|
|
1241
|
-
return lines.join("\n");
|
|
1242
|
-
}
|
|
1243
|
-
function hasSchema(src) {
|
|
1244
|
-
return !!src && (src.ref !== null || src.text !== null);
|
|
1245
|
-
}
|
|
1246
1104
|
function pascal(segment) {
|
|
1247
1105
|
return segment.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1248
1106
|
}
|
|
@@ -1252,37 +1110,6 @@ function deriveBaseName(routeName) {
|
|
|
1252
1110
|
const full = segments.map(pascal).join("");
|
|
1253
1111
|
return { method, full };
|
|
1254
1112
|
}
|
|
1255
|
-
function collectFormEntries(routes) {
|
|
1256
|
-
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1257
|
-
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1258
|
-
const candidates = [];
|
|
1259
|
-
for (const route of sorted) {
|
|
1260
|
-
const cs = route.contract.contractSource;
|
|
1261
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1262
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1263
|
-
if (!hasSchema(body) && !hasSchema(query)) continue;
|
|
1264
|
-
const { method, full } = deriveBaseName(route.name);
|
|
1265
|
-
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1266
|
-
candidates.push({ route, method, full });
|
|
1267
|
-
}
|
|
1268
|
-
const entries = [];
|
|
1269
|
-
for (const { route, method, full } of candidates) {
|
|
1270
|
-
const cs = route.contract.contractSource;
|
|
1271
|
-
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1272
|
-
const baseName = collision ? full : method;
|
|
1273
|
-
const body = { ref: cs.bodyZodRef ?? null, text: cs.bodyZodText ?? null };
|
|
1274
|
-
const query = { ref: cs.queryZodRef ?? null, text: cs.queryZodText ?? null };
|
|
1275
|
-
entries.push({
|
|
1276
|
-
routeName: route.name,
|
|
1277
|
-
baseName,
|
|
1278
|
-
body: hasSchema(body) ? body : void 0,
|
|
1279
|
-
query: hasSchema(query) ? query : void 0,
|
|
1280
|
-
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1281
|
-
warnings: cs.formWarnings ?? []
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
return entries;
|
|
1285
|
-
}
|
|
1286
1113
|
function relImport(outDir, filePath) {
|
|
1287
1114
|
let relPath = relative4(outDir, filePath).replace(/\.ts$/, "");
|
|
1288
1115
|
if (!relPath.startsWith(".")) relPath = `./${relPath}`;
|
|
@@ -1291,85 +1118,8 @@ function relImport(outDir, filePath) {
|
|
|
1291
1118
|
function refRootIdentifier(refName) {
|
|
1292
1119
|
return refName.split(".")[0] ?? refName;
|
|
1293
1120
|
}
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1296
|
-
const lines = [
|
|
1297
|
-
"// Generated by @dudousxd/nestjs-codegen. Do not edit.",
|
|
1298
|
-
`import { z } from '${zodImport}';`
|
|
1299
|
-
];
|
|
1300
|
-
const importsByFile = /* @__PURE__ */ new Map();
|
|
1301
|
-
const refAlias = /* @__PURE__ */ new Map();
|
|
1302
|
-
for (const entry of entries) {
|
|
1303
|
-
for (const src of [entry.body, entry.query]) {
|
|
1304
|
-
if (src?.ref && !src.text) {
|
|
1305
|
-
const root = refRootIdentifier(src.ref.name);
|
|
1306
|
-
const set = importsByFile.get(src.ref.filePath) ?? /* @__PURE__ */ new Set();
|
|
1307
|
-
set.add(root);
|
|
1308
|
-
importsByFile.set(src.ref.filePath, set);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
if (importsByFile.size > 0) {
|
|
1313
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
1314
|
-
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1315
|
-
const relPath = relImport(outDir, filePath);
|
|
1316
|
-
const specifiers = [];
|
|
1317
|
-
for (const root of [...roots].sort()) {
|
|
1318
|
-
if (emitted.has(root)) {
|
|
1319
|
-
const alias = `${root}_${emitted.size}`;
|
|
1320
|
-
specifiers.push(`${root} as ${alias}`);
|
|
1321
|
-
emitted.add(alias);
|
|
1322
|
-
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1323
|
-
} else {
|
|
1324
|
-
specifiers.push(root);
|
|
1325
|
-
emitted.add(root);
|
|
1326
|
-
refAlias.set(`${filePath}\0${root}`, root);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
lines.push("");
|
|
1333
|
-
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1334
|
-
if (globalSchemas.size > 0) {
|
|
1335
|
-
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1336
|
-
for (const [name, text] of globalSchemas) {
|
|
1337
|
-
lines.push(`const ${name} = ${text};`);
|
|
1338
|
-
}
|
|
1339
|
-
lines.push("");
|
|
1340
|
-
}
|
|
1341
|
-
const mapEntries = [];
|
|
1342
|
-
for (const entry of entries) {
|
|
1343
|
-
lines.push(`// ${entry.routeName}`);
|
|
1344
|
-
if (entry.warnings && entry.warnings.length > 0) {
|
|
1345
|
-
for (const w of entry.warnings) {
|
|
1346
|
-
lines.push(`// warning: ${w}`);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
const rename = renamesByEntry.get(entry) ?? null;
|
|
1350
|
-
if (entry.body) {
|
|
1351
|
-
const schemaName = `${entry.baseName}BodySchema`;
|
|
1352
|
-
const typeName = `${entry.baseName}Body`;
|
|
1353
|
-
const text = applyRenames(renderSchema(entry.body, outDir, refAlias), rename);
|
|
1354
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1355
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1356
|
-
mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${schemaName},`);
|
|
1357
|
-
}
|
|
1358
|
-
if (entry.query) {
|
|
1359
|
-
const schemaName = `${entry.baseName}QuerySchema`;
|
|
1360
|
-
const typeName = `${entry.baseName}Query`;
|
|
1361
|
-
const text = applyRenames(renderSchema(entry.query, outDir, refAlias), rename);
|
|
1362
|
-
lines.push(`export const ${schemaName} = ${text};`);
|
|
1363
|
-
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
1364
|
-
}
|
|
1365
|
-
lines.push("");
|
|
1366
|
-
}
|
|
1367
|
-
lines.push("/** Route name \u2192 body schema map. */");
|
|
1368
|
-
lines.push("export const formSchemas = {");
|
|
1369
|
-
lines.push(...mapEntries);
|
|
1370
|
-
lines.push("} as const;");
|
|
1371
|
-
lines.push("");
|
|
1372
|
-
return lines.join("\n");
|
|
1121
|
+
function hasSource(src) {
|
|
1122
|
+
return !!(src.schema || src.zodText || src.zodRef);
|
|
1373
1123
|
}
|
|
1374
1124
|
function applyRenames(text, renames) {
|
|
1375
1125
|
if (!renames || renames.size === 0) return text;
|
|
@@ -1435,46 +1185,208 @@ function planNestedSchemas(entries) {
|
|
|
1435
1185
|
}
|
|
1436
1186
|
return { globalSchemas, renamesByEntry };
|
|
1437
1187
|
}
|
|
1438
|
-
function
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1188
|
+
function buildFormsFileWithAdapter(routes, outDir, adapter, config) {
|
|
1189
|
+
const acceptsRawZod = adapter.acceptsRawZodSource === true;
|
|
1190
|
+
const sorted = [...routes].filter((r) => r.contract).sort((a, b) => a.name.localeCompare(b.name));
|
|
1191
|
+
const methodNameCounts = /* @__PURE__ */ new Map();
|
|
1192
|
+
const candidates = [];
|
|
1193
|
+
for (const route of sorted) {
|
|
1194
|
+
const cs = route.contract.contractSource;
|
|
1195
|
+
const body = {
|
|
1196
|
+
schema: cs.bodySchema ?? null,
|
|
1197
|
+
zodText: cs.bodyZodText ?? null,
|
|
1198
|
+
zodRef: cs.bodyZodRef ?? null
|
|
1199
|
+
};
|
|
1200
|
+
const query = {
|
|
1201
|
+
schema: cs.querySchema ?? null,
|
|
1202
|
+
zodText: cs.queryZodText ?? null,
|
|
1203
|
+
zodRef: cs.queryZodRef ?? null
|
|
1204
|
+
};
|
|
1205
|
+
if (!hasSource(body) && !hasSource(query)) continue;
|
|
1206
|
+
const { method, full } = deriveBaseName(route.name);
|
|
1207
|
+
methodNameCounts.set(method, (methodNameCounts.get(method) ?? 0) + 1);
|
|
1208
|
+
candidates.push({
|
|
1209
|
+
routeName: route.name,
|
|
1210
|
+
baseName: full,
|
|
1211
|
+
// resolved below
|
|
1212
|
+
body: hasSource(body) ? body : void 0,
|
|
1213
|
+
query: hasSource(query) ? query : void 0,
|
|
1214
|
+
nestedSchemas: cs.formNestedSchemas ?? null,
|
|
1215
|
+
warnings: cs.formWarnings ?? []
|
|
1216
|
+
});
|
|
1457
1217
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1218
|
+
const entries = candidates.map((c) => {
|
|
1219
|
+
const { method, full } = deriveBaseName(c.routeName);
|
|
1220
|
+
const collision = (methodNameCounts.get(method) ?? 0) > 1;
|
|
1221
|
+
return { ...c, baseName: collision ? full : method };
|
|
1222
|
+
});
|
|
1223
|
+
if (entries.length === 0) return null;
|
|
1224
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
1225
|
+
const refAlias = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const entry of entries) {
|
|
1227
|
+
for (const src of [entry.body, entry.query]) {
|
|
1228
|
+
if (src?.zodRef && !src.zodText && !src.schema) {
|
|
1229
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1230
|
+
const set = importsByFile.get(src.zodRef.filePath) ?? /* @__PURE__ */ new Set();
|
|
1231
|
+
set.add(root);
|
|
1232
|
+
importsByFile.set(src.zodRef.filePath, set);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1460
1235
|
}
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1236
|
+
const importLines = [];
|
|
1237
|
+
if (importsByFile.size > 0) {
|
|
1238
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1239
|
+
for (const [filePath, roots] of [...importsByFile.entries()].sort()) {
|
|
1240
|
+
const relPath = relImport(outDir, filePath);
|
|
1241
|
+
const specifiers = [];
|
|
1242
|
+
for (const root of [...roots].sort()) {
|
|
1243
|
+
if (emitted.has(root)) {
|
|
1244
|
+
const alias = `${root}_${emitted.size}`;
|
|
1245
|
+
specifiers.push(`${root} as ${alias}`);
|
|
1246
|
+
emitted.add(alias);
|
|
1247
|
+
refAlias.set(`${filePath}\0${root}`, alias);
|
|
1248
|
+
} else {
|
|
1249
|
+
specifiers.push(root);
|
|
1250
|
+
emitted.add(root);
|
|
1251
|
+
refAlias.set(`${filePath}\0${root}`, root);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
importLines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const { globalSchemas, renamesByEntry } = planNestedSchemas(entries);
|
|
1258
|
+
const irNamed = /* @__PURE__ */ new Map();
|
|
1259
|
+
const irTypeAliases = /* @__PURE__ */ new Map();
|
|
1260
|
+
const irAnnotations = /* @__PURE__ */ new Map();
|
|
1261
|
+
const decls = [];
|
|
1262
|
+
const mapEntries = [];
|
|
1263
|
+
let used = false;
|
|
1264
|
+
const renderSource = (src, rename) => {
|
|
1265
|
+
if (src.schema) {
|
|
1266
|
+
const r = adapter.renderModule(src.schema);
|
|
1267
|
+
for (const [n, t] of r.namedNestedSchemas) irNamed.set(n, t);
|
|
1268
|
+
if (r.namedTypeAliases) for (const [n, t] of r.namedTypeAliases) irTypeAliases.set(n, t);
|
|
1269
|
+
if (r.namedAnnotations) for (const [n, a] of r.namedAnnotations) irAnnotations.set(n, a);
|
|
1270
|
+
return { text: r.schemaText };
|
|
1271
|
+
}
|
|
1272
|
+
if (src.zodText) {
|
|
1273
|
+
if (!acceptsRawZod) {
|
|
1274
|
+
return {
|
|
1275
|
+
text: "",
|
|
1276
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
return { text: applyRenames(src.zodText, rename) };
|
|
1280
|
+
}
|
|
1281
|
+
if (src.zodRef) {
|
|
1282
|
+
if (!acceptsRawZod) {
|
|
1283
|
+
return {
|
|
1284
|
+
text: "",
|
|
1285
|
+
warn: `is a defineContract (zod) schema; not translated to ${adapter.name} \u2014 use the zod adapter.`
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
const root = refRootIdentifier(src.zodRef.name);
|
|
1289
|
+
const alias = refAlias.get(`${src.zodRef.filePath}\0${root}`) ?? root;
|
|
1290
|
+
const member = src.zodRef.name.slice(root.length);
|
|
1291
|
+
return { text: `${alias}${member}` };
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
};
|
|
1295
|
+
for (const entry of entries) {
|
|
1296
|
+
const block = [];
|
|
1297
|
+
const rename = renamesByEntry.get(entry) ?? null;
|
|
1298
|
+
let bodyConst;
|
|
1299
|
+
if (entry.warnings && entry.warnings.length > 0) {
|
|
1300
|
+
for (const w of entry.warnings) block.push(`// warning: ${w}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (entry.body) {
|
|
1303
|
+
const rendered = renderSource(entry.body, rename);
|
|
1304
|
+
if (rendered?.warn) {
|
|
1305
|
+
block.push(`// warning: ${entry.routeName} body ${rendered.warn}`);
|
|
1306
|
+
} else if (rendered) {
|
|
1307
|
+
used = true;
|
|
1308
|
+
bodyConst = `${entry.baseName}BodySchema`;
|
|
1309
|
+
block.push(`export const ${bodyConst} = ${rendered.text};`);
|
|
1310
|
+
block.push(`export type ${entry.baseName}Body = ${adapter.inferType(bodyConst)};`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (entry.query) {
|
|
1314
|
+
const rendered = renderSource(entry.query, rename);
|
|
1315
|
+
if (rendered?.warn) {
|
|
1316
|
+
block.push(`// warning: ${entry.routeName} query ${rendered.warn}`);
|
|
1317
|
+
} else if (rendered) {
|
|
1318
|
+
used = true;
|
|
1319
|
+
const queryConst = `${entry.baseName}QuerySchema`;
|
|
1320
|
+
block.push(`export const ${queryConst} = ${rendered.text};`);
|
|
1321
|
+
block.push(`export type ${entry.baseName}Query = ${adapter.inferType(queryConst)};`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (block.length === 0) continue;
|
|
1325
|
+
decls.push(`// ${entry.routeName}`, ...block, "");
|
|
1326
|
+
if (bodyConst) mapEntries.push(` ${JSON.stringify(entry.routeName)}: ${bodyConst},`);
|
|
1327
|
+
}
|
|
1328
|
+
if (!used) return null;
|
|
1329
|
+
const lines = ["// Generated by @dudousxd/nestjs-codegen. Do not edit."];
|
|
1330
|
+
if (acceptsRawZod) {
|
|
1331
|
+
const zodImport = config?.zodImport ?? "zod";
|
|
1332
|
+
lines.push(`import { z } from '${zodImport}';`);
|
|
1333
|
+
} else {
|
|
1334
|
+
for (const imp of adapter.importStatements({ used: true })) lines.push(imp);
|
|
1335
|
+
}
|
|
1336
|
+
lines.push(...importLines);
|
|
1337
|
+
lines.push("");
|
|
1338
|
+
const allNested = /* @__PURE__ */ new Map();
|
|
1339
|
+
for (const [n, t] of globalSchemas) allNested.set(n, t);
|
|
1340
|
+
for (const [n, t] of irNamed) if (!allNested.has(n)) allNested.set(n, t);
|
|
1341
|
+
if (allNested.size > 0) {
|
|
1342
|
+
lines.push("// Hoisted nested schemas (shared across endpoints).");
|
|
1343
|
+
for (const [n, alias] of irTypeAliases) {
|
|
1344
|
+
if (allNested.has(n)) lines.push(`${alias};`);
|
|
1345
|
+
}
|
|
1346
|
+
for (const [n, t] of allNested) {
|
|
1347
|
+
const annotation = irAnnotations.get(n);
|
|
1348
|
+
lines.push(`const ${n}${annotation ? `: ${annotation}` : ""} = ${t};`);
|
|
1349
|
+
}
|
|
1350
|
+
lines.push("");
|
|
1351
|
+
}
|
|
1352
|
+
lines.push(...decls);
|
|
1353
|
+
lines.push("/** Route name \u2192 body schema map. */");
|
|
1354
|
+
lines.push("export const formSchemas = {");
|
|
1355
|
+
lines.push(...mapEntries);
|
|
1356
|
+
lines.push("} as const;");
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
return lines.join("\n");
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/emit/emit-index.ts
|
|
1362
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1363
|
+
import { join as join7 } from "path";
|
|
1364
|
+
async function emitIndex(outDir, hasContracts = false, hasForms = false) {
|
|
1365
|
+
await mkdir4(outDir, { recursive: true });
|
|
1366
|
+
const exports = ["export * from './pages.js';", "export * from './routes.js';"];
|
|
1367
|
+
if (hasContracts) {
|
|
1368
|
+
exports.push("export * from './api.js';");
|
|
1369
|
+
}
|
|
1370
|
+
if (hasForms) {
|
|
1371
|
+
exports.push("export * from './forms.js';");
|
|
1372
|
+
}
|
|
1373
|
+
const content = ["// Generated by @dudousxd/nestjs-codegen. Do not edit.", ...exports, ""].join(
|
|
1374
|
+
"\n"
|
|
1375
|
+
);
|
|
1376
|
+
await writeFile4(join7(outDir, "index.d.ts"), content, "utf8");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/emit/emit-pages.ts
|
|
1380
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1381
|
+
import { join as join8, relative as relative5 } from "path";
|
|
1382
|
+
async function emitPages(pages, outDir, _options = {}) {
|
|
1383
|
+
await mkdir5(outDir, { recursive: true });
|
|
1384
|
+
const pageNameUnion = pages.length > 0 ? pages.map((p) => JSON.stringify(p.name)).join(" | ") : "never";
|
|
1385
|
+
const augBody = pages.map((p) => {
|
|
1386
|
+
const key = needsQuotes(p.name) ? JSON.stringify(p.name) : p.name;
|
|
1387
|
+
const valueType = buildAugmentationType(p, outDir);
|
|
1388
|
+
return ` ${key}: ${valueType};`;
|
|
1389
|
+
}).join("\n");
|
|
1478
1390
|
const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
|
|
1479
1391
|
const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
|
|
1480
1392
|
const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
|
|
@@ -1487,7 +1399,7 @@ ${augBody}
|
|
|
1487
1399
|
}
|
|
1488
1400
|
${sharedPropsBlock}}
|
|
1489
1401
|
`;
|
|
1490
|
-
await writeFile5(
|
|
1402
|
+
await writeFile5(join8(outDir, "pages.d.ts"), content, "utf8");
|
|
1491
1403
|
}
|
|
1492
1404
|
function buildSharedPropsBlock(sharedProps) {
|
|
1493
1405
|
if (!sharedProps) return "";
|
|
@@ -1518,11 +1430,11 @@ function needsQuotes(name) {
|
|
|
1518
1430
|
|
|
1519
1431
|
// src/emit/emit-routes.ts
|
|
1520
1432
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1521
|
-
import { join as
|
|
1433
|
+
import { join as join9 } from "path";
|
|
1522
1434
|
async function emitRoutes(routes, outDir) {
|
|
1523
1435
|
await mkdir6(outDir, { recursive: true });
|
|
1524
1436
|
const content = buildRoutesFile(routes);
|
|
1525
|
-
await writeFile6(
|
|
1437
|
+
await writeFile6(join9(outDir, "routes.ts"), content, "utf8");
|
|
1526
1438
|
}
|
|
1527
1439
|
function buildRoutesFile(routes) {
|
|
1528
1440
|
if (routes.length === 0) {
|
|
@@ -1650,30 +1562,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1650
1562
|
propsExport: pagesConfig.propsExport,
|
|
1651
1563
|
componentNameStrategy: pagesConfig.componentNameStrategy
|
|
1652
1564
|
});
|
|
1653
|
-
|
|
1654
|
-
if (config.app?.moduleEntry) {
|
|
1655
|
-
try {
|
|
1656
|
-
const tsconfigPath = config.app.tsconfig ?? join9(config.codegen.cwd, "tsconfig.json");
|
|
1657
|
-
let project;
|
|
1658
|
-
try {
|
|
1659
|
-
project = new Project2({
|
|
1660
|
-
tsConfigFilePath: tsconfigPath,
|
|
1661
|
-
skipAddingFilesFromTsConfig: true,
|
|
1662
|
-
skipLoadingLibFiles: true,
|
|
1663
|
-
skipFileDependencyResolution: true
|
|
1664
|
-
});
|
|
1665
|
-
} catch {
|
|
1666
|
-
project = new Project2({
|
|
1667
|
-
skipAddingFilesFromTsConfig: true,
|
|
1668
|
-
skipLoadingLibFiles: true,
|
|
1669
|
-
skipFileDependencyResolution: true,
|
|
1670
|
-
compilerOptions: { allowJs: true, strict: false }
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
sharedProps = discoverSharedProps(project, config.app.moduleEntry);
|
|
1674
|
-
} catch {
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1565
|
+
const sharedProps = discoverSharedPropsFromConfig(config);
|
|
1677
1566
|
await emitPages(pages, config.codegen.outDir, {
|
|
1678
1567
|
propsExport: pagesConfig.propsExport,
|
|
1679
1568
|
sharedProps
|
|
@@ -1697,7 +1586,7 @@ async function generate(config, inputRoutes = []) {
|
|
|
1697
1586
|
if (extensions.length > 0) {
|
|
1698
1587
|
const extraFiles = await collectEmittedFiles(extensions, ctx);
|
|
1699
1588
|
for (const file of extraFiles) {
|
|
1700
|
-
const dest =
|
|
1589
|
+
const dest = join10(config.codegen.outDir, file.path);
|
|
1701
1590
|
await mkdir7(dirname2(dest), { recursive: true });
|
|
1702
1591
|
await writeFile7(dest, file.contents, "utf8");
|
|
1703
1592
|
}
|
|
@@ -1706,15 +1595,20 @@ async function generate(config, inputRoutes = []) {
|
|
|
1706
1595
|
|
|
1707
1596
|
// src/watch/watcher.ts
|
|
1708
1597
|
import { readFile as readFile3 } from "fs/promises";
|
|
1709
|
-
import { join as
|
|
1598
|
+
import { join as join13 } from "path";
|
|
1710
1599
|
import chokidar from "chokidar";
|
|
1711
1600
|
|
|
1712
1601
|
// src/discovery/contracts-fast.ts
|
|
1713
|
-
import { join as
|
|
1602
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
1714
1603
|
import fg2 from "fast-glob";
|
|
1715
1604
|
import {
|
|
1716
|
-
Node as
|
|
1717
|
-
Project as Project3
|
|
1605
|
+
Node as Node8,
|
|
1606
|
+
Project as Project3
|
|
1607
|
+
} from "ts-morph";
|
|
1608
|
+
|
|
1609
|
+
// src/discovery/dto-type-resolver.ts
|
|
1610
|
+
import {
|
|
1611
|
+
Node as Node6,
|
|
1718
1612
|
SyntaxKind as SyntaxKind3
|
|
1719
1613
|
} from "ts-morph";
|
|
1720
1614
|
|
|
@@ -1729,20 +1623,13 @@ import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
|
1729
1623
|
import {
|
|
1730
1624
|
Node as Node2
|
|
1731
1625
|
} from "ts-morph";
|
|
1732
|
-
var
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
return prev;
|
|
1626
|
+
var _EMPTY_CTX = { projectRoot: "", tsconfigPaths: null };
|
|
1627
|
+
var _ctxByProject = /* @__PURE__ */ new WeakMap();
|
|
1628
|
+
function setDiscoveryContext(project, ctx) {
|
|
1629
|
+
_ctxByProject.set(project, ctx);
|
|
1737
1630
|
}
|
|
1738
|
-
function
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
function _projectRoot() {
|
|
1742
|
-
return _ctx.projectRoot;
|
|
1743
|
-
}
|
|
1744
|
-
function _tsconfigPaths() {
|
|
1745
|
-
return _ctx.tsconfigPaths;
|
|
1631
|
+
function _ctxFor(project) {
|
|
1632
|
+
return _ctxByProject.get(project) ?? _EMPTY_CTX;
|
|
1746
1633
|
}
|
|
1747
1634
|
var _debug = process.env.NESTJS_INERTIA_DEBUG === "1";
|
|
1748
1635
|
function dbg(...args) {
|
|
@@ -1784,7 +1671,7 @@ function findTypeInFile(name, file) {
|
|
|
1784
1671
|
}
|
|
1785
1672
|
return null;
|
|
1786
1673
|
}
|
|
1787
|
-
function resolveModuleSpecifier(moduleSpecifier, sourceFile,
|
|
1674
|
+
function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
|
|
1788
1675
|
if (moduleSpecifier.startsWith(".")) {
|
|
1789
1676
|
const dir = dirname3(sourceFile.getFilePath());
|
|
1790
1677
|
const noExt = moduleSpecifier.replace(/\.(js|ts)$/, "");
|
|
@@ -1794,8 +1681,9 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, _project) {
|
|
|
1794
1681
|
resolve2(dir, moduleSpecifier, "index.ts")
|
|
1795
1682
|
];
|
|
1796
1683
|
}
|
|
1797
|
-
const
|
|
1798
|
-
const
|
|
1684
|
+
const ctx = _ctxFor(project);
|
|
1685
|
+
const baseUrl = ctx.projectRoot;
|
|
1686
|
+
const tsconfigPaths = ctx.tsconfigPaths;
|
|
1799
1687
|
dbg(
|
|
1800
1688
|
"resolveModuleSpecifier",
|
|
1801
1689
|
moduleSpecifier,
|
|
@@ -1941,7 +1829,7 @@ function resolveTypeRef(nodeOrName, sourceFile, project, opts) {
|
|
|
1941
1829
|
if (!namedImport) continue;
|
|
1942
1830
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1943
1831
|
if (opts.allowBareSpecifier && !moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
|
|
1944
|
-
const tsconfigPaths =
|
|
1832
|
+
const tsconfigPaths = _ctxFor(project).tsconfigPaths;
|
|
1945
1833
|
const isAlias = tsconfigPaths != null && Object.keys(tsconfigPaths).some((p) => {
|
|
1946
1834
|
const prefix = p.replace("*", "");
|
|
1947
1835
|
return moduleSpecifier.startsWith(prefix);
|
|
@@ -2010,10 +1898,7 @@ function extractSchemaFromDto(classDecl, sourceFile, project) {
|
|
|
2010
1898
|
depth: 0
|
|
2011
1899
|
};
|
|
2012
1900
|
const root = buildObject(classDecl, sourceFile, ctx);
|
|
2013
|
-
|
|
2014
|
-
ctx.named.set(schemaName, { kind: "unknown", note: "recursive type \u2014 not expanded" });
|
|
2015
|
-
}
|
|
2016
|
-
return { root, named: ctx.named, warnings: ctx.warnings };
|
|
1901
|
+
return { root, named: ctx.named, warnings: ctx.warnings, recursive: ctx.recursiveSchemas };
|
|
2017
1902
|
}
|
|
2018
1903
|
function buildObject(classDecl, classFile, ctx) {
|
|
2019
1904
|
const props = classDecl.getProperties();
|
|
@@ -2033,7 +1918,7 @@ function buildProperty(prop, classFile, ctx) {
|
|
|
2033
1918
|
const dec = (n) => decorators.get(n);
|
|
2034
1919
|
const typeNode = prop.getTypeNode();
|
|
2035
1920
|
const typeText = typeNode?.getText() ?? "unknown";
|
|
2036
|
-
const isArrayType = !!typeNode &&
|
|
1921
|
+
const isArrayType = !!typeNode && Node3.isArrayTypeNode(typeNode);
|
|
2037
1922
|
const typeRefName = resolveTypeFactoryName(dec("Type"));
|
|
2038
1923
|
if (has("ValidateNested") || typeRefName) {
|
|
2039
1924
|
const childName = typeRefName ?? singularClassName(typeText);
|
|
@@ -2164,18 +2049,27 @@ function baseFromType(typeText, isArrayType) {
|
|
|
2164
2049
|
}
|
|
2165
2050
|
}
|
|
2166
2051
|
function buildNestedReference(className, fromFile, ctx) {
|
|
2167
|
-
if (ctx.visiting.has(className)
|
|
2052
|
+
if (ctx.visiting.has(className)) {
|
|
2168
2053
|
const reserved = ctx.emittedClasses.get(className) ?? aliasFor(className, ctx);
|
|
2169
2054
|
ctx.emittedClasses.set(className, reserved);
|
|
2170
2055
|
ctx.recursiveSchemas.add(reserved);
|
|
2171
2056
|
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2172
2057
|
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
2173
|
-
const msg = `${className} is a recursive type
|
|
2058
|
+
const msg = `${className} is a recursive type; the generated schema validates it via a lazy self-reference.`;
|
|
2174
2059
|
ctx.warnings.push(msg);
|
|
2175
2060
|
console.warn(`[nestjs-codegen] ${msg}`);
|
|
2176
2061
|
}
|
|
2177
2062
|
return { kind: "lazyRef", name: reserved };
|
|
2178
2063
|
}
|
|
2064
|
+
if (ctx.depth >= 8) {
|
|
2065
|
+
if (!ctx.warnedDecorators.has(`deep:${className}`)) {
|
|
2066
|
+
ctx.warnedDecorators.add(`deep:${className}`);
|
|
2067
|
+
const msg = `${className} nesting is too deep to expand; the generated schema uses unknown for it.`;
|
|
2068
|
+
ctx.warnings.push(msg);
|
|
2069
|
+
console.warn(`[nestjs-codegen] ${msg}`);
|
|
2070
|
+
}
|
|
2071
|
+
return { kind: "unknown", note: "nesting too deep \u2014 not expanded" };
|
|
2072
|
+
}
|
|
2179
2073
|
const existing = ctx.emittedClasses.get(className);
|
|
2180
2074
|
if (existing) return { kind: "ref", name: existing };
|
|
2181
2075
|
const schemaName = aliasFor(className, ctx);
|
|
@@ -2294,434 +2188,104 @@ function inSchemaFromDecorator(decorator) {
|
|
|
2294
2188
|
return null;
|
|
2295
2189
|
}
|
|
2296
2190
|
|
|
2297
|
-
// src/discovery/
|
|
2191
|
+
// src/discovery/filter-for.ts
|
|
2298
2192
|
import {
|
|
2299
|
-
Node as
|
|
2193
|
+
Node as Node5
|
|
2300
2194
|
} from "ts-morph";
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
"
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
"IsOptional",
|
|
2321
|
-
"IsNotEmpty",
|
|
2322
|
-
"IsArray",
|
|
2323
|
-
"ValidateNested",
|
|
2324
|
-
"Type",
|
|
2325
|
-
"IsObject",
|
|
2326
|
-
"Allow",
|
|
2327
|
-
"IsDefined"
|
|
2328
|
-
]);
|
|
2329
|
-
function extractZodFromDto(classDecl, sourceFile, project) {
|
|
2330
|
-
const ctx = {
|
|
2331
|
-
sourceFile,
|
|
2332
|
-
project,
|
|
2333
|
-
namedNestedSchemas: /* @__PURE__ */ new Map(),
|
|
2334
|
-
warnings: [],
|
|
2335
|
-
warnedDecorators: /* @__PURE__ */ new Set(),
|
|
2336
|
-
emittedClasses: /* @__PURE__ */ new Map(),
|
|
2337
|
-
visiting: /* @__PURE__ */ new Set(),
|
|
2338
|
-
recursiveSchemas: /* @__PURE__ */ new Set(),
|
|
2339
|
-
depth: 0
|
|
2340
|
-
};
|
|
2341
|
-
const schemaText = buildObjectSchema(classDecl, sourceFile, ctx);
|
|
2342
|
-
for (const schemaName of ctx.recursiveSchemas) {
|
|
2343
|
-
ctx.namedNestedSchemas.set(schemaName, "z.unknown() /* recursive type \u2014 not expanded */");
|
|
2344
|
-
}
|
|
2345
|
-
return {
|
|
2346
|
-
schemaText,
|
|
2347
|
-
namedNestedSchemas: ctx.namedNestedSchemas,
|
|
2348
|
-
warnings: ctx.warnings
|
|
2349
|
-
};
|
|
2195
|
+
|
|
2196
|
+
// src/discovery/filter-field-types.ts
|
|
2197
|
+
import {
|
|
2198
|
+
Node as Node4,
|
|
2199
|
+
SyntaxKind as SyntaxKind2
|
|
2200
|
+
} from "ts-morph";
|
|
2201
|
+
|
|
2202
|
+
// src/discovery/enum-resolution.ts
|
|
2203
|
+
function resolveEnumValues(name, sourceFile, project) {
|
|
2204
|
+
const resolved = findType(name, sourceFile, project);
|
|
2205
|
+
if (!resolved || resolved.kind !== "enum") return null;
|
|
2206
|
+
let numeric = true;
|
|
2207
|
+
const values = resolved.members.map((m) => {
|
|
2208
|
+
const parsed = JSON.parse(m);
|
|
2209
|
+
if (typeof parsed === "string") numeric = false;
|
|
2210
|
+
return String(parsed);
|
|
2211
|
+
});
|
|
2212
|
+
if (values.length === 0) return null;
|
|
2213
|
+
return { values, numeric };
|
|
2350
2214
|
}
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2215
|
+
|
|
2216
|
+
// src/discovery/filter-field-types.ts
|
|
2217
|
+
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2218
|
+
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2219
|
+
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2220
|
+
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2221
|
+
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2222
|
+
function classifyTypeKeyword(raw) {
|
|
2223
|
+
const t = raw.toLowerCase();
|
|
2224
|
+
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2225
|
+
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2226
|
+
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2227
|
+
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2228
|
+
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2229
|
+
return null;
|
|
2363
2230
|
}
|
|
2364
|
-
function
|
|
2365
|
-
return
|
|
2231
|
+
function markNullable(r, nullable) {
|
|
2232
|
+
return nullable ? { ...r, nullable: true } : r;
|
|
2366
2233
|
}
|
|
2367
|
-
function
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
if (has("Allow")) base = "z.unknown()";
|
|
2396
|
-
if (has("IsEmail")) {
|
|
2397
|
-
base = ensureStringBase(base);
|
|
2398
|
-
refinements.push(`.email(${messageArg(dec("IsEmail"))})`);
|
|
2399
|
-
}
|
|
2400
|
-
if (has("IsUrl")) {
|
|
2401
|
-
base = ensureStringBase(base);
|
|
2402
|
-
refinements.push(`.url(${messageArg(dec("IsUrl"))})`);
|
|
2403
|
-
}
|
|
2404
|
-
if (has("IsUUID")) {
|
|
2405
|
-
base = ensureStringBase(base);
|
|
2406
|
-
refinements.push(`.uuid(${messageArg(dec("IsUUID"))})`);
|
|
2407
|
-
}
|
|
2408
|
-
if (has("Matches")) {
|
|
2409
|
-
const re = firstArgText2(dec("Matches"));
|
|
2410
|
-
if (re) {
|
|
2411
|
-
base = ensureStringBase(base);
|
|
2412
|
-
refinements.push(`.regex(${re})`);
|
|
2234
|
+
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2235
|
+
if (Node4.isUnionTypeNode(typeNode)) {
|
|
2236
|
+
let nullable = false;
|
|
2237
|
+
const stringLits = [];
|
|
2238
|
+
const numberLits = [];
|
|
2239
|
+
const others = [];
|
|
2240
|
+
for (const member of typeNode.getTypeNodes()) {
|
|
2241
|
+
const kind = member.getKind();
|
|
2242
|
+
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2243
|
+
nullable = true;
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
if (Node4.isLiteralTypeNode(member)) {
|
|
2247
|
+
const lit = member.getLiteral();
|
|
2248
|
+
if (Node4.isStringLiteral(lit)) {
|
|
2249
|
+
stringLits.push(lit.getLiteralValue());
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
if (Node4.isNumericLiteral(lit)) {
|
|
2253
|
+
numberLits.push(lit.getText());
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2257
|
+
nullable = true;
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
others.push(member);
|
|
2413
2262
|
}
|
|
2263
|
+
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2264
|
+
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2265
|
+
}
|
|
2266
|
+
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2267
|
+
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2268
|
+
}
|
|
2269
|
+
if (others.length === 1) {
|
|
2270
|
+
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2271
|
+
return markNullable(inner, nullable || inner.nullable === true);
|
|
2272
|
+
}
|
|
2273
|
+
return markNullable({ kind: "unknown" }, nullable);
|
|
2414
2274
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2275
|
+
switch (typeNode.getKind()) {
|
|
2276
|
+
case SyntaxKind2.StringKeyword:
|
|
2277
|
+
return { kind: "string" };
|
|
2278
|
+
case SyntaxKind2.NumberKeyword:
|
|
2279
|
+
return { kind: "number" };
|
|
2280
|
+
case SyntaxKind2.BooleanKeyword:
|
|
2281
|
+
return { kind: "boolean" };
|
|
2282
|
+
case SyntaxKind2.AnyKeyword:
|
|
2283
|
+
case SyntaxKind2.UnknownKeyword:
|
|
2284
|
+
return { kind: "unknown" };
|
|
2285
|
+
default:
|
|
2286
|
+
break;
|
|
2427
2287
|
}
|
|
2428
|
-
if (
|
|
2429
|
-
const n = numericArg2(dec("Min"));
|
|
2430
|
-
if (n !== null) refinements.push(`.min(${n})`);
|
|
2431
|
-
}
|
|
2432
|
-
if (has("Max")) {
|
|
2433
|
-
const n = numericArg2(dec("Max"));
|
|
2434
|
-
if (n !== null) refinements.push(`.max(${n})`);
|
|
2435
|
-
}
|
|
2436
|
-
if (has("IsPositive")) refinements.push(".positive()");
|
|
2437
|
-
if (has("IsNegative")) refinements.push(".negative()");
|
|
2438
|
-
if (has("IsNotEmpty") && isStringBase(base)) refinements.push(".min(1)");
|
|
2439
|
-
if (has("IsEnum")) {
|
|
2440
|
-
const enumExpr = enumSchemaFromDecorator2(dec("IsEnum"), classFile, ctx);
|
|
2441
|
-
if (enumExpr) base = enumExpr;
|
|
2442
|
-
}
|
|
2443
|
-
if (has("IsIn")) {
|
|
2444
|
-
const inExpr = inSchemaFromDecorator2(dec("IsIn"));
|
|
2445
|
-
if (inExpr) base = inExpr;
|
|
2446
|
-
}
|
|
2447
|
-
for (const name of decorators.keys()) {
|
|
2448
|
-
if (!KNOWN_DECORATORS2.has(name)) {
|
|
2449
|
-
comments.push(`/* @${name}: not translatable to zod (server-only) */`);
|
|
2450
|
-
if (!ctx.warnedDecorators.has(name)) {
|
|
2451
|
-
ctx.warnedDecorators.add(name);
|
|
2452
|
-
const msg = `@${name} is not translatable to zod and was skipped (server-only validation).`;
|
|
2453
|
-
ctx.warnings.push(msg);
|
|
2454
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
let expr = base + refinements.join("");
|
|
2459
|
-
if (isArrayType && !expr.startsWith("z.array(")) {
|
|
2460
|
-
expr = `z.array(${expr})`;
|
|
2461
|
-
}
|
|
2462
|
-
expr = applyPresence2(expr, decorators);
|
|
2463
|
-
if (comments.length > 0) {
|
|
2464
|
-
expr = `${expr} ${comments.join(" ")}`;
|
|
2465
|
-
}
|
|
2466
|
-
return expr;
|
|
2467
|
-
}
|
|
2468
|
-
function applyPresence2(expr, decorators) {
|
|
2469
|
-
if (decorators.has("IsDefined")) return expr;
|
|
2470
|
-
if (decorators.has("IsOptional")) return `${expr}.optional()`;
|
|
2471
|
-
return expr;
|
|
2472
|
-
}
|
|
2473
|
-
function baseFromType2(typeText, isArrayType, ctx, classFile) {
|
|
2474
|
-
const inner = isArrayType ? typeText.slice(0, -2).trim() : typeText;
|
|
2475
|
-
switch (inner) {
|
|
2476
|
-
case "string":
|
|
2477
|
-
return "z.string()";
|
|
2478
|
-
case "number":
|
|
2479
|
-
return "z.number()";
|
|
2480
|
-
case "boolean":
|
|
2481
|
-
return "z.boolean()";
|
|
2482
|
-
case "Date":
|
|
2483
|
-
return "z.coerce.date()";
|
|
2484
|
-
case "File":
|
|
2485
|
-
case "Express.Multer.File":
|
|
2486
|
-
return "z.instanceof(File)";
|
|
2487
|
-
default:
|
|
2488
|
-
return "z.unknown()";
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
function ensureStringBase(base) {
|
|
2492
|
-
return isStringBase(base) ? base : "z.string()";
|
|
2493
|
-
}
|
|
2494
|
-
function isStringBase(base) {
|
|
2495
|
-
return base.startsWith("z.string(");
|
|
2496
|
-
}
|
|
2497
|
-
function buildNestedReference2(className, fromFile, ctx) {
|
|
2498
|
-
if (ctx.visiting.has(className) || ctx.depth >= 8) {
|
|
2499
|
-
const reserved = ctx.emittedClasses.get(className) ?? aliasFor2(className, ctx);
|
|
2500
|
-
ctx.emittedClasses.set(className, reserved);
|
|
2501
|
-
ctx.recursiveSchemas.add(reserved);
|
|
2502
|
-
if (!ctx.warnedDecorators.has(`recursive:${reserved}`)) {
|
|
2503
|
-
ctx.warnedDecorators.add(`recursive:${reserved}`);
|
|
2504
|
-
const msg = `${className} is a recursive type and was not expanded; the generated form schema uses z.unknown() for it.`;
|
|
2505
|
-
ctx.warnings.push(msg);
|
|
2506
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2507
|
-
}
|
|
2508
|
-
return `z.lazy(() => ${reserved})`;
|
|
2509
|
-
}
|
|
2510
|
-
const existing = ctx.emittedClasses.get(className);
|
|
2511
|
-
if (existing) return existing;
|
|
2512
|
-
const schemaName = aliasFor2(className, ctx);
|
|
2513
|
-
const resolved = findType(className, fromFile, ctx.project);
|
|
2514
|
-
if (!resolved || resolved.kind !== "class") {
|
|
2515
|
-
return "z.object({}).passthrough()";
|
|
2516
|
-
}
|
|
2517
|
-
ctx.emittedClasses.set(className, schemaName);
|
|
2518
|
-
ctx.visiting.add(className);
|
|
2519
|
-
ctx.depth += 1;
|
|
2520
|
-
const childText = buildObjectSchema(resolved.decl, resolved.file, ctx);
|
|
2521
|
-
ctx.depth -= 1;
|
|
2522
|
-
ctx.visiting.delete(className);
|
|
2523
|
-
ctx.namedNestedSchemas.set(schemaName, childText);
|
|
2524
|
-
return schemaName;
|
|
2525
|
-
}
|
|
2526
|
-
function aliasFor2(className, ctx) {
|
|
2527
|
-
const baseName = `${className}Schema`;
|
|
2528
|
-
let candidate = baseName;
|
|
2529
|
-
let i = 1;
|
|
2530
|
-
const used = new Set(ctx.namedNestedSchemas.keys());
|
|
2531
|
-
for (const v of ctx.emittedClasses.values()) used.add(v);
|
|
2532
|
-
while (used.has(candidate)) {
|
|
2533
|
-
candidate = `${baseName}_${i}`;
|
|
2534
|
-
i += 1;
|
|
2535
|
-
}
|
|
2536
|
-
return candidate;
|
|
2537
|
-
}
|
|
2538
|
-
function firstArg2(decorator) {
|
|
2539
|
-
return decorator?.getArguments()[0];
|
|
2540
|
-
}
|
|
2541
|
-
function firstArgText2(decorator) {
|
|
2542
|
-
const arg = firstArg2(decorator);
|
|
2543
|
-
return arg ? arg.getText() : null;
|
|
2544
|
-
}
|
|
2545
|
-
function numericArg2(decorator) {
|
|
2546
|
-
const arg = firstArg2(decorator);
|
|
2547
|
-
if (arg && Node4.isNumericLiteral(arg)) return arg.getText();
|
|
2548
|
-
return null;
|
|
2549
|
-
}
|
|
2550
|
-
function numericArgs2(decorator) {
|
|
2551
|
-
const args = decorator?.getArguments() ?? [];
|
|
2552
|
-
const num = (n) => n && Node4.isNumericLiteral(n) ? n.getText() : null;
|
|
2553
|
-
return [num(args[0]), num(args[1])];
|
|
2554
|
-
}
|
|
2555
|
-
function messageArg(decorator) {
|
|
2556
|
-
const args = decorator?.getArguments() ?? [];
|
|
2557
|
-
for (const arg of args) {
|
|
2558
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2559
|
-
for (const prop of arg.getProperties()) {
|
|
2560
|
-
if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
2561
|
-
const init = prop.getInitializer();
|
|
2562
|
-
if (init && Node4.isStringLiteral(init)) {
|
|
2563
|
-
return `{ message: ${init.getText()} }`;
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
return "";
|
|
2570
|
-
}
|
|
2571
|
-
function resolveTypeFactoryName2(decorator) {
|
|
2572
|
-
const arg = firstArg2(decorator);
|
|
2573
|
-
if (!arg) return null;
|
|
2574
|
-
if (Node4.isArrowFunction(arg)) {
|
|
2575
|
-
const body = arg.getBody();
|
|
2576
|
-
if (Node4.isIdentifier(body)) return body.getText();
|
|
2577
|
-
}
|
|
2578
|
-
return null;
|
|
2579
|
-
}
|
|
2580
|
-
function singularClassName2(typeText) {
|
|
2581
|
-
const inner = typeText.endsWith("[]") ? typeText.slice(0, -2).trim() : typeText;
|
|
2582
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(inner) ? inner : null;
|
|
2583
|
-
}
|
|
2584
|
-
function enumSchemaFromDecorator2(decorator, classFile, ctx) {
|
|
2585
|
-
const arg = firstArg2(decorator);
|
|
2586
|
-
if (!arg) return null;
|
|
2587
|
-
if (Node4.isIdentifier(arg)) {
|
|
2588
|
-
const name = arg.getText();
|
|
2589
|
-
const resolved = findType(name, classFile, ctx.project);
|
|
2590
|
-
if (resolved && resolved.kind === "enum") {
|
|
2591
|
-
return `z.enum([${resolved.members.join(", ")}])`;
|
|
2592
|
-
}
|
|
2593
|
-
const msg = `@IsEnum(${name}): enum could not be resolved to literal members and is not importable into the generated form schema; falling back to z.unknown().`;
|
|
2594
|
-
if (!ctx.warnedDecorators.has(`IsEnum:${name}`)) {
|
|
2595
|
-
ctx.warnedDecorators.add(`IsEnum:${name}`);
|
|
2596
|
-
ctx.warnings.push(msg);
|
|
2597
|
-
console.warn(`[nestjs-codegen/forms] ${msg}`);
|
|
2598
|
-
}
|
|
2599
|
-
return `z.unknown() /* @IsEnum(${name}): enum not resolvable to literals */`;
|
|
2600
|
-
}
|
|
2601
|
-
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2602
|
-
const values = [];
|
|
2603
|
-
for (const p of arg.getProperties()) {
|
|
2604
|
-
if (!Node4.isPropertyAssignment(p)) continue;
|
|
2605
|
-
const init = p.getInitializer();
|
|
2606
|
-
if (init && Node4.isStringLiteral(init)) values.push(init.getText());
|
|
2607
|
-
}
|
|
2608
|
-
if (values.length > 0) return `z.enum([${values.join(", ")}])`;
|
|
2609
|
-
}
|
|
2610
|
-
return null;
|
|
2611
|
-
}
|
|
2612
|
-
function inSchemaFromDecorator2(decorator) {
|
|
2613
|
-
const arg = firstArg2(decorator);
|
|
2614
|
-
if (arg && Node4.isArrayLiteralExpression(arg)) {
|
|
2615
|
-
const elements = arg.getElements();
|
|
2616
|
-
const allStrings = elements.every((e) => Node4.isStringLiteral(e));
|
|
2617
|
-
if (allStrings && elements.length > 0) {
|
|
2618
|
-
return `z.enum([${elements.map((e) => e.getText()).join(", ")}])`;
|
|
2619
|
-
}
|
|
2620
|
-
if (elements.length > 0) {
|
|
2621
|
-
return `z.union([${elements.map((e) => `z.literal(${e.getText()})`).join(", ")}])`;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
return null;
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
// src/discovery/filter-for.ts
|
|
2628
|
-
import {
|
|
2629
|
-
Node as Node6
|
|
2630
|
-
} from "ts-morph";
|
|
2631
|
-
|
|
2632
|
-
// src/discovery/filter-field-types.ts
|
|
2633
|
-
import {
|
|
2634
|
-
Node as Node5,
|
|
2635
|
-
SyntaxKind as SyntaxKind2
|
|
2636
|
-
} from "ts-morph";
|
|
2637
|
-
|
|
2638
|
-
// src/discovery/enum-resolution.ts
|
|
2639
|
-
function resolveEnumValues(name, sourceFile, project) {
|
|
2640
|
-
const resolved = findType(name, sourceFile, project);
|
|
2641
|
-
if (!resolved || resolved.kind !== "enum") return null;
|
|
2642
|
-
let numeric = true;
|
|
2643
|
-
const values = resolved.members.map((m) => {
|
|
2644
|
-
const parsed = JSON.parse(m);
|
|
2645
|
-
if (typeof parsed === "string") numeric = false;
|
|
2646
|
-
return String(parsed);
|
|
2647
|
-
});
|
|
2648
|
-
if (values.length === 0) return null;
|
|
2649
|
-
return { values, numeric };
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// src/discovery/filter-field-types.ts
|
|
2653
|
-
var STRING_TYPE_KEYWORDS = ["varchar", "text", "string", "char", "uuid", "enum"];
|
|
2654
|
-
var NUMBER_TYPE_KEYWORDS = ["int", "float", "double", "decimal", "number", "numeric", "real"];
|
|
2655
|
-
var BOOLEAN_TYPE_KEYWORDS = ["bool", "boolean", "bit"];
|
|
2656
|
-
var DATE_TYPE_KEYWORDS = ["date", "time", "timestamp", "datetime"];
|
|
2657
|
-
var JSON_TYPE_KEYWORDS = ["json", "jsonb"];
|
|
2658
|
-
function classifyTypeKeyword(raw) {
|
|
2659
|
-
const t = raw.toLowerCase();
|
|
2660
|
-
if (STRING_TYPE_KEYWORDS.some((s) => t.includes(s))) return "string";
|
|
2661
|
-
if (NUMBER_TYPE_KEYWORDS.some((s) => t.includes(s))) return "number";
|
|
2662
|
-
if (BOOLEAN_TYPE_KEYWORDS.some((s) => t.includes(s))) return "boolean";
|
|
2663
|
-
if (DATE_TYPE_KEYWORDS.some((s) => t.includes(s))) return "date";
|
|
2664
|
-
if (JSON_TYPE_KEYWORDS.some((s) => t.includes(s))) return "json";
|
|
2665
|
-
return null;
|
|
2666
|
-
}
|
|
2667
|
-
function markNullable(r, nullable) {
|
|
2668
|
-
return nullable ? { ...r, nullable: true } : r;
|
|
2669
|
-
}
|
|
2670
|
-
function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
2671
|
-
if (Node5.isUnionTypeNode(typeNode)) {
|
|
2672
|
-
let nullable = false;
|
|
2673
|
-
const stringLits = [];
|
|
2674
|
-
const numberLits = [];
|
|
2675
|
-
const others = [];
|
|
2676
|
-
for (const member of typeNode.getTypeNodes()) {
|
|
2677
|
-
const kind = member.getKind();
|
|
2678
|
-
if (kind === SyntaxKind2.NullKeyword || kind === SyntaxKind2.UndefinedKeyword) {
|
|
2679
|
-
nullable = true;
|
|
2680
|
-
continue;
|
|
2681
|
-
}
|
|
2682
|
-
if (Node5.isLiteralTypeNode(member)) {
|
|
2683
|
-
const lit = member.getLiteral();
|
|
2684
|
-
if (Node5.isStringLiteral(lit)) {
|
|
2685
|
-
stringLits.push(lit.getLiteralValue());
|
|
2686
|
-
continue;
|
|
2687
|
-
}
|
|
2688
|
-
if (Node5.isNumericLiteral(lit)) {
|
|
2689
|
-
numberLits.push(lit.getText());
|
|
2690
|
-
continue;
|
|
2691
|
-
}
|
|
2692
|
-
if (lit.getKind() === SyntaxKind2.NullKeyword) {
|
|
2693
|
-
nullable = true;
|
|
2694
|
-
continue;
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
others.push(member);
|
|
2698
|
-
}
|
|
2699
|
-
if (others.length === 0 && stringLits.length > 0 && numberLits.length === 0) {
|
|
2700
|
-
return markNullable({ kind: "string", enumValues: stringLits }, nullable);
|
|
2701
|
-
}
|
|
2702
|
-
if (others.length === 0 && numberLits.length > 0 && stringLits.length === 0) {
|
|
2703
|
-
return markNullable({ kind: "number", enumValues: numberLits, numericEnum: true }, nullable);
|
|
2704
|
-
}
|
|
2705
|
-
if (others.length === 1) {
|
|
2706
|
-
const inner = classifyTypeNode(others[0], sourceFile, project, opts);
|
|
2707
|
-
return markNullable(inner, nullable || inner.nullable === true);
|
|
2708
|
-
}
|
|
2709
|
-
return markNullable({ kind: "unknown" }, nullable);
|
|
2710
|
-
}
|
|
2711
|
-
switch (typeNode.getKind()) {
|
|
2712
|
-
case SyntaxKind2.StringKeyword:
|
|
2713
|
-
return { kind: "string" };
|
|
2714
|
-
case SyntaxKind2.NumberKeyword:
|
|
2715
|
-
return { kind: "number" };
|
|
2716
|
-
case SyntaxKind2.BooleanKeyword:
|
|
2717
|
-
return { kind: "boolean" };
|
|
2718
|
-
case SyntaxKind2.AnyKeyword:
|
|
2719
|
-
case SyntaxKind2.UnknownKeyword:
|
|
2720
|
-
return { kind: "unknown" };
|
|
2721
|
-
default:
|
|
2722
|
-
break;
|
|
2723
|
-
}
|
|
2724
|
-
if (Node5.isTypeReference(typeNode)) {
|
|
2288
|
+
if (Node4.isTypeReference(typeNode)) {
|
|
2725
2289
|
const refName = typeNode.getTypeName().getText();
|
|
2726
2290
|
if (refName === "Date") return { kind: "date" };
|
|
2727
2291
|
if (refName === "Record" || refName === "Object") return { kind: "json" };
|
|
@@ -2734,25 +2298,25 @@ function classifyTypeNode(typeNode, sourceFile, project, opts) {
|
|
|
2734
2298
|
if (typeRef) return { kind: "unknown", typeRef };
|
|
2735
2299
|
return { kind: "unknown" };
|
|
2736
2300
|
}
|
|
2737
|
-
if (
|
|
2301
|
+
if (Node4.isTypeLiteral(typeNode)) return { kind: "json" };
|
|
2738
2302
|
return { kind: "unknown" };
|
|
2739
2303
|
}
|
|
2740
2304
|
function enumFromDecoratorArgs(args, sourceFile, project) {
|
|
2741
2305
|
for (const arg of args) {
|
|
2742
|
-
if (
|
|
2306
|
+
if (Node4.isArrowFunction(arg)) {
|
|
2743
2307
|
const body = arg.getBody();
|
|
2744
|
-
if (
|
|
2308
|
+
if (Node4.isIdentifier(body)) {
|
|
2745
2309
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2746
2310
|
if (en) return en;
|
|
2747
2311
|
}
|
|
2748
2312
|
}
|
|
2749
|
-
if (
|
|
2313
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2750
2314
|
const itemsProp = arg.getProperty("items");
|
|
2751
|
-
if (itemsProp &&
|
|
2315
|
+
if (itemsProp && Node4.isPropertyAssignment(itemsProp)) {
|
|
2752
2316
|
const init = itemsProp.getInitializer();
|
|
2753
|
-
if (init &&
|
|
2317
|
+
if (init && Node4.isArrowFunction(init)) {
|
|
2754
2318
|
const body = init.getBody();
|
|
2755
|
-
if (
|
|
2319
|
+
if (Node4.isIdentifier(body)) {
|
|
2756
2320
|
const en = resolveEnumValues(body.getText(), sourceFile, project);
|
|
2757
2321
|
if (en) return en;
|
|
2758
2322
|
}
|
|
@@ -2775,7 +2339,7 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2775
2339
|
return { kind: "string" };
|
|
2776
2340
|
}
|
|
2777
2341
|
for (const arg of args) {
|
|
2778
|
-
if (
|
|
2342
|
+
if (Node4.isStringLiteral(arg)) {
|
|
2779
2343
|
const raw = arg.getLiteralValue();
|
|
2780
2344
|
const kind = classifyTypeKeyword(raw);
|
|
2781
2345
|
if (kind) {
|
|
@@ -2783,11 +2347,11 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2783
2347
|
return { kind };
|
|
2784
2348
|
}
|
|
2785
2349
|
}
|
|
2786
|
-
if (
|
|
2350
|
+
if (Node4.isObjectLiteralExpression(arg)) {
|
|
2787
2351
|
const enumProp = arg.getProperty("enum");
|
|
2788
|
-
if (enumProp &&
|
|
2352
|
+
if (enumProp && Node4.isPropertyAssignment(enumProp)) {
|
|
2789
2353
|
const init = enumProp.getInitializer();
|
|
2790
|
-
if (init &&
|
|
2354
|
+
if (init && Node4.isIdentifier(init)) {
|
|
2791
2355
|
const en = resolveEnumValues(init.getText(), sourceFile, project);
|
|
2792
2356
|
if (en) {
|
|
2793
2357
|
return en.numeric ? { kind: "number", enumValues: en.values, numericEnum: true } : { kind: "string", enumValues: en.values };
|
|
@@ -2796,9 +2360,9 @@ function classifyFromColumnDecorator(prop, sourceFile, project) {
|
|
|
2796
2360
|
}
|
|
2797
2361
|
}
|
|
2798
2362
|
const typeProp = arg.getProperty("type");
|
|
2799
|
-
if (typeProp &&
|
|
2363
|
+
if (typeProp && Node4.isPropertyAssignment(typeProp)) {
|
|
2800
2364
|
const init = typeProp.getInitializer();
|
|
2801
|
-
if (init &&
|
|
2365
|
+
if (init && Node4.isStringLiteral(init)) {
|
|
2802
2366
|
const kind = classifyTypeKeyword(init.getLiteralValue());
|
|
2803
2367
|
if (kind) return { kind };
|
|
2804
2368
|
}
|
|
@@ -2834,7 +2398,7 @@ function toFilterFieldType(name, r) {
|
|
|
2834
2398
|
|
|
2835
2399
|
// src/discovery/filter-for.ts
|
|
2836
2400
|
function classifyFilterForHint(typeInit) {
|
|
2837
|
-
if (
|
|
2401
|
+
if (Node5.isStringLiteral(typeInit)) {
|
|
2838
2402
|
switch (typeInit.getLiteralValue()) {
|
|
2839
2403
|
case "string":
|
|
2840
2404
|
return { kind: "string" };
|
|
@@ -2848,10 +2412,10 @@ function classifyFilterForHint(typeInit) {
|
|
|
2848
2412
|
return null;
|
|
2849
2413
|
}
|
|
2850
2414
|
}
|
|
2851
|
-
if (
|
|
2415
|
+
if (Node5.isArrayLiteralExpression(typeInit)) {
|
|
2852
2416
|
const values = [];
|
|
2853
2417
|
for (const el of typeInit.getElements()) {
|
|
2854
|
-
if (!
|
|
2418
|
+
if (!Node5.isStringLiteral(el)) return null;
|
|
2855
2419
|
values.push(el.getLiteralValue());
|
|
2856
2420
|
}
|
|
2857
2421
|
if (values.length === 0) return null;
|
|
@@ -2886,11 +2450,11 @@ function extractFilterForHints(classDecl, project) {
|
|
|
2886
2450
|
if (!filterForDec) continue;
|
|
2887
2451
|
const args = filterForDec.getArguments();
|
|
2888
2452
|
const keyArg = args[0];
|
|
2889
|
-
const inputKey = keyArg &&
|
|
2453
|
+
const inputKey = keyArg && Node5.isStringLiteral(keyArg) ? keyArg.getLiteralValue() : method.getName();
|
|
2890
2454
|
const optsArg = args[1];
|
|
2891
|
-
if (optsArg &&
|
|
2455
|
+
if (optsArg && Node5.isObjectLiteralExpression(optsArg)) {
|
|
2892
2456
|
const typeProp = optsArg.getProperty("type");
|
|
2893
|
-
if (typeProp &&
|
|
2457
|
+
if (typeProp && Node5.isPropertyAssignment(typeProp)) {
|
|
2894
2458
|
const typeInit = typeProp.getInitializer();
|
|
2895
2459
|
if (typeInit) {
|
|
2896
2460
|
const classified = classifyFilterForHint(typeInit);
|
|
@@ -2913,14 +2477,14 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
2913
2477
|
const args = filterDecorator.getArguments();
|
|
2914
2478
|
if (args.length === 0) continue;
|
|
2915
2479
|
const filterClassArg = args[0];
|
|
2916
|
-
if (!filterClassArg || !
|
|
2480
|
+
if (!filterClassArg || !Node5.isIdentifier(filterClassArg)) continue;
|
|
2917
2481
|
let source = "query";
|
|
2918
2482
|
const optionsArg = args[1];
|
|
2919
|
-
if (optionsArg &&
|
|
2483
|
+
if (optionsArg && Node5.isObjectLiteralExpression(optionsArg)) {
|
|
2920
2484
|
const sourceProp = optionsArg.getProperty("source");
|
|
2921
|
-
if (sourceProp &&
|
|
2485
|
+
if (sourceProp && Node5.isPropertyAssignment(sourceProp)) {
|
|
2922
2486
|
const init = sourceProp.getInitializer();
|
|
2923
|
-
if (init &&
|
|
2487
|
+
if (init && Node5.isStringLiteral(init) && init.getLiteralValue() === "body") {
|
|
2924
2488
|
source = "body";
|
|
2925
2489
|
}
|
|
2926
2490
|
}
|
|
@@ -2983,22 +2547,22 @@ function resolveRelationEntity(prop, sourceFile, project) {
|
|
|
2983
2547
|
const args = dec.getArguments();
|
|
2984
2548
|
if (args.length === 0) continue;
|
|
2985
2549
|
const arg = args[0];
|
|
2986
|
-
if (
|
|
2550
|
+
if (Node5.isObjectLiteralExpression(arg)) {
|
|
2987
2551
|
const entityProp = arg.getProperty("entity");
|
|
2988
|
-
if (entityProp &&
|
|
2552
|
+
if (entityProp && Node5.isPropertyAssignment(entityProp)) {
|
|
2989
2553
|
const init = entityProp.getInitializer();
|
|
2990
|
-
if (init &&
|
|
2554
|
+
if (init && Node5.isArrowFunction(init)) {
|
|
2991
2555
|
const body = init.getBody();
|
|
2992
|
-
if (
|
|
2556
|
+
if (Node5.isIdentifier(body)) {
|
|
2993
2557
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
2994
2558
|
if (resolved?.kind === "class") return resolved.decl;
|
|
2995
2559
|
}
|
|
2996
2560
|
}
|
|
2997
2561
|
}
|
|
2998
2562
|
}
|
|
2999
|
-
if (
|
|
2563
|
+
if (Node5.isArrowFunction(arg)) {
|
|
3000
2564
|
const body = arg.getBody();
|
|
3001
|
-
if (
|
|
2565
|
+
if (Node5.isIdentifier(body)) {
|
|
3002
2566
|
const resolved = findType(body.getText(), prop.getSourceFile(), project);
|
|
3003
2567
|
if (resolved?.kind === "class") return resolved.decl;
|
|
3004
2568
|
}
|
|
@@ -3022,11 +2586,11 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3022
2586
|
const args = filterableDecorator.getArguments();
|
|
3023
2587
|
if (args.length === 0) return [];
|
|
3024
2588
|
const optionsArg = args[0];
|
|
3025
|
-
if (!
|
|
2589
|
+
if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
|
|
3026
2590
|
const entityProp = optionsArg.getProperty("entity");
|
|
3027
|
-
if (!entityProp || !
|
|
2591
|
+
if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
|
|
3028
2592
|
const entityInit = entityProp.getInitializer();
|
|
3029
|
-
if (!entityInit || !
|
|
2593
|
+
if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
|
|
3030
2594
|
const entityName = entityInit.getText();
|
|
3031
2595
|
const filterSourceFile = filterClass.getSourceFile();
|
|
3032
2596
|
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
@@ -3042,17 +2606,17 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3042
2606
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
3043
2607
|
if (relationsDecorator) {
|
|
3044
2608
|
const relArgs = relationsDecorator.getArguments();
|
|
3045
|
-
if (relArgs.length > 0 &&
|
|
2609
|
+
if (relArgs.length > 0 && Node5.isObjectLiteralExpression(relArgs[0])) {
|
|
3046
2610
|
for (const relProp of relArgs[0].getProperties()) {
|
|
3047
|
-
if (!
|
|
2611
|
+
if (!Node5.isPropertyAssignment(relProp)) continue;
|
|
3048
2612
|
const relInit = relProp.getInitializer();
|
|
3049
|
-
if (!relInit || !
|
|
2613
|
+
if (!relInit || !Node5.isObjectLiteralExpression(relInit)) continue;
|
|
3050
2614
|
const keysProp = relInit.getProperty("keys");
|
|
3051
|
-
if (!keysProp || !
|
|
2615
|
+
if (!keysProp || !Node5.isPropertyAssignment(keysProp)) continue;
|
|
3052
2616
|
const keysInit = keysProp.getInitializer();
|
|
3053
|
-
if (!keysInit || !
|
|
2617
|
+
if (!keysInit || !Node5.isArrayLiteralExpression(keysInit)) continue;
|
|
3054
2618
|
for (const el of keysInit.getElements()) {
|
|
3055
|
-
if (
|
|
2619
|
+
if (Node5.isStringLiteral(el)) {
|
|
3056
2620
|
fields.push(toFilterFieldType(el.getLiteralValue(), { kind: "unknown" }));
|
|
3057
2621
|
}
|
|
3058
2622
|
}
|
|
@@ -3062,267 +2626,65 @@ function extractFilterableEntityFields(filterClass, project) {
|
|
|
3062
2626
|
return fields;
|
|
3063
2627
|
}
|
|
3064
2628
|
|
|
3065
|
-
// src/discovery/
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
2629
|
+
// src/discovery/dto-type-resolver.ts
|
|
2630
|
+
var WRAPPER_TYPES = {
|
|
2631
|
+
// MikroORM Ref/Reference/LoadedReference/IdentifiedReference are server-side
|
|
2632
|
+
// wrappers around related entities; the wire shape is just the referenced
|
|
2633
|
+
// entity. Unwrap to the type argument.
|
|
2634
|
+
Ref: "unwrap",
|
|
2635
|
+
Reference: "unwrap",
|
|
2636
|
+
LoadedReference: "unwrap",
|
|
2637
|
+
IdentifiedReference: "unwrap",
|
|
2638
|
+
// MikroORM Opt<T> is a marker, Loaded<T, ...> is a wrapper; both reduce to T.
|
|
2639
|
+
Opt: "unwrap",
|
|
2640
|
+
Loaded: "unwrap",
|
|
2641
|
+
// Promise<T> — unwrap
|
|
2642
|
+
Promise: "unwrap",
|
|
2643
|
+
// MikroORM Collection<T> serializes as an array of T on the wire.
|
|
2644
|
+
Collection: "arrayOf",
|
|
2645
|
+
// Array<T> generic form
|
|
2646
|
+
Array: "arrayOf"
|
|
2647
|
+
};
|
|
2648
|
+
var PASSTHROUGH_UTILITY = /* @__PURE__ */ new Set([
|
|
2649
|
+
"Record",
|
|
2650
|
+
"Omit",
|
|
2651
|
+
"Pick",
|
|
2652
|
+
"Partial",
|
|
2653
|
+
"Required",
|
|
2654
|
+
"Readonly",
|
|
2655
|
+
"Map",
|
|
2656
|
+
"Set"
|
|
2657
|
+
]);
|
|
2658
|
+
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
2659
|
+
if (depth <= 0) return "unknown";
|
|
2660
|
+
if (Node6.isArrayTypeNode(typeNode)) {
|
|
2661
|
+
const elementType = typeNode.getElementTypeNode();
|
|
2662
|
+
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3088
2663
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
project.addSourceFileAtPath(f);
|
|
2664
|
+
if (Node6.isUnionTypeNode(typeNode)) {
|
|
2665
|
+
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3092
2666
|
}
|
|
3093
|
-
|
|
3094
|
-
const prevCtx = setDiscoveryContext({
|
|
3095
|
-
projectRoot: cwd,
|
|
3096
|
-
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3097
|
-
});
|
|
3098
|
-
try {
|
|
3099
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
3100
|
-
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3101
|
-
}
|
|
3102
|
-
} finally {
|
|
3103
|
-
restoreDiscoveryContext(prevCtx);
|
|
3104
|
-
}
|
|
3105
|
-
return routes;
|
|
3106
|
-
}
|
|
3107
|
-
function zodAstToTs(node) {
|
|
3108
|
-
if (!Node7.isCallExpression(node)) return "unknown";
|
|
3109
|
-
const expr = node.getExpression();
|
|
3110
|
-
if (Node7.isPropertyAccessExpression(expr)) {
|
|
3111
|
-
const methodName = expr.getName();
|
|
3112
|
-
const receiver = expr.getExpression();
|
|
3113
|
-
if (methodName === "optional") {
|
|
3114
|
-
return `${zodAstToTs(receiver)} | undefined`;
|
|
3115
|
-
}
|
|
3116
|
-
if (methodName === "nullable") {
|
|
3117
|
-
return `${zodAstToTs(receiver)} | null`;
|
|
3118
|
-
}
|
|
3119
|
-
const args = node.getArguments();
|
|
3120
|
-
switch (methodName) {
|
|
3121
|
-
case "string":
|
|
3122
|
-
return "string";
|
|
3123
|
-
case "number":
|
|
3124
|
-
return "number";
|
|
3125
|
-
case "boolean":
|
|
3126
|
-
return "boolean";
|
|
3127
|
-
case "unknown":
|
|
3128
|
-
return "unknown";
|
|
3129
|
-
case "any":
|
|
3130
|
-
return "unknown";
|
|
3131
|
-
case "literal": {
|
|
3132
|
-
const lit = args[0];
|
|
3133
|
-
if (!lit) return "unknown";
|
|
3134
|
-
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
3135
|
-
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
3136
|
-
if (lit.getKind() === SyntaxKind3.TrueKeyword) return "true";
|
|
3137
|
-
if (lit.getKind() === SyntaxKind3.FalseKeyword) return "false";
|
|
3138
|
-
return "unknown";
|
|
3139
|
-
}
|
|
3140
|
-
case "enum": {
|
|
3141
|
-
const arrArg = args[0];
|
|
3142
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3143
|
-
const members = arrArg.getElements().map(
|
|
3144
|
-
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
3145
|
-
);
|
|
3146
|
-
return members.join(" | ");
|
|
3147
|
-
}
|
|
3148
|
-
case "array": {
|
|
3149
|
-
const inner = args[0];
|
|
3150
|
-
if (!inner) return "unknown";
|
|
3151
|
-
return `Array<${zodAstToTs(inner)}>`;
|
|
3152
|
-
}
|
|
3153
|
-
case "object": {
|
|
3154
|
-
const objArg = args[0];
|
|
3155
|
-
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
3156
|
-
const lines = [];
|
|
3157
|
-
for (const prop of objArg.getProperties()) {
|
|
3158
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3159
|
-
const key = prop.getName();
|
|
3160
|
-
const valNode = prop.getInitializer();
|
|
3161
|
-
if (!valNode) continue;
|
|
3162
|
-
const tsType = zodAstToTs(valNode);
|
|
3163
|
-
const isOpt = isOptionalChain(valNode);
|
|
3164
|
-
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
3165
|
-
}
|
|
3166
|
-
return `{ ${lines.join("; ")} }`;
|
|
3167
|
-
}
|
|
3168
|
-
case "union": {
|
|
3169
|
-
const arrArg = args[0];
|
|
3170
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3171
|
-
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
3172
|
-
}
|
|
3173
|
-
case "record": {
|
|
3174
|
-
const valArg = args.length === 1 ? args[0] : args[1];
|
|
3175
|
-
if (!valArg) return "unknown";
|
|
3176
|
-
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
3177
|
-
}
|
|
3178
|
-
case "tuple": {
|
|
3179
|
-
const arrArg = args[0];
|
|
3180
|
-
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3181
|
-
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
3182
|
-
}
|
|
3183
|
-
default:
|
|
3184
|
-
return "unknown";
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
return "unknown";
|
|
3188
|
-
}
|
|
3189
|
-
function isOptionalChain(node) {
|
|
3190
|
-
if (!Node7.isCallExpression(node)) return false;
|
|
3191
|
-
const expr = node.getExpression();
|
|
3192
|
-
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3193
|
-
}
|
|
3194
|
-
function decoratorStringArg(decoratorExpr) {
|
|
3195
|
-
if (!decoratorExpr) return void 0;
|
|
3196
|
-
if (Node7.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3197
|
-
if (Node7.isArrayLiteralExpression(decoratorExpr)) {
|
|
3198
|
-
const first = decoratorExpr.getElements()[0];
|
|
3199
|
-
if (first && Node7.isStringLiteral(first)) return first.getLiteralValue();
|
|
3200
|
-
}
|
|
3201
|
-
return void 0;
|
|
3202
|
-
}
|
|
3203
|
-
function parseDefineContractCall(callExpr) {
|
|
3204
|
-
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3205
|
-
const callee = callExpr.getExpression();
|
|
3206
|
-
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3207
|
-
if (calleeName !== "defineContract") return null;
|
|
3208
|
-
const args = callExpr.getArguments();
|
|
3209
|
-
const optsArg = args[0];
|
|
3210
|
-
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3211
|
-
let query = null;
|
|
3212
|
-
let body = null;
|
|
3213
|
-
let response = "unknown";
|
|
3214
|
-
let bodyZodText = null;
|
|
3215
|
-
let queryZodText = null;
|
|
3216
|
-
for (const prop of optsArg.getProperties()) {
|
|
3217
|
-
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3218
|
-
const propName = prop.getName();
|
|
3219
|
-
const val = prop.getInitializer();
|
|
3220
|
-
if (!val) continue;
|
|
3221
|
-
if (propName === "query") {
|
|
3222
|
-
query = zodAstToTs(val);
|
|
3223
|
-
queryZodText = val.getText();
|
|
3224
|
-
} else if (propName === "body") {
|
|
3225
|
-
body = zodAstToTs(val);
|
|
3226
|
-
bodyZodText = val.getText();
|
|
3227
|
-
} else if (propName === "response") {
|
|
3228
|
-
response = zodAstToTs(val);
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
return { query, body, response, bodyZodText, queryZodText };
|
|
3232
|
-
}
|
|
3233
|
-
function deriveClassSegment(className) {
|
|
3234
|
-
const noSuffix = className.replace(/Controller$/, "");
|
|
3235
|
-
if (!noSuffix) {
|
|
3236
|
-
throw new Error(
|
|
3237
|
-
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3238
|
-
);
|
|
3239
|
-
}
|
|
3240
|
-
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3241
|
-
}
|
|
3242
|
-
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3243
|
-
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3244
|
-
const methodPortion = methodAs ?? methodName;
|
|
3245
|
-
return `${classPortion}.${methodPortion}`;
|
|
3246
|
-
}
|
|
3247
|
-
function joinPaths(prefix, suffix) {
|
|
3248
|
-
if (!prefix && !suffix) return "/";
|
|
3249
|
-
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3250
|
-
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3251
|
-
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3252
|
-
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3253
|
-
const combined = p + s;
|
|
3254
|
-
return combined === "" ? "/" : combined;
|
|
3255
|
-
}
|
|
3256
|
-
function extractParams(path) {
|
|
3257
|
-
const matches = path.matchAll(/:(\w+)/g);
|
|
3258
|
-
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3259
|
-
}
|
|
3260
|
-
function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
3261
|
-
if (depth <= 0) return "unknown";
|
|
3262
|
-
if (Node7.isArrayTypeNode(typeNode)) {
|
|
3263
|
-
const elementType = typeNode.getElementTypeNode();
|
|
3264
|
-
return `Array<${resolveTypeNodeToString(elementType, sourceFile, project, depth)}>`;
|
|
3265
|
-
}
|
|
3266
|
-
if (Node7.isUnionTypeNode(typeNode)) {
|
|
3267
|
-
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" | ");
|
|
3268
|
-
}
|
|
3269
|
-
if (Node7.isIntersectionTypeNode(typeNode)) {
|
|
2667
|
+
if (Node6.isIntersectionTypeNode(typeNode)) {
|
|
3270
2668
|
return typeNode.getTypeNodes().map((t) => resolveTypeNodeToString(t, sourceFile, project, depth)).join(" & ");
|
|
3271
2669
|
}
|
|
3272
|
-
if (
|
|
2670
|
+
if (Node6.isParenthesizedTypeNode(typeNode)) {
|
|
3273
2671
|
return `(${resolveTypeNodeToString(typeNode.getTypeNode(), sourceFile, project, depth)})`;
|
|
3274
2672
|
}
|
|
3275
|
-
if (
|
|
2673
|
+
if (Node6.isTypeReference(typeNode)) {
|
|
3276
2674
|
const typeName = typeNode.getTypeName();
|
|
3277
|
-
const name =
|
|
2675
|
+
const name = Node6.isIdentifier(typeName) ? typeName.getText() : typeNode.getText();
|
|
3278
2676
|
if (name === "string" || name === "number" || name === "boolean") return name;
|
|
3279
2677
|
if (name === "Date") return "string";
|
|
3280
2678
|
if (name === "unknown" || name === "any" || name === "void") return "unknown";
|
|
3281
2679
|
if (name === "StreamableFile" || name === "Observable" || name === "ReadableStream")
|
|
3282
2680
|
return "unknown";
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3287
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3288
|
-
}
|
|
3289
|
-
return "unknown";
|
|
3290
|
-
}
|
|
3291
|
-
if (name === "Collection") {
|
|
3292
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3293
|
-
const firstTypeArg = typeArgs[0];
|
|
3294
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3295
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3296
|
-
}
|
|
3297
|
-
return "Array<unknown>";
|
|
3298
|
-
}
|
|
3299
|
-
if (name === "Opt" || name === "Loaded") {
|
|
3300
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3301
|
-
const firstTypeArg = typeArgs[0];
|
|
3302
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3303
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3304
|
-
}
|
|
3305
|
-
return "unknown";
|
|
3306
|
-
}
|
|
3307
|
-
if (name === "Array") {
|
|
3308
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3309
|
-
const firstTypeArg = typeArgs[0];
|
|
3310
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3311
|
-
return `Array<${resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth)}>`;
|
|
3312
|
-
}
|
|
3313
|
-
return "Array<unknown>";
|
|
2681
|
+
const wrapperMode = WRAPPER_TYPES[name];
|
|
2682
|
+
if (wrapperMode) {
|
|
2683
|
+
return unwrapFirstTypeArg(typeNode, sourceFile, project, depth, wrapperMode);
|
|
3314
2684
|
}
|
|
3315
|
-
if (
|
|
2685
|
+
if (PASSTHROUGH_UTILITY.has(name)) {
|
|
3316
2686
|
return typeNode.getText();
|
|
3317
2687
|
}
|
|
3318
|
-
if (name === "Promise") {
|
|
3319
|
-
const typeArgs = typeNode.getTypeArguments();
|
|
3320
|
-
const firstTypeArg = typeArgs[0];
|
|
3321
|
-
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
3322
|
-
return resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
3323
|
-
}
|
|
3324
|
-
return "unknown";
|
|
3325
|
-
}
|
|
3326
2688
|
const resolved = findType(name, sourceFile, project);
|
|
3327
2689
|
if (resolved) {
|
|
3328
2690
|
return expandTypeDecl(resolved, project, depth - 1);
|
|
@@ -3338,6 +2700,15 @@ function resolveTypeNodeToString(typeNode, sourceFile, project, depth) {
|
|
|
3338
2700
|
if (kind === SyntaxKind3.AnyKeyword) return "unknown";
|
|
3339
2701
|
return typeNode.getText();
|
|
3340
2702
|
}
|
|
2703
|
+
function unwrapFirstTypeArg(typeNode, sourceFile, project, depth, mode) {
|
|
2704
|
+
const typeArgs = typeNode.getTypeArguments();
|
|
2705
|
+
const firstTypeArg = typeArgs[0];
|
|
2706
|
+
if (typeArgs.length > 0 && firstTypeArg !== void 0) {
|
|
2707
|
+
const inner = resolveTypeNodeToString(firstTypeArg, sourceFile, project, depth);
|
|
2708
|
+
return mode === "arrayOf" ? `Array<${inner}>` : inner;
|
|
2709
|
+
}
|
|
2710
|
+
return mode === "arrayOf" ? "Array<unknown>" : "unknown";
|
|
2711
|
+
}
|
|
3341
2712
|
function expandTypeDecl(result, project, depth) {
|
|
3342
2713
|
if (depth < 0) return "unknown";
|
|
3343
2714
|
switch (result.kind) {
|
|
@@ -3403,7 +2774,7 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
3403
2774
|
const paramArgs = paramDecorator.getArguments();
|
|
3404
2775
|
if (paramArgs.length === 0) continue;
|
|
3405
2776
|
const nameArg = paramArgs[0];
|
|
3406
|
-
if (!
|
|
2777
|
+
if (!Node6.isStringLiteral(nameArg)) continue;
|
|
3407
2778
|
const paramName = nameArg.getLiteralValue();
|
|
3408
2779
|
const typeNode = param.getTypeNode();
|
|
3409
2780
|
const paramType = typeNode ? resolveTypeNodeToString(typeNode, sourceFile, project, 3) : "string";
|
|
@@ -3416,13 +2787,13 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3416
2787
|
if (apiResponseDecorator) {
|
|
3417
2788
|
const args = apiResponseDecorator.getArguments();
|
|
3418
2789
|
const optsArg = args[0];
|
|
3419
|
-
if (optsArg &&
|
|
2790
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3420
2791
|
for (const prop of optsArg.getProperties()) {
|
|
3421
|
-
if (!
|
|
2792
|
+
if (!Node6.isPropertyAssignment(prop)) continue;
|
|
3422
2793
|
if (prop.getName() !== "type") continue;
|
|
3423
2794
|
const val = prop.getInitializer();
|
|
3424
2795
|
if (!val) continue;
|
|
3425
|
-
if (
|
|
2796
|
+
if (Node6.isArrayLiteralExpression(val)) {
|
|
3426
2797
|
const elements = val.getElements();
|
|
3427
2798
|
const firstEl = elements[0];
|
|
3428
2799
|
if (elements.length > 0 && firstEl !== void 0) {
|
|
@@ -3442,7 +2813,7 @@ function extractResponseType(method, sourceFile, project) {
|
|
|
3442
2813
|
return "unknown";
|
|
3443
2814
|
}
|
|
3444
2815
|
function resolveIdentifierToClassType(node, sourceFile, project, depth) {
|
|
3445
|
-
if (!
|
|
2816
|
+
if (!Node6.isIdentifier(node)) return "unknown";
|
|
3446
2817
|
const name = node.getText();
|
|
3447
2818
|
const resolved = findType(name, sourceFile, project);
|
|
3448
2819
|
if (resolved) {
|
|
@@ -3489,11 +2860,11 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3489
2860
|
if (apiResp) {
|
|
3490
2861
|
const args = apiResp.getArguments();
|
|
3491
2862
|
const optsArg = args[0];
|
|
3492
|
-
if (optsArg &&
|
|
2863
|
+
if (optsArg && Node6.isObjectLiteralExpression(optsArg)) {
|
|
3493
2864
|
for (const prop of optsArg.getProperties()) {
|
|
3494
|
-
if (
|
|
2865
|
+
if (Node6.isPropertyAssignment(prop) && prop.getName() === "type") {
|
|
3495
2866
|
const val = prop.getInitializer();
|
|
3496
|
-
if (val &&
|
|
2867
|
+
if (val && Node6.isIdentifier(val)) {
|
|
3497
2868
|
const name = val.getText();
|
|
3498
2869
|
const localDecl = sourceFile.getInterface(name) || sourceFile.getClass(name) || sourceFile.getTypeAlias(name);
|
|
3499
2870
|
if (localDecl?.isExported()) {
|
|
@@ -3510,27 +2881,18 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3510
2881
|
}
|
|
3511
2882
|
}
|
|
3512
2883
|
}
|
|
3513
|
-
let bodyZodText = null;
|
|
3514
|
-
let queryZodText = null;
|
|
3515
2884
|
let bodySchema = null;
|
|
3516
2885
|
let querySchema = null;
|
|
3517
|
-
const formNested = {};
|
|
3518
2886
|
const formWarnings = [];
|
|
3519
2887
|
const bodyClass = resolveParamClass(method, "Body", sourceFile, project);
|
|
3520
2888
|
if (bodyClass) {
|
|
3521
|
-
const result = extractZodFromDto(bodyClass.decl, bodyClass.file, project);
|
|
3522
|
-
bodyZodText = result.schemaText;
|
|
3523
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
3524
|
-
formWarnings.push(...result.warnings);
|
|
3525
2889
|
bodySchema = extractSchemaFromDto(bodyClass.decl, bodyClass.file, project);
|
|
2890
|
+
formWarnings.push(...bodySchema.warnings);
|
|
3526
2891
|
}
|
|
3527
2892
|
const queryClass = resolveParamClass(method, "Query", sourceFile, project);
|
|
3528
2893
|
if (queryClass) {
|
|
3529
|
-
const result = extractZodFromDto(queryClass.decl, queryClass.file, project);
|
|
3530
|
-
queryZodText = result.schemaText;
|
|
3531
|
-
for (const [k, v] of result.namedNestedSchemas) formNested[k] = v;
|
|
3532
|
-
formWarnings.push(...result.warnings);
|
|
3533
2894
|
querySchema = extractSchemaFromDto(queryClass.decl, queryClass.file, project);
|
|
2895
|
+
formWarnings.push(...querySchema.warnings);
|
|
3534
2896
|
}
|
|
3535
2897
|
return {
|
|
3536
2898
|
query,
|
|
@@ -3543,9 +2905,6 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3543
2905
|
filterFields: filterInfo?.fieldNames ?? null,
|
|
3544
2906
|
filterFieldTypes: filterInfo?.fieldTypes ?? null,
|
|
3545
2907
|
filterSource: filterInfo?.source ?? null,
|
|
3546
|
-
bodyZodText,
|
|
3547
|
-
queryZodText,
|
|
3548
|
-
formNestedSchemas: Object.keys(formNested).length > 0 ? formNested : null,
|
|
3549
2908
|
formWarnings,
|
|
3550
2909
|
bodySchema,
|
|
3551
2910
|
querySchema
|
|
@@ -3565,6 +2924,201 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
3565
2924
|
}
|
|
3566
2925
|
return null;
|
|
3567
2926
|
}
|
|
2927
|
+
|
|
2928
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
2929
|
+
import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2930
|
+
function zodAstToTs(node) {
|
|
2931
|
+
if (!Node7.isCallExpression(node)) return "unknown";
|
|
2932
|
+
const expr = node.getExpression();
|
|
2933
|
+
if (Node7.isPropertyAccessExpression(expr)) {
|
|
2934
|
+
const methodName = expr.getName();
|
|
2935
|
+
const receiver = expr.getExpression();
|
|
2936
|
+
if (methodName === "optional") {
|
|
2937
|
+
return `${zodAstToTs(receiver)} | undefined`;
|
|
2938
|
+
}
|
|
2939
|
+
if (methodName === "nullable") {
|
|
2940
|
+
return `${zodAstToTs(receiver)} | null`;
|
|
2941
|
+
}
|
|
2942
|
+
const args = node.getArguments();
|
|
2943
|
+
switch (methodName) {
|
|
2944
|
+
case "string":
|
|
2945
|
+
return "string";
|
|
2946
|
+
case "number":
|
|
2947
|
+
return "number";
|
|
2948
|
+
case "boolean":
|
|
2949
|
+
return "boolean";
|
|
2950
|
+
case "unknown":
|
|
2951
|
+
return "unknown";
|
|
2952
|
+
case "any":
|
|
2953
|
+
return "unknown";
|
|
2954
|
+
case "literal": {
|
|
2955
|
+
const lit = args[0];
|
|
2956
|
+
if (!lit) return "unknown";
|
|
2957
|
+
if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
2958
|
+
if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
2959
|
+
if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
|
|
2960
|
+
if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
|
|
2961
|
+
return "unknown";
|
|
2962
|
+
}
|
|
2963
|
+
case "enum": {
|
|
2964
|
+
const arrArg = args[0];
|
|
2965
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2966
|
+
const members = arrArg.getElements().map(
|
|
2967
|
+
(el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
2968
|
+
);
|
|
2969
|
+
return members.join(" | ");
|
|
2970
|
+
}
|
|
2971
|
+
case "array": {
|
|
2972
|
+
const inner = args[0];
|
|
2973
|
+
if (!inner) return "unknown";
|
|
2974
|
+
return `Array<${zodAstToTs(inner)}>`;
|
|
2975
|
+
}
|
|
2976
|
+
case "object": {
|
|
2977
|
+
const objArg = args[0];
|
|
2978
|
+
if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
|
|
2979
|
+
const lines = [];
|
|
2980
|
+
for (const prop of objArg.getProperties()) {
|
|
2981
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
2982
|
+
const key = prop.getName();
|
|
2983
|
+
const valNode = prop.getInitializer();
|
|
2984
|
+
if (!valNode) continue;
|
|
2985
|
+
const tsType = zodAstToTs(valNode);
|
|
2986
|
+
const isOpt = isOptionalChain(valNode);
|
|
2987
|
+
lines.push(`${key}${isOpt ? "?" : ""}: ${tsType}`);
|
|
2988
|
+
}
|
|
2989
|
+
return `{ ${lines.join("; ")} }`;
|
|
2990
|
+
}
|
|
2991
|
+
case "union": {
|
|
2992
|
+
const arrArg = args[0];
|
|
2993
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
2994
|
+
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
2995
|
+
}
|
|
2996
|
+
case "record": {
|
|
2997
|
+
const valArg = args.length === 1 ? args[0] : args[1];
|
|
2998
|
+
if (!valArg) return "unknown";
|
|
2999
|
+
return `Record<string, ${zodAstToTs(valArg)}>`;
|
|
3000
|
+
}
|
|
3001
|
+
case "tuple": {
|
|
3002
|
+
const arrArg = args[0];
|
|
3003
|
+
if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
3004
|
+
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
3005
|
+
}
|
|
3006
|
+
default:
|
|
3007
|
+
return "unknown";
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return "unknown";
|
|
3011
|
+
}
|
|
3012
|
+
function isOptionalChain(node) {
|
|
3013
|
+
if (!Node7.isCallExpression(node)) return false;
|
|
3014
|
+
const expr = node.getExpression();
|
|
3015
|
+
return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
3016
|
+
}
|
|
3017
|
+
function parseDefineContractCall(callExpr) {
|
|
3018
|
+
if (!Node7.isCallExpression(callExpr)) return null;
|
|
3019
|
+
const callee = callExpr.getExpression();
|
|
3020
|
+
const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
3021
|
+
if (calleeName !== "defineContract") return null;
|
|
3022
|
+
const args = callExpr.getArguments();
|
|
3023
|
+
const optsArg = args[0];
|
|
3024
|
+
if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
|
|
3025
|
+
let query = null;
|
|
3026
|
+
let body = null;
|
|
3027
|
+
let response = "unknown";
|
|
3028
|
+
let bodyZodText = null;
|
|
3029
|
+
let queryZodText = null;
|
|
3030
|
+
for (const prop of optsArg.getProperties()) {
|
|
3031
|
+
if (!Node7.isPropertyAssignment(prop)) continue;
|
|
3032
|
+
const propName = prop.getName();
|
|
3033
|
+
const val = prop.getInitializer();
|
|
3034
|
+
if (!val) continue;
|
|
3035
|
+
if (propName === "query") {
|
|
3036
|
+
query = zodAstToTs(val);
|
|
3037
|
+
queryZodText = val.getText();
|
|
3038
|
+
} else if (propName === "body") {
|
|
3039
|
+
body = zodAstToTs(val);
|
|
3040
|
+
bodyZodText = val.getText();
|
|
3041
|
+
} else if (propName === "response") {
|
|
3042
|
+
response = zodAstToTs(val);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
return { query, body, response, bodyZodText, queryZodText };
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// src/discovery/contracts-fast.ts
|
|
3049
|
+
async function discoverContractsFast(opts) {
|
|
3050
|
+
const { cwd, glob, tsconfig } = opts;
|
|
3051
|
+
const tsconfigPath = tsconfig ? resolve3(tsconfig) : join11(cwd, "tsconfig.json");
|
|
3052
|
+
let project;
|
|
3053
|
+
try {
|
|
3054
|
+
project = new Project3({
|
|
3055
|
+
tsConfigFilePath: tsconfigPath,
|
|
3056
|
+
skipAddingFilesFromTsConfig: true,
|
|
3057
|
+
skipLoadingLibFiles: true,
|
|
3058
|
+
skipFileDependencyResolution: true
|
|
3059
|
+
});
|
|
3060
|
+
} catch {
|
|
3061
|
+
project = new Project3({
|
|
3062
|
+
skipAddingFilesFromTsConfig: true,
|
|
3063
|
+
skipLoadingLibFiles: true,
|
|
3064
|
+
skipFileDependencyResolution: true,
|
|
3065
|
+
compilerOptions: {
|
|
3066
|
+
allowJs: true,
|
|
3067
|
+
resolveJsonModule: false,
|
|
3068
|
+
strict: false
|
|
3069
|
+
}
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
const files = await fg2(glob, { cwd, absolute: true, onlyFiles: true });
|
|
3073
|
+
for (const f of files) {
|
|
3074
|
+
project.addSourceFileAtPath(f);
|
|
3075
|
+
}
|
|
3076
|
+
const routes = [];
|
|
3077
|
+
setDiscoveryContext(project, {
|
|
3078
|
+
projectRoot: cwd,
|
|
3079
|
+
tsconfigPaths: loadTsconfigPaths(tsconfigPath)
|
|
3080
|
+
});
|
|
3081
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
3082
|
+
routes.push(...extractFromSourceFile(sourceFile, project));
|
|
3083
|
+
}
|
|
3084
|
+
return routes;
|
|
3085
|
+
}
|
|
3086
|
+
function decoratorStringArg(decoratorExpr) {
|
|
3087
|
+
if (!decoratorExpr) return void 0;
|
|
3088
|
+
if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
3089
|
+
if (Node8.isArrayLiteralExpression(decoratorExpr)) {
|
|
3090
|
+
const first = decoratorExpr.getElements()[0];
|
|
3091
|
+
if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
|
|
3092
|
+
}
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
function deriveClassSegment(className) {
|
|
3096
|
+
const noSuffix = className.replace(/Controller$/, "");
|
|
3097
|
+
if (!noSuffix) {
|
|
3098
|
+
throw new Error(
|
|
3099
|
+
`Controller class name "${className}" derives empty route segment after stripping "Controller". Add an @As(...) override at the class level.`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
return noSuffix.charAt(0).toLowerCase() + noSuffix.slice(1);
|
|
3103
|
+
}
|
|
3104
|
+
function resolveRouteName(className, methodName, classAs, methodAs) {
|
|
3105
|
+
const classPortion = classAs ?? deriveClassSegment(className);
|
|
3106
|
+
const methodPortion = methodAs ?? methodName;
|
|
3107
|
+
return `${classPortion}.${methodPortion}`;
|
|
3108
|
+
}
|
|
3109
|
+
function joinPaths(prefix, suffix) {
|
|
3110
|
+
if (!prefix && !suffix) return "/";
|
|
3111
|
+
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3112
|
+
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
3113
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3114
|
+
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3115
|
+
const combined = p + s;
|
|
3116
|
+
return combined === "" ? "/" : combined;
|
|
3117
|
+
}
|
|
3118
|
+
function extractParams(path) {
|
|
3119
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
3120
|
+
return Array.from(matches).map((m) => ({ name: m[1], source: "path" }));
|
|
3121
|
+
}
|
|
3568
3122
|
var HTTP_METHOD_DECORATORS = {
|
|
3569
3123
|
Get: "GET",
|
|
3570
3124
|
Post: "POST",
|
|
@@ -3575,176 +3129,186 @@ var HTTP_METHOD_DECORATORS = {
|
|
|
3575
3129
|
Head: "HEAD",
|
|
3576
3130
|
All: "ALL"
|
|
3577
3131
|
};
|
|
3132
|
+
function resolveVerb(method) {
|
|
3133
|
+
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3134
|
+
const httpDecorator = method.getDecorator(decoratorName);
|
|
3135
|
+
if (httpDecorator) {
|
|
3136
|
+
const httpArgs = httpDecorator.getArguments();
|
|
3137
|
+
const pathArg = httpArgs[0];
|
|
3138
|
+
return { httpMethod: verb, handlerPath: decoratorStringArg(pathArg) ?? "" };
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
function readAsDecorator(node, label) {
|
|
3144
|
+
const asDecorator = node.getDecorator("As");
|
|
3145
|
+
if (!asDecorator) return void 0;
|
|
3146
|
+
const asName = decoratorStringArg(asDecorator.getArguments()[0]);
|
|
3147
|
+
if (!asName) {
|
|
3148
|
+
throw new Error(`@As decorator on ${label} must have a non-empty string argument.`);
|
|
3149
|
+
}
|
|
3150
|
+
return asName;
|
|
3151
|
+
}
|
|
3152
|
+
function buildRoute(args) {
|
|
3153
|
+
const {
|
|
3154
|
+
className,
|
|
3155
|
+
methodName,
|
|
3156
|
+
resolvedMethod,
|
|
3157
|
+
combinedPath,
|
|
3158
|
+
classAs,
|
|
3159
|
+
methodAs,
|
|
3160
|
+
sourceFile,
|
|
3161
|
+
seenNames,
|
|
3162
|
+
contractSource
|
|
3163
|
+
} = args;
|
|
3164
|
+
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3165
|
+
const qualifiedRef = `${className}.${methodName}`;
|
|
3166
|
+
const existing = seenNames.get(routeName);
|
|
3167
|
+
if (existing !== void 0) {
|
|
3168
|
+
throw new Error(
|
|
3169
|
+
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
seenNames.set(routeName, qualifiedRef);
|
|
3173
|
+
return {
|
|
3174
|
+
method: resolvedMethod,
|
|
3175
|
+
path: combinedPath,
|
|
3176
|
+
name: routeName,
|
|
3177
|
+
params: extractParams(combinedPath),
|
|
3178
|
+
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3179
|
+
contract: { contractSource }
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
function extractContractRoute(args) {
|
|
3183
|
+
const { cls, method, applyContractDecorator, verb, prefix, className, sourceFile, seenNames } = args;
|
|
3184
|
+
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
3185
|
+
if (!firstDecoratorArg) return null;
|
|
3186
|
+
let contractDef = null;
|
|
3187
|
+
let bodyZodRef = null;
|
|
3188
|
+
let queryZodRef = null;
|
|
3189
|
+
if (Node8.isCallExpression(firstDecoratorArg)) {
|
|
3190
|
+
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
3191
|
+
} else if (Node8.isIdentifier(firstDecoratorArg)) {
|
|
3192
|
+
const identName = firstDecoratorArg.getText();
|
|
3193
|
+
const varDecl = sourceFile.getVariableDeclaration(identName);
|
|
3194
|
+
if (!varDecl) {
|
|
3195
|
+
console.warn(
|
|
3196
|
+
`[nestjs-codegen/fast] Cannot resolve '${identName}' in ${sourceFile.getFilePath()} (cross-file imports are out-of-scope for v1) \u2014 skipping`
|
|
3197
|
+
);
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
const initializer = varDecl.getInitializer();
|
|
3201
|
+
if (!initializer) return null;
|
|
3202
|
+
contractDef = parseDefineContractCall(initializer);
|
|
3203
|
+
if (contractDef && varDecl.isExported()) {
|
|
3204
|
+
const filePath = sourceFile.getFilePath();
|
|
3205
|
+
if (contractDef.body !== null) {
|
|
3206
|
+
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3207
|
+
}
|
|
3208
|
+
if (contractDef.query !== null) {
|
|
3209
|
+
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
} else {
|
|
3213
|
+
console.warn(
|
|
3214
|
+
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3215
|
+
);
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
if (!contractDef) return null;
|
|
3219
|
+
if (!verb) return null;
|
|
3220
|
+
const resolvedPath = joinPaths(prefix, verb.handlerPath);
|
|
3221
|
+
const methodName = method.getName();
|
|
3222
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3223
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3224
|
+
return buildRoute({
|
|
3225
|
+
className,
|
|
3226
|
+
methodName,
|
|
3227
|
+
resolvedMethod: verb.httpMethod,
|
|
3228
|
+
combinedPath: resolvedPath,
|
|
3229
|
+
classAs,
|
|
3230
|
+
methodAs,
|
|
3231
|
+
sourceFile,
|
|
3232
|
+
seenNames,
|
|
3233
|
+
contractSource: {
|
|
3234
|
+
query: contractDef.query,
|
|
3235
|
+
body: contractDef.body,
|
|
3236
|
+
response: contractDef.response,
|
|
3237
|
+
// Path A: capture both the importable ref and the raw text. The emitter
|
|
3238
|
+
// prefers inlining the text (client-safe — re-exporting from a controller
|
|
3239
|
+
// would drag server-only deps into the client bundle).
|
|
3240
|
+
bodyZodRef,
|
|
3241
|
+
bodyZodText: contractDef.bodyZodText,
|
|
3242
|
+
queryZodRef,
|
|
3243
|
+
queryZodText: contractDef.queryZodText
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
function extractDtoRoute(args) {
|
|
3248
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
3249
|
+
if (!verb) return null;
|
|
3250
|
+
const combined = joinPaths(prefix, verb.handlerPath);
|
|
3251
|
+
const methodName = method.getName();
|
|
3252
|
+
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
3253
|
+
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
3254
|
+
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3255
|
+
return buildRoute({
|
|
3256
|
+
className,
|
|
3257
|
+
methodName,
|
|
3258
|
+
resolvedMethod: verb.httpMethod,
|
|
3259
|
+
combinedPath: combined,
|
|
3260
|
+
classAs,
|
|
3261
|
+
methodAs,
|
|
3262
|
+
sourceFile,
|
|
3263
|
+
seenNames,
|
|
3264
|
+
contractSource: {
|
|
3265
|
+
query: dtoContract?.query ?? null,
|
|
3266
|
+
body: dtoContract?.body ?? null,
|
|
3267
|
+
response: dtoContract?.response ?? "unknown",
|
|
3268
|
+
queryRef: dtoContract?.queryRef ?? null,
|
|
3269
|
+
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3270
|
+
responseRef: dtoContract?.responseRef ?? null,
|
|
3271
|
+
filterFields: dtoContract?.filterFields ?? null,
|
|
3272
|
+
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3273
|
+
filterSource: dtoContract?.filterSource ?? null,
|
|
3274
|
+
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3275
|
+
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3276
|
+
querySchema: dtoContract?.querySchema ?? null
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3578
3280
|
function extractFromSourceFile(sourceFile, project) {
|
|
3579
3281
|
const routes = [];
|
|
3580
3282
|
const seenNames = /* @__PURE__ */ new Map();
|
|
3581
|
-
const
|
|
3582
|
-
for (const cls of classes) {
|
|
3283
|
+
for (const cls of sourceFile.getClasses()) {
|
|
3583
3284
|
const controllerDecorator = cls.getDecorator("Controller");
|
|
3584
3285
|
if (!controllerDecorator) continue;
|
|
3585
|
-
const
|
|
3586
|
-
const
|
|
3587
|
-
const prefix = decoratorStringArg(firstArg3) ?? "";
|
|
3286
|
+
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
3287
|
+
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
3588
3288
|
const className = cls.getName() ?? "Unknown";
|
|
3589
3289
|
for (const method of cls.getMethods()) {
|
|
3590
|
-
|
|
3591
|
-
let handlerPath = "";
|
|
3592
|
-
for (const [decoratorName, verb] of Object.entries(HTTP_METHOD_DECORATORS)) {
|
|
3593
|
-
const httpDecorator = method.getDecorator(decoratorName);
|
|
3594
|
-
if (httpDecorator) {
|
|
3595
|
-
httpMethod = verb;
|
|
3596
|
-
const httpArgs = httpDecorator.getArguments();
|
|
3597
|
-
const pathArg = httpArgs[0];
|
|
3598
|
-
handlerPath = decoratorStringArg(pathArg) ?? "";
|
|
3599
|
-
break;
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3290
|
+
const verb = resolveVerb(method);
|
|
3602
3291
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
contractDef = parseDefineContractCall(initializer);
|
|
3624
|
-
if (contractDef && varDecl.isExported()) {
|
|
3625
|
-
const filePath = sourceFile.getFilePath();
|
|
3626
|
-
if (contractDef.body !== null) {
|
|
3627
|
-
bodyZodRef = { name: `${identName}.body`, filePath };
|
|
3628
|
-
}
|
|
3629
|
-
if (contractDef.query !== null) {
|
|
3630
|
-
queryZodRef = { name: `${identName}.query`, filePath };
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
} else {
|
|
3634
|
-
console.warn(
|
|
3635
|
-
`[nestjs-codegen/fast] @ApplyContract arg is not an identifier or call expression in ${sourceFile.getFilePath()} \u2014 skipping`
|
|
3636
|
-
);
|
|
3637
|
-
continue;
|
|
3638
|
-
}
|
|
3639
|
-
if (!contractDef) continue;
|
|
3640
|
-
if (!httpMethod) continue;
|
|
3641
|
-
const resolvedMethod = httpMethod;
|
|
3642
|
-
const resolvedPath = joinPaths(prefix, handlerPath);
|
|
3643
|
-
const combined = resolvedPath;
|
|
3644
|
-
const params = extractParams(combined);
|
|
3645
|
-
const methodName = method.getName();
|
|
3646
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3647
|
-
let classAs;
|
|
3648
|
-
if (classAsDecorator) {
|
|
3649
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3650
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3651
|
-
if (!classAsName) {
|
|
3652
|
-
throw new Error(
|
|
3653
|
-
`@As decorator on class ${className} must have a non-empty string argument.`
|
|
3654
|
-
);
|
|
3655
|
-
}
|
|
3656
|
-
classAs = classAsName;
|
|
3657
|
-
}
|
|
3658
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3659
|
-
let methodAs;
|
|
3660
|
-
if (methodAsDecorator) {
|
|
3661
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3662
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3663
|
-
if (!methodAsName) {
|
|
3664
|
-
throw new Error(
|
|
3665
|
-
`@As decorator on ${className}.${methodName} must have a non-empty string argument.`
|
|
3666
|
-
);
|
|
3667
|
-
}
|
|
3668
|
-
methodAs = methodAsName;
|
|
3669
|
-
}
|
|
3670
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3671
|
-
const qualifiedRef = `${className}.${methodName}`;
|
|
3672
|
-
const existing = seenNames.get(routeName);
|
|
3673
|
-
if (existing !== void 0) {
|
|
3674
|
-
throw new Error(
|
|
3675
|
-
`Route name collision: "${routeName}" is used by both "${existing}" and "${qualifiedRef}". Use @As(...) to give one of them a unique name.`
|
|
3676
|
-
);
|
|
3677
|
-
}
|
|
3678
|
-
seenNames.set(routeName, qualifiedRef);
|
|
3679
|
-
routes.push({
|
|
3680
|
-
method: resolvedMethod,
|
|
3681
|
-
path: combined,
|
|
3682
|
-
name: routeName,
|
|
3683
|
-
params,
|
|
3684
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3685
|
-
contract: {
|
|
3686
|
-
contractSource: {
|
|
3687
|
-
query: contractDef.query,
|
|
3688
|
-
body: contractDef.body,
|
|
3689
|
-
response: contractDef.response,
|
|
3690
|
-
// Path A: capture both the importable ref and the raw text. The
|
|
3691
|
-
// emitter prefers inlining the text (client-safe — re-exporting from
|
|
3692
|
-
// a controller would drag server-only deps into the client bundle).
|
|
3693
|
-
bodyZodRef,
|
|
3694
|
-
bodyZodText: contractDef.bodyZodText,
|
|
3695
|
-
queryZodRef,
|
|
3696
|
-
queryZodText: contractDef.queryZodText
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
});
|
|
3700
|
-
} else {
|
|
3701
|
-
if (!httpMethod) continue;
|
|
3702
|
-
const combined = joinPaths(prefix, handlerPath);
|
|
3703
|
-
const params = extractParams(combined);
|
|
3704
|
-
const methodName = method.getName();
|
|
3705
|
-
const classAsDecorator = cls.getDecorator("As");
|
|
3706
|
-
let classAs;
|
|
3707
|
-
if (classAsDecorator) {
|
|
3708
|
-
const classAsArgs = classAsDecorator.getArguments();
|
|
3709
|
-
const classAsName = decoratorStringArg(classAsArgs[0]);
|
|
3710
|
-
if (classAsName) classAs = classAsName;
|
|
3711
|
-
}
|
|
3712
|
-
const methodAsDecorator = method.getDecorator("As");
|
|
3713
|
-
let methodAs;
|
|
3714
|
-
if (methodAsDecorator) {
|
|
3715
|
-
const methodAsArgs = methodAsDecorator.getArguments();
|
|
3716
|
-
const methodAsName = decoratorStringArg(methodAsArgs[0]);
|
|
3717
|
-
if (methodAsName) methodAs = methodAsName;
|
|
3718
|
-
}
|
|
3719
|
-
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
3720
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
3721
|
-
routes.push({
|
|
3722
|
-
method: httpMethod,
|
|
3723
|
-
path: combined,
|
|
3724
|
-
name: routeName,
|
|
3725
|
-
params,
|
|
3726
|
-
controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
|
|
3727
|
-
contract: {
|
|
3728
|
-
contractSource: {
|
|
3729
|
-
query: dtoContract?.query ?? null,
|
|
3730
|
-
body: dtoContract?.body ?? null,
|
|
3731
|
-
response: dtoContract?.response ?? "unknown",
|
|
3732
|
-
queryRef: dtoContract?.queryRef ?? null,
|
|
3733
|
-
bodyRef: dtoContract?.bodyRef ?? null,
|
|
3734
|
-
responseRef: dtoContract?.responseRef ?? null,
|
|
3735
|
-
filterFields: dtoContract?.filterFields ?? null,
|
|
3736
|
-
filterFieldTypes: dtoContract?.filterFieldTypes ?? null,
|
|
3737
|
-
filterSource: dtoContract?.filterSource ?? null,
|
|
3738
|
-
bodyZodText: dtoContract?.bodyZodText ?? null,
|
|
3739
|
-
queryZodText: dtoContract?.queryZodText ?? null,
|
|
3740
|
-
formNestedSchemas: dtoContract?.formNestedSchemas ?? null,
|
|
3741
|
-
formWarnings: dtoContract?.formWarnings ?? [],
|
|
3742
|
-
bodySchema: dtoContract?.bodySchema ?? null,
|
|
3743
|
-
querySchema: dtoContract?.querySchema ?? null
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
});
|
|
3747
|
-
}
|
|
3292
|
+
const route = applyContractDecorator ? extractContractRoute({
|
|
3293
|
+
cls,
|
|
3294
|
+
method,
|
|
3295
|
+
applyContractDecorator,
|
|
3296
|
+
verb,
|
|
3297
|
+
prefix,
|
|
3298
|
+
className,
|
|
3299
|
+
sourceFile,
|
|
3300
|
+
seenNames
|
|
3301
|
+
}) : extractDtoRoute({
|
|
3302
|
+
cls,
|
|
3303
|
+
method,
|
|
3304
|
+
verb,
|
|
3305
|
+
prefix,
|
|
3306
|
+
className,
|
|
3307
|
+
sourceFile,
|
|
3308
|
+
project,
|
|
3309
|
+
seenNames
|
|
3310
|
+
});
|
|
3311
|
+
if (route) routes.push(route);
|
|
3748
3312
|
}
|
|
3749
3313
|
}
|
|
3750
3314
|
return routes;
|
|
@@ -3753,7 +3317,7 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
3753
3317
|
// src/watch/lock-file.ts
|
|
3754
3318
|
import { open } from "fs/promises";
|
|
3755
3319
|
import { mkdir as mkdir8, readFile as readFile2, unlink } from "fs/promises";
|
|
3756
|
-
import { join as
|
|
3320
|
+
import { join as join12 } from "path";
|
|
3757
3321
|
var LOCK_FILE = ".watcher.lock";
|
|
3758
3322
|
function isProcessAlive(pid) {
|
|
3759
3323
|
try {
|
|
@@ -3765,7 +3329,7 @@ function isProcessAlive(pid) {
|
|
|
3765
3329
|
}
|
|
3766
3330
|
async function acquireLock(outDir) {
|
|
3767
3331
|
await mkdir8(outDir, { recursive: true });
|
|
3768
|
-
const lockPath =
|
|
3332
|
+
const lockPath = join12(outDir, LOCK_FILE);
|
|
3769
3333
|
const lockData = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3770
3334
|
try {
|
|
3771
3335
|
const fd = await open(lockPath, "wx");
|
|
@@ -3805,7 +3369,7 @@ async function watch(config, onChange) {
|
|
|
3805
3369
|
if (lock === null) {
|
|
3806
3370
|
let holderPid = "unknown";
|
|
3807
3371
|
try {
|
|
3808
|
-
const raw = await readFile3(
|
|
3372
|
+
const raw = await readFile3(join13(config.codegen.outDir, ".watcher.lock"), "utf8");
|
|
3809
3373
|
const data = JSON.parse(raw);
|
|
3810
3374
|
if (data.pid !== void 0) holderPid = String(data.pid);
|
|
3811
3375
|
} catch {
|
|
@@ -3833,7 +3397,7 @@ async function watch(config, onChange) {
|
|
|
3833
3397
|
}
|
|
3834
3398
|
let pagesDebounceTimer;
|
|
3835
3399
|
const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
|
|
3836
|
-
const pagesWatcher = chokidar.watch(
|
|
3400
|
+
const pagesWatcher = chokidar.watch(join13(config.codegen.cwd, pagesGlob), {
|
|
3837
3401
|
ignoreInitial: true,
|
|
3838
3402
|
persistent: true,
|
|
3839
3403
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3859,7 +3423,7 @@ async function watch(config, onChange) {
|
|
|
3859
3423
|
pagesWatcher.on("change", schedulePagesRegenerate);
|
|
3860
3424
|
pagesWatcher.on("unlink", schedulePagesRegenerate);
|
|
3861
3425
|
let contractsDebounceTimer;
|
|
3862
|
-
const contractsWatcher = chokidar.watch(
|
|
3426
|
+
const contractsWatcher = chokidar.watch(join13(config.codegen.cwd, config.contracts.glob), {
|
|
3863
3427
|
ignoreInitial: true,
|
|
3864
3428
|
persistent: true,
|
|
3865
3429
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3889,7 +3453,7 @@ async function watch(config, onChange) {
|
|
|
3889
3453
|
contractsWatcher.on("add", scheduleContractsRegenerate);
|
|
3890
3454
|
contractsWatcher.on("change", scheduleContractsRegenerate);
|
|
3891
3455
|
contractsWatcher.on("unlink", scheduleContractsRegenerate);
|
|
3892
|
-
const formsWatcher = chokidar.watch(
|
|
3456
|
+
const formsWatcher = chokidar.watch(join13(config.codegen.cwd, config.forms.watch), {
|
|
3893
3457
|
ignoreInitial: true,
|
|
3894
3458
|
persistent: true,
|
|
3895
3459
|
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
|
|
@@ -3915,8 +3479,57 @@ async function watch(config, onChange) {
|
|
|
3915
3479
|
};
|
|
3916
3480
|
}
|
|
3917
3481
|
|
|
3482
|
+
// src/ir/render-ts-type.ts
|
|
3483
|
+
function tsKey(name) {
|
|
3484
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
3485
|
+
}
|
|
3486
|
+
function renderTsType(node, ctx) {
|
|
3487
|
+
switch (node.kind) {
|
|
3488
|
+
case "string":
|
|
3489
|
+
return "string";
|
|
3490
|
+
case "number":
|
|
3491
|
+
return "number";
|
|
3492
|
+
case "boolean":
|
|
3493
|
+
return "boolean";
|
|
3494
|
+
case "date":
|
|
3495
|
+
return "Date";
|
|
3496
|
+
case "unknown":
|
|
3497
|
+
return "unknown";
|
|
3498
|
+
case "instanceof":
|
|
3499
|
+
return node.ctor;
|
|
3500
|
+
case "enum":
|
|
3501
|
+
return node.literals.join(" | ");
|
|
3502
|
+
case "literal":
|
|
3503
|
+
return node.raw;
|
|
3504
|
+
case "union":
|
|
3505
|
+
return node.options.map((o) => renderTsType(o, ctx)).join(" | ");
|
|
3506
|
+
case "array":
|
|
3507
|
+
return `Array<${renderTsType(node.element, ctx)}>`;
|
|
3508
|
+
case "optional":
|
|
3509
|
+
return `${renderTsType(node.inner, ctx)} | undefined`;
|
|
3510
|
+
case "annotated":
|
|
3511
|
+
return renderTsType(node.inner, ctx);
|
|
3512
|
+
case "object": {
|
|
3513
|
+
if (node.fields.length === 0) return node.passthrough ? "Record<string, unknown>" : "{}";
|
|
3514
|
+
const inner = node.fields.map((f) => {
|
|
3515
|
+
if (f.value.kind === "optional") {
|
|
3516
|
+
return `${tsKey(f.key)}?: ${renderTsType(f.value.inner, ctx)}`;
|
|
3517
|
+
}
|
|
3518
|
+
return `${tsKey(f.key)}: ${renderTsType(f.value, ctx)}`;
|
|
3519
|
+
}).join("; ");
|
|
3520
|
+
return `{ ${inner} }`;
|
|
3521
|
+
}
|
|
3522
|
+
case "ref":
|
|
3523
|
+
case "lazyRef": {
|
|
3524
|
+
if (ctx.recursive.has(node.name)) return ctx.typeNameFor(node.name);
|
|
3525
|
+
const target = ctx.named.get(node.name);
|
|
3526
|
+
return target ? renderTsType(target, ctx) : "unknown";
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3918
3531
|
// src/index.ts
|
|
3919
|
-
var VERSION = "0.
|
|
3532
|
+
var VERSION = "0.4.0";
|
|
3920
3533
|
export {
|
|
3921
3534
|
CodegenError,
|
|
3922
3535
|
ConfigError,
|
|
@@ -3930,9 +3543,9 @@ export {
|
|
|
3930
3543
|
extractSchemaFromDto,
|
|
3931
3544
|
generate,
|
|
3932
3545
|
loadConfig,
|
|
3546
|
+
renderTsType,
|
|
3933
3547
|
resolveAdapter,
|
|
3934
3548
|
resolveConfig,
|
|
3935
|
-
watch
|
|
3936
|
-
zodAdapter
|
|
3549
|
+
watch
|
|
3937
3550
|
};
|
|
3938
3551
|
//# sourceMappingURL=index.js.map
|