@dudousxd/nestjs-codegen 0.18.0 → 0.20.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 CHANGED
@@ -1,5 +1,107 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f8da873: Describe a table's routes with the filter its factory was HANDED, not the one it
8
+ generated.
9
+
10
+ A factory given a hand-written filter through an option —
11
+ `createTableController({ entity: Wo, filter: WoFilter })` — applies `WoFilter` to
12
+ every route it produces, but the decorators inside the factory body can only name
13
+ the fallback it generates internally (`@ApplyFilter(GeneratedFilter)`; the
14
+ supplied class exists only at the call site). Discovery read that decorator
15
+ argument and nothing else, so everything emitted for those routes came from the
16
+ fallback:
17
+
18
+ - the hand-written filter's `@FilterFor` virtual fields were **absent** from the
19
+ client — filtering by them does not type-check, though the server accepts them;
20
+ - a filter that NARROWS with `allowed` was typed with the fallback's **wider**
21
+ entity-derived field set — the client is told it may filter by fields the
22
+ server will reject, which type-checks at the call site and fails at runtime.
23
+
24
+ The route's mixin binding already carried the call-site `filter` class. It is now
25
+ consulted, in this precedence (first candidate that yields a readable field set
26
+ wins):
27
+
28
+ 1. a filter named BY IDENTIFIER in the route's own `@ApplyFilter(SomeFilter)` —
29
+ an overriding method is the only place a per-route statement can be made;
30
+ 2. the factory's `filter` option;
31
+ 3. the existing walk — `@ApplyFilter(<Const>.filter)` through the factory static,
32
+ a lexically-scoped local class, then a module-level lookup.
33
+
34
+ `@ApplyFilter(<Const>.filter)` sits below (2) deliberately, matching
35
+ `@dudousxd/nestjs-filter-codegen`: it forwards the factory's product rather than
36
+ naming a filter of its own, so statically it always lands on the generated
37
+ fallback.
38
+
39
+ Two consequences follow:
40
+
41
+ - **`allowed` / `blocked` now narrow the emitted field set.** They gate exactly
42
+ the auto-field set, so they are applied to the entity-derived fields;
43
+ `@FilterFor` keys are appended afterwards, as the runtime resolves an explicit
44
+ handler without consulting `allowed`.
45
+ - **A hand-written filter's own `@Filterable({ entity })` wins** over the
46
+ factory's call-site entity. The call-site entity is a repair for a GENERATED
47
+ filter, whose `entity` names the factory's parameter and resolves to nothing; a
48
+ hand-written filter names a real class, and that is the class the runtime
49
+ queries. It stays a fallback for when the declared entity does not resolve.
50
+
51
+ Nothing gets worse: a candidate that resolves but reads as empty (no properties,
52
+ no `@Filterable`, no `@FilterFor`) is skipped rather than returned, so an opaque
53
+ call-site filter degrades to today's result instead of to `filterFields: never`.
54
+
55
+ ## 0.19.0
56
+
57
+ ### Minor Changes
58
+
59
+ - 8ffea16: Discover controller factories called through a property, and warn — loudly —
60
+ when a factory heritage clause cannot be followed at all.
61
+
62
+ A controller whose base came from anything other than a bare function name
63
+ contributed ZERO routes to the generated client:
64
+
65
+ ```ts
66
+ class A extends tables.createTableController(Entity, {}) {} // namespace import
67
+ class B extends TableFactory.create(Entity, {}) {} // static method
68
+ class C extends factories.table(Entity, {}) {} // re-export object
69
+ ```
70
+
71
+ Discovery required the callee of the factory call to be an `Identifier`, so each
72
+ of these resolved to nothing — and unlike the earlier filter gaps, this does not
73
+ mutilate a route's contract, it deletes the whole controller. `tsc` green,
74
+ codegen green, no warning: the first sign is a client call that does not exist.
75
+
76
+ The callee is now resolved through its name node, which covers a namespace import
77
+ (`ns.factory(...)`), a static method (`Factory.create(...)`) and a property of a
78
+ re-export object (`factories.table(...)`, including the `{ createTableController }`
79
+ shorthand). Bare identifiers are unchanged. A callee with no name node — an
80
+ element access (`factories['table'](...)`) or a callee that is itself a call
81
+ (`makeFactory()(Entity)`) — would mean evaluating the program to name the
82
+ function, so it stays unresolved.
83
+
84
+ Unresolved is no longer silent. A `@Controller`-decorated class whose heritage is
85
+ a factory-shaped CALL that discovery cannot follow now prints one line naming the
86
+ file, the class, the callee and what it costs:
87
+
88
+ ```
89
+ [nestjs-codegen/fast] SearchUtilsController in /src/utils/search.controller.ts
90
+ extends makeTableController()(...) but its callee is not a name that can be
91
+ resolved statically — it contributes NO routes to the generated client.
92
+ ```
93
+
94
+ Scoped tightly on purpose: it is a warning and never a throw, and it stays quiet
95
+ for `extends SomeBaseClass` (not a call) and for any class without `@Controller`
96
+ (extending a call expression is ordinary code). So the next unsupported shape
97
+ costs a line on stderr instead of a day.
98
+
99
+ `MixinBinding.factoryName` now carries the resolved DECLARATION's name rather
100
+ than the call-site text — a qualified `tables.createTableController` would never
101
+ match the by-name lookup consumers do inside `factoryFilePath`. Identical for
102
+ every existing bare-identifier call site, bar an import alias, where the
103
+ declaration name is the one that resolves.
104
+
3
105
  ## 0.18.0
4
106
 
5
107
  ### Minor Changes
package/dist/cli/main.cjs CHANGED
@@ -3491,10 +3491,46 @@ function resolveReturnedClass(factoryDecl) {
3491
3491
  }
3492
3492
  return void 0;
3493
3493
  }
3494
+ function factoryCalleeName(call) {
3495
+ const callee = unwrapExpression(call.getExpression());
3496
+ if (import_ts_morph6.Node.isIdentifier(callee)) return callee;
3497
+ if (import_ts_morph6.Node.isPropertyAccessExpression(callee)) {
3498
+ const name = callee.getNameNode();
3499
+ return import_ts_morph6.Node.isIdentifier(name) ? name : void 0;
3500
+ }
3501
+ return void 0;
3502
+ }
3494
3503
  function resolveFactoryDeclaration(call) {
3495
- const callee = call.getExpression();
3496
- if (!import_ts_morph6.Node.isIdentifier(callee)) return void 0;
3497
- return callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph6.Node.isFunctionDeclaration(n));
3504
+ const name = factoryCalleeName(call);
3505
+ return name ? resolveFactoryFromName(name, /* @__PURE__ */ new Set()) : void 0;
3506
+ }
3507
+ function resolveFactoryFromName(name, seen) {
3508
+ if (seen.has(name)) return void 0;
3509
+ seen.add(name);
3510
+ const decls = name.getDefinitions().map((d) => d.getDeclarationNode()).filter((n) => n !== void 0);
3511
+ for (const decl of decls) {
3512
+ if (import_ts_morph6.Node.isFunctionDeclaration(decl) || import_ts_morph6.Node.isMethodDeclaration(decl)) return decl;
3513
+ if (import_ts_morph6.Node.isPropertyAssignment(decl)) {
3514
+ const init = decl.getInitializer();
3515
+ const inner = init ? unwrapExpression(init) : void 0;
3516
+ if (inner && import_ts_morph6.Node.isIdentifier(inner)) {
3517
+ const resolved = resolveFactoryFromName(inner, seen);
3518
+ if (resolved) return resolved;
3519
+ }
3520
+ continue;
3521
+ }
3522
+ if (import_ts_morph6.Node.isShorthandPropertyAssignment(decl)) {
3523
+ const nameNode = decl.getNameNode();
3524
+ const resolved = import_ts_morph6.Node.isIdentifier(nameNode) ? resolveFactoryFromName(nameNode, seen) : void 0;
3525
+ if (resolved) return resolved;
3526
+ }
3527
+ }
3528
+ return void 0;
3529
+ }
3530
+ function warnUnresolvedFactory(cls, calleeText, reason) {
3531
+ console.warn(
3532
+ `[nestjs-codegen/fast] ${cls.getName() ?? "<anonymous class>"} in ${cls.getSourceFile().getFilePath()} extends ${calleeText}(...) but ${reason} \u2014 it contributes NO routes to the generated client. Resolvable factory callees: a name (\`factory(...)\`), a namespace or object property (\`ns.factory(...)\`), or a static method (\`Factory.create(...)\`), each resolving to a function or method declaration that returns a class.`
3533
+ );
3498
3534
  }
3499
3535
  function resolveFactoryStaticClass(node) {
3500
3536
  if (!import_ts_morph6.Node.isPropertyAccessExpression(node)) return void 0;
@@ -3516,12 +3552,26 @@ function resolveFactoryStaticClass(node) {
3516
3552
  function resolveInheritedMethods(cls) {
3517
3553
  const expr = resolveHeritageCall(cls.getExtends()?.getExpression());
3518
3554
  if (!expr) return void 0;
3519
- const callee = expr.getExpression();
3520
- if (!import_ts_morph6.Node.isIdentifier(callee)) return void 0;
3555
+ const isController = cls.getDecorator("Controller") !== void 0;
3556
+ const calleeText = expr.getExpression().getText();
3521
3557
  const factoryDecl = resolveFactoryDeclaration(expr);
3522
- if (!factoryDecl) return void 0;
3558
+ if (!factoryDecl) {
3559
+ if (isController) {
3560
+ warnUnresolvedFactory(
3561
+ cls,
3562
+ calleeText,
3563
+ factoryCalleeName(expr) ? "its callee does not resolve to a function or method declaration" : "its callee is not a name that can be resolved statically"
3564
+ );
3565
+ }
3566
+ return void 0;
3567
+ }
3523
3568
  const returnedClass = resolveReturnedClass(factoryDecl);
3524
- if (!returnedClass) return void 0;
3569
+ if (!returnedClass) {
3570
+ if (isController) {
3571
+ warnUnresolvedFactory(cls, calleeText, "that factory does not return a class declaration");
3572
+ }
3573
+ return void 0;
3574
+ }
3525
3575
  const classArgs = [];
3526
3576
  const namedClassArgs = {};
3527
3577
  for (const arg of expr.getArguments()) {
@@ -3539,7 +3589,12 @@ function resolveInheritedMethods(cls) {
3539
3589
  }
3540
3590
  return {
3541
3591
  methods: returnedClass.getMethods(),
3542
- factoryName: callee.getText(),
3592
+ // The DECLARATION's name, not the call-site text: consumers look the factory
3593
+ // back up by name inside `factoryFilePath`, which a qualified
3594
+ // `tables.createTableController` would never match. For a bare identifier
3595
+ // the two coincide (bar an import alias, where the declaration name is the
3596
+ // one that resolves anyway).
3597
+ factoryName: factoryDecl.getName() ?? calleeText,
3543
3598
  factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
3544
3599
  classArgs,
3545
3600
  namedClassArgs
@@ -3598,10 +3653,15 @@ function resolveInstantiatedReturnType(cls, methodName) {
3598
3653
 
3599
3654
  // src/discovery/filter-for.ts
3600
3655
  function resolveMixinEntityClass(mixin, project) {
3601
- const entityArg = mixin?.namedClassArgs?.entity ?? mixin?.classArgs[0];
3602
- if (!entityArg) return void 0;
3603
- const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3604
- return file?.getClass(entityArg.name);
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);
3605
3665
  }
3606
3666
  function classifyFilterForHint(typeInit) {
3607
3667
  if (import_ts_morph7.Node.isStringLiteral(typeInit)) {
@@ -3701,31 +3761,55 @@ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3701
3761
  }
3702
3762
  const filterClassName = filterClassArg.getText();
3703
3763
  const resolved = factoryStaticClass ? void 0 : findType(filterClassName, declFile, project);
3704
- const classDecl = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
3705
- if (classDecl) {
3706
- let fieldTypes = extractClassPropertyTypes(classDecl, project);
3707
- if (fieldTypes.length === 0) {
3708
- fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3709
- }
3710
- const filterForHints = extractFilterForHints(classDecl, project);
3711
- if (filterForHints.size > 0) {
3712
- const byName = new Map(fieldTypes.map((f) => [f.name, f]));
3713
- for (const [key, classified] of filterForHints) {
3714
- byName.set(key, toFilterFieldType(key, classified));
3715
- }
3716
- fieldTypes = [...byName.values()];
3717
- }
3718
- if (fieldTypes.length === 0) return null;
3719
- const fieldNames = fieldTypes.map((f) => f.name);
3764
+ const declaredClass = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
3765
+ const ownRoute = !mixin || 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;
3720
3774
  return {
3721
- fieldNames,
3775
+ fieldNames: fieldTypes.map((f) => f.name),
3722
3776
  fieldTypes,
3723
3777
  source
3724
3778
  };
3725
3779
  }
3780
+ if (declaredClass) return null;
3726
3781
  }
3727
3782
  return null;
3728
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
+ }
3729
3813
  var RELATION_DECORATORS = /* @__PURE__ */ new Set(["OneToMany", "ManyToOne", "ManyToMany", "OneToOne"]);
3730
3814
  function collectEntityFields(entityDecl, sourceFile, project, prefix, visited) {
3731
3815
  const entityName = entityDecl.getName() ?? "";
@@ -3804,21 +3888,53 @@ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3804
3888
  if (!resolved || resolved.kind !== "class") return void 0;
3805
3889
  return resolved.decl;
3806
3890
  }
3807
- function extractFilterableEntityFields(filterClass, project, entityOverride) {
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) {
3808
3926
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3809
3927
  if (!filterableDecorator) return [];
3810
3928
  const args = filterableDecorator.getArguments();
3811
3929
  if (args.length === 0) return [];
3812
3930
  const optionsArg = args[0];
3813
3931
  if (!import_ts_morph7.Node.isObjectLiteralExpression(optionsArg)) return [];
3814
- const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3932
+ const declaredEntity = resolveDeclaredEntity(optionsArg, filterClass, project);
3933
+ const entityDecl = preferDeclaredEntity ? declaredEntity ?? entityOverride : entityOverride ?? declaredEntity;
3815
3934
  if (!entityDecl) return [];
3816
- const fields = collectEntityFields(
3817
- entityDecl,
3818
- entityDecl.getSourceFile(),
3819
- project,
3820
- "",
3821
- /* @__PURE__ */ new Set()
3935
+ const fields = narrowToDeclaredFields(
3936
+ collectEntityFields(entityDecl, entityDecl.getSourceFile(), project, "", /* @__PURE__ */ new Set()),
3937
+ optionsArg
3822
3938
  );
3823
3939
  const relationsDecorator = filterClass.getDecorators().find((d) => d.getName() === "Relations");
3824
3940
  if (relationsDecorator) {
@@ -5083,7 +5199,7 @@ async function watch(config, onChange, options = {}) {
5083
5199
  }
5084
5200
 
5085
5201
  // src/index.ts
5086
- var VERSION = "0.18.0";
5202
+ var VERSION = "0.20.0";
5087
5203
 
5088
5204
  // src/cli/codegen.ts
5089
5205
  async function runCodegen(opts = {}) {