-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: use a transformer in the enumfallback method instead of a v…
…alidator
- Loading branch information
Showing
6 changed files
with
175 additions
and
67 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,5 +19,8 @@ | |
"diagnostics": false, | ||
"isolatedModules": true | ||
} | ||
}, | ||
"peerDependencies": { | ||
"@nestjs/common": ">=8" | ||
} | ||
} |
43 changes: 18 additions & 25 deletions
43
packages/class-transformers/src/enum-fallback.decorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,27 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import { ValidationArguments, ValidationOptions, registerDecorator } from 'class-validator'; | ||
import { applyDecorators } from '@nestjs/common'; | ||
import { Transform, TransformOptions } from 'class-transformer'; | ||
|
||
/** | ||
* Checks if a given value is the member of the provided enum and call a fallback function | ||
* if the value is invalid | ||
*/ | ||
export function EnumFallback(enumType: any, fallback: (value: any) => any, validationOptions?: ValidationOptions) { | ||
return function (object: Record<string, any>, propertyName: string) { | ||
registerDecorator({ | ||
name: 'enumFallback', | ||
target: object.constructor, | ||
propertyName, | ||
constraints: [enumType, fallback], | ||
options: validationOptions, | ||
validator: { | ||
validate(value: any, args: ValidationArguments) { | ||
const [enumTypeValue, fallbackFn] = args.constraints; | ||
if (value === undefined || !Object.values(enumTypeValue).includes(value)) { | ||
// eslint-disable-next-line | ||
const updatedObject = Object.assign({}, args.object, { | ||
[propertyName]: fallbackFn(value), | ||
}); | ||
// eslint-disable-next-line | ||
Object.assign(args.object, updatedObject); | ||
} | ||
interface EnumFallbackOptions<T> { | ||
type: unknown; | ||
fallback: (value: T) => T; | ||
} | ||
|
||
export function EnumFallback<T>(options: EnumFallbackOptions<T>, transformOptions?: TransformOptions) { | ||
const { type, fallback } = options; | ||
|
||
return applyDecorators( | ||
Transform(({ value }) => { | ||
if (value === undefined || !Object.values(type as string[]).includes(value)) { | ||
return fallback(value); | ||
} | ||
|
||
return true; | ||
}, | ||
}, | ||
}); | ||
}; | ||
return value; | ||
}, transformOptions), | ||
); | ||
} |
55 changes: 20 additions & 35 deletions
55
packages/class-transformers/test/enum-fallback-decorator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,29 @@ | ||
import { EnumFallback } from '../src'; | ||
import { ArgumentMetadata, ValidationPipe } from '@nestjs/common'; | ||
|
||
enum UserRole { | ||
ADMIN = 'ADMIN', | ||
READER = 'READER', | ||
} | ||
|
||
class UserDto { | ||
@EnumFallback(UserRole, (_value: UserRole) => UserRole.READER) | ||
public role?: UserRole; | ||
} | ||
import { INestApplication } from '@nestjs/common'; | ||
import { FakeAppController, UserRole, createTestAppModule } from './helpers'; | ||
import * as request from 'supertest'; | ||
|
||
describe('EnumFallback Decorator', () => { | ||
it('should apply the fallback functions when we have an invalid value', async () => { | ||
const userDto = new UserDto(); | ||
userDto.role = 'WRITER' as UserRole; | ||
let app: INestApplication; | ||
let appController: FakeAppController; | ||
|
||
let target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true }); | ||
const metadata: ArgumentMetadata = { | ||
type: 'body', | ||
metatype: UserDto, | ||
data: '', | ||
}; | ||
const result = await target.transform(userDto, metadata); | ||
|
||
expect(result.role).toEqual(UserRole.READER); | ||
beforeAll(async () => { | ||
app = await createTestAppModule(); | ||
await app.init(); | ||
}); | ||
afterAll(async () => { | ||
await app.close(); | ||
}); | ||
|
||
it('should not apply the fallback function when the value is valid', async () => { | ||
const userDto = new UserDto(); | ||
userDto.role = UserRole.ADMIN; | ||
beforeEach(async () => { | ||
appController = app.get<FakeAppController>(FakeAppController); | ||
}); | ||
|
||
let target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true }); | ||
const metadata: ArgumentMetadata = { | ||
type: 'body', | ||
metatype: UserDto, | ||
data: '', | ||
}; | ||
const result = await target.transform(userDto, metadata); | ||
it('should apply the fallback functions when we have an invalid value', async () => { | ||
const result = appController.createUser({ role: 'WRITER' as UserRole }); | ||
expect((result as { role: UserRole }).role).toEqual(UserRole.ADMIN); | ||
}); | ||
|
||
expect(result.role).toBe(UserRole.ADMIN); | ||
it('should not apply the fallback function when the value is valid', async () => { | ||
await request(app.getHttpServer()).post('/user').send({ role: 'READER' }).expect(201); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { | ||
Body, | ||
Controller, | ||
ExecutionContext, | ||
INestApplication, | ||
Logger, | ||
Module, | ||
Post, | ||
ValidationPipe, | ||
} from '@nestjs/common'; | ||
import { EnumFallback } from '../src'; | ||
import { APP_GUARD } from '@nestjs/core'; | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
|
||
const fakeJWT: string = | ||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IddsqkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; | ||
|
||
export enum UserRole { | ||
ADMIN = 'ADMIN', | ||
READER = 'READER', | ||
} | ||
|
||
export class UserDto { | ||
private static readonly logger: Logger = new Logger(UserDto.name); | ||
|
||
@EnumFallback({ | ||
type: UserRole, | ||
fallback: (value: UserRole) => { | ||
UserDto.logger.error(`Invalid user role ${value}`); | ||
return UserRole.ADMIN; | ||
}, | ||
}) | ||
public role?: UserRole; | ||
} | ||
|
||
/** | ||
* Mock of the AuthGuard class | ||
*/ | ||
class AuthGuardMock { | ||
/** | ||
* Check token | ||
*/ | ||
public canActivate(context: ExecutionContext): boolean { | ||
const request: { user: { someId: string }; accessTokenJWT: string } = context.switchToHttp().getRequest(); | ||
request.user = { | ||
someId: 'id', | ||
}; | ||
request.accessTokenJWT = fakeJWT; | ||
return true; | ||
} | ||
} | ||
|
||
/** | ||
* Test Controller | ||
*/ | ||
@Controller() | ||
export class FakeAppController { | ||
@Post('/user') | ||
public createUser(@Body() user: UserDto): unknown { | ||
console.log('[AL] user', user); | ||
return user; | ||
} | ||
} | ||
|
||
/** | ||
* Fake app module | ||
*/ | ||
/* eslint-disable */ | ||
@Module({ | ||
controllers: [FakeAppController], | ||
providers: [ | ||
{ | ||
provide: APP_GUARD, | ||
useClass: AuthGuardMock, | ||
}, | ||
], | ||
}) | ||
class AppModule {} | ||
/* eslint-enable */ | ||
|
||
export async function createTestAppModule(): Promise<INestApplication> { | ||
const moduleFixture: TestingModule = await Test.createTestingModule({ | ||
imports: [AppModule], | ||
}).compile(); | ||
const app = moduleFixture.createNestApplication({}); | ||
|
||
app.useGlobalPipes( | ||
new ValidationPipe({ | ||
whitelist: true, | ||
}), | ||
); | ||
|
||
return app; | ||
} |