diff --git a/package-lock.json b/package-lock.json index c2e26807246..7eb5a61b2ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4087,8 +4087,7 @@ "append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=", - "dev": true + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" }, "append-transform": { "version": "2.0.0", @@ -5215,8 +5214,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-more-ints": { "version": "1.0.0", @@ -10869,6 +10867,113 @@ } } }, + "fastify-multer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/fastify-multer/-/fastify-multer-1.5.4.tgz", + "integrity": "sha512-YgT0XvT5cvc6Yo0qWN0hsDdn4w5eEB02KOk1vqSHK5AmpRw/Up9q2dzXcLcTHeL/B89+AiDSx6ljkZ67vkp1cw==", + "requires": { + "append-field": "^1.0.0", + "busboy": "~0.3.1", + "concat-stream": "^2.0.0", + "fastify-plugin": "~1.6.0", + "mkdirp": "^0.5.1", + "on-finished": "^2.3.0", + "type-is": "~1.6.18", + "xtend": "^4.0.2" + }, + "dependencies": { + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "fastify-plugin": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-1.6.1.tgz", + "integrity": "sha512-APBcb27s+MjaBIerFirYmBLatoPCgmHZM6XP0K+nDL9k0yX8NJPWDY1RAC3bh6z+AB5ULS2j31BUfLMT3uaZ4A==", + "requires": { + "semver": "^6.3.0" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } + }, "fastify-multipart": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-1.0.6.tgz", @@ -17466,8 +17571,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { "version": "3.0.2", @@ -17597,7 +17701,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -22558,8 +22661,7 @@ "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "dev": true + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, "string-argv": { "version": "0.3.1", @@ -23450,8 +23552,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typedarray-to-buffer": { "version": "3.1.5", @@ -23996,8 +24097,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", diff --git a/package.json b/package.json index 4ec4d7e2827..b8f3ae5eaf2 100644 --- a/package.json +++ b/package.json @@ -60,14 +60,15 @@ "express": "4.17.1", "fast-json-stringify": "2.0.0", "fast-safe-stringify": "2.0.7", + "fastify-multer": "^1.5.4", "iterare": "1.2.0", "object-hash": "2.0.3", "path-to-regexp": "3.2.0", "reflect-metadata": "0.1.13", "rxjs": "6.5.5", "socket.io": "2.3.0", - "uuid": "8.0.0", - "tslib": "2.0.0" + "tslib": "2.0.0", + "uuid": "8.0.0" }, "devDependencies": { "@codechecks/client": "0.1.10", diff --git a/packages/platform-fastify/index.ts b/packages/platform-fastify/index.ts index 2630a9325ed..3b8f43b2a9c 100644 --- a/packages/platform-fastify/index.ts +++ b/packages/platform-fastify/index.ts @@ -7,3 +7,4 @@ export * from './adapters'; export * from './interfaces'; +export * from './multer'; diff --git a/packages/platform-fastify/multer/files.constants.ts b/packages/platform-fastify/multer/files.constants.ts new file mode 100644 index 00000000000..fd2ef8091cc --- /dev/null +++ b/packages/platform-fastify/multer/files.constants.ts @@ -0,0 +1 @@ +export const MULTER_MODULE_OPTIONS = 'MULTER_MODULE_OPTIONS'; diff --git a/packages/platform-fastify/multer/index.ts b/packages/platform-fastify/multer/index.ts new file mode 100644 index 00000000000..7afd4aab899 --- /dev/null +++ b/packages/platform-fastify/multer/index.ts @@ -0,0 +1,3 @@ +export * from './interceptors'; +export * from './interfaces'; +export * from './multer.module'; diff --git a/packages/platform-fastify/multer/interceptors/any-files.interceptor.ts b/packages/platform-fastify/multer/interceptors/any-files.interceptor.ts new file mode 100644 index 00000000000..04bf690fc32 --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/any-files.interceptor.ts @@ -0,0 +1,56 @@ +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; + +type MulterInstance = any; + +export function AnyFilesInterceptor( + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.any()(ctx.getRequest(), ctx.getResponse(), (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts b/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts new file mode 100644 index 00000000000..41ce19f5f0d --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts @@ -0,0 +1,64 @@ +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { + MulterField, + MulterOptions, +} from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; + +type MulterInstance = any; + +export function FileFieldsInterceptor( + uploadFields: MulterField[], + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.fields(uploadFields)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/file.interceptor.ts b/packages/platform-fastify/multer/interceptors/file.interceptor.ts new file mode 100644 index 00000000000..e424e1d2bec --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/file.interceptor.ts @@ -0,0 +1,61 @@ +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; + +type MulterInstance = any; + +export function FileInterceptor( + fieldName: string, + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.single(fieldName)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/files.interceptor.ts b/packages/platform-fastify/multer/interceptors/files.interceptor.ts new file mode 100644 index 00000000000..d4a46f2bad7 --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/files.interceptor.ts @@ -0,0 +1,62 @@ +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; + +type MulterInstance = any; + +export function FilesInterceptor( + fieldName: string, + maxCount?: number, + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.array(fieldName, maxCount)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/index.ts b/packages/platform-fastify/multer/interceptors/index.ts new file mode 100644 index 00000000000..5f76d7d477c --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/index.ts @@ -0,0 +1,4 @@ +export * from './any-files.interceptor'; +export * from './file-fields.interceptor'; +export * from './file.interceptor'; +export * from './files.interceptor'; diff --git a/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts b/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts new file mode 100644 index 00000000000..6472ee3124e --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts @@ -0,0 +1,19 @@ +import { Type } from '@nestjs/common'; +import { ModuleMetadata } from '@nestjs/common/interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; + +export type MulterModuleOptions = MulterOptions; + +export interface MulterOptionsFactory { + createMulterOptions(): Promise | MulterModuleOptions; +} + +export interface MulterModuleAsyncOptions + extends Pick { + useExisting?: Type; + useClass?: Type; + useFactory?: ( + ...args: any[] + ) => Promise | MulterModuleOptions; + inject?: any[]; +} diff --git a/packages/platform-fastify/multer/interfaces/index.ts b/packages/platform-fastify/multer/interfaces/index.ts new file mode 100644 index 00000000000..be94ec24afd --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/index.ts @@ -0,0 +1 @@ +export * from './files-upload-module.interface'; diff --git a/packages/platform-fastify/multer/interfaces/multer-options.interface.ts b/packages/platform-fastify/multer/interfaces/multer-options.interface.ts new file mode 100644 index 00000000000..6bcb81b4eed --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/multer-options.interface.ts @@ -0,0 +1,63 @@ +/** + * @see https://github.com/expressjs/multer + */ +export interface MulterOptions { + dest?: string; + /** The storage engine to use for uploaded files. */ + storage?: any; + /** + * An object specifying the size limits of the following optional properties. This object is passed to busboy + * directly, and the details of properties can be found on https://github.com/mscdex/busboy#busboy-methods + */ + limits?: { + /** Max field name size (Default: 100 bytes) */ + fieldNameSize?: number; + /** Max field value size (Default: 1MB) */ + fieldSize?: number; + /** Max number of non- file fields (Default: Infinity) */ + fields?: number; + /** For multipart forms, the max file size (in bytes)(Default: Infinity) */ + fileSize?: number; + /** For multipart forms, the max number of file fields (Default: Infinity) */ + files?: number; + /** For multipart forms, the max number of parts (fields + files)(Default: Infinity) */ + parts?: number; + /** For multipart forms, the max number of header key=> value pairs to parse Default: 2000(same as node's http). */ + headerPairs?: number; + }; + + /** Keep the full path of files instead of just the base name (Default: false) */ + preservePath?: boolean; + + fileFilter?( + req: any, + file: { + /** Field name specified in the form */ + fieldname: string; + /** Name of the file on the user's computer */ + originalname: string; + /** Encoding type of the file */ + encoding: string; + /** Mime type of the file */ + mimetype: string; + /** Size of the file in bytes */ + size: number; + /** The folder to which the file has been saved (DiskStorage) */ + destination: string; + /** The name of the file within the destination (DiskStorage) */ + filename: string; + /** Location of the uploaded file (DiskStorage) */ + path: string; + /** A Buffer of the entire file (MemoryStorage) */ + buffer: Buffer; + }, + callback: (error: Error | null, acceptFile: boolean) => void, + ): void; +} + +export interface MulterField { + /** The field name. */ + name: string; + /** Optional maximum number of files per field to accept. */ + maxCount?: number; +} diff --git a/packages/platform-fastify/multer/multer.constants.ts b/packages/platform-fastify/multer/multer.constants.ts new file mode 100644 index 00000000000..f63cf9e819c --- /dev/null +++ b/packages/platform-fastify/multer/multer.constants.ts @@ -0,0 +1 @@ +export const MULTER_MODULE_ID = 'MULTER_MODULE_ID'; diff --git a/packages/platform-fastify/multer/multer.module.ts b/packages/platform-fastify/multer/multer.module.ts new file mode 100644 index 00000000000..99545c1689d --- /dev/null +++ b/packages/platform-fastify/multer/multer.module.ts @@ -0,0 +1,74 @@ +import { DynamicModule, Module, Provider } from '@nestjs/common'; +import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; +import { MULTER_MODULE_OPTIONS } from './files.constants'; +import { + MulterModuleAsyncOptions, + MulterModuleOptions, + MulterOptionsFactory, +} from './interfaces/files-upload-module.interface'; +import { MULTER_MODULE_ID } from './multer.constants'; + +@Module({}) +export class MulterModule { + static register(options: MulterModuleOptions = {}): DynamicModule { + return { + module: MulterModule, + providers: [ + { provide: MULTER_MODULE_OPTIONS, useValue: options }, + { + provide: MULTER_MODULE_ID, + useValue: randomStringGenerator(), + }, + ], + exports: [MULTER_MODULE_OPTIONS], + }; + } + + static registerAsync(options: MulterModuleAsyncOptions): DynamicModule { + return { + module: MulterModule, + imports: options.imports, + providers: [ + ...this.createAsyncProviders(options), + { + provide: MULTER_MODULE_ID, + useValue: randomStringGenerator(), + }, + ], + exports: [MULTER_MODULE_OPTIONS], + }; + } + + private static createAsyncProviders( + options: MulterModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; + } + return [ + this.createAsyncOptionsProvider(options), + { + provide: options.useClass, + useClass: options.useClass, + }, + ]; + } + + private static createAsyncOptionsProvider( + options: MulterModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: MULTER_MODULE_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + return { + provide: MULTER_MODULE_OPTIONS, + useFactory: async (optionsFactory: MulterOptionsFactory) => + optionsFactory.createMulterOptions(), + inject: [options.useExisting || options.useClass], + }; + } +} diff --git a/packages/platform-fastify/multer/multer/multer.constants.ts b/packages/platform-fastify/multer/multer/multer.constants.ts new file mode 100644 index 00000000000..e3c7b4557f2 --- /dev/null +++ b/packages/platform-fastify/multer/multer/multer.constants.ts @@ -0,0 +1,9 @@ +export const multerExceptions = { + LIMIT_PART_COUNT: 'Too many parts', + LIMIT_FILE_SIZE: 'File too large', + LIMIT_FILE_COUNT: 'Too many files', + LIMIT_FIELD_KEY: 'Field name too long', + LIMIT_FIELD_VALUE: 'Field value too long', + LIMIT_FIELD_COUNT: 'Too many fields', + LIMIT_UNEXPECTED_FILE: 'Unexpected field', +}; diff --git a/packages/platform-fastify/multer/multer/multer.utils.ts b/packages/platform-fastify/multer/multer/multer.utils.ts new file mode 100644 index 00000000000..81e19739ca9 --- /dev/null +++ b/packages/platform-fastify/multer/multer/multer.utils.ts @@ -0,0 +1,24 @@ +import { + BadRequestException, + HttpException, + PayloadTooLargeException, +} from '@nestjs/common'; +import { multerExceptions } from './multer.constants'; + +export function transformException(error: Error | undefined) { + if (!error || error instanceof HttpException) { + return error; + } + switch (error.message) { + case multerExceptions.LIMIT_FILE_SIZE: + return new PayloadTooLargeException(error.message); + case multerExceptions.LIMIT_FILE_COUNT: + case multerExceptions.LIMIT_FIELD_KEY: + case multerExceptions.LIMIT_FIELD_VALUE: + case multerExceptions.LIMIT_FIELD_COUNT: + case multerExceptions.LIMIT_UNEXPECTED_FILE: + case multerExceptions.LIMIT_PART_COUNT: + return new BadRequestException(error.message); + } + return error; +} diff --git a/packages/platform-fastify/package.json b/packages/platform-fastify/package.json index 0d1bd2ed9e2..fbe3bbc23dc 100644 --- a/packages/platform-fastify/package.json +++ b/packages/platform-fastify/package.json @@ -20,6 +20,8 @@ "fastify": "2.14.1", "fastify-cors": "3.0.3", "fastify-formbody": "3.2.0", + "fastify-multer": "^1.5.4", + "fastify-multipart": "^1.0.6", "path-to-regexp": "3.2.0", "tslib": "2.0.0" }, diff --git a/packages/platform-fastify/test/multer/interceptors/any-files.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/any-files.interceptor.spec.ts new file mode 100644 index 00000000000..796d389920a --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/any-files.interceptor.spec.ts @@ -0,0 +1,45 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { AnyFilesInterceptor } from '../../../multer/interceptors/any-files.interceptor'; + +describe('FilesInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = AnyFilesInterceptor(); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call any() with expected params', async () => { + const target = new (AnyFilesInterceptor())(); + + const callback = (req, res, next) => next(); + const arraySpy = sinon + .stub((target as any).multer, 'any') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(arraySpy.called).to.be.true; + expect(arraySpy.calledWith()).to.be.true; + }); + it('should transform exception', async () => { + const target = new (AnyFilesInterceptor())(); + const err = {}; + const callback = (req, res, next) => next(err); + (target as any).multer = { + any: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts new file mode 100644 index 00000000000..2d7fbf2ae7d --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts @@ -0,0 +1,64 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FileFieldsInterceptor } from '../../../multer/interceptors/file-fields.interceptor'; + +describe('FileFieldsInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FileFieldsInterceptor([ + { name: 'file', maxCount: 1 }, + { name: 'anotherFile', maxCount: 1 }, + ]); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call object with expected params', async () => { + const fieldName1 = 'file'; + const maxCount1 = 1; + const fieldName2 = 'anotherFile'; + const maxCount2 = 2; + const argument = [ + { name: fieldName1, maxCount: maxCount1 }, + { name: fieldName2, maxCount: maxCount2 }, + ]; + const target = new (FileFieldsInterceptor(argument))(); + + const callback = (req, res, next) => next(); + const fieldsSpy = sinon + .stub((target as any).multer, 'fields') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(fieldsSpy.called).to.be.true; + expect(fieldsSpy.calledWith(argument)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName1 = 'file'; + const maxCount1 = 1; + const fieldName2 = 'anotherFile'; + const maxCount2 = 2; + const argument = [ + { name: fieldName1, maxCount: maxCount1 }, + { name: fieldName2, maxCount: maxCount2 }, + ]; + const target = new (FileFieldsInterceptor(argument))(); + const err = {}; + const callback = (req, res, next) => next(err); + (target as any).fields = { + array: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts new file mode 100644 index 00000000000..39f003e1465 --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts @@ -0,0 +1,46 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FileInterceptor } from '../../../multer/interceptors/file.interceptor'; + +describe('FileInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FileInterceptor('file'); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call single() with expected params', async () => { + const fieldName = 'file'; + const target = new (FileInterceptor(fieldName))(); + const callback = (req, res, next) => next(); + const singleSpy = sinon + .stub((target as any).multer, 'single') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(singleSpy.called).to.be.true; + expect(singleSpy.calledWith(fieldName)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName = 'file'; + const target = new (FileInterceptor(fieldName))(); + const err = {}; + const callback = (req, res, next) => next(err); + (target as any).multer = { + single: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts new file mode 100644 index 00000000000..20c6b0803de --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts @@ -0,0 +1,48 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FilesInterceptor } from '../../../multer/interceptors/files.interceptor'; + +describe('FilesInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FilesInterceptor('file'); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call array() with expected params', async () => { + const fieldName = 'file'; + const maxCount = 10; + const target = new (FilesInterceptor(fieldName, maxCount))(); + + const callback = (req, res, next) => next(); + const arraySpy = sinon + .stub((target as any).multer, 'array') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(arraySpy.called).to.be.true; + expect(arraySpy.calledWith(fieldName, maxCount)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName = 'file'; + const target = new (FilesInterceptor(fieldName))(); + const err = {}; + const callback = (req, res, next) => next(err); + (target as any).multer = { + array: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/multer/multer.module.spec.ts b/packages/platform-fastify/test/multer/multer/multer.module.spec.ts new file mode 100644 index 00000000000..e8c92f44ada --- /dev/null +++ b/packages/platform-fastify/test/multer/multer/multer.module.spec.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { MULTER_MODULE_OPTIONS } from '../../../multer/files.constants'; +import { MulterModule } from '../../../multer/multer.module'; + +describe('MulterModule', () => { + describe('register', () => { + it('should provide an options', () => { + const options = { + test: 'test', + }; + const dynamicModule = MulterModule.register(options as any); + + expect(dynamicModule.providers).to.have.length(2); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + expect(dynamicModule.providers).to.deep.include({ + provide: MULTER_MODULE_OPTIONS, + useValue: options, + }); + }); + }); + + describe('register async', () => { + describe('when useFactory', () => { + it('should provide an options', () => { + const options: any = {}; + const asyncOptions = { + useFactory: () => options, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions); + + expect(dynamicModule.providers).to.have.length(2); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + expect(dynamicModule.providers).to.deep.include({ + provide: MULTER_MODULE_OPTIONS, + useFactory: asyncOptions.useFactory, + inject: [], + }); + }); + }); + + describe('when useExisting', () => { + it('should provide an options', () => { + const asyncOptions = { + useExisting: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + + expect(dynamicModule.providers).to.have.length(2); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + }); + }); + + describe('when useClass', () => { + it('should provide an options', () => { + const asyncOptions = { + useClass: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + + expect(dynamicModule.providers).to.have.length(3); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + }); + it('provider should call "createMulterOptions"', async () => { + const asyncOptions = { + useClass: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + const optionsFactory = { + createMulterOptions: sinon.spy(), + }; + await ((dynamicModule.providers[0] as any).useFactory as any)( + optionsFactory, + ); + expect(optionsFactory.createMulterOptions.called).to.be.true; + }); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts b/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts new file mode 100644 index 00000000000..7ad1eec9069 --- /dev/null +++ b/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts @@ -0,0 +1,41 @@ +import { + BadRequestException, + HttpException, + PayloadTooLargeException, +} from '@nestjs/common'; +import { expect } from 'chai'; +import { multerExceptions } from '../../../multer/multer/multer.constants'; +import { transformException } from '../../../multer/multer/multer.utils'; + +describe('transformException', () => { + describe('if error does not exist', () => { + it('behave as identity', () => { + const err = undefined; + expect(transformException(err)).to.be.eq(err); + }); + }); + describe('if error is instance of HttpException', () => { + it('behave as identity', () => { + const err = new HttpException('response', 500); + expect(transformException(err)).to.be.eq(err); + }); + }); + describe('if error exists and is not instance of HttpException', () => { + describe('and is LIMIT_FILE_SIZE exception', () => { + it('should return "PayloadTooLargeException"', () => { + const err = { message: multerExceptions.LIMIT_FILE_SIZE }; + expect(transformException(err as any)).to.be.instanceof( + PayloadTooLargeException, + ); + }); + }); + describe('and is multer exception but not a LIMIT_FILE_SIZE', () => { + it('should return "BadRequestException"', () => { + const err = { message: multerExceptions.LIMIT_FIELD_KEY }; + expect(transformException(err as any)).to.be.instanceof( + BadRequestException, + ); + }); + }); + }); +});