-
-
Notifications
You must be signed in to change notification settings - Fork 7.7k
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
Feature/4752 file validators pipe #9718
Merged
kamilmysliwiec
merged 10 commits into
nestjs:9.0.0
from
thiagomini:feature/4752-file-validators-pipe
Jun 20, 2022
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e9d311a
feat(common): add parse file pipe
thiagomini 466abf6
test(common): add test for parse file pipe
thiagomini b277f95
fix(test): remove multer types
thiagomini c43dbd8
test(common): fix import to relative path
thiagomini b178eb5
refactor(common): add braces
thiagomini d62d96b
test(common): add builder tests
thiagomini 2ad8648
docs(common): add validators docs
thiagomini bdd9397
refactor(common): parse file pipe
thiagomini ad0d1bc
refactor(common): file type validator
thiagomini 2e8426b
test(sample): add file upload e2e test
thiagomini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent code structure, it was very well thought out in that aspect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Hebert, although this still lacks promise handling, so I'll have to add that before merging