From c895953255334c54ff4085a4bb6c562d22f76626 Mon Sep 17 00:00:00 2001 From: Salim Ben Dakhlia Date: Tue, 19 Dec 2023 17:26:47 +0100 Subject: [PATCH 1/6] feat(class-transformers): add the EnumFallback decorator --- package-lock.json | 25 +++++-- package.json | 4 +- packages/class-transformers/README.md | 30 +++++++++ packages/class-transformers/jest.config.js | 8 +++ packages/class-transformers/package.json | 16 +++++ .../src/enum-fallback.decorator.ts | 40 +++++++++++ packages/class-transformers/src/index.ts | 1 + .../test/enum-fallback-decorator.test.ts | 66 +++++++++++++++++++ packages/class-transformers/tsconfig.json | 15 +++++ 9 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 packages/class-transformers/README.md create mode 100644 packages/class-transformers/jest.config.js create mode 100644 packages/class-transformers/package.json create mode 100644 packages/class-transformers/src/enum-fallback.decorator.ts create mode 100644 packages/class-transformers/src/index.ts create mode 100644 packages/class-transformers/test/enum-fallback-decorator.test.ts create mode 100644 packages/class-transformers/tsconfig.json 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..027dfdda 100644 --- a/package.json +++ b/package.json @@ -89,12 +89,12 @@ } }, "dependencies": { + "@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..54011fbc --- /dev/null +++ b/packages/class-transformers/README.md @@ -0,0 +1,30 @@ +# 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; +} +``` 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..ac5cf851 --- /dev/null +++ b/packages/class-transformers/package.json @@ -0,0 +1,16 @@ +{ + "name": "@algoan/nestjs-class-transformers", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc -p .", + "test:cov": "jest --coverage", + "test": "jest" + }, + "author": "", + "license": "ISC", + "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..a3446275 --- /dev/null +++ b/packages/class-transformers/src/enum-fallback.decorator.ts @@ -0,0 +1,40 @@ +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. + */ +export const EnumFallback = ( + params: EnumFallbackOptions, + transformOptions?: TransformOptions, +): PropertyDecorator => { + const { type, fallback } = params; + + return Transform(({ value }) => { + // 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; + }, transformOptions); +}; diff --git a/packages/class-transformers/src/index.ts b/packages/class-transformers/src/index.ts new file mode 100644 index 00000000..29305aec --- /dev/null +++ b/packages/class-transformers/src/index.ts @@ -0,0 +1 @@ +export * from './enum-fallback.decorator'; 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..f7acc353 --- /dev/null +++ b/packages/class-transformers/test/enum-fallback-decorator.test.ts @@ -0,0 +1,66 @@ +/* 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'); + }); +}); 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 From 67ba1ed1859901a4fee1ffa0c0c7976a6cfd4201 Mon Sep 17 00:00:00 2001 From: Salim Ben Dakhlia Date: Tue, 19 Dec 2023 17:28:13 +0100 Subject: [PATCH 2/6] chore: add new package configuration --- README.md | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) 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.json b/package.json index 027dfdda..7b2a8ea3 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ } }, "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", From 7213b435afeb96bb7180bbbc6e934495bd70c8cf Mon Sep 17 00:00:00 2001 From: Amar Lankri Date: Thu, 21 Dec 2023 13:14:56 +0100 Subject: [PATCH 3/6] feat(class-transformers): expose internal class-transformers package --- packages/class-transformers/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/class-transformers/src/index.ts b/packages/class-transformers/src/index.ts index 29305aec..c2c85fdd 100644 --- a/packages/class-transformers/src/index.ts +++ b/packages/class-transformers/src/index.ts @@ -1 +1,2 @@ export * from './enum-fallback.decorator'; +export * from 'class-transformer'; From 6d649d5366936eb05c1b00ae3c89187f63536522 Mon Sep 17 00:00:00 2001 From: Amar Lankri Date: Thu, 21 Dec 2023 13:46:33 +0100 Subject: [PATCH 4/6] feat(class-transformers): support array for EnumFallback decorator --- packages/class-transformers/README.md | 18 +++++++++++ .../src/enum-fallback.decorator.ts | 30 ++++++++++++------- .../test/enum-fallback-decorator.test.ts | 11 +++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/class-transformers/README.md b/packages/class-transformers/README.md index 54011fbc..3b327983 100644 --- a/packages/class-transformers/README.md +++ b/packages/class-transformers/README.md @@ -28,3 +28,21 @@ class User { 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/src/enum-fallback.decorator.ts b/packages/class-transformers/src/enum-fallback.decorator.ts index a3446275..c5e4ad16 100644 --- a/packages/class-transformers/src/enum-fallback.decorator.ts +++ b/packages/class-transformers/src/enum-fallback.decorator.ts @@ -18,23 +18,31 @@ export interface EnumFallbackOptions { /** * 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 => { - const { type, fallback } = params; +): PropertyDecorator => + Transform((transformParams) => { + const value = transformParams.value; - return Transform(({ value }) => { - // eslint-disable-next-line no-null/no-null - if (value === undefined || value === null) { - return value; - } - - if (!Object.values(type).includes(value)) { - return fallback(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; - }, transformOptions); + } + + if (!Object.values(type).includes(value)) { + return fallback(value); + } + + return value; }; diff --git a/packages/class-transformers/test/enum-fallback-decorator.test.ts b/packages/class-transformers/test/enum-fallback-decorator.test.ts index f7acc353..f5565e91 100644 --- a/packages/class-transformers/test/enum-fallback-decorator.test.ts +++ b/packages/class-transformers/test/enum-fallback-decorator.test.ts @@ -63,4 +63,15 @@ describe('EnumFallback Decorator', () => { 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]); + }); }); From aa9bc43fae9d07c37272cc43a0f7ba9a22280392 Mon Sep 17 00:00:00 2001 From: Amar Lankri Date: Thu, 21 Dec 2023 15:04:21 +0100 Subject: [PATCH 5/6] refactor(class-transformers): fix package.json --- packages/class-transformers/package.json | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/class-transformers/package.json b/packages/class-transformers/package.json index ac5cf851..c65128a6 100644 --- a/packages/class-transformers/package.json +++ b/packages/class-transformers/package.json @@ -1,15 +1,37 @@ { "name": "@algoan/nestjs-class-transformers", "version": "1.0.0", - "description": "", - "main": "index.js", + "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" }, - "author": "", + "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" } From 1f6ed44fe076a89d4aba831a30c8d7f0e23cce75 Mon Sep 17 00:00:00 2001 From: Amar Lankri Date: Fri, 22 Dec 2023 17:44:33 +0100 Subject: [PATCH 6/6] test(class-transformers): add test for enum fallback decorator --- .../test/enum-fallback-decorator.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/class-transformers/test/enum-fallback-decorator.test.ts b/packages/class-transformers/test/enum-fallback-decorator.test.ts index f5565e91..68e8b6bb 100644 --- a/packages/class-transformers/test/enum-fallback-decorator.test.ts +++ b/packages/class-transformers/test/enum-fallback-decorator.test.ts @@ -74,4 +74,28 @@ describe('EnumFallback Decorator', () => { 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'); + }); });