-
Notifications
You must be signed in to change notification settings - Fork 40
[CLASS_TRANSFORMERS] Extends class-transform package with EnumFallback decorator #837
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
Changes from all commits
c895953
67ba1ed
7213b43
6d649d5
aa9bc43
1f6ed44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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[]; | ||
} | ||
``` |
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: ".", | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so let the client handle it himself. |
||
} | ||
|
||
return value; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './enum-fallback.decorator'; | ||
export * from 'class-transformer'; |
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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
}); | ||
}); |
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" | ||
] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.