@dudousxd/nestjs-codegen 0.19.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,57 @@
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
+
3
55
  ## 0.19.0
4
56
 
5
57
  ### 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
- const entityArg = mixin?.namedClassArgs?.entity ?? mixin?.classArgs[0];
3657
- if (!entityArg) return void 0;
3658
- const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3659
- 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);
3660
3665
  }
3661
3666
  function classifyFilterForHint(typeInit) {
3662
3667
  if (import_ts_morph7.Node.isStringLiteral(typeInit)) {
@@ -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 classDecl = factoryStaticClass ?? (resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg));
3760
- if (classDecl) {
3761
- let fieldTypes = extractClassPropertyTypes(classDecl, project);
3762
- if (fieldTypes.length === 0) {
3763
- fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3764
- }
3765
- const filterForHints = extractFilterForHints(classDecl, project);
3766
- if (filterForHints.size > 0) {
3767
- const byName = new Map(fieldTypes.map((f) => [f.name, f]));
3768
- for (const [key, classified] of filterForHints) {
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 || 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 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) {
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 entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3932
+ const declaredEntity = resolveDeclaredEntity(optionsArg, filterClass, project);
3933
+ const entityDecl = preferDeclaredEntity ? declaredEntity ?? entityOverride : entityOverride ?? declaredEntity;
3870
3934
  if (!entityDecl) return [];
3871
- const fields = collectEntityFields(
3872
- entityDecl,
3873
- entityDecl.getSourceFile(),
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) {
@@ -5138,7 +5199,7 @@ async function watch(config, onChange, options = {}) {
5138
5199
  }
5139
5200
 
5140
5201
  // src/index.ts
5141
- var VERSION = "0.19.0";
5202
+ var VERSION = "0.20.0";
5142
5203
 
5143
5204
  // src/cli/codegen.ts
5144
5205
  async function runCodegen(opts = {}) {