diff --git a/README.md b/README.md index ea0e8568..e9c0c5bf 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ A package containing overriden class validators. See [the documentation here](./packages/class-validators). +## NestJS class transformers + +A package containing custom class transformers. + +See [the documentation here](./packages/class-transformers). + # 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 6c237cfe..df058138 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/*" ], "dependencies": { + "@algoan/nestjs-class-transformers": "file:packages/class-transformers", "@algoan/nestjs-class-validators": "file:packages/class-validators", "@algoan/nestjs-custom-decorators": "file:packages/custom-decorators", "@algoan/nestjs-google-pubsub-client": "file:packages/google-pubsub-client", @@ -98,6 +99,10 @@ "typescript": ">= 3" } }, + "node_modules/@algoan/nestjs-class-transformers": { + "resolved": "packages/class-transformers", + "link": true + }, "node_modules/@algoan/nestjs-class-validators": { "resolved": "packages/class-validators", "link": true @@ -6102,8 +6107,7 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "devOptional": true + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "node_modules/class-validator": { "version": "0.14.0", @@ -19092,6 +19096,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/class-transformers": { + "name": "@algoan/nestjs-class-transformers", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "class-transformer": "^0.5.1" + } + }, "packages/class-validators": { "name": "@algoan/nestjs-class-validators", "version": "0.0.2", @@ -19210,6 +19222,12 @@ "eslint-plugin-prefer-arrow": "^1.2.3" } }, + "@algoan/nestjs-class-transformers": { + "version": "file:packages/class-transformers", + "requires": { + "class-transformer": "^0.5.1" + } + }, "@algoan/nestjs-class-validators": { "version": "file:packages/class-validators", "requires": { @@ -23696,8 +23714,7 @@ "class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "devOptional": true + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "class-validator": { "version": "0.14.0", diff --git a/package.json b/package.json index 35dedf3a..7b2a8ea3 100644 --- a/package.json +++ b/package.json @@ -89,12 +89,13 @@ } }, "dependencies": { + "@algoan/nestjs-class-transformers": "file:packages/class-transformers", + "@algoan/nestjs-class-validators": "file:packages/class-validators", "@algoan/nestjs-custom-decorators": "file:packages/custom-decorators", "@algoan/nestjs-google-pubsub-client": "file:packages/google-pubsub-client", "@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-class-validators": "file:packages/class-validators" + "@algoan/nestjs-pagination": "file:packages/pagination" } } diff --git a/packages/class-transformers/README.md b/packages/class-transformers/README.md new file mode 100644 index 00000000..3b327983 --- /dev/null +++ b/packages/class-transformers/README.md @@ -0,0 +1,48 @@ +# Nestjs class transformers + +Extends [class-transformers package](https://github.com/typestack/class-transformer) with additional features. + +## Installation + +```bash +npm install --save @algoan/nestjs-class-transformers +``` + +## EnumFallback + +### Usage + +```ts +import { EnumFallback } from '@algoan/nestjs-class-transformers'; + +export enum UserRole { + ADMIN = 'ADMIN', + READER = 'READER', +} + +class User { + @EnumFallback({ + type: UserRole, + fallback: (value: UserRole) => UserRole.READER // if the role is not "ADMIN" or "READER", then the role will be "READER". + }) + public role?: UserRole; +} +``` + +It works with array too: +```ts +import { EnumFallback } from '@algoan/nestjs-class-transformers'; + +export enum UserRole { + ADMIN = 'ADMIN', + READER = 'READER', +} + +class User { + @EnumFallback({ + type: UserRole, + fallback: (value: UserRole) => UserRole.READER // if an array element is not "ADMIN" or "READER", then the role will be "READER". + }) + public roles: UserRole[]; +} +``` diff --git a/packages/class-transformers/jest.config.js b/packages/class-transformers/jest.config.js new file mode 100644 index 00000000..d2f09a13 --- /dev/null +++ b/packages/class-transformers/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + ...require('../jest.common'), + + coverageDirectory: "coverage", + collectCoverageFrom: ["./src/**/*.ts"], + + rootDir: ".", +}; \ No newline at end of file diff --git a/packages/class-transformers/package.json b/packages/class-transformers/package.json new file mode 100644 index 00000000..c65128a6 --- /dev/null +++ b/packages/class-transformers/package.json @@ -0,0 +1,38 @@ +{ + "name": "@algoan/nestjs-class-transformers", + "version": "1.0.0", + "description": "Class transformers for NestJS", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", + "scripts": { + "build": "tsc -p .", + "test:cov": "jest --coverage", + "test": "jest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/algoan/nestjs-components.git", + "directory": "packages/class-transformers" + }, + "files": [ + "dist/src" + ], + "keywords": [ + "nodejs", + "typescript", + "nestjs", + "class-transformers" + ], + "author": "Algoan", + "license": "ISC", + "bugs": { + "url": "https://github.com/algoan/nestjs-components.git" + }, + "homepage": "https://github.com/algoan/nestjs-components/packages/nestjs-class-transformers", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "class-transformer": "^0.5.1" + } +} diff --git a/packages/class-transformers/src/enum-fallback.decorator.ts b/packages/class-transformers/src/enum-fallback.decorator.ts new file mode 100644 index 00000000..c5e4ad16 --- /dev/null +++ b/packages/class-transformers/src/enum-fallback.decorator.ts @@ -0,0 +1,48 @@ +import { Transform, TransformOptions } from 'class-transformer'; + +/** + * Options for EnumFallback decorator. + */ +export interface EnumFallbackOptions { + /** + * The enum type to check against. + */ + type: { [s: string]: T }; + /** + * A function that returns the fallback value. + * @param value The invalid value. + */ + fallback: (value: T) => T; +} + +/** + * Return given literal value if it is included in the specific enum type. + * Otherwise, return the value provided by the given fallback function. + * If the value is an array, the fallback function will be applied to each element. + */ +export const EnumFallback = ( + params: EnumFallbackOptions, + transformOptions?: TransformOptions, +): PropertyDecorator => + Transform((transformParams) => { + const value = transformParams.value; + + if (Array.isArray(value)) { + return value.map((element) => transformEnumValue(element, params)); + } else { + return transformEnumValue(value, params); + } + }, transformOptions); + +const transformEnumValue = (value: T, { type, fallback }: EnumFallbackOptions): T => { + // eslint-disable-next-line no-null/no-null + if (value === undefined || value === null) { + return value; + } + + if (!Object.values(type).includes(value)) { + return fallback(value); + } + + return value; +}; diff --git a/packages/class-transformers/src/index.ts b/packages/class-transformers/src/index.ts new file mode 100644 index 00000000..c2c85fdd --- /dev/null +++ b/packages/class-transformers/src/index.ts @@ -0,0 +1,2 @@ +export * from './enum-fallback.decorator'; +export * from 'class-transformer'; diff --git a/packages/class-transformers/test/enum-fallback-decorator.test.ts b/packages/class-transformers/test/enum-fallback-decorator.test.ts new file mode 100644 index 00000000..68e8b6bb --- /dev/null +++ b/packages/class-transformers/test/enum-fallback-decorator.test.ts @@ -0,0 +1,101 @@ +/* eslint-disable no-null/no-null */ +/* eslint-disable max-classes-per-file */ +import { plainToInstance } from 'class-transformer'; +import { EnumFallback } from '../src'; + +describe('EnumFallback Decorator', () => { + enum UserRole { + ADMIN = 'ADMIN', + READER = 'READER', + } + + it('should return the given value if it is valid', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: 'ADMIN' }); + + expect(user.role).toEqual(UserRole.ADMIN); + }); + + it('should return the fallback value if the given value is invalid', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: 'WRITER' }); + + expect(user.role).toEqual(UserRole.READER); + }); + + it('should return undefined if the given value is undefined', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: undefined }); + + expect(user.role).toBeUndefined(); + }); + + it('should return undefined if the given value is null', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: null }); + + expect(user.role).toBe(null); + }); + + it('should take into account the transform options', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }, { groups: ['group1'] }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: 'WRITER' }, { groups: ['group2'] }); + + expect(user.role).toEqual('WRITER'); + }); + + it('should return the fallback value for each invalid element if the property is an array', async () => { + class User { + @EnumFallback({ type: UserRole, fallback: () => UserRole.READER }) + public roles?: UserRole[]; + } + + const user = plainToInstance(User, { roles: ['WRITER', 'ADMIN'] }); + + expect(user.roles).toEqual([UserRole.READER, UserRole.ADMIN]); + }); + + it('should allow to run side effects in fallback function if the given value is invalid', async () => { + const log = jest.fn(); + // eslint-disable-next-line no-console + console.log = log; + + class User { + @EnumFallback({ + type: UserRole, + fallback: () => { + // eslint-disable-next-line no-console + console.log('fallback function called'); + + return UserRole.READER; + }, + }) + public role?: UserRole; + } + + const user = plainToInstance(User, { role: 'WRITER' }); + + expect(user.role).toEqual(UserRole.READER); + expect(log).toHaveBeenCalledWith('fallback function called'); + }); +}); diff --git a/packages/class-transformers/tsconfig.json b/packages/class-transformers/tsconfig.json new file mode 100644 index 00000000..3fa02cc1 --- /dev/null +++ b/packages/class-transformers/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "experimentalDecorators": true, + "outDir": "dist", + }, + "include": [ + "./src/**/*", + "./test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] + } \ No newline at end of file