@dudousxd/nestjs-codegen 0.14.2 → 0.15.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.
@@ -1,5 +1,6 @@
1
- import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
2
- import { U as UserConfig } from '../index-DT8SgPxp.cjs';
1
+ import * as _nestjs_common from '@nestjs/common';
2
+ import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy, ExecutionContext } from '@nestjs/common';
3
+ import { U as UserConfig } from '../index-CjIDPMsV.cjs';
3
4
  import 'ts-morph';
4
5
 
5
6
  /**
@@ -82,4 +83,56 @@ declare class NestjsCodegenModule {
82
83
  static forRoot(options?: CodegenModuleOptions): DynamicModule;
83
84
  }
84
85
 
85
- export { CODEGEN_MODULE_OPTIONS, type CodegenModuleOptions, NestjsCodegenModule, NestjsCodegenService, shouldRun };
86
+ /**
87
+ * Normalize a raw query value — `string | string[] | undefined | null` (and, for
88
+ * back-compat with the comma-joined wire format, a comma-separated string) — into
89
+ * a clean `string[]`.
90
+ *
91
+ * Why this exists: Express (and therefore Nest's default query parser) hands back a
92
+ * **bare `string`** when a querystring key carries exactly one value (`?ids=a`), and a
93
+ * `string[]` only when it carries two or more (`?ids=a&ids=b`). `ParseArrayPipe` rejects
94
+ * the single-value form, so the *common* case (one item selected) 400s while the
95
+ * multi-value case passes — an inverted footgun. This helper accepts every shape:
96
+ *
97
+ * - `undefined` / `null` → `[]`
98
+ * - `'a'` (single value) → `['a']`
99
+ * - `['a', 'b']` (repeated param) → `['a', 'b']`
100
+ * - `'a,b'` (comma-joined wire) → `['a', 'b']` (see `@dudousxd/nestjs-client`
101
+ * `arrayFormat: 'comma'`, the client default)
102
+ *
103
+ * Empty/whitespace-only entries are dropped. The comma-split is a compatibility fallback:
104
+ * once the client sends `arrayFormat: 'repeat'` (`?ids=a&ids=b`), it degrades to a no-op
105
+ * and only the single-value bare-string case still needs normalizing.
106
+ *
107
+ * Exported standalone so it can back a `class-transformer` `@Transform` on a DTO field
108
+ * (`@Transform(({ value }) => toStringList(value))`) as well as the {@link QueryList}
109
+ * param decorator.
110
+ */
111
+ declare function toStringList(raw: unknown): string[];
112
+ /**
113
+ * Resolve a `string[]` from a request's query param `key` via {@link toStringList}.
114
+ * The seam the {@link QueryList} decorator is built on — exported so callers can reuse
115
+ * the exact resolution in a bespoke `createParamDecorator`. Returns `[]` when no `key`
116
+ * is given (a param decorator cannot infer the target property name).
117
+ */
118
+ declare function resolveQueryList(key: string | undefined, ctx: ExecutionContext): string[];
119
+ /**
120
+ * Param decorator that reads an array query param safely, always yielding a clean
121
+ * `string[]` regardless of whether the client sent one value, many, or a comma-joined
122
+ * string. Use it instead of `@Query(key, ParseArrayPipe)` for *optional* array query
123
+ * params:
124
+ *
125
+ * ```ts
126
+ * @Get()
127
+ * list(@QueryList('baseIds') baseIds: string[]) {
128
+ * // baseIds is always a clean string[] — [] when the param is absent,
129
+ * // ['a'] for ?baseIds=a, ['a','b'] for ?baseIds=a&baseIds=b or ?baseIds=a,b
130
+ * }
131
+ * ```
132
+ *
133
+ * See {@link toStringList} for the exact normalization and the single-value footgun it
134
+ * closes.
135
+ */
136
+ declare const QueryList: (...dataOrPipes: (string | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>> | undefined)[]) => ParameterDecorator;
137
+
138
+ export { CODEGEN_MODULE_OPTIONS, type CodegenModuleOptions, NestjsCodegenModule, NestjsCodegenService, QueryList, resolveQueryList, shouldRun, toStringList };
@@ -1,5 +1,6 @@
1
- import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
2
- import { U as UserConfig } from '../index-DT8SgPxp.js';
1
+ import * as _nestjs_common from '@nestjs/common';
2
+ import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy, ExecutionContext } from '@nestjs/common';
3
+ import { U as UserConfig } from '../index-CjIDPMsV.js';
3
4
  import 'ts-morph';
4
5
 
5
6
  /**
@@ -82,4 +83,56 @@ declare class NestjsCodegenModule {
82
83
  static forRoot(options?: CodegenModuleOptions): DynamicModule;
83
84
  }
84
85
 
85
- export { CODEGEN_MODULE_OPTIONS, type CodegenModuleOptions, NestjsCodegenModule, NestjsCodegenService, shouldRun };
86
+ /**
87
+ * Normalize a raw query value — `string | string[] | undefined | null` (and, for
88
+ * back-compat with the comma-joined wire format, a comma-separated string) — into
89
+ * a clean `string[]`.
90
+ *
91
+ * Why this exists: Express (and therefore Nest's default query parser) hands back a
92
+ * **bare `string`** when a querystring key carries exactly one value (`?ids=a`), and a
93
+ * `string[]` only when it carries two or more (`?ids=a&ids=b`). `ParseArrayPipe` rejects
94
+ * the single-value form, so the *common* case (one item selected) 400s while the
95
+ * multi-value case passes — an inverted footgun. This helper accepts every shape:
96
+ *
97
+ * - `undefined` / `null` → `[]`
98
+ * - `'a'` (single value) → `['a']`
99
+ * - `['a', 'b']` (repeated param) → `['a', 'b']`
100
+ * - `'a,b'` (comma-joined wire) → `['a', 'b']` (see `@dudousxd/nestjs-client`
101
+ * `arrayFormat: 'comma'`, the client default)
102
+ *
103
+ * Empty/whitespace-only entries are dropped. The comma-split is a compatibility fallback:
104
+ * once the client sends `arrayFormat: 'repeat'` (`?ids=a&ids=b`), it degrades to a no-op
105
+ * and only the single-value bare-string case still needs normalizing.
106
+ *
107
+ * Exported standalone so it can back a `class-transformer` `@Transform` on a DTO field
108
+ * (`@Transform(({ value }) => toStringList(value))`) as well as the {@link QueryList}
109
+ * param decorator.
110
+ */
111
+ declare function toStringList(raw: unknown): string[];
112
+ /**
113
+ * Resolve a `string[]` from a request's query param `key` via {@link toStringList}.
114
+ * The seam the {@link QueryList} decorator is built on — exported so callers can reuse
115
+ * the exact resolution in a bespoke `createParamDecorator`. Returns `[]` when no `key`
116
+ * is given (a param decorator cannot infer the target property name).
117
+ */
118
+ declare function resolveQueryList(key: string | undefined, ctx: ExecutionContext): string[];
119
+ /**
120
+ * Param decorator that reads an array query param safely, always yielding a clean
121
+ * `string[]` regardless of whether the client sent one value, many, or a comma-joined
122
+ * string. Use it instead of `@Query(key, ParseArrayPipe)` for *optional* array query
123
+ * params:
124
+ *
125
+ * ```ts
126
+ * @Get()
127
+ * list(@QueryList('baseIds') baseIds: string[]) {
128
+ * // baseIds is always a clean string[] — [] when the param is absent,
129
+ * // ['a'] for ?baseIds=a, ['a','b'] for ?baseIds=a&baseIds=b or ?baseIds=a,b
130
+ * }
131
+ * ```
132
+ *
133
+ * See {@link toStringList} for the exact normalization and the single-value footgun it
134
+ * closes.
135
+ */
136
+ declare const QueryList: (...dataOrPipes: (string | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>> | undefined)[]) => ParameterDecorator;
137
+
138
+ export { CODEGEN_MODULE_OPTIONS, type CodegenModuleOptions, NestjsCodegenModule, NestjsCodegenService, QueryList, resolveQueryList, shouldRun, toStringList };
@@ -702,6 +702,9 @@ function buildErrorType(c) {
702
702
  }
703
703
  return c.contractSource.error ?? "unknown";
704
704
  }
705
+ function filterFieldLiterals(fields) {
706
+ return fields?.length ? fields.map((f) => JSON.stringify(f)) : [];
707
+ }
705
708
  function emitRouterTypeBlock(tree, indent, outDir, serialization) {
706
709
  const pad = " ".repeat(indent);
707
710
  const lines = [];
@@ -728,7 +731,8 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
728
731
  const params = buildParamsType(c.params);
729
732
  const safeMethod = JSON.stringify(method);
730
733
  const safeUrl = JSON.stringify(c.path);
731
- const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
734
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
735
+ const filterFields = filterLiterals.length ? filterLiterals.join(" | ") : "never";
732
736
  const stream = c.contractSource.stream ? "true" : "false";
733
737
  const binary = c.contractSource.binaryResponse ? "true" : "false";
734
738
  lines.push(
@@ -768,6 +772,7 @@ function buildRequestModel(c) {
768
772
  const TA = buildRouterTypeAccess(c.name);
769
773
  const withParams = hasPathParams(c.params);
770
774
  const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
775
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
771
776
  const fields = [];
772
777
  if (withParams) fields.push(`params: ${TA}['params']`);
773
778
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -797,7 +802,12 @@ function buildRequestModel(c) {
797
802
  // (`[name]` rather than `[name, undefined]`) so the bare `.queryKey()` is a
798
803
  // clean prefix that partial-matches every parametrized variant — making it
799
804
  // directly usable for `invalidateQueries`.
800
- queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
805
+ queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`,
806
+ // Runtime counterpart to the type-level `filterFields` union: the same
807
+ // discovered field list, emitted as a literal `[...] as const` so apps can
808
+ // validate a dynamic/user-supplied field string with `isFilterField(...)`
809
+ // instead of casting. Omitted for routes with no filter.
810
+ ...filterLiterals.length ? { filterFieldsExpr: `[${filterLiterals.join(", ")}] as const` } : {}
801
811
  };
802
812
  }
803
813
  function renderFetcherRequest(req, binaryResponse) {
@@ -832,9 +842,24 @@ function emitReqHelper() {
832
842
  ""
833
843
  ];
834
844
  }
845
+ function emitFilterFieldGuard() {
846
+ return [
847
+ "/** 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. */",
848
+ "export function isFilterField<const K extends string>(",
849
+ " fields: readonly K[],",
850
+ " value: string,",
851
+ "): value is K {",
852
+ " return (fields as readonly string[]).includes(value);",
853
+ "}",
854
+ ""
855
+ ];
856
+ }
835
857
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
836
858
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
837
859
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
860
+ if (req.filterFieldsExpr) {
861
+ lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
862
+ }
838
863
  if (streamExpr) {
839
864
  lines.push(`${pad} stream: () => ${streamExpr},`);
840
865
  }
@@ -1106,6 +1131,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1106
1131
  lines.push("};");
1107
1132
  lines.push("");
1108
1133
  lines.push(...emitReqHelper());
1134
+ if (contracted.some((r) => r.contract?.contractSource.filterFields?.length)) {
1135
+ lines.push(...emitFilterFieldGuard());
1136
+ }
1109
1137
  lines.push("export function createApi(fetcher: Fetcher) {");
1110
1138
  lines.push(" return {");
1111
1139
  lines.push(
@@ -4622,7 +4650,7 @@ async function watch(config, onChange, options = {}) {
4622
4650
  }
4623
4651
 
4624
4652
  // src/index.ts
4625
- var VERSION = "0.14.2";
4653
+ var VERSION = "0.15.0";
4626
4654
 
4627
4655
  // src/generate-manifest.ts
4628
4656
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -4822,10 +4850,26 @@ var NestjsCodegenModule = class {
4822
4850
  NestjsCodegenModule = __decorateClass([
4823
4851
  Module({})
4824
4852
  ], NestjsCodegenModule);
4853
+
4854
+ // src/nest/query-list.ts
4855
+ import { createParamDecorator } from "@nestjs/common";
4856
+ function toStringList(raw) {
4857
+ if (raw === void 0 || raw === null) return [];
4858
+ const arr = Array.isArray(raw) ? raw : String(raw).split(",");
4859
+ return arr.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0);
4860
+ }
4861
+ function resolveQueryList(key, ctx) {
4862
+ const request = ctx.switchToHttp().getRequest();
4863
+ return toStringList(key ? request.query?.[key] : void 0);
4864
+ }
4865
+ var QueryList = createParamDecorator(resolveQueryList);
4825
4866
  export {
4826
4867
  CODEGEN_MODULE_OPTIONS,
4827
4868
  NestjsCodegenModule,
4828
4869
  NestjsCodegenService,
4829
- shouldRun
4870
+ QueryList,
4871
+ resolveQueryList,
4872
+ shouldRun,
4873
+ toStringList
4830
4874
  };
4831
4875
  //# sourceMappingURL=index.js.map