@dudousxd/nestjs-codegen 0.19.0 → 0.21.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 +79 -0
- package/dist/cli/main.cjs +95 -34
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +95 -34
- package/dist/cli/main.js.map +1 -1
- package/dist/index.cjs +95 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +95 -34
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +95 -34
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.js +95 -34
- package/dist/nest/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @dudousxd/nestjs-codegen
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 71b4485: Tell an overridden route from an inherited one by the class it is declared on, not by its file
|
|
8
|
+
|
|
9
|
+
`extractApplyFilterInfo` decided whether a route was the controller's OWN by
|
|
10
|
+
comparing the method's file to the factory's (`declFile !== mixin.factoryFilePath`).
|
|
11
|
+
That is a proxy for the real question, and it breaks on the one shape where the
|
|
12
|
+
two come apart: a controller factory declared in the SAME file as a controller
|
|
13
|
+
that calls it. There, a genuine override reads as inherited, its
|
|
14
|
+
`@ApplyFilter(SomeFilter)` is mistaken for the factory talking about itself, and
|
|
15
|
+
the factory's `filter` option silently outranks the filter the override names —
|
|
16
|
+
typing the route against a filter the runtime does not use.
|
|
17
|
+
|
|
18
|
+
`extractDtoContract` now takes the `@Controller` class the route was discovered
|
|
19
|
+
on and passes it down, so the check is `method.getParent() === controllerClass`:
|
|
20
|
+
the ground truth the discovery pass already computed when it merged own and
|
|
21
|
+
inherited methods. The parameter is optional — omitting it degrades to the
|
|
22
|
+
previous file comparison, so an external caller of the exported
|
|
23
|
+
`extractDtoContract` keeps working unchanged.
|
|
24
|
+
|
|
25
|
+
This also settles a disagreement between packages. `@dudousxd/nestjs-filter-codegen`
|
|
26
|
+
asks the same question via `controllerClass.getMethod(name)` and was already
|
|
27
|
+
right about the colocated case; the two have to agree on what an override IS, not
|
|
28
|
+
merely on how to rank one, or the same route ends up described twice, differently.
|
|
29
|
+
|
|
30
|
+
## 0.20.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
- f8da873: Describe a table's routes with the filter its factory was HANDED, not the one it
|
|
35
|
+
generated.
|
|
36
|
+
|
|
37
|
+
A factory given a hand-written filter through an option —
|
|
38
|
+
`createTableController({ entity: Wo, filter: WoFilter })` — applies `WoFilter` to
|
|
39
|
+
every route it produces, but the decorators inside the factory body can only name
|
|
40
|
+
the fallback it generates internally (`@ApplyFilter(GeneratedFilter)`; the
|
|
41
|
+
supplied class exists only at the call site). Discovery read that decorator
|
|
42
|
+
argument and nothing else, so everything emitted for those routes came from the
|
|
43
|
+
fallback:
|
|
44
|
+
|
|
45
|
+
- the hand-written filter's `@FilterFor` virtual fields were **absent** from the
|
|
46
|
+
client — filtering by them does not type-check, though the server accepts them;
|
|
47
|
+
- a filter that NARROWS with `allowed` was typed with the fallback's **wider**
|
|
48
|
+
entity-derived field set — the client is told it may filter by fields the
|
|
49
|
+
server will reject, which type-checks at the call site and fails at runtime.
|
|
50
|
+
|
|
51
|
+
The route's mixin binding already carried the call-site `filter` class. It is now
|
|
52
|
+
consulted, in this precedence (first candidate that yields a readable field set
|
|
53
|
+
wins):
|
|
54
|
+
|
|
55
|
+
1. a filter named BY IDENTIFIER in the route's own `@ApplyFilter(SomeFilter)` —
|
|
56
|
+
an overriding method is the only place a per-route statement can be made;
|
|
57
|
+
2. the factory's `filter` option;
|
|
58
|
+
3. the existing walk — `@ApplyFilter(<Const>.filter)` through the factory static,
|
|
59
|
+
a lexically-scoped local class, then a module-level lookup.
|
|
60
|
+
|
|
61
|
+
`@ApplyFilter(<Const>.filter)` sits below (2) deliberately, matching
|
|
62
|
+
`@dudousxd/nestjs-filter-codegen`: it forwards the factory's product rather than
|
|
63
|
+
naming a filter of its own, so statically it always lands on the generated
|
|
64
|
+
fallback.
|
|
65
|
+
|
|
66
|
+
Two consequences follow:
|
|
67
|
+
|
|
68
|
+
- **`allowed` / `blocked` now narrow the emitted field set.** They gate exactly
|
|
69
|
+
the auto-field set, so they are applied to the entity-derived fields;
|
|
70
|
+
`@FilterFor` keys are appended afterwards, as the runtime resolves an explicit
|
|
71
|
+
handler without consulting `allowed`.
|
|
72
|
+
- **A hand-written filter's own `@Filterable({ entity })` wins** over the
|
|
73
|
+
factory's call-site entity. The call-site entity is a repair for a GENERATED
|
|
74
|
+
filter, whose `entity` names the factory's parameter and resolves to nothing; a
|
|
75
|
+
hand-written filter names a real class, and that is the class the runtime
|
|
76
|
+
queries. It stays a fallback for when the declared entity does not resolve.
|
|
77
|
+
|
|
78
|
+
Nothing gets worse: a candidate that resolves but reads as empty (no properties,
|
|
79
|
+
no `@Filterable`, no `@FilterFor`) is skipped rather than returned, so an opaque
|
|
80
|
+
call-site filter degrades to today's result instead of to `filterFields: never`.
|
|
81
|
+
|
|
3
82
|
## 0.19.0
|
|
4
83
|
|
|
5
84
|
### Minor Changes
|
package/dist/cli/main.cjs
CHANGED
|
@@ -3653,10 +3653,15 @@ function resolveInstantiatedReturnType(cls, methodName) {
|
|
|
3653
3653
|
|
|
3654
3654
|
// src/discovery/filter-for.ts
|
|
3655
3655
|
function resolveMixinEntityClass(mixin, project) {
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
return
|
|
3656
|
+
return resolveClassRef(mixin?.namedClassArgs?.entity ?? mixin?.classArgs[0], project);
|
|
3657
|
+
}
|
|
3658
|
+
function resolveMixinFilterClass(mixin, project) {
|
|
3659
|
+
return resolveClassRef(mixin?.namedClassArgs?.filter, project);
|
|
3660
|
+
}
|
|
3661
|
+
function resolveClassRef(ref, project) {
|
|
3662
|
+
if (!ref) return void 0;
|
|
3663
|
+
const file = project.getSourceFile(ref.filePath) ?? project.addSourceFileAtPathIfExists(ref.filePath);
|
|
3664
|
+
return file?.getClass(ref.name);
|
|
3660
3665
|
}
|
|
3661
3666
|
function classifyFilterForHint(typeInit) {
|
|
3662
3667
|
if (import_ts_morph7.Node.isStringLiteral(typeInit)) {
|
|
@@ -3731,7 +3736,7 @@ function extractFilterForHints(classDecl, project) {
|
|
|
3731
3736
|
}
|
|
3732
3737
|
return hints;
|
|
3733
3738
|
}
|
|
3734
|
-
function extractApplyFilterInfo(method, sourceFile, project, mixin) {
|
|
3739
|
+
function extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass) {
|
|
3735
3740
|
const declFile = method.getSourceFile();
|
|
3736
3741
|
const mixinEntity = resolveMixinEntityClass(mixin, project);
|
|
3737
3742
|
for (const param of method.getParameters()) {
|
|
@@ -3756,31 +3761,55 @@ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
|
|
|
3756
3761
|
}
|
|
3757
3762
|
const filterClassName = filterClassArg.getText();
|
|
3758
3763
|
const resolved = factoryStaticClass ? void 0 : findType(filterClassName, declFile, project);
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
byName.set(key, toFilterFieldType(key, classified));
|
|
3770
|
-
}
|
|
3771
|
-
fieldTypes = [...byName.values()];
|
|
3772
|
-
}
|
|
3773
|
-
if (fieldTypes.length === 0) return null;
|
|
3774
|
-
const fieldNames = fieldTypes.map((f) => f.name);
|
|
3764
|
+
const declaredClass = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
|
|
3765
|
+
const ownRoute = !mixin || (controllerClass ? method.getParent() === controllerClass : declFile.getFilePath() !== mixin.factoryFilePath);
|
|
3766
|
+
const namedByRoute = ownRoute && import_ts_morph7.Node.isIdentifier(filterClassArg) ? declaredClass : void 0;
|
|
3767
|
+
for (const candidate of orderFilterCandidates({
|
|
3768
|
+
namedByRoute,
|
|
3769
|
+
supplied: resolveMixinFilterClass(mixin, project),
|
|
3770
|
+
declared: declaredClass
|
|
3771
|
+
})) {
|
|
3772
|
+
const fieldTypes = collectFilterFields(candidate, project, mixinEntity);
|
|
3773
|
+
if (fieldTypes.length === 0) continue;
|
|
3775
3774
|
return {
|
|
3776
|
-
fieldNames,
|
|
3775
|
+
fieldNames: fieldTypes.map((f) => f.name),
|
|
3777
3776
|
fieldTypes,
|
|
3778
3777
|
source
|
|
3779
3778
|
};
|
|
3780
3779
|
}
|
|
3780
|
+
if (declaredClass) return null;
|
|
3781
3781
|
}
|
|
3782
3782
|
return null;
|
|
3783
3783
|
}
|
|
3784
|
+
function orderFilterCandidates(sources) {
|
|
3785
|
+
const ordered = [];
|
|
3786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3787
|
+
const push = (decl, preferDeclaredEntity) => {
|
|
3788
|
+
if (!decl || seen.has(decl)) return;
|
|
3789
|
+
seen.add(decl);
|
|
3790
|
+
ordered.push({ decl, preferDeclaredEntity });
|
|
3791
|
+
};
|
|
3792
|
+
push(sources.namedByRoute, true);
|
|
3793
|
+
push(sources.supplied, true);
|
|
3794
|
+
push(sources.declared, false);
|
|
3795
|
+
return ordered;
|
|
3796
|
+
}
|
|
3797
|
+
function collectFilterFields(candidate, project, mixinEntity) {
|
|
3798
|
+
const { decl, preferDeclaredEntity } = candidate;
|
|
3799
|
+
let fieldTypes = extractClassPropertyTypes(decl, project);
|
|
3800
|
+
if (fieldTypes.length === 0) {
|
|
3801
|
+
fieldTypes = extractFilterableEntityFields(decl, project, mixinEntity, preferDeclaredEntity);
|
|
3802
|
+
}
|
|
3803
|
+
const filterForHints = extractFilterForHints(decl, project);
|
|
3804
|
+
if (filterForHints.size > 0) {
|
|
3805
|
+
const byName = new Map(fieldTypes.map((f) => [f.name, f]));
|
|
3806
|
+
for (const [key, classified] of filterForHints) {
|
|
3807
|
+
byName.set(key, toFilterFieldType(key, classified));
|
|
3808
|
+
}
|
|
3809
|
+
fieldTypes = [...byName.values()];
|
|
3810
|
+
}
|
|
3811
|
+
return fieldTypes;
|
|
3812
|
+
}
|
|
3784
3813
|
var RELATION_DECORATORS = /* @__PURE__ */ new Set(["OneToMany", "ManyToOne", "ManyToMany", "OneToOne"]);
|
|
3785
3814
|
function collectEntityFields(entityDecl, sourceFile, project, prefix, visited) {
|
|
3786
3815
|
const entityName = entityDecl.getName() ?? "";
|
|
@@ -3859,21 +3888,53 @@ function resolveDeclaredEntity(optionsArg, filterClass, project) {
|
|
|
3859
3888
|
if (!resolved || resolved.kind !== "class") return void 0;
|
|
3860
3889
|
return resolved.decl;
|
|
3861
3890
|
}
|
|
3862
|
-
function
|
|
3891
|
+
function readFieldNameList(optionsArg, key) {
|
|
3892
|
+
if (!import_ts_morph7.Node.isObjectLiteralExpression(optionsArg)) return void 0;
|
|
3893
|
+
const prop = optionsArg.getProperty(key);
|
|
3894
|
+
if (!prop || !import_ts_morph7.Node.isPropertyAssignment(prop)) return void 0;
|
|
3895
|
+
const init = prop.getInitializer();
|
|
3896
|
+
if (!init || !import_ts_morph7.Node.isArrayLiteralExpression(init)) return void 0;
|
|
3897
|
+
const names = /* @__PURE__ */ new Set();
|
|
3898
|
+
for (const el of init.getElements()) {
|
|
3899
|
+
if (import_ts_morph7.Node.isStringLiteral(el)) {
|
|
3900
|
+
names.add(el.getLiteralValue());
|
|
3901
|
+
continue;
|
|
3902
|
+
}
|
|
3903
|
+
if (import_ts_morph7.Node.isObjectLiteralExpression(el)) {
|
|
3904
|
+
const fieldProp = el.getProperty("field");
|
|
3905
|
+
if (fieldProp && import_ts_morph7.Node.isPropertyAssignment(fieldProp)) {
|
|
3906
|
+
const fieldInit = fieldProp.getInitializer();
|
|
3907
|
+
if (fieldInit && import_ts_morph7.Node.isStringLiteral(fieldInit)) {
|
|
3908
|
+
names.add(fieldInit.getLiteralValue());
|
|
3909
|
+
continue;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return void 0;
|
|
3914
|
+
}
|
|
3915
|
+
return names;
|
|
3916
|
+
}
|
|
3917
|
+
function narrowToDeclaredFields(fields, optionsArg) {
|
|
3918
|
+
const allowed = readFieldNameList(optionsArg, "allowed");
|
|
3919
|
+
const blocked = readFieldNameList(optionsArg, "blocked");
|
|
3920
|
+
if (!allowed && !blocked) return fields;
|
|
3921
|
+
return fields.filter(
|
|
3922
|
+
(f) => (!allowed || allowed.has(f.name)) && !(blocked?.has(f.name) ?? false)
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
function extractFilterableEntityFields(filterClass, project, entityOverride, preferDeclaredEntity = false) {
|
|
3863
3926
|
const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
|
|
3864
3927
|
if (!filterableDecorator) return [];
|
|
3865
3928
|
const args = filterableDecorator.getArguments();
|
|
3866
3929
|
if (args.length === 0) return [];
|
|
3867
3930
|
const optionsArg = args[0];
|
|
3868
3931
|
if (!import_ts_morph7.Node.isObjectLiteralExpression(optionsArg)) return [];
|
|
3869
|
-
const
|
|
3932
|
+
const declaredEntity = resolveDeclaredEntity(optionsArg, filterClass, project);
|
|
3933
|
+
const entityDecl = preferDeclaredEntity ? declaredEntity ?? entityOverride : entityOverride ?? declaredEntity;
|
|
3870
3934
|
if (!entityDecl) return [];
|
|
3871
|
-
const fields =
|
|
3872
|
-
entityDecl,
|
|
3873
|
-
|
|
3874
|
-
project,
|
|
3875
|
-
"",
|
|
3876
|
-
/* @__PURE__ */ new Set()
|
|
3935
|
+
const fields = narrowToDeclaredFields(
|
|
3936
|
+
collectEntityFields(entityDecl, entityDecl.getSourceFile(), project, "", /* @__PURE__ */ new Set()),
|
|
3937
|
+
optionsArg
|
|
3877
3938
|
);
|
|
3878
3939
|
const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
|
|
3879
3940
|
if (relationsDecorator) {
|
|
@@ -4292,9 +4353,9 @@ function unwrapNamedContainer(node, names) {
|
|
|
4292
4353
|
}
|
|
4293
4354
|
return node;
|
|
4294
4355
|
}
|
|
4295
|
-
function extractDtoContract(method, sourceFile, project, mixin) {
|
|
4356
|
+
function extractDtoContract(method, sourceFile, project, mixin, controllerClass) {
|
|
4296
4357
|
let body = extractBodyType(method, sourceFile, project);
|
|
4297
|
-
const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
|
|
4358
|
+
const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin, controllerClass);
|
|
4298
4359
|
const query = extractQueryType(method, sourceFile, project);
|
|
4299
4360
|
const uploads = extractUploadedFiles(method);
|
|
4300
4361
|
const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
|
|
@@ -4857,7 +4918,7 @@ function extractDtoRoute(args) {
|
|
|
4857
4918
|
const methodName = method.getName();
|
|
4858
4919
|
const classAs = readAsDecorator(cls, `class ${className}`);
|
|
4859
4920
|
const methodAs = readAsDecorator(method, `${className}.${methodName}`);
|
|
4860
|
-
const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
|
|
4921
|
+
const dtoContract = extractDtoContract(method, sourceFile, project, mixin, cls);
|
|
4861
4922
|
const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
|
|
4862
4923
|
return buildRoute({
|
|
4863
4924
|
className,
|
|
@@ -5138,7 +5199,7 @@ async function watch(config, onChange, options = {}) {
|
|
|
5138
5199
|
}
|
|
5139
5200
|
|
|
5140
5201
|
// src/index.ts
|
|
5141
|
-
var VERSION = "0.
|
|
5202
|
+
var VERSION = "0.21.0";
|
|
5142
5203
|
|
|
5143
5204
|
// src/cli/codegen.ts
|
|
5144
5205
|
async function runCodegen(opts = {}) {
|