-
-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9718 from thiagomini/feature/4752-file-validators…
…-pipe Feature/4752 file validators pipe
- Loading branch information
Showing
14 changed files
with
573 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
packages/common/test/pipes/file/file-type.validator.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
56
packages/common/test/pipes/file/max-file-size.validator.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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})`, | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.