diff --git a/packages/common/pipes/index.ts b/packages/common/pipes/index.ts index e4485627c6a..a6936549377 100644 --- a/packages/common/pipes/index.ts +++ b/packages/common/pipes/index.ts @@ -2,5 +2,7 @@ export * from './default-value.pipe'; export * from './parse-array.pipe'; export * from './parse-bool.pipe'; export * from './parse-int.pipe'; +export * from './parse-float.pipe'; +export * from './parse-enum.pipe'; export * from './parse-uuid.pipe'; export * from './validation.pipe'; diff --git a/packages/common/pipes/parse-enum.pipe.ts b/packages/common/pipes/parse-enum.pipe.ts new file mode 100644 index 00000000000..36eaa89ede6 --- /dev/null +++ b/packages/common/pipes/parse-enum.pipe.ts @@ -0,0 +1,59 @@ +import { ArgumentMetadata, HttpStatus, Injectable, Optional } from '../index'; +import { PipeTransform } from '../interfaces/features/pipe-transform.interface'; +import { + ErrorHttpStatusCode, + HttpErrorByCode, +} from '../utils/http-error-by-code.util'; + +export interface ParseEnumPipeOptions { + errorHttpStatusCode?: ErrorHttpStatusCode; + exceptionFactory?: (error: string) => any; +} + +/** + * Defines the built-in ParseEnum Pipe + * + * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes) + * + * @publicApi + */ +@Injectable() +export class ParseEnumPipe implements PipeTransform { + protected exceptionFactory: (error: string) => any; + + constructor( + protected readonly enumType: T, + @Optional() options?: ParseEnumPipeOptions, + ) { + options = options || {}; + const { + exceptionFactory, + errorHttpStatusCode = HttpStatus.BAD_REQUEST, + } = options; + + this.exceptionFactory = + exceptionFactory || + (error => new HttpErrorByCode[errorHttpStatusCode](error)); + } + + /** + * Method that accesses and performs optional transformation on argument for + * in-flight requests. + * + * @param value currently processed route argument + * @param metadata contains metadata about the currently processed route argument + */ + async transform(value: T, metadata: ArgumentMetadata): Promise { + if (!this.isEnum(value, this.enumType)) { + throw this.exceptionFactory( + 'Validation failed (enum string is expected)', + ); + } + return value; + } + + protected isEnum(value: T, entity: any): boolean { + const enumValues = Object.keys(entity).map(k => entity[k]); + return enumValues.indexOf(value) >= 0; + } +} diff --git a/packages/common/pipes/parse-float.pipe.ts b/packages/common/pipes/parse-float.pipe.ts new file mode 100644 index 00000000000..1dc449dc405 --- /dev/null +++ b/packages/common/pipes/parse-float.pipe.ts @@ -0,0 +1,55 @@ +import { ArgumentMetadata, HttpStatus, Injectable, Optional } from '../index'; +import { PipeTransform } from '../interfaces/features/pipe-transform.interface'; +import { + ErrorHttpStatusCode, + HttpErrorByCode, +} from '../utils/http-error-by-code.util'; + +export interface ParseFloatPipeOptions { + errorHttpStatusCode?: ErrorHttpStatusCode; + exceptionFactory?: (error: string) => any; +} + +/** + * Defines the built-in ParseFloat Pipe + * + * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes) + * + * @publicApi + */ +@Injectable() +export class ParseFloatPipe implements PipeTransform { + protected exceptionFactory: (error: string) => any; + + constructor(@Optional() options?: ParseFloatPipeOptions) { + options = options || {}; + const { + exceptionFactory, + errorHttpStatusCode = HttpStatus.BAD_REQUEST, + } = options; + + this.exceptionFactory = + exceptionFactory || + (error => new HttpErrorByCode[errorHttpStatusCode](error)); + } + + /** + * Method that accesses and performs optional transformation on argument for + * in-flight requests. + * + * @param value currently processed route argument + * @param metadata contains metadata about the currently processed route argument + */ + async transform(value: string, metadata: ArgumentMetadata): Promise { + const isNumeric = + ['string', 'number'].includes(typeof value) && + !isNaN(parseFloat(value)) && + isFinite(value as any); + if (!isNumeric) { + throw this.exceptionFactory( + 'Validation failed (numeric string is expected)', + ); + } + return parseFloat(value); + } +} diff --git a/packages/common/test/pipes/parse-enum.pipe.spec.ts b/packages/common/test/pipes/parse-enum.pipe.spec.ts new file mode 100644 index 00000000000..88095e22d7c --- /dev/null +++ b/packages/common/test/pipes/parse-enum.pipe.spec.ts @@ -0,0 +1,39 @@ +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { ArgumentMetadata } from '../../interfaces'; +import { ParseEnumPipe } from '../../pipes/parse-enum.pipe'; +import { HttpException } from '../../exceptions'; + +class CustomTestError extends HttpException { + constructor() { + super('This is a TestException', 418); + } +} + +describe('ParseEnumPipe', () => { + enum Direction { + Up = 'UP', + } + let target: ParseEnumPipe; + beforeEach(() => { + target = new ParseEnumPipe(Direction, { + exceptionFactory: (error: any) => new CustomTestError(), + }); + }); + describe('transform', () => { + describe('when validation passes', () => { + it('should return enum value', async () => { + expect(await target.transform('UP', {} as ArgumentMetadata)).to.equal( + Direction.Up, + ); + }); + }); + describe('when validation fails', () => { + it('should throw an error', async () => { + return expect( + target.transform('DOWN', {} as ArgumentMetadata), + ).to.be.rejectedWith(CustomTestError); + }); + }); + }); +}); diff --git a/packages/common/test/pipes/parse-float.pipe.spec.ts b/packages/common/test/pipes/parse-float.pipe.spec.ts new file mode 100644 index 00000000000..ba92cba4dc3 --- /dev/null +++ b/packages/common/test/pipes/parse-float.pipe.spec.ts @@ -0,0 +1,37 @@ +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { ArgumentMetadata } from '../../interfaces'; +import { ParseFloatPipe } from '../../pipes/parse-float.pipe'; +import { HttpException } from '../../exceptions'; + +class CustomTestError extends HttpException { + constructor() { + super('This is a TestException', 418); + } +} + +describe('ParseFloatPipe', () => { + let target: ParseFloatPipe; + beforeEach(() => { + target = new ParseFloatPipe({ + exceptionFactory: (error: any) => new CustomTestError(), + }); + }); + describe('transform', () => { + describe('when validation passes', () => { + it('should return number', async () => { + const num = '3.33'; + expect(await target.transform(num, {} as ArgumentMetadata)).to.equal( + parseFloat(num), + ); + }); + }); + describe('when validation fails', () => { + it('should throw an error', async () => { + return expect( + target.transform('123.123abc', {} as ArgumentMetadata), + ).to.be.rejectedWith(CustomTestError); + }); + }); + }); +});