Skip to content

Commit

Permalink
refactor: use a transformer in the enumfallback method instead of a v…
Browse files Browse the repository at this point in the history
…alidator
  • Loading branch information
Bendakh committed Dec 20, 2023
1 parent 0d566b0 commit d3a7ea2
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 67 deletions.
41 changes: 37 additions & 4 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +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-class-transformers": "file:packages/class-transformers"
"@algoan/nestjs-pagination": "file:packages/pagination"
}
}
3 changes: 3 additions & 0 deletions packages/class-transformers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
"diagnostics": false,
"isolatedModules": true
}
},
"peerDependencies": {
"@nestjs/common": ">=8"
}
}
43 changes: 18 additions & 25 deletions packages/class-transformers/src/enum-fallback.decorator.ts
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 packages/class-transformers/test/enum-fallback-decorator.test.ts
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);
});
});
94 changes: 94 additions & 0 deletions packages/class-transformers/test/helpers.ts
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;
}

0 comments on commit d3a7ea2

Please sign in to comment.