diff --git a/README.md b/README.md index 21b3f5a6..ea0e8568 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A collection of [NestJS](https://docs.nestjs.com) components. This repository is - [NestJS Google Cloud PubSub MicroService](#nestjs-google-cloud-pubsub-microservice) - [NestJS Google Cloud PubSub Client Proxy](#nestjs-google-cloud-pubsub-client-proxy) - [NestJS custom decorators](#nestjs-custom-decorators) + - [NestJS class validators](#nestjs-class-validators) ## NestJS Pagination @@ -57,6 +58,12 @@ A set of custom decorators for NestJS. See [the documentation here](./packages/custom-decorators). +## NestJS class validators + +A package containing overriden class validators. + +See [the documentation here](./packages/class-validators). + # Contribution This repository is managed by [Lerna.js](https://lerna.js.org). If you want to contribute, you need to follow these instructions: diff --git a/package-lock.json b/package-lock.json index f9855d18..720dc3b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19092,7 +19092,8 @@ "version": "1.3.13", "license": "ISC", "dependencies": { - "jwt-decode": "^4.0.0" + "jwt-decode": "^4.0.0", + "class-validator": "^0.14.0" }, "peerDependencies": { "@nestjs/common": ">=8", @@ -19200,7 +19201,8 @@ "@algoan/nestjs-custom-decorators": { "version": "file:packages/custom-decorators", "requires": { - "jwt-decode": "^4.0.0" + "jwt-decode": "^4.0.0", + "class-validator": "^0.14.0" } }, "@algoan/nestjs-google-pubsub-client": { @@ -33357,3 +33359,4 @@ } } } + diff --git a/package.json b/package.json index 62ff8e02..35dedf3a 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@algoan/nestjs-google-pubsub-microservice": "file:packages/google-pubsub-microservice", "@algoan/nestjs-http-exception-filter": "file:packages/http-exception-filter", "@algoan/nestjs-logging-interceptor": "file:packages/logging-interceptor", - "@algoan/nestjs-pagination": "file:packages/pagination" + "@algoan/nestjs-pagination": "file:packages/pagination", + "@algoan/nestjs-class-validators": "file:packages/class-validators" } } diff --git a/packages/class-validators/README.md b/packages/class-validators/README.md new file mode 100644 index 00000000..d2d029e4 --- /dev/null +++ b/packages/class-validators/README.md @@ -0,0 +1,21 @@ +# Nestjs class validators + +A set of class validators based on the class-validator package (https://www.npmjs.com/package/@nestjs/class-validator/v/0.13.1). + +## Installation + +```bash +npm install --save @algoan/nestjs-class-validators +``` + +## IsEnum + +A class validator that validates the enum type. + +### Usage + +```ts + @IsEnum(UserType) + public userType: UserType; +``` + diff --git a/packages/class-validators/package.json b/packages/class-validators/package.json new file mode 100644 index 00000000..906aef2e --- /dev/null +++ b/packages/class-validators/package.json @@ -0,0 +1,14 @@ +{ + "name": "@algoan/nestjs-class-validators", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "class-validator": "^0.14.0" + } +} diff --git a/packages/class-validators/src/index.ts b/packages/class-validators/src/index.ts new file mode 100644 index 00000000..2ee4efa3 --- /dev/null +++ b/packages/class-validators/src/index.ts @@ -0,0 +1,2 @@ +export { IsEnum } from './is-enum.decorator'; +export * from 'class-validator'; diff --git a/packages/class-validators/src/is-enum.decorator.ts b/packages/class-validators/src/is-enum.decorator.ts new file mode 100644 index 00000000..629ece3c --- /dev/null +++ b/packages/class-validators/src/is-enum.decorator.ts @@ -0,0 +1,13 @@ +import { IsEnum as OriginalIsEnum, ValidationOptions } from 'class-validator'; + +/** + * Checks if a given value is the member of the provided enum. + * + * This an override for the class-validator IsEnum validator. + * The error message is enhanced with the invalid value + */ +export const IsEnum = (entity: object, validationOptions?: ValidationOptions): PropertyDecorator => + OriginalIsEnum(entity, { + message: '$property has the value $value but must be one of the following values: $constraint2', + ...validationOptions, + }); diff --git a/packages/class-validators/test/is-enum-decorator.test.ts b/packages/class-validators/test/is-enum-decorator.test.ts new file mode 100644 index 00000000..66a15157 --- /dev/null +++ b/packages/class-validators/test/is-enum-decorator.test.ts @@ -0,0 +1,79 @@ +import { IsEnum } from '../src/is-enum.decorator'; +import { Validator, ValidatorOptions } from 'class-validator'; + +function validateValues( + object: { testProperty: any }, + values: any[], + validatorOptions?: ValidatorOptions, +): Promise { + const validator = new Validator(); + const promises = values.map((value) => { + object.testProperty = value; + return validator.validate(object, validatorOptions).then((errors) => { + expect(errors.length).toEqual(0); + if (errors.length > 0) { + console.log(`Unexpected errors: ${JSON.stringify(errors)}`); + throw new Error('Unexpected validation errors'); + } + }); + }); + + return Promise.all(promises); +} + +function checkReturnedError( + object: { testProperty: any }, + values: any[], + validationType: string, + messages: string[], + validatorOptions?: ValidatorOptions, +): Promise { + let messagesIncrementor: number = 0; + const validator = new Validator(); + const promises = values.map((value) => { + object.testProperty = value; + return validator.validate(object, validatorOptions).then((errors) => { + expect(errors.length).toEqual(1); + expect(errors[0].target).toEqual(object); + expect(errors[0].property).toEqual('testProperty'); + expect(errors[0].constraints).toEqual({ [validationType]: messages[messagesIncrementor] }); + expect(errors[0].value).toEqual(value); + messagesIncrementor++; + }); + }); + + return Promise.all(promises); +} + +describe('Tests related to the custom IsEnum Decorator', () => { + enum CustomEnum { + FIRST_ITEM = 'first-item', + SECOND_ITEM = 'second-item', + } + + class TestClass { + @IsEnum(CustomEnum) + testProperty: CustomEnum = CustomEnum.FIRST_ITEM; + } + + it('should validate the correct values', () => { + return validateValues(new TestClass(), [ + CustomEnum.FIRST_ITEM, + CustomEnum.SECOND_ITEM, + 'first-item', + 'second-item', + ]); + }); + + it('should not validate invalid values and return the correct errors', () => { + const validationType = 'isEnum'; + const firstMessage = + 'testProperty has the value false-value-1 but must be one of the following values: first-item, second-item'; + const secondMessage = + 'testProperty has the value false-value-2 but must be one of the following values: first-item, second-item'; + return checkReturnedError(new TestClass(), ['false-value-1', 'false-value-2'], validationType, [ + firstMessage, + secondMessage, + ]); + }); +});