@nestjs/platform-fastify 12.0.3 → 12.1.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.
Files changed (58) hide show
  1. package/Readme.md +4 -0
  2. package/adapters/fastify-adapter.d.ts +48 -5
  3. package/adapters/fastify-adapter.js +137 -9
  4. package/index.d.ts +1 -0
  5. package/index.js +1 -0
  6. package/interfaces/external/fastify-multipart-options.interface.d.ts +53 -0
  7. package/interfaces/external/fastify-multipart-options.interface.js +1 -0
  8. package/interfaces/external/index.d.ts +1 -0
  9. package/interfaces/external/index.js +1 -0
  10. package/multipart/files.constants.d.ts +1 -0
  11. package/multipart/files.constants.js +1 -0
  12. package/multipart/index.d.ts +4 -0
  13. package/multipart/index.js +4 -0
  14. package/multipart/interceptors/any-files.interceptor.d.ts +12 -0
  15. package/multipart/interceptors/any-files.interceptor.js +13 -0
  16. package/multipart/interceptors/file-fields.interceptor.d.ts +13 -0
  17. package/multipart/interceptors/file-fields.interceptor.js +14 -0
  18. package/multipart/interceptors/file-stream.interceptor.d.ts +31 -0
  19. package/multipart/interceptors/file-stream.interceptor.js +32 -0
  20. package/multipart/interceptors/file.interceptor.d.ts +17 -0
  21. package/multipart/interceptors/file.interceptor.js +18 -0
  22. package/multipart/interceptors/files.interceptor.d.ts +14 -0
  23. package/multipart/interceptors/files.interceptor.js +15 -0
  24. package/multipart/interceptors/index.d.ts +6 -0
  25. package/multipart/interceptors/index.js +6 -0
  26. package/multipart/interceptors/no-files.interceptor.d.ts +12 -0
  27. package/multipart/interceptors/no-files.interceptor.js +13 -0
  28. package/multipart/interfaces/files-upload-module.interface.d.ts +18 -0
  29. package/multipart/interfaces/files-upload-module.interface.js +1 -0
  30. package/multipart/interfaces/index.d.ts +3 -0
  31. package/multipart/interfaces/index.js +3 -0
  32. package/multipart/interfaces/multipart-file.interface.d.ts +70 -0
  33. package/multipart/interfaces/multipart-file.interface.js +1 -0
  34. package/multipart/interfaces/multipart-options.interface.d.ts +77 -0
  35. package/multipart/interfaces/multipart-options.interface.js +1 -0
  36. package/multipart/multipart/append-field.util.d.ts +6 -0
  37. package/multipart/multipart/append-field.util.js +112 -0
  38. package/multipart/multipart/multipart-interceptor.factory.d.ts +13 -0
  39. package/multipart/multipart/multipart-interceptor.factory.js +130 -0
  40. package/multipart/multipart/multipart.constants.d.ts +55 -0
  41. package/multipart/multipart/multipart.constants.js +74 -0
  42. package/multipart/multipart/multipart.error.d.ts +10 -0
  43. package/multipart/multipart/multipart.error.js +15 -0
  44. package/multipart/multipart/multipart.parser.d.ts +39 -0
  45. package/multipart/multipart/multipart.parser.js +369 -0
  46. package/multipart/multipart/multipart.utils.d.ts +28 -0
  47. package/multipart/multipart/multipart.utils.js +54 -0
  48. package/multipart/multipart.constants.d.ts +1 -0
  49. package/multipart/multipart.constants.js +1 -0
  50. package/multipart/multipart.module.d.ts +18 -0
  51. package/multipart/multipart.module.js +75 -0
  52. package/multipart/storage/disk.storage.d.ts +24 -0
  53. package/multipart/storage/disk.storage.js +58 -0
  54. package/multipart/storage/index.d.ts +2 -0
  55. package/multipart/storage/index.js +2 -0
  56. package/multipart/storage/memory.storage.d.ts +8 -0
  57. package/multipart/storage/memory.storage.js +23 -0
  58. package/package.json +8 -3
@@ -0,0 +1,13 @@
1
+ import { createMultipartInterceptor } from '../multipart/multipart-interceptor.factory.js';
2
+ /**
3
+ * Accepts text fields only (exposed in `req.body`); any file fails the
4
+ * request with a 400. The Fastify counterpart of platform-express's
5
+ * `NoFilesInterceptor`.
6
+ *
7
+ * @param localOptions options merged over the `MultipartModule` defaults
8
+ *
9
+ * @publicApi
10
+ */
11
+ export function NoFilesInterceptor(localOptions) {
12
+ return createMultipartInterceptor({ fields: [], strategy: 'NONE' }, localOptions);
13
+ }
@@ -0,0 +1,18 @@
1
+ import type { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common';
2
+ import type { MultipartOptions } from './multipart-options.interface.js';
3
+ export type MultipartModuleOptions = MultipartOptions;
4
+ /**
5
+ * @publicApi
6
+ */
7
+ export interface MultipartOptionsFactory {
8
+ createMultipartOptions(): Promise<MultipartModuleOptions> | MultipartModuleOptions;
9
+ }
10
+ /**
11
+ * @publicApi
12
+ */
13
+ export interface MultipartModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
14
+ useExisting?: Type<MultipartOptionsFactory>;
15
+ useClass?: Type<MultipartOptionsFactory>;
16
+ useFactory?: (...args: any[]) => Promise<MultipartModuleOptions> | MultipartModuleOptions;
17
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
18
+ }
@@ -0,0 +1,3 @@
1
+ export * from './files-upload-module.interface.js';
2
+ export * from './multipart-file.interface.js';
3
+ export * from './multipart-options.interface.js';
@@ -0,0 +1,3 @@
1
+ export * from './files-upload-module.interface.js';
2
+ export * from './multipart-file.interface.js';
3
+ export * from './multipart-options.interface.js';
@@ -0,0 +1,70 @@
1
+ import type { Readable } from 'stream';
2
+ /**
3
+ * A stored upload, in the shape multer produces (`Express.Multer.File`), so
4
+ * `@UploadedFile()`, `ParseFilePipe` and the built-in file validators work
5
+ * unchanged. Memory storage sets `buffer`; disk storage sets `destination`,
6
+ * `filename` and `path`.
7
+ *
8
+ * @publicApi
9
+ */
10
+ export interface UploadedMultipartFile {
11
+ /** Field name specified in the form */
12
+ fieldname: string;
13
+ /** Name of the file on the user's computer */
14
+ originalname: string;
15
+ /** Encoding type of the file */
16
+ encoding: string;
17
+ /** Mime type of the file */
18
+ mimetype: string;
19
+ /** Size of the file in bytes */
20
+ size: number;
21
+ /** A Buffer of the entire file (memory storage) */
22
+ buffer?: Buffer;
23
+ /** The folder to which the file has been saved (disk storage) */
24
+ destination?: string;
25
+ /** The name of the file within the destination (disk storage) */
26
+ filename?: string;
27
+ /** Location of the uploaded file (disk storage) */
28
+ path?: string;
29
+ }
30
+ /**
31
+ * What a storage engine and `fileFilter` see before the file is stored.
32
+ *
33
+ * @publicApi
34
+ */
35
+ export interface IncomingMultipartFile {
36
+ /** Field name specified in the form */
37
+ fieldname: string;
38
+ /** Name of the file on the user's computer */
39
+ originalname: string;
40
+ /** Encoding type of the file */
41
+ encoding: string;
42
+ /** Mime type of the file */
43
+ mimetype: string;
44
+ /**
45
+ * The file contents. Non-enumerable; meant to be consumed by
46
+ * `MultipartStorageEngine._handleFile` only.
47
+ */
48
+ readonly stream: Readable;
49
+ }
50
+ /**
51
+ * The file `FileStreamInterceptor` hands to the route handler. Its size is
52
+ * not known up front.
53
+ *
54
+ * @publicApi
55
+ */
56
+ export interface MultipartFileStream {
57
+ /** Field name specified in the form */
58
+ fieldname: string;
59
+ /** Name of the file on the user's computer */
60
+ originalname: string;
61
+ /** Encoding type of the file */
62
+ encoding: string;
63
+ /** Mime type of the file */
64
+ mimetype: string;
65
+ /**
66
+ * The file contents. Errors with a `PayloadTooLargeException` once
67
+ * `limits.fileSize` is exceeded.
68
+ */
69
+ stream: Readable;
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import type { IncomingMultipartFile, UploadedMultipartFile } from './multipart-file.interface.js';
2
+ /**
3
+ * multer's storage engine contract (`_handleFile` / `_removeFile`), kept
4
+ * as-is so existing multer storage engines can be passed as `storage`.
5
+ *
6
+ * @publicApi
7
+ */
8
+ export interface MultipartStorageEngine {
9
+ _handleFile(req: any, file: IncomingMultipartFile, callback: (error?: any, info?: Partial<UploadedMultipartFile>) => void): void;
10
+ _removeFile(req: any, file: UploadedMultipartFile, callback: (error: Error | null) => void): void;
11
+ }
12
+ /**
13
+ * Same keys as platform-express's `MulterLimits`.
14
+ *
15
+ * Limits that are not set fall back to the options `@fastify/multipart` was
16
+ * registered with, and then to its own defaults. Unlike multer (which
17
+ * defaults `fileSize` and `parts` to `Infinity`), `@fastify/multipart` caps
18
+ * `fileSize` at the Fastify instance's `bodyLimit` (1 MiB unless configured)
19
+ * and `parts` at 1000.
20
+ *
21
+ * @publicApi
22
+ */
23
+ export interface MultipartLimits {
24
+ /**
25
+ * Max field name size in bytes (Default: unlimited). Unlike the other
26
+ * limits, it does not fall back to the options `@fastify/multipart` was
27
+ * registered with.
28
+ */
29
+ fieldNameSize?: number;
30
+ /** Max field value size (Default: 1MB) */
31
+ fieldSize?: number;
32
+ /** Max number of non-file fields (Default: Infinity) */
33
+ fields?: number;
34
+ /** For multipart forms, the max file size in bytes (Default: the Fastify `bodyLimit`, 1 MiB) */
35
+ fileSize?: number;
36
+ /** For multipart forms, the max number of file fields (Default: Infinity) */
37
+ files?: number;
38
+ /** For multipart forms, the max number of parts (fields + files) (Default: 1000) */
39
+ parts?: number;
40
+ /** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
41
+ headerPairs?: number;
42
+ }
43
+ /**
44
+ * Same keys and semantics as platform-express's `MulterOptions`, so option
45
+ * objects can be moved between adapters as-is.
46
+ *
47
+ * @publicApi
48
+ */
49
+ export interface MultipartOptions {
50
+ /** Where to store the files (disk storage). Ignored when `storage` is set. */
51
+ dest?: string;
52
+ /** The storage engine to use for uploaded files (Default: memory storage). */
53
+ storage?: MultipartStorageEngine;
54
+ /**
55
+ * An object specifying the size limits, or a function of the request that
56
+ * returns one. Merged over the limits `@fastify/multipart` was registered with.
57
+ */
58
+ limits?: MultipartLimits | ((req: any) => MultipartLimits);
59
+ /** Keep the full path of files instead of just the base name (Default: false) */
60
+ preservePath?: boolean;
61
+ /** Default character set for part header values (e.g. filename) (Default: 'latin1') */
62
+ defParamCharset?: string;
63
+ /**
64
+ * Decides which files are stored. Call `callback(null, false)` to skip a
65
+ * file, or `callback(error, false)` to fail the request with `error`.
66
+ */
67
+ fileFilter?(req: any, file: IncomingMultipartFile, callback: (error: Error | null, acceptFile: boolean) => void): void;
68
+ }
69
+ /**
70
+ * @publicApi
71
+ */
72
+ export interface MultipartField {
73
+ /** The field name. */
74
+ name: string;
75
+ /** Optional maximum number of files per field to accept. */
76
+ maxCount?: number;
77
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Appends a text field to `store` using multer's bracket notation.
3
+ * Returns `false` (leaving `store` untouched) for names that would pollute a
4
+ * prototype.
5
+ */
6
+ export declare function appendField(store: object, key: string, value: unknown): boolean;
@@ -0,0 +1,112 @@
1
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
2
+ function parsePath(key) {
3
+ const failure = () => [{ type: 'object', key, last: true }];
4
+ const firstKey = /^[^[]*/.exec(key)[0];
5
+ if (!firstKey) {
6
+ return failure();
7
+ }
8
+ let pos = firstKey.length;
9
+ let tail = { type: 'object', key: firstKey };
10
+ const steps = [tail];
11
+ while (pos < key.length) {
12
+ if (key[pos] === '[' && key[pos + 1] === ']') {
13
+ pos += 2;
14
+ tail.append = true;
15
+ if (pos !== key.length) {
16
+ return failure();
17
+ }
18
+ continue;
19
+ }
20
+ const rest = key.substring(pos);
21
+ let match = /^\[(\d+)\]/.exec(rest);
22
+ if (match) {
23
+ pos += match[0].length;
24
+ tail.nextType = 'array';
25
+ tail = { type: 'array', key: parseInt(match[1], 10) };
26
+ steps.push(tail);
27
+ continue;
28
+ }
29
+ match = /^\[([^\]]+)\]/.exec(rest);
30
+ if (match) {
31
+ pos += match[0].length;
32
+ tail.nextType = 'object';
33
+ tail = { type: 'object', key: match[1] };
34
+ steps.push(tail);
35
+ continue;
36
+ }
37
+ return failure();
38
+ }
39
+ tail.last = true;
40
+ return steps;
41
+ }
42
+ function valueType(value) {
43
+ if (value === undefined) {
44
+ return 'undefined';
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return 'array';
48
+ }
49
+ if (typeof value === 'object') {
50
+ return 'object';
51
+ }
52
+ return 'scalar';
53
+ }
54
+ function setLastValue(ctx, step, current, value) {
55
+ switch (valueType(current)) {
56
+ case 'undefined':
57
+ ctx[step.key] = step.append ? [value] : value;
58
+ break;
59
+ case 'array':
60
+ ctx[step.key].push(value);
61
+ break;
62
+ case 'object':
63
+ return setLastValue(current, { type: 'object', key: '', last: true }, current[''], value);
64
+ case 'scalar':
65
+ ctx[step.key] = [ctx[step.key], value];
66
+ break;
67
+ }
68
+ return ctx;
69
+ }
70
+ function setValue(ctx, step, current, value) {
71
+ if (step.last) {
72
+ return setLastValue(ctx, step, current, value);
73
+ }
74
+ let obj;
75
+ switch (valueType(current)) {
76
+ case 'undefined':
77
+ ctx[step.key] = step.nextType === 'array' ? [] : Object.create(null);
78
+ return ctx[step.key];
79
+ case 'object':
80
+ return current;
81
+ case 'array':
82
+ if (step.nextType === 'array') {
83
+ return current;
84
+ }
85
+ obj = Object.create(null);
86
+ ctx[step.key] = obj;
87
+ current.forEach((item, i) => {
88
+ if (item !== undefined) {
89
+ obj['' + i] = item;
90
+ }
91
+ });
92
+ return obj;
93
+ case 'scalar':
94
+ obj = Object.create(null);
95
+ obj[''] = current;
96
+ ctx[step.key] = obj;
97
+ return obj;
98
+ }
99
+ }
100
+ /**
101
+ * Appends a text field to `store` using multer's bracket notation.
102
+ * Returns `false` (leaving `store` untouched) for names that would pollute a
103
+ * prototype.
104
+ */
105
+ export function appendField(store, key, value) {
106
+ const steps = parsePath(key);
107
+ if (steps.some(step => FORBIDDEN_KEYS.has(String(step.key)))) {
108
+ return false;
109
+ }
110
+ steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
111
+ return true;
112
+ }
@@ -0,0 +1,13 @@
1
+ import { type NestInterceptor, type Type } from '@nestjs/common';
2
+ import type { MultipartOptions } from '../interfaces/index.js';
3
+ import { type UploadSpec } from './multipart.parser.js';
4
+ /**
5
+ * Creates an interceptor that parses the multipart body with multer's
6
+ * semantics, as described by `spec`.
7
+ */
8
+ export declare function createMultipartInterceptor(spec: UploadSpec, localOptions?: MultipartOptions): Type<NestInterceptor>;
9
+ /**
10
+ * Creates an interceptor that hands the route handler the stream of a single
11
+ * file in `fieldName`.
12
+ */
13
+ export declare function createMultipartStreamInterceptor(fieldName: string, localOptions?: MultipartOptions): Type<NestInterceptor>;
@@ -0,0 +1,130 @@
1
+ import { __decorate, __metadata, __param } from "tslib";
2
+ import { Inject, mixin, Optional, StreamableFile, } from '@nestjs/common';
3
+ import { HttpAdapterHost } from '@nestjs/core';
4
+ import { catchError, concatMap, from, throwError } from 'rxjs';
5
+ import { MULTIPART_MODULE_OPTIONS } from '../files.constants.js';
6
+ import { MISSING_PLUGIN_MESSAGE, PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE, WRONG_ADAPTER_MESSAGE, } from './multipart.constants.js';
7
+ import { drainRequest, processMultipart, processStream, resolveStorage, validateLimits, } from './multipart.parser.js';
8
+ import { mergeMultipartOptions } from './multipart.utils.js';
9
+ const verifiedInstances = new WeakSet();
10
+ /**
11
+ * Asks the FastifyAdapter to register the `@fastify/multipart` plugin (it
12
+ * skips that when the plugin is registered already, or when its
13
+ * `multipart` option is `false`), and fails the application's startup with
14
+ * an actionable message if the plugin still is not registered once every
15
+ * plugin has loaded. Without the plugin, Fastify answers every multipart
16
+ * request with a bare 415 before any interceptor runs.
17
+ *
18
+ * This runs from `onModuleInit`, before Fastify's `ready()`: the plugin
19
+ * registered here loads with the others, before the server starts, and
20
+ * the check is deferred to an `onReady` hook.
21
+ */
22
+ function setUpHttpAdapter(adapterHost) {
23
+ const httpAdapter = adapterHost?.httpAdapter;
24
+ if (!httpAdapter) {
25
+ return;
26
+ }
27
+ if (httpAdapter.getType() !== 'fastify') {
28
+ throw new Error(WRONG_ADAPTER_MESSAGE);
29
+ }
30
+ const instance = httpAdapter.getInstance();
31
+ if (verifiedInstances.has(instance)) {
32
+ return;
33
+ }
34
+ const isRegistered = () => instance.hasRequestDecorator('isMultipart');
35
+ try {
36
+ httpAdapter.useMultipart?.();
37
+ instance.addHook('onReady', async () => {
38
+ if (!isRegistered()) {
39
+ throw new Error(MISSING_PLUGIN_MESSAGE);
40
+ }
41
+ });
42
+ }
43
+ catch {
44
+ // The instance has already started (e.g. the interceptor was created
45
+ // by a lazy-loaded module): no plugin can be registered any more, and
46
+ // every plugin is loaded.
47
+ if (!isRegistered()) {
48
+ throw new Error(PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE);
49
+ }
50
+ }
51
+ verifiedInstances.add(instance);
52
+ }
53
+ class MultipartInterceptorHost {
54
+ adapterHost;
55
+ options;
56
+ constructor(moduleOptions = {}, adapterHost, localOptions) {
57
+ this.adapterHost = adapterHost;
58
+ this.options = mergeMultipartOptions(moduleOptions, localOptions);
59
+ if (typeof this.options.limits !== 'function') {
60
+ validateLimits(this.options.limits);
61
+ }
62
+ }
63
+ onModuleInit() {
64
+ setUpHttpAdapter(this.adapterHost);
65
+ }
66
+ }
67
+ /**
68
+ * Creates an interceptor that parses the multipart body with multer's
69
+ * semantics, as described by `spec`.
70
+ */
71
+ export function createMultipartInterceptor(spec, localOptions) {
72
+ let MixinInterceptor = class MixinInterceptor extends MultipartInterceptorHost {
73
+ // Resolved once per interceptor, as multer does, so that a `dest`
74
+ // folder is created at bootstrap.
75
+ storage;
76
+ constructor(options = {}, adapterHost) {
77
+ super(options, adapterHost, localOptions);
78
+ this.storage = resolveStorage(this.options);
79
+ }
80
+ async intercept(context, next) {
81
+ const req = context.switchToHttp().getRequest();
82
+ await processMultipart(req, spec, this.options, this.storage);
83
+ return next.handle();
84
+ }
85
+ };
86
+ MixinInterceptor = __decorate([
87
+ __param(0, Optional()),
88
+ __param(0, Inject(MULTIPART_MODULE_OPTIONS)),
89
+ __param(1, Optional()),
90
+ __param(1, Inject(HttpAdapterHost)),
91
+ __metadata("design:paramtypes", [Object, HttpAdapterHost])
92
+ ], MixinInterceptor);
93
+ return mixin(MixinInterceptor);
94
+ }
95
+ /**
96
+ * Creates an interceptor that hands the route handler the stream of a single
97
+ * file in `fieldName`.
98
+ */
99
+ export function createMultipartStreamInterceptor(fieldName, localOptions) {
100
+ let MixinInterceptor = class MixinInterceptor extends MultipartInterceptorHost {
101
+ constructor(options = {}, adapterHost) {
102
+ super(options, adapterHost, localOptions);
103
+ }
104
+ async intercept(context, next) {
105
+ const ctx = context.switchToHttp();
106
+ const req = ctx.getRequest();
107
+ const reply = ctx.getResponse();
108
+ await processStream(req, fieldName, this.options);
109
+ // Whatever the handler leaves unread is discarded before the response
110
+ // is sent, as multer does: responding while the client is still
111
+ // uploading makes Node.js close the connection under it (EPIPE /
112
+ // ECONNRESET on the client) when it is not kept alive, and leaves a
113
+ // kept-alive connection with an unread body. A StreamableFile
114
+ // response is the exception, as it may be the upload itself, which
115
+ // only drains while the response is sent; what it leaves unread is
116
+ // discarded once the response is out.
117
+ reply.raw.once('close', () => void drainRequest(req.raw));
118
+ const drainBefore = (result) => from(drainRequest(req.raw).then(() => result));
119
+ return next.handle().pipe(concatMap(result => result instanceof StreamableFile ? [result] : drainBefore(result)), catchError(err => drainBefore(undefined).pipe(concatMap(() => throwError(() => err)))));
120
+ }
121
+ };
122
+ MixinInterceptor = __decorate([
123
+ __param(0, Optional()),
124
+ __param(0, Inject(MULTIPART_MODULE_OPTIONS)),
125
+ __param(1, Optional()),
126
+ __param(1, Inject(HttpAdapterHost)),
127
+ __metadata("design:paramtypes", [Object, HttpAdapterHost])
128
+ ], MixinInterceptor);
129
+ return mixin(MixinInterceptor);
130
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Same codes and messages as multer's `MulterError`, copied from
3
+ * `@nestjs/platform-express` (`multer/multer/multer.constants.ts`) so that
4
+ * clients see identical error responses on both adapters without this
5
+ * package depending on platform-express.
6
+ */
7
+ export declare const multerExceptions: {
8
+ readonly LIMIT_PART_COUNT: "Too many parts";
9
+ readonly LIMIT_FILE_SIZE: "File too large";
10
+ readonly LIMIT_FILE_COUNT: "Too many files";
11
+ readonly LIMIT_FIELD_KEY: "Field name too long";
12
+ readonly LIMIT_FIELD_VALUE: "Field value too long";
13
+ readonly LIMIT_FIELD_COUNT: "Too many fields";
14
+ readonly LIMIT_UNEXPECTED_FILE: "Unexpected file field";
15
+ readonly MISSING_FIELD_NAME: "Field name missing";
16
+ readonly LIMIT_FIELD_NESTING: "Field name nesting too deep";
17
+ readonly INVALID_FIELD_NAME: "Invalid field name";
18
+ };
19
+ export type MultipartErrorCode = keyof typeof multerExceptions;
20
+ /**
21
+ * The messages platform-express reports for busboy 1.x parser errors
22
+ * (prefixed with "Multipart: "), which is what multer bundles.
23
+ */
24
+ export declare const busboyExceptions: {
25
+ readonly MULTIPART_BOUNDARY_NOT_FOUND: "Multipart: Boundary not found";
26
+ readonly MULTIPART_UNEXPECTED_END_OF_FORM: "Multipart: Unexpected end of form";
27
+ readonly MULTIPART_UNEXPECTED_END_OF_FILE: "Multipart: Unexpected end of file";
28
+ };
29
+ /**
30
+ * `@fastify/busboy` raises plain `Error`s without a `code`, so these are the
31
+ * only messages matched verbatim. Everything `@fastify/multipart` raises is
32
+ * matched by its `code` instead (see `fastifyMultipartErrorCodes`).
33
+ *
34
+ * @see https://github.com/fastify/busboy/blob/main/deps/dicer/lib/Dicer.js
35
+ */
36
+ export declare const fastifyBusboyMessages: {
37
+ readonly BOUNDARY_NOT_FOUND: "Multipart: Boundary not found";
38
+ readonly UNEXPECTED_END_OF_FORM: "Unexpected end of multipart data";
39
+ readonly PART_TERMINATED_EARLY: "terminated early due to unexpected end";
40
+ };
41
+ /**
42
+ * `@fastify/multipart` error codes (created with `@fastify/error`) mapped to
43
+ * the multer code that describes the same condition.
44
+ *
45
+ * @see https://github.com/fastify/fastify-multipart/blob/main/index.js
46
+ */
47
+ export declare const fastifyMultipartErrorCodes: Record<string, MultipartErrorCode>;
48
+ /**
49
+ * `@fastify/multipart` error codes that already describe a client error and
50
+ * are reported as a 400 with their own message.
51
+ */
52
+ export declare const fastifyMultipartBadRequestCodes: string[];
53
+ export declare const MISSING_PLUGIN_MESSAGE: string;
54
+ export declare const PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE: string;
55
+ export declare const WRONG_ADAPTER_MESSAGE: string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Same codes and messages as multer's `MulterError`, copied from
3
+ * `@nestjs/platform-express` (`multer/multer/multer.constants.ts`) so that
4
+ * clients see identical error responses on both adapters without this
5
+ * package depending on platform-express.
6
+ */
7
+ export const multerExceptions = {
8
+ // from https://github.com/expressjs/multer/blob/master/lib/multer-error.js
9
+ LIMIT_PART_COUNT: 'Too many parts',
10
+ LIMIT_FILE_SIZE: 'File too large',
11
+ LIMIT_FILE_COUNT: 'Too many files',
12
+ LIMIT_FIELD_KEY: 'Field name too long',
13
+ LIMIT_FIELD_VALUE: 'Field value too long',
14
+ LIMIT_FIELD_COUNT: 'Too many fields',
15
+ LIMIT_UNEXPECTED_FILE: 'Unexpected file field',
16
+ MISSING_FIELD_NAME: 'Field name missing',
17
+ LIMIT_FIELD_NESTING: 'Field name nesting too deep',
18
+ // A multer 2.4 code that platform-express's table lacks. Raised here for
19
+ // field names that would pollute a prototype, which multer accepts.
20
+ INVALID_FIELD_NAME: 'Invalid field name',
21
+ };
22
+ /**
23
+ * The messages platform-express reports for busboy 1.x parser errors
24
+ * (prefixed with "Multipart: "), which is what multer bundles.
25
+ */
26
+ export const busboyExceptions = {
27
+ MULTIPART_BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
28
+ MULTIPART_UNEXPECTED_END_OF_FORM: 'Multipart: Unexpected end of form',
29
+ MULTIPART_UNEXPECTED_END_OF_FILE: 'Multipart: Unexpected end of file',
30
+ };
31
+ /**
32
+ * `@fastify/busboy` raises plain `Error`s without a `code`, so these are the
33
+ * only messages matched verbatim. Everything `@fastify/multipart` raises is
34
+ * matched by its `code` instead (see `fastifyMultipartErrorCodes`).
35
+ *
36
+ * @see https://github.com/fastify/busboy/blob/main/deps/dicer/lib/Dicer.js
37
+ */
38
+ export const fastifyBusboyMessages = {
39
+ BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
40
+ // Emitted on the parser when the body ends early...
41
+ UNEXPECTED_END_OF_FORM: 'Unexpected end of multipart data',
42
+ // ...and on the file stream that was being read at that moment. Both are
43
+ // reported as "Unexpected end of form", as multer does for a truncated body.
44
+ PART_TERMINATED_EARLY: 'terminated early due to unexpected end',
45
+ };
46
+ /**
47
+ * `@fastify/multipart` error codes (created with `@fastify/error`) mapped to
48
+ * the multer code that describes the same condition.
49
+ *
50
+ * @see https://github.com/fastify/fastify-multipart/blob/main/index.js
51
+ */
52
+ export const fastifyMultipartErrorCodes = {
53
+ FST_PARTS_LIMIT: 'LIMIT_PART_COUNT',
54
+ FST_FILES_LIMIT: 'LIMIT_FILE_COUNT',
55
+ FST_FIELDS_LIMIT: 'LIMIT_FIELD_COUNT',
56
+ FST_REQ_FILE_TOO_LARGE: 'LIMIT_FILE_SIZE',
57
+ FST_PROTO_VIOLATION: 'INVALID_FIELD_NAME',
58
+ };
59
+ /**
60
+ * `@fastify/multipart` error codes that already describe a client error and
61
+ * are reported as a 400 with their own message.
62
+ */
63
+ export const fastifyMultipartBadRequestCodes = [
64
+ 'FST_MP_PREMATURE_CLOSE',
65
+ 'FST_INVALID_JSON_FIELD_ERROR',
66
+ ];
67
+ export const MISSING_PLUGIN_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" require the "@fastify/multipart" plugin, ' +
68
+ 'which the FastifyAdapter registers unless its "multipart" option is false. ' +
69
+ 'Either remove that option, or register the plugin yourself (`await app.register(multipart)`).';
70
+ export const PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" require the "@fastify/multipart" plugin, ' +
71
+ 'which was not registered before the Fastify instance started. ' +
72
+ 'Register it at startup: `new FastifyAdapter({ multipart: true })`, or `await app.register(multipart)`.';
73
+ export const WRONG_ADAPTER_MESSAGE = 'File upload interceptors from "@nestjs/platform-fastify/multipart" only work with the FastifyAdapter. ' +
74
+ 'With the ExpressAdapter, import them from "@nestjs/platform-express" instead.';
@@ -0,0 +1,10 @@
1
+ import { type MultipartErrorCode } from './multipart.constants.js';
2
+ /**
3
+ * Mirrors multer's `MulterError`: identified by `code`, and may carry the
4
+ * name of the offending `field`.
5
+ */
6
+ export declare class MultipartError extends Error {
7
+ readonly code: MultipartErrorCode;
8
+ readonly field?: string | undefined;
9
+ constructor(code: MultipartErrorCode, field?: string | undefined);
10
+ }
@@ -0,0 +1,15 @@
1
+ import { multerExceptions, } from './multipart.constants.js';
2
+ /**
3
+ * Mirrors multer's `MulterError`: identified by `code`, and may carry the
4
+ * name of the offending `field`.
5
+ */
6
+ export class MultipartError extends Error {
7
+ code;
8
+ field;
9
+ constructor(code, field) {
10
+ super(multerExceptions[code]);
11
+ this.code = code;
12
+ this.field = field;
13
+ this.name = 'MultipartError';
14
+ }
15
+ }
@@ -0,0 +1,39 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+ import type { IncomingMessage } from 'http';
3
+ import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine } from '../interfaces/index.js';
4
+ /**
5
+ * Where accepted files end up, named after multer's `FileAppender` strategies.
6
+ */
7
+ export type FileStrategy = 'VALUE' | 'ARRAY' | 'OBJECT' | 'NONE';
8
+ export interface UploadSpec {
9
+ /** Accepted file fields; `'ANY'` accepts every field without a count limit. */
10
+ fields: MultipartField[] | 'ANY';
11
+ strategy: FileStrategy;
12
+ }
13
+ /** Same resolution order as multer: `storage`, then `dest`, then memory. */
14
+ export declare function resolveStorage(options: MultipartOptions): MultipartStorageEngine;
15
+ /**
16
+ * Stops parsing and discards the rest of the body, so that an error response
17
+ * does not race a client that is still uploading (multer does the same
18
+ * before it reports an error).
19
+ */
20
+ export declare function drainRequest(raw: IncomingMessage): Promise<void>;
21
+ /**
22
+ * Rejects limits that are not a non-negative integer or `Infinity`, as
23
+ * multer does: busboy compares most limits with strict equality, so such a
24
+ * value would silently disable the limit.
25
+ */
26
+ export declare function validateLimits(limits: MultipartLimits | undefined): void;
27
+ /**
28
+ * Parses the multipart body with multer's semantics and populates
29
+ * `req.body` and `req.file` / `req.files`. Requests that are not multipart
30
+ * are passed through untouched, as multer does.
31
+ */
32
+ export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<void>;
33
+ /**
34
+ * Parses up to and including the first file in `fieldName`, then stops and
35
+ * hands that file's stream to the route handler (`req.file.stream`). Text
36
+ * fields sent before the file are in `req.body`; anything after it is not
37
+ * parsed.
38
+ */
39
+ export declare function processStream(request: FastifyRequest, fieldName: string, options: MultipartOptions): Promise<void>;