Skip to content

Commit f388c9f

Browse files
committed
feat(event-handler): add decorator functioanlity for error handlers
1 parent b74b3b4 commit f388c9f

File tree

2 files changed

+237
-11
lines changed

2 files changed

+237
-11
lines changed

packages/event-handler/src/rest/BaseRouter.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,68 @@ abstract class BaseRouter {
6868
public errorHandler<T extends Error>(
6969
errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
7070
handler: ErrorHandler<T>
71-
): void {
72-
this.errorHandlerRegistry.register(errorType, handler);
71+
): void;
72+
public errorHandler<T extends Error>(
73+
errorType: ErrorConstructor<T> | ErrorConstructor<T>[]
74+
): MethodDecorator;
75+
public errorHandler<T extends Error>(
76+
errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
77+
handler?: ErrorHandler<T>
78+
): MethodDecorator | undefined {
79+
if (handler && typeof handler === 'function') {
80+
this.errorHandlerRegistry.register(errorType, handler);
81+
return;
82+
}
83+
84+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
85+
this.errorHandlerRegistry.register(errorType, descriptor?.value);
86+
return descriptor;
87+
};
7388
}
7489

7590
/**
7691
* Registers a custom handler for 404 Not Found errors.
7792
*
7893
* @param handler - The error handler function for NotFoundError
7994
*/
80-
public notFound(handler: ErrorHandler<NotFoundError>): void {
81-
this.errorHandlerRegistry.register(NotFoundError, handler);
95+
public notFound(handler: ErrorHandler<NotFoundError>): void;
96+
public notFound(): MethodDecorator;
97+
public notFound(
98+
handler?: ErrorHandler<NotFoundError>
99+
): MethodDecorator | undefined {
100+
if (handler && typeof handler === 'function') {
101+
this.errorHandlerRegistry.register(NotFoundError, handler);
102+
return;
103+
}
104+
105+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
106+
this.errorHandlerRegistry.register(NotFoundError, descriptor?.value);
107+
return descriptor;
108+
};
82109
}
83110

84111
/**
85112
* Registers a custom handler for 405 Method Not Allowed errors.
86113
*
87114
* @param handler - The error handler function for MethodNotAllowedError
88115
*/
89-
public methodNotAllowed(handler: ErrorHandler<MethodNotAllowedError>): void {
90-
this.errorHandlerRegistry.register(MethodNotAllowedError, handler);
116+
public methodNotAllowed(handler: ErrorHandler<MethodNotAllowedError>): void;
117+
public methodNotAllowed(): MethodDecorator;
118+
public methodNotAllowed(
119+
handler?: ErrorHandler<MethodNotAllowedError>
120+
): MethodDecorator | undefined {
121+
if (handler && typeof handler === 'function') {
122+
this.errorHandlerRegistry.register(MethodNotAllowedError, handler);
123+
return;
124+
}
125+
126+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
127+
this.errorHandlerRegistry.register(
128+
MethodNotAllowedError,
129+
descriptor?.value
130+
);
131+
return descriptor;
132+
};
91133
}
92134

93135
public abstract resolve(
@@ -110,13 +152,17 @@ abstract class BaseRouter {
110152
* back to a default handler.
111153
*
112154
* @param error - The error to handle
155+
* @param options - Optional resolve options for scope binding
113156
* @returns A Response object with appropriate status code and error details
114157
*/
115-
protected async handleError(error: Error): Promise<Response> {
158+
protected async handleError(
159+
error: Error,
160+
options?: ResolveOptions
161+
): Promise<Response> {
116162
const handler = this.errorHandlerRegistry.resolve(error);
117163
if (handler !== null) {
118164
try {
119-
const body = await handler(error);
165+
const body = await handler.apply(options?.scope ?? this, [error]);
120166
return new Response(JSON.stringify(body), {
121167
status: body.statusCode,
122168
headers: { 'Content-Type': 'application/json' },

packages/event-handler/tests/unit/rest/BaseRouter.test.ts

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,20 @@ describe('Class: BaseRouter', () => {
4040
}
4141
}
4242

43-
public async resolve(event: unknown, context: Context): Promise<unknown> {
43+
public async resolve(
44+
event: unknown,
45+
context: Context,
46+
options?: any
47+
): Promise<unknown> {
4448
this.#isEvent(event);
4549
const { method, path } = event;
4650
const route = this.routeRegistry.resolve(method, path);
4751
try {
4852
if (route == null)
4953
throw new NotFoundError(`Route ${method} ${path} not found`);
50-
return route.handler(event, context);
54+
return await route.handler(event, context);
5155
} catch (error) {
52-
return await this.handleError(error as Error);
56+
return await this.handleError(error as Error, options);
5357
}
5458
}
5559
}
@@ -589,5 +593,181 @@ describe('Class: BaseRouter', () => {
589593
// Assess
590594
expect(result.headers.get('Content-Type')).toBe('application/json');
591595
});
596+
597+
describe('decorators', () => {
598+
it('works with errorHandler decorator', async () => {
599+
// Prepare
600+
const app = new TestResolver();
601+
602+
class Lambda {
603+
@app.errorHandler(BadRequestError)
604+
public async handleBadRequest(error: BadRequestError) {
605+
return {
606+
statusCode: HttpErrorCodes.BAD_REQUEST,
607+
error: 'Bad Request',
608+
message: `Decorated: ${error.message}`,
609+
};
610+
}
611+
612+
@app.get('/test')
613+
public async getTest() {
614+
throw new BadRequestError('test error');
615+
}
616+
617+
public async handler(event: unknown, context: Context) {
618+
return app.resolve(event, context);
619+
}
620+
}
621+
622+
const lambda = new Lambda();
623+
624+
// Act
625+
const result = (await lambda.handler(
626+
{ path: '/test', method: 'GET' },
627+
context
628+
)) as Response;
629+
630+
// Assess
631+
expect(result).toBeInstanceOf(Response);
632+
expect(result.status).toBe(HttpErrorCodes.BAD_REQUEST);
633+
expect(await result.text()).toBe(
634+
JSON.stringify({
635+
statusCode: HttpErrorCodes.BAD_REQUEST,
636+
error: 'Bad Request',
637+
message: 'Decorated: test error',
638+
})
639+
);
640+
});
641+
642+
it('works with notFound decorator', async () => {
643+
// Prepare
644+
const app = new TestResolver();
645+
646+
class Lambda {
647+
@app.notFound()
648+
public async handleNotFound(error: NotFoundError) {
649+
return {
650+
statusCode: HttpErrorCodes.NOT_FOUND,
651+
error: 'Not Found',
652+
message: `Decorated: ${error.message}`,
653+
};
654+
}
655+
656+
public async handler(event: unknown, context: Context) {
657+
return app.resolve(event, context);
658+
}
659+
}
660+
661+
const lambda = new Lambda();
662+
663+
// Act
664+
const result = (await lambda.handler(
665+
{ path: '/nonexistent', method: 'GET' },
666+
context
667+
)) as Response;
668+
669+
// Assess
670+
expect(result).toBeInstanceOf(Response);
671+
expect(result.status).toBe(HttpErrorCodes.NOT_FOUND);
672+
expect(await result.text()).toBe(
673+
JSON.stringify({
674+
statusCode: HttpErrorCodes.NOT_FOUND,
675+
error: 'Not Found',
676+
message: 'Decorated: Route GET /nonexistent not found',
677+
})
678+
);
679+
});
680+
681+
it('works with methodNotAllowed decorator', async () => {
682+
// Prepare
683+
const app = new TestResolver();
684+
685+
class Lambda {
686+
@app.methodNotAllowed()
687+
public async handleMethodNotAllowed(error: MethodNotAllowedError) {
688+
return {
689+
statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED,
690+
error: 'Method Not Allowed',
691+
message: `Decorated: ${error.message}`,
692+
};
693+
}
694+
695+
@app.get('/test')
696+
public async getTest() {
697+
throw new MethodNotAllowedError('POST not allowed');
698+
}
699+
700+
public async handler(event: unknown, context: Context) {
701+
return app.resolve(event, context);
702+
}
703+
}
704+
705+
const lambda = new Lambda();
706+
707+
// Act
708+
const result = (await lambda.handler(
709+
{ path: '/test', method: 'GET' },
710+
context
711+
)) as Response;
712+
713+
// Assess
714+
expect(result).toBeInstanceOf(Response);
715+
expect(result.status).toBe(HttpErrorCodes.METHOD_NOT_ALLOWED);
716+
expect(await result.text()).toBe(
717+
JSON.stringify({
718+
statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED,
719+
error: 'Method Not Allowed',
720+
message: 'Decorated: POST not allowed',
721+
})
722+
);
723+
});
724+
725+
it('preserves scope when using error handler decorators', async () => {
726+
// Prepare
727+
const app = new TestResolver();
728+
729+
class Lambda {
730+
public scope = 'scoped';
731+
732+
@app.errorHandler(BadRequestError)
733+
public async handleBadRequest(error: BadRequestError) {
734+
return {
735+
statusCode: HttpErrorCodes.BAD_REQUEST,
736+
error: 'Bad Request',
737+
message: `${this.scope}: ${error.message}`,
738+
};
739+
}
740+
741+
@app.get('/test')
742+
public async getTest() {
743+
throw new BadRequestError('test error');
744+
}
745+
746+
public async handler(event: unknown, context: Context) {
747+
return app.resolve(event, context, { scope: this });
748+
}
749+
}
750+
751+
const lambda = new Lambda();
752+
const handler = lambda.handler.bind(lambda);
753+
754+
// Act
755+
const result = (await handler(
756+
{ path: '/test', method: 'GET' },
757+
context
758+
)) as Response;
759+
760+
// Assess
761+
expect(result).toBeInstanceOf(Response);
762+
expect(result.status).toBe(HttpErrorCodes.BAD_REQUEST);
763+
expect(await result.text()).toBe(
764+
JSON.stringify({
765+
statusCode: HttpErrorCodes.BAD_REQUEST,
766+
error: 'Bad Request',
767+
message: 'scoped: test error',
768+
})
769+
);
770+
});
771+
});
592772
});
593773
});

0 commit comments

Comments
 (0)