@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.
- package/CHANGELOG.md +13 -0
- package/dist/cli/main.cjs +71 -10
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +71 -10
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.cjs.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-DT8SgPxp.d.cts → index-CjIDPMsV.d.cts} +7 -0
- package/dist/{index-DT8SgPxp.d.ts → index-CjIDPMsV.d.ts} +7 -0
- package/dist/index.cjs +71 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +71 -10
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +92 -12
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +56 -3
- package/dist/nest/index.d.ts +56 -3
- package/dist/nest/index.js +88 -11
- package/dist/nest/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.14.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 79d5e73: Fix a drift-guard false positive that permanently blocked incremental regeneration for shared configs: the config hash folded functions in via `toString()`, but the same shared config object yields different function source text per entry point (the CLI loads TS via Node's type stripping; the Nest module runs tsc/SWC-compiled dist), so a genuinely-shared config was flagged as drifted the moment both entry points touched the same outDir. Functions now hash by name only — every setting that can actually diverge is plain data and is still hashed in full. The drift error also now NAMES the top-level keys that differ (via new per-key hashes recorded in the manifest as `configKeyHashes`) instead of a generic example.
|
|
15
|
+
|
|
3
16
|
## 0.14.1
|
|
4
17
|
|
|
5
18
|
### 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
|
|
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(
|
|
@@ -2185,19 +2213,41 @@ function isManifestShape(value) {
|
|
|
2185
2213
|
if (typeof candidate.hash !== "string") return false;
|
|
2186
2214
|
if (candidate.entryPoint !== void 0 && !isEntryPoint(candidate.entryPoint)) return false;
|
|
2187
2215
|
if (candidate.configHash !== void 0 && typeof candidate.configHash !== "string") return false;
|
|
2216
|
+
if (candidate.configKeyHashes !== void 0 && !isStringRecord(candidate.configKeyHashes)) {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2188
2219
|
if (!Array.isArray(candidate.files)) return false;
|
|
2189
2220
|
return candidate.files.every((entry) => typeof entry === "string");
|
|
2190
2221
|
}
|
|
2222
|
+
function isStringRecord(value) {
|
|
2223
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2224
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
2225
|
+
}
|
|
2191
2226
|
function serializeConfig(config) {
|
|
2227
|
+
return serializeConfigValue(config, `unserializable:${config.codegen.outDir}`);
|
|
2228
|
+
}
|
|
2229
|
+
function serializeConfigValue(value, unserializableMarker) {
|
|
2192
2230
|
try {
|
|
2193
|
-
return JSON.stringify(
|
|
2194
|
-
if (typeof
|
|
2195
|
-
return
|
|
2231
|
+
return JSON.stringify(value, (_key, entry) => {
|
|
2232
|
+
if (typeof entry === "function") return `[fn:${entry.name}]`;
|
|
2233
|
+
return entry;
|
|
2196
2234
|
});
|
|
2197
2235
|
} catch {
|
|
2198
|
-
return
|
|
2236
|
+
return unserializableMarker;
|
|
2199
2237
|
}
|
|
2200
2238
|
}
|
|
2239
|
+
function computeConfigKeyHashes(config) {
|
|
2240
|
+
const hashes = {};
|
|
2241
|
+
for (const [key, value] of Object.entries(config)) {
|
|
2242
|
+
if (value === void 0) continue;
|
|
2243
|
+
hashes[key] = (0, import_node_crypto.createHash)("sha256").update(serializeConfigValue(value, `unserializable:${key}`)).digest("hex");
|
|
2244
|
+
}
|
|
2245
|
+
return hashes;
|
|
2246
|
+
}
|
|
2247
|
+
function diffConfigKeyHashes(previous, current) {
|
|
2248
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
2249
|
+
return [...keys].filter((key) => previous[key] !== current[key]).sort();
|
|
2250
|
+
}
|
|
2201
2251
|
async function discoverInputFiles(config) {
|
|
2202
2252
|
const globs = [config.contracts.glob, config.forms.watch];
|
|
2203
2253
|
if (config.pages) globs.push(config.pages.glob);
|
|
@@ -2232,6 +2282,7 @@ async function readManifest(outDir) {
|
|
|
2232
2282
|
hash: parsed.hash,
|
|
2233
2283
|
...parsed.entryPoint ? { entryPoint: parsed.entryPoint } : {},
|
|
2234
2284
|
...parsed.configHash ? { configHash: parsed.configHash } : {},
|
|
2285
|
+
...parsed.configKeyHashes ? { configKeyHashes: parsed.configKeyHashes } : {},
|
|
2235
2286
|
files: parsed.files
|
|
2236
2287
|
};
|
|
2237
2288
|
} catch {
|
|
@@ -2285,8 +2336,9 @@ function debugWarn(message) {
|
|
|
2285
2336
|
}
|
|
2286
2337
|
|
|
2287
2338
|
// src/generate.ts
|
|
2288
|
-
function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint) {
|
|
2289
|
-
|
|
2339
|
+
function driftGuardMessage(outDir, previousEntryPoint, currentEntryPoint, differingKeys) {
|
|
2340
|
+
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)";
|
|
2341
|
+
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.`;
|
|
2290
2342
|
}
|
|
2291
2343
|
async function generate(config, inputRoutes = [], entryPoint = "cli") {
|
|
2292
2344
|
setCodegenDebug(config.debug);
|
|
@@ -2297,9 +2349,17 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
|
|
|
2297
2349
|
return;
|
|
2298
2350
|
}
|
|
2299
2351
|
const configHash = computeConfigHash(config);
|
|
2352
|
+
const configKeyHashes = computeConfigKeyHashes(config);
|
|
2300
2353
|
if (config.driftGuard && manifest?.entryPoint && manifest.entryPoint !== entryPoint && manifest.configHash && manifest.configHash !== configHash) {
|
|
2301
2354
|
throw new DriftGuardError(
|
|
2302
|
-
driftGuardMessage(
|
|
2355
|
+
driftGuardMessage(
|
|
2356
|
+
config.codegen.outDir,
|
|
2357
|
+
manifest.entryPoint,
|
|
2358
|
+
entryPoint,
|
|
2359
|
+
// A pre-key-hash manifest can't tell us WHICH keys differ — pass none
|
|
2360
|
+
// rather than diffing against {} (which would name every key).
|
|
2361
|
+
manifest.configKeyHashes ? diffConfigKeyHashes(manifest.configKeyHashes, configKeyHashes) : []
|
|
2362
|
+
)
|
|
2303
2363
|
);
|
|
2304
2364
|
}
|
|
2305
2365
|
const extensions = config.extensions ?? [];
|
|
@@ -2369,6 +2429,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
|
|
|
2369
2429
|
hash: inputsHash,
|
|
2370
2430
|
entryPoint,
|
|
2371
2431
|
configHash,
|
|
2432
|
+
configKeyHashes,
|
|
2372
2433
|
files: outputFiles
|
|
2373
2434
|
});
|
|
2374
2435
|
}
|
|
@@ -4801,7 +4862,7 @@ async function watch(config, onChange, options = {}) {
|
|
|
4801
4862
|
}
|
|
4802
4863
|
|
|
4803
4864
|
// src/index.ts
|
|
4804
|
-
var VERSION = "0.
|
|
4865
|
+
var VERSION = "0.15.0";
|
|
4805
4866
|
|
|
4806
4867
|
// src/cli/codegen.ts
|
|
4807
4868
|
async function runCodegen(opts = {}) {
|