@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+ - 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.
9
+
3
10
  ## 0.14.2
4
11
 
5
12
  ### 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 filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
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(
@@ -4834,7 +4862,7 @@ async function watch(config, onChange, options = {}) {
4834
4862
  }
4835
4863
 
4836
4864
  // src/index.ts
4837
- var VERSION = "0.14.2";
4865
+ var VERSION = "0.15.0";
4838
4866
 
4839
4867
  // src/cli/codegen.ts
4840
4868
  async function runCodegen(opts = {}) {