@dudousxd/nestjs-codegen 0.14.1 → 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.
@@ -42,7 +42,10 @@ __export(nest_exports, {
42
42
  CODEGEN_MODULE_OPTIONS: () => CODEGEN_MODULE_OPTIONS,
43
43
  NestjsCodegenModule: () => NestjsCodegenModule,
44
44
  NestjsCodegenService: () => NestjsCodegenService,
45
- shouldRun: () => shouldRun
45
+ QueryList: () => QueryList,
46
+ resolveQueryList: () => resolveQueryList,
47
+ shouldRun: () => shouldRun,
48
+ toStringList: () => toStringList
46
49
  });
47
50
  module.exports = __toCommonJS(nest_exports);
48
51
 
@@ -738,6 +741,9 @@ function buildErrorType(c) {
738
741
  }
739
742
  return c.contractSource.error ?? "unknown";
740
743
  }
744
+ function filterFieldLiterals(fields) {
745
+ return fields?.length ? fields.map((f) => JSON.stringify(f)) : [];
746
+ }
741
747
  function emitRouterTypeBlock(tree, indent, outDir, serialization) {
742
748
  const pad = " ".repeat(indent);
743
749
  const lines = [];
@@ -764,7 +770,8 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
764
770
  const params = buildParamsType(c.params);
765
771
  const safeMethod = JSON.stringify(method);
766
772
  const safeUrl = JSON.stringify(c.path);
767
- const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
773
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
774
+ const filterFields = filterLiterals.length ? filterLiterals.join(" | ") : "never";
768
775
  const stream = c.contractSource.stream ? "true" : "false";
769
776
  const binary = c.contractSource.binaryResponse ? "true" : "false";
770
777
  lines.push(
@@ -804,6 +811,7 @@ function buildRequestModel(c) {
804
811
  const TA = buildRouterTypeAccess(c.name);
805
812
  const withParams = hasPathParams(c.params);
806
813
  const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
814
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
807
815
  const fields = [];
808
816
  if (withParams) fields.push(`params: ${TA}['params']`);
809
817
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -833,7 +841,12 @@ function buildRequestModel(c) {
833
841
  // (`[name]` rather than `[name, undefined]`) so the bare `.queryKey()` is a
834
842
  // clean prefix that partial-matches every parametrized variant — making it
835
843
  // directly usable for `invalidateQueries`.
836
- queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
844
+ queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`,
845
+ // Runtime counterpart to the type-level `filterFields` union: the same
846
+ // discovered field list, emitted as a literal `[...] as const` so apps can
847
+ // validate a dynamic/user-supplied field string with `isFilterField(...)`
848
+ // instead of casting. Omitted for routes with no filter.
849
+ ...filterLiterals.length ? { filterFieldsExpr: `[${filterLiterals.join(", ")}] as const` } : {}
837
850
  };
838
851
  }
839
852
  function renderFetcherRequest(req, binaryResponse) {
@@ -868,9 +881,24 @@ function emitReqHelper() {
868
881
  ""
869
882
  ];
870
883
  }
884
+ function emitFilterFieldGuard() {
885
+ return [
886
+ "/** 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. */",
887
+ "export function isFilterField<const K extends string>(",
888
+ " fields: readonly K[],",
889
+ " value: string,",
890
+ "): value is K {",
891
+ " return (fields as readonly string[]).includes(value);",
892
+ "}",
893
+ ""
894
+ ];
895
+ }
871
896
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
872
897
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
873
898
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
899
+ if (req.filterFieldsExpr) {
900
+ lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
901
+ }
874
902
  if (streamExpr) {
875
903
  lines.push(`${pad} stream: () => ${streamExpr},`);
876
904
  }
@@ -1142,6 +1170,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1142
1170
  lines.push("};");
1143
1171
  lines.push("");
1144
1172
  lines.push(...emitReqHelper());
1173
+ if (contracted.some((r) => r.contract?.contractSource.filterFields?.length)) {
1174
+ lines.push(...emitFilterFieldGuard());
1175
+ }
1145
1176
  lines.push("export function createApi(fetcher: Fetcher) {");
1146
1177
  lines.push(" return {");
1147
1178
  lines.push(
@@ -2135,8 +2166,9 @@ function debugWarn(message) {
2135
2166
  }
2136
2167
 
2137
2168
  // src/generate.ts
2138
- function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint) {
2139
- return `[nestjs-codegen] Config drift detected in "${outDir}": the last generate ran from the "${previousEntryPoint}" entry point, this run is from the "${currentEntryPoint}" entry point, and their resolved configs differ (e.g. \`serialization: "json"\` vs \`"superjson"\`). Both entry points must read the SAME config \u2014 export a shared config object (e.g. codegen.config.ts) and import it from BOTH nestjs-codegen.config.ts (CLI) and NestjsCodegenModule.forRoot() (Nest module), or set \`driftGuard: false\` on either config to opt out of this check.`;
2169
+ function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
2170
+ const differ = differingKeys.length > 0 ? `their resolved configs differ at: ${differingKeys.map((key) => `\`${key}\``).join(", ")}` : "their resolved configs differ (re-run after this generate records per-key hashes to see which keys)";
2171
+ return `[nestjs-codegen] Config drift detected in "${outDir}": the last generate ran from the "${previousEntryPoint}" entry point, this run is from the "${currentEntryPoint}" entry point, and ${differ}. Both entry points must read the SAME config \u2014 export a shared config object (e.g. codegen.config.ts) and import it from BOTH nestjs-codegen.config.ts (CLI) and NestjsCodegenModule.forRoot() (Nest module), or set \`driftGuard: false\` on either config to opt out of this check.`;
2140
2172
  }
2141
2173
  async function generate(config, inputRoutes = [], entryPoint = "cli") {
2142
2174
  setCodegenDebug(config.debug);
@@ -2147,9 +2179,17 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2147
2179
  return;
2148
2180
  }
2149
2181
  const configHash = computeConfigHash(config);
2182
+ const configKeyHashes = computeConfigKeyHashes(config);
2150
2183
  if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
2151
2184
  throw new DriftGuardError(
2152
- driftGuardMessage(config.codegen.outDir, manifest.entryPoint, entryPoint)
2185
+ driftGuardMessage(
2186
+ config.codegen.outDir,
2187
+ manifest.entryPoint,
2188
+ entryPoint,
2189
+ // A pre-key-hash manifest can't tell us WHICH keys differ — pass none
2190
+ // rather than diffing against {} (which would name every key).
2191
+ manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
2192
+ )
2153
2193
  );
2154
2194
  }
2155
2195
  const extensions = config.extensions ?? [];
@@ -2219,6 +2259,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
2219
2259
  hash: inputsHash,
2220
2260
  entryPoint,
2221
2261
  configHash,
2262
+ configKeyHashes,
2222
2263
  files: outputFiles
2223
2264
  });
2224
2265
  }
@@ -4633,7 +4674,7 @@ async function watch(config, onChange, options = {}) {
4633
4674
  }
4634
4675
 
4635
4676
  // src/index.ts
4636
- var VERSION = "0.14.1";
4677
+ var VERSION = "0.15.0";
4637
4678
 
4638
4679
  // src/generate-manifest.ts
4639
4680
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -4654,19 +4695,41 @@ function isManifestShape(value) {
4654
4695
  if (typeof candidate.hash !== "string") return false;
4655
4696
  if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
4656
4697
  if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
4698
+ if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
4699
+ return false;
4700
+ }
4657
4701
  if (!Array.isArray(candidate.files)) return false;
4658
4702
  return candidate.files.every((entry) => typeof entry === "string");
4659
4703
  }
4704
+ function isStringRecord(value) {
4705
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4706
+ return Object.values(value).every((entry) => typeof entry === "string");
4707
+ }
4660
4708
  function serializeConfig(config) {
4709
+ return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
4710
+ }
4711
+ function serializeConfigValue(value, unserializableMarker) {
4661
4712
  try {
4662
- return JSON.stringify(config, (_key, value) => {
4663
- if (typeof value === "function") return `[fn:${value.name}]${value.toString()}`;
4664
- return value;
4713
+ return JSON.stringify(value, (_key, entry) => {
4714
+ if (typeof entry === "function") return `[fn:${entry.name}]`;
4715
+ return entry;
4665
4716
  });
4666
4717
  } catch {
4667
- return `unserializable:${config.codegen.outDir}:${config.contracts.glob}`;
4718
+ return unserializableMarker;
4668
4719
  }
4669
4720
  }
4721
+ function computeConfigKeyHashes(config) {
4722
+ const hashes = {};
4723
+ for (const [key, value] of Object.entries(config)) {
4724
+ if (value === void 0) continue;
4725
+ hashes[key] = (0, import_node_crypto.createHash)("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
4726
+ }
4727
+ return hashes;
4728
+ }
4729
+ function diffConfigKeyHashes(previous, current) {
4730
+ const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
4731
+ return [...keys].filter((key) => previous[key] !== current[key]).sort();
4732
+ }
4670
4733
  async function discoverInputFiles(config) {
4671
4734
  const globs = [config.contracts.glob, config.forms.watch];
4672
4735
  if (config.pages) globs.push(config.pages.glob);
@@ -4701,6 +4764,7 @@ async function readManifest(outDir) {
4701
4764
  hash: parsed.hash,
4702
4765
  ...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
4703
4766
  ...parsed.configHash ? { configHash: parsed.configHash } : {},
4767
+ ...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
4704
4768
  files: parsed.files
4705
4769
  };
4706
4770
  } catch {
@@ -4810,11 +4874,27 @@ var NestjsCodegenModule = class {
4810
4874
  NestjsCodegenModule = __decorateClass([
4811
4875
  (0, import_common.Module)({})
4812
4876
  ], NestjsCodegenModule);
4877
+
4878
+ // src/nest/query-list.ts
4879
+ var import_common2 = require("@nestjs/common");
4880
+ function toStringList(raw) {
4881
+ if (raw === void 0 || raw === null) return [];
4882
+ const arr = Array.isArray(raw) ? raw : String(raw).split(",");
4883
+ return arr.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0);
4884
+ }
4885
+ function resolveQueryList(key, ctx) {
4886
+ const request = ctx.switchToHttp().getRequest();
4887
+ return toStringList(key ? request.query?.[key] : void 0);
4888
+ }
4889
+ var QueryList = (0, import_common2.createParamDecorator)(resolveQueryList);
4813
4890
  // Annotate the CommonJS export names for ESM import in node:
4814
4891
  0 && (module.exports = {
4815
4892
  CODEGEN_MODULE_OPTIONS,
4816
4893
  NestjsCodegenModule,
4817
4894
  NestjsCodegenService,
4818
- shouldRun
4895
+ QueryList,
4896
+ resolveQueryList,
4897
+ shouldRun,
4898
+ toStringList
4819
4899
  });
4820
4900
  //# sourceMappingURL=index.cjs.map