Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform): add fastify-multipart support for fastify platform #6935

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/platform-fastify/multipart/files.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MULTIPART_MODULE_OPTIONS = 'MULTIPART_MODULE_OPTIONS';
4 changes: 4 additions & 0 deletions packages/platform-fastify/multipart/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './interceptors';
export * from './interfaces';
export * from './multipart.module';
export * from './utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
import { MultipartOptions } from '../interfaces/multipart-options.interface';
import { MultipartWrapper, transformException } from '../multipart';

export const AnyFilesInterceptor = (
localOptions?: MultipartOptions,
): Type<NestInterceptor> => {
class MixinInterceptor implements NestInterceptor {
protected options: MultipartOptions;
protected multipart: MultipartWrapper;

public constructor(
@Optional()
@Inject(MULTIPART_MODULE_OPTIONS)
options: MultipartOptions = {},
) {
this.multipart = new MultipartWrapper({
...options,
...localOptions,
});
}

public async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
const fieldname = 'files';
try {
req[fieldname] = await this.multipart.any()(req);
} catch (err) {
throw transformException(err);
}
return next.handle();
}
}

const Interceptor = mixin(MixinInterceptor);
return Interceptor as Type<NestInterceptor>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
import {
MultipartOptions,
UploadField,
} from '../interfaces/multipart-options.interface';
import { MultipartWrapper, transformException } from '../multipart';

export const FileFieldsInterceptor = (
uploadFields: UploadField[],
localOptions?: MultipartOptions,
): Type<NestInterceptor> => {
class MixinInterceptor implements NestInterceptor {
protected multipart: MultipartWrapper;

public constructor(
@Optional()
@Inject(MULTIPART_MODULE_OPTIONS)
options: MultipartOptions = {},
) {
this.multipart = new MultipartWrapper({
...options,
...localOptions,
});
}

public async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
const fieldname = 'files';
try {
req[fieldname] = await this.multipart.fileFields(uploadFields)(req);
} catch (err) {
throw transformException(err);
}
return next.handle();
}
}

const Interceptor = mixin(MixinInterceptor);
return Interceptor as Type<NestInterceptor>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
import { MultipartOptions } from '../interfaces/multipart-options.interface';
import { MultipartWrapper, transformException } from '../multipart';

export const FileInterceptor = (
fieldname: string,
localOptions?: MultipartOptions,
): Type<NestInterceptor> => {
class MixinInterceptor implements NestInterceptor {
protected multipart: MultipartWrapper;

public constructor(
@Optional()
@Inject(MULTIPART_MODULE_OPTIONS)
options: MultipartOptions = {},
) {
this.multipart = new MultipartWrapper({
...options,
...localOptions,
});
}

public async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
try {
req[fieldname] = await this.multipart.file(fieldname)(req);
} catch (err) {
throw transformException(err);
}
return next.handle();
}
}

const Interceptor = mixin(MixinInterceptor);
return Interceptor as Type<NestInterceptor>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { MULTIPART_MODULE_OPTIONS } from '../files.constants';
import { MultipartOptions } from '../interfaces/multipart-options.interface';
import { MultipartWrapper, transformException } from '../multipart';

export const FilesInterceptor = (
fieldname: string,
maxCount?: number,
localOptions?: MultipartOptions,
): Type<NestInterceptor> => {
class MixinInterceptor implements NestInterceptor {
protected multipart: MultipartWrapper;

public constructor(
@Optional()
@Inject(MULTIPART_MODULE_OPTIONS)
options: MultipartOptions = {},
) {
this.multipart = new MultipartWrapper({
...options,
...localOptions,
});
}

public async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
try {
req[fieldname] = await this.multipart.files(fieldname, maxCount)(req);
} catch (err) {
throw transformException(err);
}
return next.handle();
}
}

const Interceptor = mixin(MixinInterceptor);
return Interceptor as Type<NestInterceptor>;
};
4 changes: 4 additions & 0 deletions packages/platform-fastify/multipart/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './any-files.interceptor';
export * from './file-fields.interceptor';
export * from './file.interceptor';
export * from './files.interceptor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Type } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { MultipartOptions } from './multipart-options.interface';

export type MultipartModuleOptions = MultipartOptions;

export interface MultipartOptionsFactory {
createMultipartOptions():
| Promise<MultipartModuleOptions>
| MultipartModuleOptions;
}

export interface MultipartModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<MultipartOptionsFactory>;
useClass?: Type<MultipartOptionsFactory>;
useFactory?: (
...args: any[]
) => Promise<MultipartModuleOptions> | MultipartModuleOptions;
inject?: any[];
}
3 changes: 3 additions & 0 deletions packages/platform-fastify/multipart/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './files-upload-module.interface';
export * from './multipart-file.interface';
export * from './multipart-options.interface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface MultipartDiskFile extends MultipartFile {
path: string;
destination: string;
}

interface MultipartFields {
[x: string]: FastifyMultipartFile | FastifyMultipartFile[];
}

export interface FastifyMultipartFile {
toBuffer: () => Promise<Buffer>;
file: NodeJS.ReadStream;
filepath: string;
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fields: MultipartFields;
}

export interface MultipartFile extends FastifyMultipartFile {
originalname: string;
size: number;
}

export type InterceptorFile = MultipartFile | MultipartDiskFile;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FastifyMultipartFile } from './multipart-file.interface';

export interface MultipartOptions {
/** Destination folder, if not undefined uploaded file will be saved locally in dest path */
dest?: string;
/**
* 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 (in bytes) (Default: 100 bytes) */
fieldnameSize?: number;
/** Max field value size (in bytes) (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;
};
/** These are the HTTP headers of the incoming request, which are used by individual parsers */
headers?: any;
/** highWaterMark to use for this Busboy instance (Default: WritableStream default). */
highWaterMark?: number;
/** highWaterMark to use for file streams (Default: ReadableStream default) */
fileHwm?: number;
/** Default character set to use when one isn't defined (Default: 'utf8') */
defCharset?: string;
/** If paths in the multipart 'filename' field shall be preserved. (Default: false) */
preservePath?: boolean;
/** Function to control which files are accepted */
fileFilter?(
req: any,
file: FastifyMultipartFile,
callback: (error: Error | null, acceptFile?: boolean) => void,
): void;
}

export interface UploadField {
/** The field name. */
name: string;
/** Optional maximum number of files per field to accept. */
maxCount?: number;
}
1 change: 1 addition & 0 deletions packages/platform-fastify/multipart/multipart.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MULTIPART_MODULE_ID = 'MULTIPART_MODULE_ID';
74 changes: 74 additions & 0 deletions packages/platform-fastify/multipart/multipart.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { MULTIPART_MODULE_OPTIONS } from './files.constants';
import {
MultipartModuleAsyncOptions,
MultipartModuleOptions,
MultipartOptionsFactory,
} from './interfaces/files-upload-module.interface';
import { MULTIPART_MODULE_ID } from './multipart.constants';

@Module({})
export class MultipartModule {
static register(options: MultipartModuleOptions = {}): DynamicModule {
return {
module: MultipartModule,
providers: [
{ provide: MULTIPART_MODULE_OPTIONS, useValue: options },
{
provide: MULTIPART_MODULE_ID,
useValue: randomStringGenerator(),
},
],
exports: [MULTIPART_MODULE_OPTIONS],
};
}

static registerAsync(options: MultipartModuleAsyncOptions): DynamicModule {
return {
module: MultipartModule,
imports: options.imports,
providers: [
...this.createAsyncProviders(options),
{
provide: MULTIPART_MODULE_ID,
useValue: randomStringGenerator(),
},
],
exports: [MULTIPART_MODULE_OPTIONS],
};
}

private static createAsyncProviders(
options: MultipartModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}

private static createAsyncOptionsProvider(
options: MultipartModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: MULTIPART_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: MULTIPART_MODULE_OPTIONS,
useFactory: async (optionsFactory: MultipartOptionsFactory) =>
optionsFactory.createMultipartOptions(),
inject: [options.useExisting || options.useClass],
};
}
}
Loading