@dudousxd/nestjs-codegen 0.14.2 → 0.16.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 +49 -0
- package/dist/cli/main.cjs +207 -57
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +206 -56
- package/dist/cli/main.js.map +1 -1
- 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.map +1 -1
- package/dist/{index-DT8SgPxp.d.cts → index-LQNP7Ms3.d.cts} +30 -0
- package/dist/{index-DT8SgPxp.d.ts → index-LQNP7Ms3.d.ts} +30 -0
- package/dist/index.cjs +207 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +206 -56
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +227 -59
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +56 -3
- package/dist/nest/index.d.ts +56 -3
- package/dist/nest/index.js +222 -57
- package/dist/nest/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @dudousxd/nestjs-codegen
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Discover routes on mixin (factory-produced) controllers.
|
|
8
|
+
|
|
9
|
+
A `@Controller` class whose heritage clause is a factory call now contributes the
|
|
10
|
+
decorated methods of the class that factory returns:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
@Controller("base/util/search")
|
|
14
|
+
export class SearchUtilController extends createTableController(Util, {
|
|
15
|
+
dto: UtilDTO,
|
|
16
|
+
}) {}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
NestJS already routes inherited methods at runtime via the prototype chain; the
|
|
20
|
+
static discovery pass now follows the same link. Each such route carries a new
|
|
21
|
+
`controllerRef.mixin` binding recording the factory and its call-site class
|
|
22
|
+
arguments — the decorator arguments _inside_ a factory reference its own
|
|
23
|
+
parameters, so the concrete entity is only knowable from the call site.
|
|
24
|
+
|
|
25
|
+
That binding drives two things:
|
|
26
|
+
|
|
27
|
+
- **`filterFields`** are derived from the call-site entity, so
|
|
28
|
+
`@ApplyFilter(GeneratedFilter)` works even though the filter class is declared
|
|
29
|
+
inside the factory and its `@Filterable({ entity })` names a parameter. This is
|
|
30
|
+
also what flips these routes from mutation to query in the emitted client.
|
|
31
|
+
- **Response types** are instantiated against the derived class, so
|
|
32
|
+
`Paginated<D>` resolves to the concrete `D`. This needs the type checker, which
|
|
33
|
+
needs lib files — the discovery Project sets `skipLoadingLibFiles` for
|
|
34
|
+
cold-start speed, so mixin response types resolve through a second Project
|
|
35
|
+
built lazily on the first mixin controller.
|
|
36
|
+
|
|
37
|
+
**Behaviour change:** `joinPaths` now always emits a leading slash.
|
|
38
|
+
`@Controller('items')` + `@Post(':id')` previously produced `items/:id` while
|
|
39
|
+
`@Controller('items')` alone produced `/items` — the prefix+suffix branch was the
|
|
40
|
+
only one that did not add it. The client's `buildUrl()` normalises before
|
|
41
|
+
requesting, so no URL was ever broken by this, but the raw value reaches the
|
|
42
|
+
emitted `ROUTES` map and the OpenAPI export, where a path without a leading slash
|
|
43
|
+
is invalid.
|
|
44
|
+
|
|
45
|
+
## 0.15.0
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- 20db5c0: Emit `filterFields` as a runtime `as const` array on each filter leaf, alongside the existing type-level union, plus an `isFilterField` type guard exported from the generated `api.ts`. Previously the filterable field set existed only as a type, so a field name arriving as a plain `string` from runtime state (a saved view, a user-picked column) could not be passed to `filterQuery().where()` without a cast. Now `api.route.leaf().filterFields` is a `readonly [...] as const` value and `isFilterField(leaf.filterFields, value)` narrows an arbitrary string to the field union, so dynamic field names validate at runtime instead of being asserted with `as`. The runtime array is generated from the same discovered field list as the type-level union (single source in the emitter), so the value can never drift from the type. Purely additive — the guard is emitted only when a route carries filter fields, and leaves without a filter gain no new member.
|
|
50
|
+
- 9b5298b: Add a `@QueryList()` param decorator and a `toStringList` normalizer to the `/nest` subpath for receiving array query params safely. Express (and Nest's default query parser) returns a bare `string` for a single-value query param (`?ids=a`) and a `string[]` only for two or more (`?ids=a&ids=b`), so `ParseArrayPipe` 400s the common single-select case. `@QueryList('ids')` normalizes `string | string[] | comma-joined string | undefined` into a clean `string[]` (`['a']`, `['a','b']`, `[]`), and `toStringList` is exported for the equivalent `class-transformer` `@Transform` on a DTO field. Pairs with the client's `arrayFormat` option: once the client sends `arrayFormat: 'repeat'`, the comma-split becomes a no-op fallback that still covers hand-rolled and `curl` callers. Documented under a new "Receiving array query params" docs page.
|
|
51
|
+
|
|
3
52
|
## 0.14.2
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/dist/cli/main.cjs
CHANGED
|
@@ -775,6 +775,9 @@ function buildErrorType(c) {
|
|
|
775
775
|
}
|
|
776
776
|
return c.contractSource.error ?? "unknown";
|
|
777
777
|
}
|
|
778
|
+
function filterFieldLiterals(fields) {
|
|
779
|
+
return fields?.length ? fields.map((f) => JSON.stringify(f)) : [];
|
|
780
|
+
}
|
|
778
781
|
function emitRouterTypeBlock(tree, indent, outDir, serialization) {
|
|
779
782
|
const pad = " ".repeat(indent);
|
|
780
783
|
const lines = [];
|
|
@@ -801,7 +804,8 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
|
|
|
801
804
|
const params = buildParamsType(c.params);
|
|
802
805
|
const safeMethod = JSON.stringify(method);
|
|
803
806
|
const safeUrl = JSON.stringify(c.path);
|
|
804
|
-
const
|
|
807
|
+
const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
|
|
808
|
+
const filterFields = filterLiterals.length ? filterLiterals.join(" | ") : "never";
|
|
805
809
|
const stream = c.contractSource.stream ? "true" : "false";
|
|
806
810
|
const binary = c.contractSource.binaryResponse ? "true" : "false";
|
|
807
811
|
lines.push(
|
|
@@ -841,6 +845,7 @@ function buildRequestModel(c) {
|
|
|
841
845
|
const TA = buildRouterTypeAccess(c.name);
|
|
842
846
|
const withParams = hasPathParams(c.params);
|
|
843
847
|
const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
|
|
848
|
+
const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
|
|
844
849
|
const fields = [];
|
|
845
850
|
if (withParams) fields.push(`params: ${TA}['params']`);
|
|
846
851
|
if (hasQuery) fields.push(`query?: ${TA}['query']`);
|
|
@@ -870,7 +875,12 @@ function buildRequestModel(c) {
|
|
|
870
875
|
// (`[name]` rather than `[name, undefined]`) so the bare `.queryKey()` is a
|
|
871
876
|
// clean prefix that partial-matches every parametrized variant — making it
|
|
872
877
|
// directly usable for `invalidateQueries`.
|
|
873
|
-
queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)
|
|
878
|
+
queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`,
|
|
879
|
+
// Runtime counterpart to the type-level `filterFields` union: the same
|
|
880
|
+
// discovered field list, emitted as a literal `[...] as const` so apps can
|
|
881
|
+
// validate a dynamic/user-supplied field string with `isFilterField(...)`
|
|
882
|
+
// instead of casting. Omitted for routes with no filter.
|
|
883
|
+
...filterLiterals.length ? { filterFieldsExpr: `[${filterLiterals.join(", ")}] as const` } : {}
|
|
874
884
|
};
|
|
875
885
|
}
|
|
876
886
|
function renderFetcherRequest(req, binaryResponse) {
|
|
@@ -905,9 +915,24 @@ function emitReqHelper() {
|
|
|
905
915
|
""
|
|
906
916
|
];
|
|
907
917
|
}
|
|
918
|
+
function emitFilterFieldGuard() {
|
|
919
|
+
return [
|
|
920
|
+
"/** Runtime guard: narrows `value` to one of the leaf's `filterFields` (a `readonly K[] as const`), so a dynamic field string can be passed to `.where()` without a cast. */",
|
|
921
|
+
"export function isFilterField<const K extends string>(",
|
|
922
|
+
" fields: readonly K[],",
|
|
923
|
+
" value: string,",
|
|
924
|
+
"): value is K {",
|
|
925
|
+
" return (fields as readonly string[]).includes(value);",
|
|
926
|
+
"}",
|
|
927
|
+
""
|
|
928
|
+
];
|
|
929
|
+
}
|
|
908
930
|
function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
|
|
909
931
|
const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
|
|
910
932
|
lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
|
|
933
|
+
if (req.filterFieldsExpr) {
|
|
934
|
+
lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
|
|
935
|
+
}
|
|
911
936
|
if (streamExpr) {
|
|
912
937
|
lines.push(`${pad} stream: () => ${streamExpr},`);
|
|
913
938
|
}
|
|
@@ -1179,6 +1204,9 @@ function buildApiFile(routes, outDir, opts = {}) {
|
|
|
1179
1204
|
lines.push("};");
|
|
1180
1205
|
lines.push("");
|
|
1181
1206
|
lines.push(...emitReqHelper());
|
|
1207
|
+
if (contracted.some((r) => r.contract?.contractSource.filterFields?.length)) {
|
|
1208
|
+
lines.push(...emitFilterFieldGuard());
|
|
1209
|
+
}
|
|
1182
1210
|
lines.push("export function createApi(fetcher: Fetcher) {");
|
|
1183
1211
|
lines.push(" return {");
|
|
1184
1212
|
lines.push(
|
|
@@ -2414,7 +2442,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
2414
2442
|
// src/discovery/contracts-fast.ts
|
|
2415
2443
|
var import_node_path15 = require("path");
|
|
2416
2444
|
var import_fast_glob3 = __toESM(require("fast-glob"), 1);
|
|
2417
|
-
var
|
|
2445
|
+
var import_ts_morph10 = require("ts-morph");
|
|
2418
2446
|
|
|
2419
2447
|
// src/discovery/dto-type-resolver.ts
|
|
2420
2448
|
var import_ts_morph7 = require("ts-morph");
|
|
@@ -3393,6 +3421,12 @@ function toFilterFieldType(name, r) {
|
|
|
3393
3421
|
}
|
|
3394
3422
|
|
|
3395
3423
|
// src/discovery/filter-for.ts
|
|
3424
|
+
function resolveMixinEntityClass(mixin, project) {
|
|
3425
|
+
const entityArg = mixin?.classArgs[0];
|
|
3426
|
+
if (!entityArg) return void 0;
|
|
3427
|
+
const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
|
|
3428
|
+
return file?.getClass(entityArg.name);
|
|
3429
|
+
}
|
|
3396
3430
|
function classifyFilterForHint(typeInit) {
|
|
3397
3431
|
if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
|
|
3398
3432
|
switch (typeInit.getLiteralValue()) {
|
|
@@ -3466,7 +3500,9 @@ function extractFilterForHints(classDecl, project) {
|
|
|
3466
3500
|
}
|
|
3467
3501
|
return hints;
|
|
3468
3502
|
}
|
|
3469
|
-
function extractApplyFilterInfo(method, sourceFile, project) {
|
|
3503
|
+
function extractApplyFilterInfo(method, sourceFile, project, mixin) {
|
|
3504
|
+
const declFile = method.getSourceFile();
|
|
3505
|
+
const mixinEntity = resolveMixinEntityClass(mixin, project);
|
|
3470
3506
|
for (const param of method.getParameters()) {
|
|
3471
3507
|
const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
|
|
3472
3508
|
if (!filterDecorator) continue;
|
|
@@ -3486,12 +3522,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
|
|
|
3486
3522
|
}
|
|
3487
3523
|
}
|
|
3488
3524
|
const filterClassName = filterClassArg.getText();
|
|
3489
|
-
const resolved = findType(filterClassName,
|
|
3490
|
-
|
|
3491
|
-
|
|
3525
|
+
const resolved = findType(filterClassName, declFile, project);
|
|
3526
|
+
const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
|
|
3527
|
+
if (classDecl) {
|
|
3492
3528
|
let fieldTypes = extractClassPropertyTypes(classDecl, project);
|
|
3493
3529
|
if (fieldTypes.length === 0) {
|
|
3494
|
-
fieldTypes = extractFilterableEntityFields(classDecl, project);
|
|
3530
|
+
fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
|
|
3495
3531
|
}
|
|
3496
3532
|
const filterForHints = extractFilterForHints(classDecl, project);
|
|
3497
3533
|
if (filterForHints.size > 0) {
|
|
@@ -3576,22 +3612,29 @@ function extractClassPropertyTypes(classDecl, project) {
|
|
|
3576
3612
|
}
|
|
3577
3613
|
return fields;
|
|
3578
3614
|
}
|
|
3579
|
-
function
|
|
3615
|
+
function resolveLocalClassDeclaration(identifier) {
|
|
3616
|
+
if (!import_ts_morph6.Node.isIdentifier(identifier)) return void 0;
|
|
3617
|
+
return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && import_ts_morph6.Node.isClassDeclaration(n));
|
|
3618
|
+
}
|
|
3619
|
+
function resolveDeclaredEntity(optionsArg, filterClass, project) {
|
|
3620
|
+
if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return void 0;
|
|
3621
|
+
const entityProp = optionsArg.getProperty("entity");
|
|
3622
|
+
if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return void 0;
|
|
3623
|
+
const entityInit = entityProp.getInitializer();
|
|
3624
|
+
if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return void 0;
|
|
3625
|
+
const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
|
|
3626
|
+
if (!resolved || resolved.kind !== "class") return void 0;
|
|
3627
|
+
return resolved.decl;
|
|
3628
|
+
}
|
|
3629
|
+
function extractFilterableEntityFields(filterClass, project, entityOverride) {
|
|
3580
3630
|
const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
|
|
3581
3631
|
if (!filterableDecorator) return [];
|
|
3582
3632
|
const args = filterableDecorator.getArguments();
|
|
3583
3633
|
if (args.length === 0) return [];
|
|
3584
3634
|
const optionsArg = args[0];
|
|
3585
3635
|
if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
|
|
3586
|
-
const
|
|
3587
|
-
if (!
|
|
3588
|
-
const entityInit = entityProp.getInitializer();
|
|
3589
|
-
if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
|
|
3590
|
-
const entityName = entityInit.getText();
|
|
3591
|
-
const filterSourceFile = filterClass.getSourceFile();
|
|
3592
|
-
const resolvedEntity = findType(entityName, filterSourceFile, project);
|
|
3593
|
-
if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
|
|
3594
|
-
const entityDecl = resolvedEntity.decl;
|
|
3636
|
+
const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
|
|
3637
|
+
if (!entityDecl) return [];
|
|
3595
3638
|
const fields = collectEntityFields(
|
|
3596
3639
|
entityDecl,
|
|
3597
3640
|
entityDecl.getSourceFile(),
|
|
@@ -4016,9 +4059,9 @@ function unwrapNamedContainer(node, names) {
|
|
|
4016
4059
|
}
|
|
4017
4060
|
return node;
|
|
4018
4061
|
}
|
|
4019
|
-
function extractDtoContract(method, sourceFile, project) {
|
|
4062
|
+
function extractDtoContract(method, sourceFile, project, mixin) {
|
|
4020
4063
|
let body = extractBodyType(method, sourceFile, project);
|
|
4021
|
-
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
4064
|
+
const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
|
|
4022
4065
|
const query = extractQueryType(method, sourceFile, project);
|
|
4023
4066
|
const uploads = extractUploadedFiles(method);
|
|
4024
4067
|
const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
|
|
@@ -4128,12 +4171,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
|
4128
4171
|
return null;
|
|
4129
4172
|
}
|
|
4130
4173
|
|
|
4131
|
-
// src/discovery/
|
|
4174
|
+
// src/discovery/heritage.ts
|
|
4132
4175
|
var import_ts_morph8 = require("ts-morph");
|
|
4176
|
+
function unwrapExpression(node) {
|
|
4177
|
+
let current = node;
|
|
4178
|
+
while (import_ts_morph8.Node.isAsExpression(current) || import_ts_morph8.Node.isSatisfiesExpression(current) || import_ts_morph8.Node.isParenthesizedExpression(current) || import_ts_morph8.Node.isTypeAssertion(current)) {
|
|
4179
|
+
current = current.getExpression();
|
|
4180
|
+
}
|
|
4181
|
+
return current;
|
|
4182
|
+
}
|
|
4183
|
+
function resolveReturnedClass(factoryDecl) {
|
|
4184
|
+
const body = factoryDecl.getBody();
|
|
4185
|
+
if (!body) return void 0;
|
|
4186
|
+
const returns = body.getDescendants().filter((n) => import_ts_morph8.Node.isReturnStatement(n));
|
|
4187
|
+
for (const ret of returns) {
|
|
4188
|
+
const expr = ret.getExpression();
|
|
4189
|
+
if (!expr) continue;
|
|
4190
|
+
const inner = unwrapExpression(expr);
|
|
4191
|
+
if (import_ts_morph8.Node.isClassExpression(inner)) {
|
|
4192
|
+
return inner;
|
|
4193
|
+
}
|
|
4194
|
+
if (import_ts_morph8.Node.isIdentifier(inner)) {
|
|
4195
|
+
const name = inner.getText();
|
|
4196
|
+
const decl = body.getDescendants().find((n) => import_ts_morph8.Node.isClassDeclaration(n) && n.getName() === name);
|
|
4197
|
+
if (decl) return decl;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
return void 0;
|
|
4201
|
+
}
|
|
4202
|
+
function resolveInheritedMethods(cls) {
|
|
4203
|
+
const expr = cls.getExtends()?.getExpression();
|
|
4204
|
+
if (!expr || !import_ts_morph8.Node.isCallExpression(expr)) return void 0;
|
|
4205
|
+
const callee = expr.getExpression();
|
|
4206
|
+
if (!import_ts_morph8.Node.isIdentifier(callee)) return void 0;
|
|
4207
|
+
const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isFunctionDeclaration(n));
|
|
4208
|
+
if (!factoryDecl) return void 0;
|
|
4209
|
+
const returnedClass = resolveReturnedClass(factoryDecl);
|
|
4210
|
+
if (!returnedClass) return void 0;
|
|
4211
|
+
const classArgs = [];
|
|
4212
|
+
for (const arg of expr.getArguments()) {
|
|
4213
|
+
if (!import_ts_morph8.Node.isIdentifier(arg)) continue;
|
|
4214
|
+
const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isClassDeclaration(n));
|
|
4215
|
+
if (decl) {
|
|
4216
|
+
classArgs.push({
|
|
4217
|
+
name: decl.getName() ?? arg.getText(),
|
|
4218
|
+
filePath: decl.getSourceFile().getFilePath()
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
return {
|
|
4223
|
+
methods: returnedClass.getMethods(),
|
|
4224
|
+
factoryName: callee.getText(),
|
|
4225
|
+
factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
|
|
4226
|
+
classArgs
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
var mixinTypeProject;
|
|
4230
|
+
function getMixinTypeProject() {
|
|
4231
|
+
mixinTypeProject ??= new import_ts_morph8.Project({
|
|
4232
|
+
skipAddingFilesFromTsConfig: true,
|
|
4233
|
+
compilerOptions: { strict: true }
|
|
4234
|
+
});
|
|
4235
|
+
return mixinTypeProject;
|
|
4236
|
+
}
|
|
4237
|
+
function clearMixinTypeProject() {
|
|
4238
|
+
mixinTypeProject = void 0;
|
|
4239
|
+
}
|
|
4240
|
+
function resolveInstantiatedReturnType(cls, methodName) {
|
|
4241
|
+
const filePath = cls.getSourceFile().getFilePath();
|
|
4242
|
+
const className = cls.getName();
|
|
4243
|
+
if (!className) return void 0;
|
|
4244
|
+
const project = getMixinTypeProject();
|
|
4245
|
+
const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
|
|
4246
|
+
const typedCls = sourceFile?.getClass(className);
|
|
4247
|
+
if (!typedCls) return void 0;
|
|
4248
|
+
const prop = typedCls.getType().getProperty(methodName);
|
|
4249
|
+
if (!prop) return void 0;
|
|
4250
|
+
const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
|
|
4251
|
+
if (!returnType) return void 0;
|
|
4252
|
+
const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
|
|
4253
|
+
return unwrapped.getText(typedCls);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
// src/discovery/zod-ast-to-ts.ts
|
|
4257
|
+
var import_ts_morph9 = require("ts-morph");
|
|
4133
4258
|
function zodAstToTs(node) {
|
|
4134
|
-
if (!
|
|
4259
|
+
if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
|
|
4135
4260
|
const expr = node.getExpression();
|
|
4136
|
-
if (
|
|
4261
|
+
if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
|
|
4137
4262
|
const methodName = expr.getName();
|
|
4138
4263
|
const receiver = expr.getExpression();
|
|
4139
4264
|
if (methodName === "optional") {
|
|
@@ -4157,17 +4282,17 @@ function zodAstToTs(node) {
|
|
|
4157
4282
|
case "literal": {
|
|
4158
4283
|
const lit = args[0];
|
|
4159
4284
|
if (!lit) return "unknown";
|
|
4160
|
-
if (
|
|
4161
|
-
if (
|
|
4162
|
-
if (lit.getKind() ===
|
|
4163
|
-
if (lit.getKind() ===
|
|
4285
|
+
if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
|
|
4286
|
+
if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
|
|
4287
|
+
if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
|
|
4288
|
+
if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
|
|
4164
4289
|
return "unknown";
|
|
4165
4290
|
}
|
|
4166
4291
|
case "enum": {
|
|
4167
4292
|
const arrArg = args[0];
|
|
4168
|
-
if (!arrArg || !
|
|
4293
|
+
if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
4169
4294
|
const members = arrArg.getElements().map(
|
|
4170
|
-
(el) =>
|
|
4295
|
+
(el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
|
|
4171
4296
|
);
|
|
4172
4297
|
return members.join(" | ");
|
|
4173
4298
|
}
|
|
@@ -4178,10 +4303,10 @@ function zodAstToTs(node) {
|
|
|
4178
4303
|
}
|
|
4179
4304
|
case "object": {
|
|
4180
4305
|
const objArg = args[0];
|
|
4181
|
-
if (!objArg || !
|
|
4306
|
+
if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
|
|
4182
4307
|
const lines = [];
|
|
4183
4308
|
for (const prop of objArg.getProperties()) {
|
|
4184
|
-
if (!
|
|
4309
|
+
if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
|
|
4185
4310
|
const key = prop.getName();
|
|
4186
4311
|
const valNode = prop.getInitializer();
|
|
4187
4312
|
if (!valNode) continue;
|
|
@@ -4193,7 +4318,7 @@ function zodAstToTs(node) {
|
|
|
4193
4318
|
}
|
|
4194
4319
|
case "union": {
|
|
4195
4320
|
const arrArg = args[0];
|
|
4196
|
-
if (!arrArg || !
|
|
4321
|
+
if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
4197
4322
|
return arrArg.getElements().map(zodAstToTs).join(" | ");
|
|
4198
4323
|
}
|
|
4199
4324
|
case "record": {
|
|
@@ -4203,7 +4328,7 @@ function zodAstToTs(node) {
|
|
|
4203
4328
|
}
|
|
4204
4329
|
case "tuple": {
|
|
4205
4330
|
const arrArg = args[0];
|
|
4206
|
-
if (!arrArg || !
|
|
4331
|
+
if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
|
|
4207
4332
|
return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
|
|
4208
4333
|
}
|
|
4209
4334
|
default:
|
|
@@ -4213,18 +4338,18 @@ function zodAstToTs(node) {
|
|
|
4213
4338
|
return "unknown";
|
|
4214
4339
|
}
|
|
4215
4340
|
function isOptionalChain(node) {
|
|
4216
|
-
if (!
|
|
4341
|
+
if (!import_ts_morph9.Node.isCallExpression(node)) return false;
|
|
4217
4342
|
const expr = node.getExpression();
|
|
4218
|
-
return
|
|
4343
|
+
return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
|
|
4219
4344
|
}
|
|
4220
4345
|
function parseDefineContractCall(callExpr) {
|
|
4221
|
-
if (!
|
|
4346
|
+
if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
|
|
4222
4347
|
const callee = callExpr.getExpression();
|
|
4223
|
-
const calleeName =
|
|
4348
|
+
const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
4224
4349
|
if (calleeName !== "defineContract") return null;
|
|
4225
4350
|
const args = callExpr.getArguments();
|
|
4226
4351
|
const optsArg = args[0];
|
|
4227
|
-
if (!optsArg || !
|
|
4352
|
+
if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
|
|
4228
4353
|
let query = null;
|
|
4229
4354
|
let body = null;
|
|
4230
4355
|
let response = "unknown";
|
|
@@ -4232,7 +4357,7 @@ function parseDefineContractCall(callExpr) {
|
|
|
4232
4357
|
let bodyZodText = null;
|
|
4233
4358
|
let queryZodText = null;
|
|
4234
4359
|
for (const prop of optsArg.getProperties()) {
|
|
4235
|
-
if (!
|
|
4360
|
+
if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
|
|
4236
4361
|
const propName = prop.getName();
|
|
4237
4362
|
const val = prop.getInitializer();
|
|
4238
4363
|
if (!val) continue;
|
|
@@ -4261,6 +4386,7 @@ async function discoverContractsFast(opts) {
|
|
|
4261
4386
|
project.addSourceFileAtPath(f);
|
|
4262
4387
|
}
|
|
4263
4388
|
bindDiscoveryContext(project, cwd, tsconfigPath);
|
|
4389
|
+
clearMixinTypeProject();
|
|
4264
4390
|
return extractAllRoutes(project);
|
|
4265
4391
|
}
|
|
4266
4392
|
function resolveTsconfigPath(cwd, tsconfig) {
|
|
@@ -4268,14 +4394,14 @@ function resolveTsconfigPath(cwd, tsconfig) {
|
|
|
4268
4394
|
}
|
|
4269
4395
|
function createDiscoveryProject(tsconfigPath) {
|
|
4270
4396
|
try {
|
|
4271
|
-
return new
|
|
4397
|
+
return new import_ts_morph10.Project({
|
|
4272
4398
|
tsConfigFilePath: tsconfigPath,
|
|
4273
4399
|
skipAddingFilesFromTsConfig: true,
|
|
4274
4400
|
skipLoadingLibFiles: true,
|
|
4275
4401
|
skipFileDependencyResolution: true
|
|
4276
4402
|
});
|
|
4277
4403
|
} catch {
|
|
4278
|
-
return new
|
|
4404
|
+
return new import_ts_morph10.Project({
|
|
4279
4405
|
skipAddingFilesFromTsConfig: true,
|
|
4280
4406
|
skipLoadingLibFiles: true,
|
|
4281
4407
|
skipFileDependencyResolution: true,
|
|
@@ -4382,15 +4508,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
|
|
|
4382
4508
|
runExtraction() {
|
|
4383
4509
|
clearTypeResolutionCaches(this.project);
|
|
4384
4510
|
clearEnumCache(this.project);
|
|
4511
|
+
clearMixinTypeProject();
|
|
4385
4512
|
return extractRoutesFrom(this.project, this.controllerPaths);
|
|
4386
4513
|
}
|
|
4387
4514
|
};
|
|
4388
4515
|
function decoratorStringArg(decoratorExpr) {
|
|
4389
4516
|
if (!decoratorExpr) return void 0;
|
|
4390
|
-
if (
|
|
4391
|
-
if (
|
|
4517
|
+
if (import_ts_morph10.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
|
|
4518
|
+
if (import_ts_morph10.Node.isArrayLiteralExpression(decoratorExpr)) {
|
|
4392
4519
|
const first = decoratorExpr.getElements()[0];
|
|
4393
|
-
if (first &&
|
|
4520
|
+
if (first && import_ts_morph10.Node.isStringLiteral(first)) return first.getLiteralValue();
|
|
4394
4521
|
}
|
|
4395
4522
|
return void 0;
|
|
4396
4523
|
}
|
|
@@ -4412,7 +4539,8 @@ function joinPaths(prefix, suffix) {
|
|
|
4412
4539
|
if (!prefix && !suffix) return "/";
|
|
4413
4540
|
if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
4414
4541
|
if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
4415
|
-
const
|
|
4542
|
+
const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
4543
|
+
const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
4416
4544
|
const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
4417
4545
|
const combined = p + s;
|
|
4418
4546
|
return combined === "" ? "/" : combined;
|
|
@@ -4466,7 +4594,8 @@ function buildRoute(args) {
|
|
|
4466
4594
|
methodAs,
|
|
4467
4595
|
sourceFile,
|
|
4468
4596
|
seenNames,
|
|
4469
|
-
contractSource
|
|
4597
|
+
contractSource,
|
|
4598
|
+
mixin
|
|
4470
4599
|
} = args;
|
|
4471
4600
|
const routeName = resolveRouteName(className, methodName, classAs, methodAs);
|
|
4472
4601
|
const qualifiedRef = `${className}.${methodName}`;
|
|
@@ -4482,7 +4611,12 @@ function buildRoute(args) {
|
|
|
4482
4611
|
path: combinedPath,
|
|
4483
4612
|
name: routeName,
|
|
4484
4613
|
params: extractParams(combinedPath),
|
|
4485
|
-
controllerRef: {
|
|
4614
|
+
controllerRef: {
|
|
4615
|
+
className,
|
|
4616
|
+
methodName,
|
|
4617
|
+
filePath: sourceFile.getFilePath(),
|
|
4618
|
+
...mixin ? { mixin } : {}
|
|
4619
|
+
},
|
|
4486
4620
|
contract: { contractSource }
|
|
4487
4621
|
};
|
|
4488
4622
|
}
|
|
@@ -4496,16 +4630,17 @@ function extractContractRoute(args) {
|
|
|
4496
4630
|
className,
|
|
4497
4631
|
sourceFile,
|
|
4498
4632
|
project,
|
|
4499
|
-
seenNames
|
|
4633
|
+
seenNames,
|
|
4634
|
+
mixin
|
|
4500
4635
|
} = args;
|
|
4501
4636
|
const firstDecoratorArg = applyContractDecorator.getArguments()[0];
|
|
4502
4637
|
if (!firstDecoratorArg) return null;
|
|
4503
4638
|
let contractDef = null;
|
|
4504
4639
|
let bodyZodRef = null;
|
|
4505
4640
|
let queryZodRef = null;
|
|
4506
|
-
if (
|
|
4641
|
+
if (import_ts_morph10.Node.isCallExpression(firstDecoratorArg)) {
|
|
4507
4642
|
contractDef = parseDefineContractCall(firstDecoratorArg);
|
|
4508
|
-
} else if (
|
|
4643
|
+
} else if (import_ts_morph10.Node.isIdentifier(firstDecoratorArg)) {
|
|
4509
4644
|
const identName = firstDecoratorArg.getText();
|
|
4510
4645
|
const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
|
|
4511
4646
|
if (!resolvedVar) {
|
|
@@ -4548,6 +4683,7 @@ function extractContractRoute(args) {
|
|
|
4548
4683
|
methodAs,
|
|
4549
4684
|
sourceFile,
|
|
4550
4685
|
seenNames,
|
|
4686
|
+
mixin,
|
|
4551
4687
|
contractSource: {
|
|
4552
4688
|
query: contractDef.query,
|
|
4553
4689
|
body: contractDef.body,
|
|
@@ -4564,13 +4700,14 @@ function extractContractRoute(args) {
|
|
|
4564
4700
|
});
|
|
4565
4701
|
}
|
|
4566
4702
|
function extractDtoRoute(args) {
|
|
4567
|
-
const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
|
|
4703
|
+
const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
|
|
4568
4704
|
if (!verb) return null;
|
|
4569
4705
|
const combined = joinPaths(prefix, verb.handlerPath);
|
|
4570
4706
|
const methodName = method.getName();
|
|
4571
4707
|
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
4572
4708
|
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
4573
|
-
const dtoContract = extractDtoContract(method, sourceFile, project);
|
|
4709
|
+
const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
|
|
4710
|
+
const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
|
|
4574
4711
|
return buildRoute({
|
|
4575
4712
|
className,
|
|
4576
4713
|
methodName,
|
|
@@ -4580,10 +4717,11 @@ function extractDtoRoute(args) {
|
|
|
4580
4717
|
methodAs,
|
|
4581
4718
|
sourceFile,
|
|
4582
4719
|
seenNames,
|
|
4720
|
+
mixin,
|
|
4583
4721
|
contractSource: {
|
|
4584
4722
|
query: dtoContract?.query ?? null,
|
|
4585
4723
|
body: dtoContract?.body ?? null,
|
|
4586
|
-
response: dtoContract?.response ?? "unknown",
|
|
4724
|
+
response: mixinResponse ?? dtoContract?.response ?? "unknown",
|
|
4587
4725
|
error: dtoContract?.error ?? null,
|
|
4588
4726
|
queryRef: dtoContract?.queryRef ?? null,
|
|
4589
4727
|
bodyRef: dtoContract?.bodyRef ?? null,
|
|
@@ -4612,7 +4750,17 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
4612
4750
|
const firstArg2 = controllerDecorator.getArguments()[0];
|
|
4613
4751
|
const prefix = decoratorStringArg(firstArg2) ?? "";
|
|
4614
4752
|
const className = cls.getName() ?? "Unknown";
|
|
4615
|
-
|
|
4753
|
+
const inherited = resolveInheritedMethods(cls);
|
|
4754
|
+
const mixinBinding = inherited ? {
|
|
4755
|
+
factoryName: inherited.factoryName,
|
|
4756
|
+
factoryFilePath: inherited.factoryFilePath,
|
|
4757
|
+
classArgs: inherited.classArgs
|
|
4758
|
+
} : void 0;
|
|
4759
|
+
const methods = [
|
|
4760
|
+
...cls.getMethods().map((method) => ({ method })),
|
|
4761
|
+
...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
|
|
4762
|
+
];
|
|
4763
|
+
for (const { method, mixin } of methods) {
|
|
4616
4764
|
const verb = resolveVerb(method);
|
|
4617
4765
|
const applyContractDecorator = method.getDecorator("ApplyContract");
|
|
4618
4766
|
const route = applyContractDecorator ? extractContractRoute({
|
|
@@ -4624,7 +4772,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
4624
4772
|
className,
|
|
4625
4773
|
sourceFile,
|
|
4626
4774
|
project,
|
|
4627
|
-
seenNames
|
|
4775
|
+
seenNames,
|
|
4776
|
+
mixin
|
|
4628
4777
|
}) : extractDtoRoute({
|
|
4629
4778
|
cls,
|
|
4630
4779
|
method,
|
|
@@ -4633,7 +4782,8 @@ function extractFromSourceFile(sourceFile, project) {
|
|
|
4633
4782
|
className,
|
|
4634
4783
|
sourceFile,
|
|
4635
4784
|
project,
|
|
4636
|
-
seenNames
|
|
4785
|
+
seenNames,
|
|
4786
|
+
mixin
|
|
4637
4787
|
});
|
|
4638
4788
|
if (route) routes.push(route);
|
|
4639
4789
|
}
|
|
@@ -4834,7 +4984,7 @@ async function watch(config, onChange, options = {}) {
|
|
|
4834
4984
|
}
|
|
4835
4985
|
|
|
4836
4986
|
// src/index.ts
|
|
4837
|
-
var VERSION = "0.
|
|
4987
|
+
var VERSION = "0.16.0";
|
|
4838
4988
|
|
|
4839
4989
|
// src/cli/codegen.ts
|
|
4840
4990
|
async function runCodegen(opts = {}) {
|