@dudousxd/nestjs-codegen 0.12.0 → 0.13.1
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 +51 -0
- package/dist/cli/main.cjs +88 -5
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.js +88 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/extension/index.d.cts +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/{index-CxkGbILp.d.cts → index-D8RIMVpU.d.cts} +15 -0
- package/dist/{index-CxkGbILp.d.ts → index-D8RIMVpU.d.ts} +15 -0
- package/dist/index.cjs +88 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +88 -5
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +88 -5
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +1 -1
- package/dist/nest/index.d.ts +1 -1
- package/dist/nest/index.js +88 -5
- package/dist/nest/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @dudousxd/nestjs-codegen
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6b51c7b: fix(multipart): intersect the uploaded-file field at emit time so it survives a named `bodyRef`, and leave deliberately-loose bodies untouched.
|
|
8
|
+
|
|
9
|
+
Two fixes to the multipart upload routes shipped in 0.13.0:
|
|
10
|
+
|
|
11
|
+
- **Named body refs now include the file field.** Discovery carries the uploaded-file
|
|
12
|
+
field(s) in a new `multipartBody` (kept off `body`), and the emitter intersects it onto
|
|
13
|
+
whichever body expression it picks — a named `bodyRef` (`BaseFileUploadDto`) or the inline
|
|
14
|
+
text. Previously the merge lived on the inline `body` string, so a route whose `@Body`
|
|
15
|
+
resolved to an imported DTO emitted the plain `BaseFileUploadDto` and dropped the file
|
|
16
|
+
field (`api.X({ body: { ...fields, file } })` failed to type-check).
|
|
17
|
+
|
|
18
|
+
- **Deliberately-loose bodies are left alone.** A `@Body() x: SomeDto | any` handler resolves
|
|
19
|
+
to a top-level `unknown`/`any` union arm; intersecting `(Dto | unknown) & { file }` collapses
|
|
20
|
+
it and wrongly tightens the type. The emitter now detects a permissive body and skips the
|
|
21
|
+
intersection, keeping the author's loose `@Body()` (the route is still flagged `multipart`).
|
|
22
|
+
|
|
23
|
+
## 0.13.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- a044e73: feat: typed `multipart/form-data` upload routes (`@UploadedFile()` / Multer interceptors).
|
|
28
|
+
|
|
29
|
+
The codegen now understands handlers that accept uploaded files, so multipart uploads
|
|
30
|
+
become first-class typed routes (`api.X({ body: { ...fields, file } })`) instead of
|
|
31
|
+
needing the `fetchRaw` escape hatch.
|
|
32
|
+
|
|
33
|
+
**core (`@dudousxd/nestjs-codegen`):**
|
|
34
|
+
|
|
35
|
+
- Discovery detects `@UploadedFile()` / `@UploadedFiles()` handlers and reads the HTTP
|
|
36
|
+
field name(s) + arity from the Multer interceptor in `@UseInterceptors(...)`:
|
|
37
|
+
- `FileInterceptor('file')` → `file: File | Blob`
|
|
38
|
+
- `FilesInterceptor('files')` → `files: Array<File | Blob>`
|
|
39
|
+
- `FileFieldsInterceptor([{ name: 'a' }, { name: 'b' }])` → `a: Array<File | Blob>; b: Array<File | Blob>`
|
|
40
|
+
- `AnyFilesInterceptor()` → flagged multipart (no statically known field names)
|
|
41
|
+
- The uploaded-file field(s) are merged into the route `body` as an intersection with the
|
|
42
|
+
`@Body` DTO (`SomeDto & { file: File | Blob }`), typed for the browser as `File | Blob`
|
|
43
|
+
(never the server-side `Express.Multer.File`).
|
|
44
|
+
- The route carries a new `multipart` flag, emitted into the generated client so the call
|
|
45
|
+
passes `multipart: true` to the fetcher.
|
|
46
|
+
|
|
47
|
+
**client (`@dudousxd/nestjs-client`):**
|
|
48
|
+
|
|
49
|
+
- `RequestOpts` gains `multipart?: boolean`. When set, the fetcher serializes the body
|
|
50
|
+
object to a `FormData` (scalars as strings, `Date` as ISO, `File`/`Blob` as file parts,
|
|
51
|
+
arrays as repeated parts) instead of JSON, letting the runtime set the multipart
|
|
52
|
+
boundary. `onUploadProgress` already rides the same path.
|
|
53
|
+
|
|
3
54
|
## 0.12.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/dist/cli/main.cjs
CHANGED
|
@@ -785,7 +785,15 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
|
|
|
785
785
|
const isFilterQuery = c.contractSource.filterSource === "query" && !!c.contractSource.filterFields?.length;
|
|
786
786
|
const query = queryRef ? queryRef.isArray ? `Array<${queryRef.name}>` : queryRef.name : isFilterQuery ? emitFilterQueryType(c) : c.contractSource.query ?? "never";
|
|
787
787
|
const bodyRef = c.contractSource.bodyRef;
|
|
788
|
-
|
|
788
|
+
let body = method === "GET" ? "never" : bodyRef ? bodyRef.isArray ? `Array<${bodyRef.name}>` : bodyRef.name : c.contractSource.body ?? "never";
|
|
789
|
+
const multipartBody = c.contractSource.multipartBody;
|
|
790
|
+
if (c.contractSource.multipart && multipartBody) {
|
|
791
|
+
if (body === "never") {
|
|
792
|
+
body = multipartBody;
|
|
793
|
+
} else if (!bodyAcceptsAnything(body)) {
|
|
794
|
+
body = `(${body}) & ${multipartBody}`;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
789
797
|
const response = buildResponseType(c, outDir, serialization);
|
|
790
798
|
const error = buildErrorType(c);
|
|
791
799
|
const params = buildParamsType(c.params);
|
|
@@ -804,6 +812,25 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
|
|
|
804
812
|
}
|
|
805
813
|
return lines;
|
|
806
814
|
}
|
|
815
|
+
function topLevelUnionArms(type) {
|
|
816
|
+
const arms = [];
|
|
817
|
+
let depth = 0;
|
|
818
|
+
let start = 0;
|
|
819
|
+
for (let i = 0; i < type.length; i++) {
|
|
820
|
+
const ch = type[i];
|
|
821
|
+
if (ch === "{" || ch === "[" || ch === "<" || ch === "(") depth++;
|
|
822
|
+
else if (ch === "}" || ch === "]" || ch === ">" || ch === ")") depth--;
|
|
823
|
+
else if (ch === "|" && depth === 0) {
|
|
824
|
+
arms.push(type.slice(start, i).trim());
|
|
825
|
+
start = i + 1;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
arms.push(type.slice(start).trim());
|
|
829
|
+
return arms;
|
|
830
|
+
}
|
|
831
|
+
function bodyAcceptsAnything(body) {
|
|
832
|
+
return topLevelUnionArms(body).some((arm) => arm === "unknown" || arm === "any");
|
|
833
|
+
}
|
|
807
834
|
function buildRequestModel(c) {
|
|
808
835
|
const m = c.method.toLowerCase();
|
|
809
836
|
const flat = JSON.stringify(c.name);
|
|
@@ -820,6 +847,7 @@ function buildRequestModel(c) {
|
|
|
820
847
|
const optsParts = [];
|
|
821
848
|
if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
|
|
822
849
|
if (hasBody) optsParts.push("body: input?.body");
|
|
850
|
+
if (hasBody && c.contractSource.multipart) optsParts.push("multipart: true");
|
|
823
851
|
const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
|
|
824
852
|
return {
|
|
825
853
|
routeName: c.name,
|
|
@@ -3700,6 +3728,55 @@ function extractParamsType(method, sourceFile, project) {
|
|
|
3700
3728
|
}
|
|
3701
3729
|
return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
|
|
3702
3730
|
}
|
|
3731
|
+
function extractUploadedFiles(method) {
|
|
3732
|
+
const FILE = "File | Blob";
|
|
3733
|
+
const entries = [];
|
|
3734
|
+
let multipart = false;
|
|
3735
|
+
const hasUploadedFileParam = method.getParameters().some(
|
|
3736
|
+
(p) => p.getDecorators().some((d) => {
|
|
3737
|
+
const name = d.getName();
|
|
3738
|
+
return name === "UploadedFile" || name === "UploadedFiles";
|
|
3739
|
+
})
|
|
3740
|
+
);
|
|
3741
|
+
for (const decorator of method.getDecorators()) {
|
|
3742
|
+
if (decorator.getName() !== "UseInterceptors") continue;
|
|
3743
|
+
for (const arg of decorator.getArguments()) {
|
|
3744
|
+
if (!import_ts_morph7.Node.isCallExpression(arg)) continue;
|
|
3745
|
+
const interceptor = arg.getExpression().getText();
|
|
3746
|
+
const callArgs = arg.getArguments();
|
|
3747
|
+
const firstArg2 = callArgs[0];
|
|
3748
|
+
if (interceptor === "FileInterceptor") {
|
|
3749
|
+
if (firstArg2 && import_ts_morph7.Node.isStringLiteral(firstArg2)) {
|
|
3750
|
+
entries.push(`${firstArg2.getLiteralValue()}: ${FILE}`);
|
|
3751
|
+
multipart = true;
|
|
3752
|
+
}
|
|
3753
|
+
} else if (interceptor === "FilesInterceptor") {
|
|
3754
|
+
if (firstArg2 && import_ts_morph7.Node.isStringLiteral(firstArg2)) {
|
|
3755
|
+
entries.push(`${firstArg2.getLiteralValue()}: Array<${FILE}>`);
|
|
3756
|
+
multipart = true;
|
|
3757
|
+
}
|
|
3758
|
+
} else if (interceptor === "FileFieldsInterceptor") {
|
|
3759
|
+
if (firstArg2 && import_ts_morph7.Node.isArrayLiteralExpression(firstArg2)) {
|
|
3760
|
+
for (const el of firstArg2.getElements()) {
|
|
3761
|
+
if (!import_ts_morph7.Node.isObjectLiteralExpression(el)) continue;
|
|
3762
|
+
const nameProp = el.getProperty("name");
|
|
3763
|
+
if (nameProp && import_ts_morph7.Node.isPropertyAssignment(nameProp)) {
|
|
3764
|
+
const init = nameProp.getInitializer();
|
|
3765
|
+
if (init && import_ts_morph7.Node.isStringLiteral(init)) {
|
|
3766
|
+
entries.push(`${init.getLiteralValue()}: Array<${FILE}>`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
multipart = true;
|
|
3771
|
+
}
|
|
3772
|
+
} else if (interceptor === "AnyFilesInterceptor") {
|
|
3773
|
+
multipart = true;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
if (hasUploadedFileParam) multipart = true;
|
|
3778
|
+
return { fields: entries.length > 0 ? entries.join("; ") : null, multipart };
|
|
3779
|
+
}
|
|
3703
3780
|
function extractResponseType(method, sourceFile, project) {
|
|
3704
3781
|
const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
|
|
3705
3782
|
if (apiResponseDecorator) {
|
|
@@ -3834,6 +3911,8 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3834
3911
|
let body = extractBodyType(method, sourceFile, project);
|
|
3835
3912
|
const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
|
|
3836
3913
|
const query = extractQueryType(method, sourceFile, project);
|
|
3914
|
+
const uploads = extractUploadedFiles(method);
|
|
3915
|
+
const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
|
|
3837
3916
|
const streamElement = detectStreamElement(method);
|
|
3838
3917
|
const isStream = streamElement !== null;
|
|
3839
3918
|
if (filterInfo && filterInfo.source === "body") {
|
|
@@ -3843,7 +3922,7 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3843
3922
|
const paramsType = extractParamsType(method, sourceFile, project);
|
|
3844
3923
|
const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
|
|
3845
3924
|
const errorInfo = extractErrorType(method, sourceFile, project);
|
|
3846
|
-
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
|
|
3925
|
+
if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart) {
|
|
3847
3926
|
return null;
|
|
3848
3927
|
}
|
|
3849
3928
|
let bodyRef = null;
|
|
@@ -3916,7 +3995,9 @@ function extractDtoContract(method, sourceFile, project) {
|
|
|
3916
3995
|
formWarnings,
|
|
3917
3996
|
bodySchema,
|
|
3918
3997
|
querySchema,
|
|
3919
|
-
stream: isStream
|
|
3998
|
+
stream: isStream,
|
|
3999
|
+
multipart: uploads.multipart,
|
|
4000
|
+
multipartBody
|
|
3920
4001
|
};
|
|
3921
4002
|
}
|
|
3922
4003
|
function resolveParamClass(method, decoratorName, sourceFile, project) {
|
|
@@ -4401,7 +4482,9 @@ function extractDtoRoute(args) {
|
|
|
4401
4482
|
formWarnings: dtoContract?.formWarnings ?? [],
|
|
4402
4483
|
bodySchema: dtoContract?.bodySchema ?? null,
|
|
4403
4484
|
querySchema: dtoContract?.querySchema ?? null,
|
|
4404
|
-
stream: dtoContract?.stream ?? false
|
|
4485
|
+
stream: dtoContract?.stream ?? false,
|
|
4486
|
+
multipart: dtoContract?.multipart ?? false,
|
|
4487
|
+
multipartBody: dtoContract?.multipartBody ?? null
|
|
4405
4488
|
}
|
|
4406
4489
|
});
|
|
4407
4490
|
}
|
|
@@ -4631,7 +4714,7 @@ async function watch(config, onChange, options = {}) {
|
|
|
4631
4714
|
}
|
|
4632
4715
|
|
|
4633
4716
|
// src/index.ts
|
|
4634
|
-
var VERSION = "0.
|
|
4717
|
+
var VERSION = "0.13.1";
|
|
4635
4718
|
|
|
4636
4719
|
// src/cli/codegen.ts
|
|
4637
4720
|
async function runCodegen(opts = {}) {
|