Skip to content

Commit

Permalink
Merge pull request #9718 from thiagomini/feature/4752-file-validators…
Browse files Browse the repository at this point in the history
…-pipe

Feature/4752 file validators pipe
  • Loading branch information
kamilmysliwiec committed Jun 20, 2022
2 parents f41ee3d + 2e8426b commit 95cea89
Show file tree
Hide file tree
Showing 14 changed files with 573 additions and 1 deletion.
30 changes: 30 additions & 0 deletions packages/common/pipes/file/file-type.validator.ts
@@ -0,0 +1,30 @@
import { FileValidator } from './file-validator.interface';

export type FileTypeValidatorOptions = {
fileType: string;
};

/**
* Defines the built-in FileType File Validator
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
*
* @publicApi
*/
export class FileTypeValidator extends FileValidator<FileTypeValidatorOptions> {
buildErrorMessage(): string {
return `Validation failed (expected type is ${this.validationOptions.fileType})`;
}

isValid(file: any): boolean {
if (!this.validationOptions) {
return true;
}

if (!file.mimetype) {
return false;
}

return (file.mimetype as string).endsWith(this.validationOptions.fileType);
}
}
18 changes: 18 additions & 0 deletions packages/common/pipes/file/file-validator.interface.ts
@@ -0,0 +1,18 @@
/**
* Interface describing FileValidators, which can be added to a {@link ParseFilePipe}.
*/
export abstract class FileValidator<TValidationOptions = Record<string, any>> {
constructor(protected readonly validationOptions: TValidationOptions) {}

/**
* Indicates if this file should be considered valid, according to the options passed in the constructor.
* @param file the file from the request object
*/
abstract isValid(file?: any): boolean | Promise<boolean>;

/**
* Builds an error message in case the validation fails.
* @param file the file from the request object
*/
abstract buildErrorMessage(file: any): string;
}
6 changes: 6 additions & 0 deletions packages/common/pipes/file/index.ts
@@ -0,0 +1,6 @@
export * from './file-type.validator';
export * from './file-validator.interface';
export * from './max-file-size.validator';
export * from './parse-file-options.interface';
export * from './parse-file.pipe';
export * from './parse-file-pipe.builder';
26 changes: 26 additions & 0 deletions packages/common/pipes/file/max-file-size.validator.ts
@@ -0,0 +1,26 @@
import { FileValidator } from './file-validator.interface';

export type MaxFileSizeValidatorOptions = {
maxSize: number;
};

/**
* Defines the built-in MaxSize File Validator
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
*
* @publicApi
*/
export class MaxFileSizeValidator extends FileValidator<MaxFileSizeValidatorOptions> {
buildErrorMessage(): string {
return `Validation failed (expected size is less than ${this.validationOptions.maxSize})`;
}

public isValid(file: any): boolean {
if (!this.validationOptions) {
return true;
}

return file.size < this.validationOptions.maxSize;
}
}
8 changes: 8 additions & 0 deletions packages/common/pipes/file/parse-file-options.interface.ts
@@ -0,0 +1,8 @@
import { ErrorHttpStatusCode } from '../../utils/http-error-by-code.util';
import { FileValidator } from './file-validator.interface';

export interface ParseFileOptions {
validators?: FileValidator[];
errorHttpStatusCode?: ErrorHttpStatusCode;
exceptionFactory?: (error: string) => any;
}
37 changes: 37 additions & 0 deletions packages/common/pipes/file/parse-file-pipe.builder.ts
@@ -0,0 +1,37 @@
import {
FileTypeValidator,
FileTypeValidatorOptions,
} from './file-type.validator';
import { FileValidator } from './file-validator.interface';
import {
MaxFileSizeValidator,
MaxFileSizeValidatorOptions,
} from './max-file-size.validator';
import { ParseFileOptions } from './parse-file-options.interface';
import { ParseFilePipe } from './parse-file.pipe';

export class ParseFilePipeBuilder {
private validators: FileValidator[] = [];

addMaxSizeValidator(options: MaxFileSizeValidatorOptions) {
this.validators.push(new MaxFileSizeValidator(options));
return this;
}

addFileTypeValidator(options: FileTypeValidatorOptions) {
this.validators.push(new FileTypeValidator(options));
return this;
}

build(
additionalOptions?: Omit<ParseFileOptions, 'validators'>,
): ParseFilePipe {
const parseFilePipe = new ParseFilePipe({
...additionalOptions,
validators: this.validators,
});

this.validators = [];
return parseFilePipe;
}
}
68 changes: 68 additions & 0 deletions packages/common/pipes/file/parse-file.pipe.ts
@@ -0,0 +1,68 @@
import { Injectable, Optional } from '../../decorators/core';
import { HttpStatus } from '../../enums';
import { HttpErrorByCode } from '../../utils/http-error-by-code.util';
import { PipeTransform } from '../../interfaces/features/pipe-transform.interface';
import { ParseFileOptions } from './parse-file-options.interface';
import { FileValidator } from './file-validator.interface';
import { throws } from 'assert';

/**
* Defines the built-in ParseFile Pipe. This pipe can be used to validate incoming files
* with `@UploadedFile()` decorator. You can use either other specific built-in validators
* or provide one of your own, simply implementing it through {@link FileValidator}
* interface and adding it to ParseFilePipe's constructor.
*
* @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
*
* @publicApi
*/
@Injectable()
export class ParseFilePipe implements PipeTransform<any> {
protected exceptionFactory: (error: string) => any;
private readonly validators: FileValidator[];

constructor(@Optional() options: ParseFileOptions = {}) {
const {
exceptionFactory,
errorHttpStatusCode = HttpStatus.BAD_REQUEST,
validators = [],
} = options;

this.exceptionFactory =
exceptionFactory ||
(error => new HttpErrorByCode[errorHttpStatusCode](error));

this.validators = validators;
}

async transform(value: any): Promise<any> {
if (this.validators.length) {
await this.validate(value);
}
return value;
}

protected async validate(file: any): Promise<any> {
for (const validator of this.validators) {
await this.validateOrThrow(file, validator);
}

return file;
}

private async validateOrThrow(file: any, validator: FileValidator) {
const isValid = await validator.isValid(file);

if (!isValid) {
const errorMessage = validator.buildErrorMessage(file);
throw this.exceptionFactory(errorMessage);
}
}

/**
* @returns list of validators used in this pipe.
*/
getValidators() {
return this.validators;
}
}
1 change: 1 addition & 0 deletions packages/common/pipes/index.ts
Expand Up @@ -6,3 +6,4 @@ export * from './parse-float.pipe';
export * from './parse-enum.pipe';
export * from './parse-uuid.pipe';
export * from './validation.pipe';
export * from './file';
65 changes: 65 additions & 0 deletions packages/common/test/pipes/file/file-type.validator.spec.ts
@@ -0,0 +1,65 @@
import { FileTypeValidator } from '../../../pipes';
import { expect } from 'chai';

describe('FileTypeValidator', () => {
describe('isValid', () => {
it('should return true when the file mimetype is the same as the specified', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});

const requestFile = {
mimetype: 'image/jpeg',
};

expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});

it('should return true when the file mimetype ends with the specified option type', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});

const requestFile = {
mimetype: 'image/jpeg',
};

expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});

it('should return false when the file mimetype is different from the specified', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});

const requestFile = {
mimetype: 'image/png',
};

expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});

it('should return false when the file mimetype was not provided', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});

const requestFile = {};

expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
});

describe('buildErrorMessage', () => {
it('should return a string with the format "Validation failed (expected type is #fileType)"', () => {
const fileType = 'image/jpeg';
const fileTypeValidator = new FileTypeValidator({
fileType,
});

expect(fileTypeValidator.buildErrorMessage()).to.equal(
`Validation failed (expected type is ${fileType})`,
);
});
});
});
56 changes: 56 additions & 0 deletions packages/common/test/pipes/file/max-file-size.validator.spec.ts
@@ -0,0 +1,56 @@
import { expect } from 'chai';
import { MaxFileSizeValidator } from '../../../pipes';

describe('MaxFileSizeValidator', () => {
const oneKb = 1024;

describe('isValid', () => {
it('should return true when the file size is less than the maximum size', () => {
const maxFileSizeValidator = new MaxFileSizeValidator({
maxSize: oneKb,
});

const requestFile = {
size: 100,
};

expect(maxFileSizeValidator.isValid(requestFile)).to.equal(true);
});

it('should return false when the file size is greater than the maximum size', () => {
const maxFileSizeValidator = new MaxFileSizeValidator({
maxSize: oneKb,
});

const requestFile = {
size: oneKb + 1,
};

expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false);
});

it('should return false when the file size is equal to the maximum size', () => {
const maxFileSizeValidator = new MaxFileSizeValidator({
maxSize: oneKb,
});

const requestFile = {
size: oneKb,
};

expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false);
});
});

describe('buildErrorMessage', () => {
it('should return a string with the format "Validation failed (expected size is less than #maxSize")', () => {
const maxFileSizeValidator = new MaxFileSizeValidator({
maxSize: oneKb,
});

expect(maxFileSizeValidator.buildErrorMessage()).to.equal(
`Validation failed (expected size is less than ${oneKb})`,
);
});
});
});

0 comments on commit 95cea89

Please sign in to comment.