@dudousxd/nestjs-codegen 0.12.0 → 0.13.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,36 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a044e73: feat: typed `multipart/form-data` upload routes (`@UploadedFile()` / Multer interceptors).
8
+
9
+ The codegen now understands handlers that accept uploaded files, so multipart uploads
10
+ become first-class typed routes (`api.X({ body: { ...fields, file } })`) instead of
11
+ needing the `fetchRaw` escape hatch.
12
+
13
+ **core (`@dudousxd/nestjs-codegen`):**
14
+
15
+ - Discovery detects `@UploadedFile()` / `@UploadedFiles()` handlers and reads the HTTP
16
+ field name(s) + arity from the Multer interceptor in `@UseInterceptors(...)`:
17
+ - `FileInterceptor('file')` → `file: File | Blob`
18
+ - `FilesInterceptor('files')` → `files: Array<File | Blob>`
19
+ - `FileFieldsInterceptor([{ name: 'a' }, { name: 'b' }])` → `a: Array<File | Blob>; b: Array<File | Blob>`
20
+ - `AnyFilesInterceptor()` → flagged multipart (no statically known field names)
21
+ - The uploaded-file field(s) are merged into the route `body` as an intersection with the
22
+ `@Body` DTO (`SomeDto & { file: File | Blob }`), typed for the browser as `File | Blob`
23
+ (never the server-side `Express.Multer.File`).
24
+ - The route carries a new `multipart` flag, emitted into the generated client so the call
25
+ passes `multipart: true` to the fetcher.
26
+
27
+ **client (`@dudousxd/nestjs-client`):**
28
+
29
+ - `RequestOpts` gains `multipart?: boolean`. When set, the fetcher serializes the body
30
+ object to a `FormData` (scalars as strings, `Date` as ISO, `File`/`Blob` as file parts,
31
+ arrays as repeated parts) instead of JSON, letting the runtime set the multipart
32
+ boundary. `onUploadProgress` already rides the same path.
33
+
3
34
  ## 0.12.0
4
35
 
5
36
  ### Minor Changes
package/dist/cli/main.cjs CHANGED
@@ -820,6 +820,7 @@ function buildRequestModel(c) {
820
820
  const optsParts = [];
821
821
  if (hasQuery) optsParts.push("query: input?.query as Record<string, unknown> | undefined");
822
822
  if (hasBody) optsParts.push("body: input?.body");
823
+ if (hasBody && c.contractSource.multipart) optsParts.push("multipart: true");
823
824
  const optsExpr = optsParts.length ? `{ ${optsParts.join(", ")} }` : "{}";
824
825
  return {
825
826
  routeName: c.name,
@@ -3700,6 +3701,55 @@ function extractParamsType(method, sourceFile, project) {
3700
3701
  }
3701
3702
  return entries.length > 0 ? `{ ${entries.join("; ")} }` : null;
3702
3703
  }
3704
+ function extractUploadedFiles(method) {
3705
+ const FILE = "File | Blob";
3706
+ const entries = [];
3707
+ let multipart = false;
3708
+ const hasUploadedFileParam = method.getParameters().some(
3709
+ (p) => p.getDecorators().some((d) => {
3710
+ const name = d.getName();
3711
+ return name === "UploadedFile" || name === "UploadedFiles";
3712
+ })
3713
+ );
3714
+ for (const decorator of method.getDecorators()) {
3715
+ if (decorator.getName() !== "UseInterceptors") continue;
3716
+ for (const arg of decorator.getArguments()) {
3717
+ if (!import_ts_morph7.Node.isCallExpression(arg)) continue;
3718
+ const interceptor = arg.getExpression().getText();
3719
+ const callArgs = arg.getArguments();
3720
+ const firstArg2 = callArgs[0];
3721
+ if (interceptor === "FileInterceptor") {
3722
+ if (firstArg2 && import_ts_morph7.Node.isStringLiteral(firstArg2)) {
3723
+ entries.push(`${firstArg2.getLiteralValue()}: ${FILE}`);
3724
+ multipart = true;
3725
+ }
3726
+ } else if (interceptor === "FilesInterceptor") {
3727
+ if (firstArg2 && import_ts_morph7.Node.isStringLiteral(firstArg2)) {
3728
+ entries.push(`${firstArg2.getLiteralValue()}: Array<${FILE}>`);
3729
+ multipart = true;
3730
+ }
3731
+ } else if (interceptor === "FileFieldsInterceptor") {
3732
+ if (firstArg2 && import_ts_morph7.Node.isArrayLiteralExpression(firstArg2)) {
3733
+ for (const el of firstArg2.getElements()) {
3734
+ if (!import_ts_morph7.Node.isObjectLiteralExpression(el)) continue;
3735
+ const nameProp = el.getProperty("name");
3736
+ if (nameProp && import_ts_morph7.Node.isPropertyAssignment(nameProp)) {
3737
+ const init = nameProp.getInitializer();
3738
+ if (init && import_ts_morph7.Node.isStringLiteral(init)) {
3739
+ entries.push(`${init.getLiteralValue()}: Array<${FILE}>`);
3740
+ }
3741
+ }
3742
+ }
3743
+ multipart = true;
3744
+ }
3745
+ } else if (interceptor === "AnyFilesInterceptor") {
3746
+ multipart = true;
3747
+ }
3748
+ }
3749
+ }
3750
+ if (hasUploadedFileParam) multipart = true;
3751
+ return { fields: entries.length > 0 ? entries.join("; ") : null, multipart };
3752
+ }
3703
3753
  function extractResponseType(method, sourceFile, project) {
3704
3754
  const apiResponseDecorator = method.getDecorators().find((d) => d.getName() === "ApiResponse" && (apiResponseStatus(d) ?? 0) < 400);
3705
3755
  if (apiResponseDecorator) {
@@ -3834,6 +3884,11 @@ function extractDtoContract(method, sourceFile, project) {
3834
3884
  let body = extractBodyType(method, sourceFile, project);
3835
3885
  const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
3836
3886
  const query = extractQueryType(method, sourceFile, project);
3887
+ const uploads = extractUploadedFiles(method);
3888
+ if (uploads.fields) {
3889
+ const fileObject = `{ ${uploads.fields} }`;
3890
+ body = body ? `(${body}) & ${fileObject}` : fileObject;
3891
+ }
3837
3892
  const streamElement = detectStreamElement(method);
3838
3893
  const isStream = streamElement !== null;
3839
3894
  if (filterInfo && filterInfo.source === "body") {
@@ -3843,7 +3898,7 @@ function extractDtoContract(method, sourceFile, project) {
3843
3898
  const paramsType = extractParamsType(method, sourceFile, project);
3844
3899
  const response = isStream ? resolveTypeNodeToString(streamElement, sourceFile, project, 3) : extractResponseType(method, sourceFile, project);
3845
3900
  const errorInfo = extractErrorType(method, sourceFile, project);
3846
- if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream) {
3901
+ if (body === null && query === null && paramsType === null && response === "unknown" && errorInfo === null && filterInfo === null && !isStream && !uploads.multipart) {
3847
3902
  return null;
3848
3903
  }
3849
3904
  let bodyRef = null;
@@ -3916,7 +3971,8 @@ function extractDtoContract(method, sourceFile, project) {
3916
3971
  formWarnings,
3917
3972
  bodySchema,
3918
3973
  querySchema,
3919
- stream: isStream
3974
+ stream: isStream,
3975
+ multipart: uploads.multipart
3920
3976
  };
3921
3977
  }
3922
3978
  function resolveParamClass(method, decoratorName, sourceFile, project) {
@@ -4401,7 +4457,8 @@ function extractDtoRoute(args) {
4401
4457
  formWarnings: dtoContract?.formWarnings ?? [],
4402
4458
  bodySchema: dtoContract?.bodySchema ?? null,
4403
4459
  querySchema: dtoContract?.querySchema ?? null,
4404
- stream: dtoContract?.stream ?? false
4460
+ stream: dtoContract?.stream ?? false,
4461
+ multipart: dtoContract?.multipart ?? false
4405
4462
  }
4406
4463
  });
4407
4464
  }
@@ -4631,7 +4688,7 @@ async function watch(config, onChange, options = {}) {
4631
4688
  }
4632
4689
 
4633
4690
  // src/index.ts
4634
- var VERSION = "0.12.0";
4691
+ var VERSION = "0.13.0";
4635
4692
 
4636
4693
  // src/cli/codegen.ts
4637
4694
  async function runCodegen(opts = {}) {