Skip to content

Commit 5f13aa2

Browse files
committed
Implemented enum parsing for exchange oracle
1 parent 4872c39 commit 5f13aa2

File tree

12 files changed

+560
-30
lines changed

12 files changed

+560
-30
lines changed

packages/apps/fortune/exchange-oracle/server/src/app.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ import { CronJobModule } from './modules/cron-job/cron-job.module';
1515
import { HealthModule } from './modules/health/health.module';
1616
import { EnvConfigModule } from './common/config/config.module';
1717
import { ScheduleModule } from '@nestjs/schedule';
18+
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
1819

1920
@Module({
2021
providers: [
2122
{
2223
provide: APP_INTERCEPTOR,
2324
useClass: SnakeCaseInterceptor,
2425
},
26+
{
27+
provide: APP_INTERCEPTOR,
28+
useClass: TransformEnumInterceptor,
29+
},
2530
JwtHttpStrategy,
2631
],
2732
imports: [

packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export enum JobStatus {
2-
ACTIVE = 'ACTIVE',
3-
COMPLETED = 'COMPLETED',
4-
CANCELED = 'CANCELED',
2+
ACTIVE = 'active',
3+
COMPLETED = 'completed',
4+
CANCELED = 'canceled',
55
}
66

77
export enum JobSortField {
@@ -22,12 +22,12 @@ export enum JobFieldName {
2222
}
2323

2424
export enum AssignmentStatus {
25-
ACTIVE = 'ACTIVE',
26-
VALIDATION = 'VALIDATION',
27-
COMPLETED = 'COMPLETED',
28-
EXPIRED = 'EXPIRED',
29-
CANCELED = 'CANCELED',
30-
REJECTED = 'REJECTED',
25+
ACTIVE = 'active',
26+
VALIDATION = 'validation',
27+
COMPLETED = 'completed',
28+
EXPIRED = 'expired',
29+
CANCELED = 'canceled',
30+
REJECTED = 'rejected',
3131
}
3232

3333
export enum AssignmentSortField {
@@ -40,5 +40,5 @@ export enum AssignmentSortField {
4040
}
4141

4242
export enum JobType {
43-
FORTUNE = 'FORTUNE',
44-
}
43+
FORTUNE = 'fortune',
44+
}

packages/apps/fortune/exchange-oracle/server/src/common/enums/role.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export enum AuthSignatureRole {
66
}
77

88
export enum Role {
9-
Worker = 'WORKER',
10-
HumanApp = 'HUMAN_APP',
9+
Worker = 'worker',
10+
HumanApp = 'human_app',
1111
}

packages/apps/fortune/exchange-oracle/server/src/common/enums/webhook.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export enum EventType {
88
}
99

1010
export enum WebhookStatus {
11-
PENDING = 'PENDING',
12-
COMPLETED = 'COMPLETED',
13-
FAILED = 'FAILED',
11+
PENDING = 'pending',
12+
COMPLETED = 'completed',
13+
FAILED = 'failed',
1414
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { TransformEnumInterceptor } from './transform-enum.interceptor';
2+
import {
3+
ExecutionContext,
4+
CallHandler,
5+
BadRequestException,
6+
} from '@nestjs/common';
7+
import { of } from 'rxjs';
8+
import { IsNumber, IsString, Min } from 'class-validator';
9+
import { JobType } from '../../common/enums/job';
10+
import { ApiProperty } from '@nestjs/swagger';
11+
import { IsEnumCaseInsensitive } from '../utils/enums';
12+
13+
export class MockDto {
14+
@ApiProperty({
15+
enum: JobType,
16+
})
17+
@IsEnumCaseInsensitive(JobType)
18+
public jobType: JobType;
19+
20+
@ApiProperty()
21+
@IsNumber()
22+
@Min(0.5)
23+
public amount: number;
24+
25+
@ApiProperty()
26+
@IsString()
27+
public address: string;
28+
}
29+
30+
describe('TransformEnumInterceptor', () => {
31+
let interceptor: TransformEnumInterceptor;
32+
let executionContext: ExecutionContext;
33+
let callHandler: CallHandler;
34+
35+
beforeEach(() => {
36+
interceptor = new TransformEnumInterceptor();
37+
38+
// Mocking ExecutionContext and CallHandler
39+
executionContext = {
40+
switchToHttp: jest.fn().mockReturnValue({
41+
getRequest: jest.fn().mockReturnValue({
42+
body: {
43+
jobType: 'FORTUNE',
44+
amount: 5,
45+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
46+
},
47+
}),
48+
}),
49+
getHandler: jest.fn().mockReturnValue({
50+
name: 'create', // Assume the handler is named 'create'
51+
}),
52+
getClass: jest.fn().mockReturnValue({
53+
prototype: {},
54+
}),
55+
} as unknown as ExecutionContext;
56+
57+
callHandler = {
58+
handle: jest.fn().mockReturnValue(of({})),
59+
};
60+
61+
// Mock Reflect.getMetadata to return DTO and Enum types
62+
Reflect.getMetadata = jest.fn((metadataKey, target, propertyKey) => {
63+
// Mock design:paramtypes to return MockDto as the parameter type
64+
if (metadataKey === 'design:paramtypes') {
65+
return [MockDto];
66+
}
67+
68+
// Mock custom:enum to return the corresponding enum for each property
69+
if (metadataKey === 'custom:enum' && propertyKey === 'jobType') {
70+
return JobType;
71+
}
72+
return undefined; // For non-enum properties, return undefined
73+
}) as any;
74+
});
75+
76+
it('should transform enum values to lowercase', async () => {
77+
// Run the interceptor
78+
await interceptor.intercept(executionContext, callHandler).toPromise();
79+
80+
// Access the modified request body
81+
const request = executionContext.switchToHttp().getRequest();
82+
83+
// Expectations
84+
expect(request.body.jobType).toBe('fortune'); // Should be transformed to lowercase
85+
expect(request.body).toEqual({
86+
jobType: 'fortune',
87+
amount: 5,
88+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
89+
});
90+
expect(callHandler.handle).toBeCalled(); // Ensure the handler is called
91+
});
92+
93+
it('should throw an error if the value is not a valid enum', async () => {
94+
// Modify the request body to have an invalid enum value for jobType
95+
executionContext.switchToHttp = jest.fn().mockReturnValue({
96+
getRequest: jest.fn().mockReturnValue({
97+
body: {
98+
jobType: 'invalidEnum', // Invalid enum value for jobType
99+
amount: 5,
100+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
101+
},
102+
}),
103+
});
104+
105+
try {
106+
// Run the interceptor
107+
await interceptor.intercept(executionContext, callHandler).toPromise();
108+
} catch (err) {
109+
// Expect an error to be thrown
110+
expect(err).toBeInstanceOf(BadRequestException);
111+
expect(err.response.statusCode).toBe(400);
112+
expect(err.response.message).toContain('Validation failed');
113+
}
114+
});
115+
116+
it('should not transform non-enum properties', async () => {
117+
// Run the interceptor with a non-enum property (amount and address)
118+
await interceptor.intercept(executionContext, callHandler).toPromise();
119+
120+
// Access the modified request body
121+
const request = executionContext.switchToHttp().getRequest();
122+
123+
// Expectations
124+
expect(request.body.amount).toBe(5); // Non-enum property should remain unchanged
125+
expect(request.body.address).toBe(
126+
'0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
127+
); // Non-enum string should remain unchanged
128+
expect(callHandler.handle).toBeCalled();
129+
});
130+
131+
it('should handle nested objects with enums', async () => {
132+
// Modify the request body to have a nested object with enum value
133+
executionContext.switchToHttp = jest.fn().mockReturnValue({
134+
getRequest: jest.fn().mockReturnValue({
135+
body: {
136+
transaction: {
137+
jobType: 'FORTUNE',
138+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
139+
},
140+
amount: 5,
141+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
142+
},
143+
}),
144+
});
145+
146+
// Run the interceptor
147+
await interceptor.intercept(executionContext, callHandler).toPromise();
148+
149+
// Access the modified request body
150+
const request = executionContext.switchToHttp().getRequest();
151+
152+
// Expectations
153+
expect(request.body.transaction.jobType).toBe('fortune');
154+
expect(request.body).toEqual({
155+
transaction: {
156+
jobType: 'fortune',
157+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
158+
},
159+
amount: 5,
160+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
161+
});
162+
expect(callHandler.handle).toHaveBeenCalled();
163+
});
164+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
Injectable,
3+
NestInterceptor,
4+
ExecutionContext,
5+
CallHandler,
6+
BadRequestException,
7+
} from '@nestjs/common';
8+
import { Observable } from 'rxjs';
9+
import { map } from 'rxjs/operators';
10+
import { plainToInstance, ClassConstructor } from 'class-transformer';
11+
import { validateSync } from 'class-validator';
12+
import 'reflect-metadata';
13+
14+
@Injectable()
15+
export class TransformEnumInterceptor implements NestInterceptor {
16+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
17+
const request = context.switchToHttp().getRequest();
18+
const body = request.body;
19+
20+
// Retrieve the class of the controller's DTO (if applicable)
21+
const targetClass = this.getTargetClass(context);
22+
23+
if (targetClass && body) {
24+
request.body = this.transformEnums(body, targetClass);
25+
}
26+
27+
return next.handle().pipe(map((data) => data));
28+
}
29+
30+
private getTargetClass(
31+
context: ExecutionContext,
32+
): ClassConstructor<any> | null {
33+
const handler = context.getHandler();
34+
const controller = context.getClass();
35+
36+
// Get the parameter types of the route handler
37+
const routeArgs = Reflect.getMetadata(
38+
'design:paramtypes',
39+
controller.prototype,
40+
handler.name,
41+
);
42+
43+
// Return the first parameter's constructor if the handler has a class (DTO)
44+
return routeArgs && routeArgs.length > 0
45+
? (routeArgs[0] as ClassConstructor<any>)
46+
: null;
47+
}
48+
49+
private transformEnums(body: any, targetClass: ClassConstructor<any>): any {
50+
// Convert the body to an instance of the target class
51+
let transformedInstance = plainToInstance(targetClass, body);
52+
53+
// Transform the enums before validation
54+
transformedInstance = this.lowercaseEnumProperties(
55+
body,
56+
transformedInstance,
57+
targetClass,
58+
);
59+
60+
// Validate the transformed body
61+
const validationErrors = validateSync(transformedInstance);
62+
if (validationErrors.length > 0) {
63+
throw new BadRequestException('Validation failed');
64+
}
65+
66+
return body;
67+
}
68+
69+
private lowercaseEnumProperties(
70+
body: any,
71+
instance: any,
72+
targetClass: ClassConstructor<any>,
73+
): any {
74+
for (const property in body) {
75+
if (body.hasOwnProperty(property)) {
76+
const instanceValue = instance[property];
77+
78+
// Retrieve enum metadata if available
79+
const enumType = Reflect.getMetadata(
80+
'custom:enum',
81+
targetClass.prototype,
82+
property,
83+
);
84+
85+
if (enumType && typeof instanceValue === 'string') {
86+
// Check if it's an enum and convert to lowercase
87+
if (Object.values(enumType).includes(instanceValue.toLowerCase())) {
88+
body[property] = instanceValue.toLowerCase();
89+
}
90+
} else if (
91+
typeof body[property] === 'object' &&
92+
!Array.isArray(body[property])
93+
) {
94+
// Recursively handle nested objects
95+
this.lowercaseEnumProperties(
96+
body[property],
97+
instance[property],
98+
targetClass,
99+
);
100+
}
101+
}
102+
}
103+
return body;
104+
}
105+
}

packages/apps/fortune/exchange-oracle/server/src/common/pagination/pagination.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from 'class-validator';
1010
import { Type } from 'class-transformer';
1111
import { SortDirection } from '../enums/collection';
12+
import { IsEnumCaseInsensitive } from '../utils/enums';
1213

1314
export class PageDto<T> {
1415
@ApiProperty()
@@ -66,7 +67,7 @@ export abstract class PageOptionsDto {
6667
pageSize?: number = 5;
6768

6869
@ApiPropertyOptional({ enum: SortDirection, default: SortDirection.ASC })
69-
@IsEnum(SortDirection)
70+
@IsEnumCaseInsensitive(SortDirection)
7071
@IsOptional()
7172
sort?: SortDirection = SortDirection.ASC;
7273

0 commit comments

Comments
 (0)