Skip to content
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

[CLASS_TRANSFORMERS] Extends class-transform package with EnumFallback decorator #837

Merged
merged 6 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 21 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
48 changes: 48 additions & 0 deletions packages/class-transformers/README.md
Original file line number Diff line number Diff line change
@@ -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[];
}
```
8 changes: 8 additions & 0 deletions packages/class-transformers/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
...require('../jest.common'),

coverageDirectory: "coverage",
collectCoverageFrom: ["./src/**/*.ts"],

rootDir: ".",
};
38 changes: 38 additions & 0 deletions packages/class-transformers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@algoan/nestjs-class-transformers",
g-ongenae marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}
48 changes: 48 additions & 0 deletions packages/class-transformers/src/enum-fallback.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Transform, TransformOptions } from 'class-transformer';

/**
* Options for EnumFallback decorator.
*/
export interface EnumFallbackOptions<T> {
/**
* 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 = <T>(
params: EnumFallbackOptions<T>,
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 = <T>(value: T, { type, fallback }: EnumFallbackOptions<T>): 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is error in the fallback function, you think we should throw error or return a specific value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so 🤔 Unlike the logging feature, this decorator may have a real impact on the application behaviour. In my opinion, it is more like the function you pass to Array.map for instance. It means that if the given function fails, it is not the responsibility to the library to handle it.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so let the client handle it himself.

}

return value;
};
2 changes: 2 additions & 0 deletions packages/class-transformers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './enum-fallback.decorator';
export * from 'class-transformer';
101 changes: 101 additions & 0 deletions packages/class-transformers/test/enum-fallback-decorator.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a test when the fallback function is more than just returning a value? e.g. verifying a logging as we discussed

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');
});
});
15 changes: 15 additions & 0 deletions packages/class-transformers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"outDir": "dist",
},
"include": [
"./src/**/*",
"./test/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Loading