Skip to content

Commit 7366df9

Browse files
committed
add methods for all http verbs and unit tests for them
1 parent 4211252 commit 7366df9

File tree

3 files changed

+258
-15
lines changed

3 files changed

+258
-15
lines changed

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

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1+
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
12
import { isRecord } from '@aws-lambda-powertools/commons/typeutils';
23
import {
34
getStringFromEnv,
45
isDevMode,
56
} from '@aws-lambda-powertools/commons/utils/env';
6-
import type { GenericLogger } from '../types/appsync-events.js';
7+
import type { Context } from 'aws-lambda';
8+
import type { ResolveOptions } from '../types/index.js';
79
import type {
810
RouteHandler,
911
RouteOptions,
1012
RouterOptions,
1113
} from '../types/rest.js';
1214

15+
const HttpVerbs = [
16+
'GET',
17+
'POST',
18+
'PUT',
19+
'PATCH',
20+
'DELETE',
21+
'HEAD',
22+
'OPTIONS',
23+
] as const;
24+
1325
abstract class BaseRouter {
1426
protected context: Record<string, unknown>; // TODO: should this be a map instead?
1527
/**
@@ -37,34 +49,107 @@ abstract class BaseRouter {
3749
this.isDev = isDevMode();
3850
}
3951

52+
public abstract resolve(
53+
event: unknown,
54+
context: Context,
55+
options?: ResolveOptions
56+
): Promise<unknown>;
57+
4058
public abstract route(handler: RouteHandler, options: RouteOptions): void;
4159

42-
public get(path: string, handler: RouteHandler, options?: RouteOptions): void;
43-
public get(path: string, options?: RouteOptions): MethodDecorator;
44-
public get(
60+
#handleHttpMethod(
61+
method: (typeof HttpVerbs)[number],
4562
path: string,
4663
handler?: RouteHandler | RouteOptions,
4764
options?: RouteOptions
4865
): MethodDecorator | undefined {
4966
if (handler && typeof handler === 'function') {
50-
this.route(handler, {
51-
...(options || {}),
52-
method: 'GET',
53-
path,
54-
});
67+
this.route(handler, { ...(options || {}), method, path });
5568
return;
5669
}
5770

5871
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
5972
const routeOptions = isRecord(handler) ? handler : options;
60-
this.route(descriptor.value, {
61-
...(routeOptions || {}),
62-
method: 'GET',
63-
path,
64-
});
73+
this.route(descriptor.value, { ...(routeOptions || {}), method, path });
6574
return descriptor;
6675
};
6776
}
77+
78+
public get(path: string, handler: RouteHandler, options?: RouteOptions): void;
79+
public get(path: string, options?: RouteOptions): MethodDecorator;
80+
public get(
81+
path: string,
82+
handler?: RouteHandler | RouteOptions,
83+
options?: RouteOptions
84+
): MethodDecorator | undefined {
85+
return this.#handleHttpMethod('GET', path, handler, options);
86+
}
87+
88+
public post(
89+
path: string,
90+
handler: RouteHandler,
91+
options?: RouteOptions
92+
): void;
93+
public post(path: string, options?: RouteOptions): MethodDecorator;
94+
public post(
95+
path: string,
96+
handler?: RouteHandler | RouteOptions,
97+
options?: RouteOptions
98+
): MethodDecorator | undefined {
99+
return this.#handleHttpMethod('POST', path, handler, options);
100+
}
101+
102+
public put(path: string, handler: RouteHandler, options?: RouteOptions): void;
103+
public put(path: string, options?: RouteOptions): MethodDecorator;
104+
public put(
105+
path: string,
106+
handler?: RouteHandler | RouteOptions,
107+
options?: RouteOptions
108+
): MethodDecorator | undefined {
109+
return this.#handleHttpMethod('PUT', path, handler, options);
110+
}
111+
112+
public patch(
113+
path: string,
114+
handler: RouteHandler,
115+
options?: RouteOptions
116+
): void;
117+
public patch(path: string, options?: RouteOptions): MethodDecorator;
118+
public patch(
119+
path: string,
120+
handler?: RouteHandler | RouteOptions,
121+
options?: RouteOptions
122+
): MethodDecorator | undefined {
123+
return this.#handleHttpMethod('PATCH', path, handler, options);
124+
}
125+
126+
public delete(
127+
path: string,
128+
handler: RouteHandler,
129+
options?: RouteOptions
130+
): void;
131+
public delete(path: string, options?: RouteOptions): MethodDecorator;
132+
public delete(
133+
path: string,
134+
handler?: RouteHandler | RouteOptions,
135+
options?: RouteOptions
136+
): MethodDecorator | undefined {
137+
return this.#handleHttpMethod('DELETE', path, handler, options);
138+
}
139+
140+
public head(
141+
path: string,
142+
handler: RouteHandler,
143+
options?: RouteOptions
144+
): void;
145+
public head(path: string, options?: RouteOptions): MethodDecorator;
146+
public head(
147+
path: string,
148+
handler?: RouteHandler | RouteOptions,
149+
options?: RouteOptions
150+
): MethodDecorator | undefined {
151+
return this.#handleHttpMethod('HEAD', path, handler, options);
152+
}
68153
}
69154

70155
export { BaseRouter };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
12
import type { BaseRouter } from '../rest/BaseRouter.js';
2-
import type { GenericLogger } from './appsync-events.js';
33

44
/**
55
* Options for the {@link BaseRouter} class
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import context from '@aws-lambda-powertools/testing-utils/context';
2+
import type { Context } from 'aws-lambda';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { BaseRouter } from '../../../src/rest/BaseRouter.js';
5+
import type { ResolveOptions } from '../../../src/types/index.js';
6+
import type {
7+
RouteHandler,
8+
RouteOptions,
9+
RouterOptions,
10+
} from '../../../src/types/rest.js';
11+
12+
describe('BaseRouter', () => {
13+
class TestResolver extends BaseRouter {
14+
public readonly handlers: Map<string, RouteHandler> = new Map();
15+
16+
constructor(options?: RouterOptions) {
17+
super(options);
18+
this.logger.debug('test debug');
19+
this.logger.warn('test warn');
20+
this.logger.error('test error');
21+
}
22+
23+
#isEvent(obj: unknown): asserts obj is { path: string; method: string } {
24+
if (
25+
typeof obj !== 'object' ||
26+
obj === null ||
27+
!('path' in obj) ||
28+
!('method' in obj)
29+
) {
30+
throw new Error('Invalid event object');
31+
}
32+
}
33+
34+
public route(handler: RouteHandler, options: RouteOptions) {
35+
if (options.path == null || options.method == null)
36+
throw new Error('path or method cannot be null');
37+
this.handlers.set(options.path + options.method, handler);
38+
}
39+
40+
public resolve(
41+
event: unknown,
42+
context: Context,
43+
options?: ResolveOptions
44+
): Promise<unknown> {
45+
this.#isEvent(event);
46+
const { method, path } = event;
47+
const handler = this.handlers.get(path + method);
48+
if (handler == null) throw new Error('404');
49+
return handler(event, context);
50+
}
51+
}
52+
53+
beforeEach(() => {
54+
vi.unstubAllEnvs();
55+
});
56+
57+
it.each([
58+
['GET', 'get'],
59+
['POST', 'post'],
60+
['PUT', 'put'],
61+
['PATCH', 'patch'],
62+
['DELETE', 'delete'],
63+
['HEAD', 'head'],
64+
])('should route %s requests', async (method, verb) => {
65+
const app = new TestResolver();
66+
(
67+
app[
68+
verb as 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'
69+
] as Function
70+
)('test', () => `${verb}-test`);
71+
const actual = await app.resolve({ path: 'test', method }, context);
72+
expect(actual).toEqual(`${verb}-test`);
73+
});
74+
75+
it('should use console.warn and console,error when logger is not provided', () => {
76+
const app = new TestResolver();
77+
expect(console.debug).not.toHaveBeenCalled();
78+
expect(console.error).toHaveBeenCalledWith('test error');
79+
expect(console.warn).toHaveBeenCalledWith('test warn');
80+
});
81+
82+
it('should use console.debug in DEBUG mode when logger is not provided', () => {
83+
vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG');
84+
const app = new TestResolver();
85+
expect(console.debug).toHaveBeenCalledWith('test debug');
86+
expect(console.error).toHaveBeenCalledWith('test error');
87+
expect(console.warn).toHaveBeenCalledWith('test warn');
88+
});
89+
90+
it('should use custom logger when provided', () => {
91+
vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG');
92+
93+
const logger = {
94+
debug: vi.fn(),
95+
info: vi.fn(),
96+
warn: vi.fn(),
97+
error: vi.fn(),
98+
};
99+
100+
const app = new TestResolver({ logger });
101+
expect(logger.error).toHaveBeenCalledWith('test error');
102+
expect(logger.warn).toHaveBeenCalledWith('test warn');
103+
expect(logger.debug).toHaveBeenCalledWith('test debug');
104+
});
105+
106+
describe('decorators', () => {
107+
const app = new TestResolver();
108+
109+
class Lambda {
110+
@app.get('test', {})
111+
public async getTest() {
112+
return 'get-test';
113+
}
114+
115+
@app.post('test')
116+
public async postTest() {
117+
return 'post-test';
118+
}
119+
120+
@app.put('test')
121+
public async putTest() {
122+
return 'put-test';
123+
}
124+
125+
@app.patch('test')
126+
public async patchTest() {
127+
return 'patch-test';
128+
}
129+
130+
@app.delete('test')
131+
public async deleteTest() {
132+
return 'delete-test';
133+
}
134+
135+
@app.head('test')
136+
public async headTest() {
137+
return 'head-test';
138+
}
139+
140+
public async handler(event: unknown, context: Context) {
141+
return app.resolve(event, context, {});
142+
}
143+
}
144+
145+
it.each([
146+
['GET', 'get-test'],
147+
['POST', 'post-test'],
148+
['PUT', 'put-test'],
149+
['PATCH', 'patch-test'],
150+
['DELETE', 'delete-test'],
151+
['HEAD', 'head-test'],
152+
])('should route %s requests with decorators', async (method, expected) => {
153+
const lambda = new Lambda();
154+
const actual = await lambda.handler({ path: 'test', method }, context);
155+
expect(actual).toEqual(expected);
156+
});
157+
});
158+
});

0 commit comments

Comments
 (0)