Skip to content

Commit 2eac09d

Browse files
committed
Improved jsdocs and added utility function for prefixed path resolution
1 parent a7789dc commit 2eac09d

File tree

5 files changed

+89
-51
lines changed

5 files changed

+89
-51
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
} from '../types/rest.js';
1010
import { ParameterValidationError } from './errors.js';
1111
import { Route } from './Route.js';
12-
import { compilePath, validatePathPattern } from './utils.js';
12+
import {
13+
compilePath,
14+
resolvePrefixedPath,
15+
validatePathPattern,
16+
} from './utils.js';
1317

1418
class RouteHandlerRegistry {
1519
readonly #staticRoutes: Map<string, Route> = new Map();
@@ -199,6 +203,7 @@ class RouteHandlerRegistry {
199203
* It takes the static and dynamic routes from the provided registry and adds them to the current registry.
200204
*
201205
* @param routeHandlerRegistry - The registry instance to merge with the current instance
206+
* @param options - Configuration options for merging the router
202207
* @param options.prefix - An optional prefix to be added to the paths defined in the router
203208
*/
204209
public merge(
@@ -210,17 +215,10 @@ class RouteHandlerRegistry {
210215
...routeHandlerRegistry.#dynamicRoutes,
211216
];
212217
for (const route of routes) {
213-
let path = route.path;
214-
if (options?.prefix) {
215-
path =
216-
route.path === '/'
217-
? options.prefix
218-
: `${options.prefix}${route.path}`;
219-
}
220218
this.register(
221219
new Route(
222220
route.method as HttpMethod,
223-
path,
221+
resolvePrefixedPath(route.path, options?.prefix),
224222
route.handler,
225223
route.middleware
226224
)

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
isAPIGatewayProxyEvent,
4242
isAPIGatewayProxyResult,
4343
isHttpMethod,
44+
resolvePrefixedPath,
4445
} from './utils.js';
4546

4647
class Router {
@@ -293,10 +294,7 @@ class Router {
293294
public route(handler: RouteHandler, options: RestRouteOptions): void {
294295
const { method, path, middleware = [] } = options;
295296
const methods = Array.isArray(method) ? method : [method];
296-
let resolvedPath = path;
297-
if (this.prefix) {
298-
resolvedPath = path === '/' ? this.prefix : `${this.prefix}${path}`;
299-
}
297+
const resolvedPath = resolvePrefixedPath(path, this.prefix);
300298

301299
for (const method of methods) {
302300
this.routeRegistry.register(
@@ -553,7 +551,13 @@ class Router {
553551
}
554552

555553
/**
556-
* Merges the routes, context and middleware from the passed router instance into this router instance
554+
* Merges the routes, context and middleware from the passed router instance into this router instance.
555+
*
556+
* **Override Behaviors:**
557+
* - **Context**: Properties from the included router override existing properties with the same key in the current router. A warning is logged when conflicts occur.
558+
* - **Routes**: Routes from the included router are added to the current router's registry. If a route with the same method and path already exists, the included router's route takes precedence.
559+
* - **Error Handlers**: Error handlers from the included router are merged with existing handlers. If handlers for the same error type exist in both routers, the included router's handler takes precedence.
560+
* - **Middleware**: Middleware from the included router is appended to the current router's middleware array. All middleware executes in registration order (current router's middleware first, then included router's middleware).
557561
*
558562
* @example
559563
* ```typescript
@@ -576,7 +580,8 @@ class Router {
576580
* return app.resolve(event, context);
577581
* };
578582
* ```
579-
* @param router - The Router from which to merge the routes, context and middleware
583+
* @param router - The `Router` from which to merge the routes, context and middleware
584+
* @param options - Configuration options for merging the router
580585
* @param options.prefix - An optional prefix to be added to the paths defined in the router
581586
*/
582587
public includeRouter(router: Router, options?: { prefix: Path }): void {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,16 @@ export const composeMiddleware = (middleware: Middleware[]): Middleware => {
196196
return result;
197197
};
198198
};
199+
200+
/**
201+
* Resolves a prefixed path by combining the provided path and prefix.
202+
*
203+
* @param path - The path to resolve
204+
* @param prefix - The prefix to prepend to the path
205+
*/
206+
export const resolvePrefixedPath = (path: Path, prefix?: Path): Path => {
207+
if (prefix) {
208+
return path === '/' ? prefix : `${prefix}${path}`;
209+
}
210+
return path;
211+
};

packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import context from '@aws-lambda-powertools/testing-utils/context';
2-
import { describe, expect, it } from 'vitest';
2+
import { describe, expect, it, vi } from 'vitest';
33
import {
44
HttpStatusCodes,
55
HttpVerbs,
@@ -147,44 +147,48 @@ describe('Class: Router - Basic Routing', () => {
147147

148148
it('routes to the included router when using split routers', async () => {
149149
// Prepare
150-
const baseRouter = new Router();
151-
baseRouter.get('/', async () => ({ api: 'root' }));
152-
baseRouter.get('/version', async () => ({ api: 'listVersions' }));
153-
baseRouter.get('/version/:id', async () => ({ api: 'getVersion' }));
154-
baseRouter.notFound(async () => ({ error: 'NotFound' }));
155-
156-
const todoRouter = new Router();
150+
const todoRouter = new Router({ logger: console });
151+
todoRouter.use(async ({ next }) => {
152+
console.log('todoRouter middleware');
153+
await next();
154+
});
157155
todoRouter.get('/', async () => ({ api: 'listTodos' }));
158-
todoRouter.post('/create', async () => ({ api: 'createTodo' }));
159-
todoRouter.get('/:id', async () => ({ api: 'getTodo' }));
160-
161-
const taskRouter = new Router();
162-
taskRouter.get('/', async () => ({ api: 'listTasks' }));
163-
taskRouter.post('/create', async () => ({ api: 'createTask' }));
164-
taskRouter.get('/:taskId', async () => ({ api: 'getTask' }));
156+
todoRouter.notFound(async () => {
157+
return {
158+
error: 'Route not found',
159+
};
160+
});
161+
const consoleLogSpy = vi.spyOn(console, 'log');
162+
const consoleWarnSpy = vi.spyOn(console, 'warn');
165163

166164
const app = new Router();
167-
app.includeRouter(baseRouter);
165+
app.use(async ({ next }) => {
166+
console.log('app middleware');
167+
await next();
168+
});
169+
app.get('/todos', async () => ({ api: 'rootTodos' }));
170+
app.get('/', async () => ({ api: 'root' }));
168171
app.includeRouter(todoRouter, { prefix: '/todos' });
169-
app.includeRouter(taskRouter, { prefix: '/todos/:id/tasks' });
170172

171-
// Act & Assess
172-
const testCases = [
173-
['/', 'GET', 'api', 'root'],
174-
['/version', 'GET', 'api', 'listVersions'],
175-
['/version/1', 'GET', 'api', 'getVersion'],
176-
['/todos', 'GET', 'api', 'listTodos'],
177-
['/todos/create', 'POST', 'api', 'createTodo'],
178-
['/todos/1', 'GET', 'api', 'getTodo'],
179-
['/todos/1/tasks', 'GET', 'api', 'listTasks'],
180-
['/todos/1/tasks/create', 'POST', 'api', 'createTask'],
181-
['/todos/1/tasks/1', 'GET', 'api', 'getTask'],
182-
['/non-existent', 'GET', 'error', 'NotFound'],
183-
] as const;
184-
185-
for (const [path, method, key, expected] of testCases) {
186-
const result = await app.resolve(createTestEvent(path, method), context);
187-
expect(JSON.parse(result.body)[key]).toBe(expected);
188-
}
173+
// Act
174+
const rootResult = await app.resolve(createTestEvent('/', 'GET'), context);
175+
const listTodosResult = await app.resolve(
176+
createTestEvent('/todos', 'GET'),
177+
context
178+
);
179+
const notFoundResult = await app.resolve(
180+
createTestEvent('/non-existent', 'GET'),
181+
context
182+
);
183+
184+
expect(JSON.parse(rootResult.body).api).toEqual('root');
185+
expect(JSON.parse(listTodosResult.body).api).toEqual('listTodos');
186+
expect(JSON.parse(notFoundResult.body).error).toEqual('Route not found');
187+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, 'app middleware');
188+
expect(consoleLogSpy).toHaveBeenNthCalledWith(2, 'todoRouter middleware');
189+
expect(consoleWarnSpy).toHaveBeenNthCalledWith(
190+
1,
191+
'Handler for method: GET and path: /todos already exists. The previous handler will be replaced.'
192+
);
189193
});
190194
});

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
isAPIGatewayProxyEvent,
1010
isAPIGatewayProxyResult,
1111
} from '../../../src/rest/index.js';
12-
import { compilePath, validatePathPattern } from '../../../src/rest/utils.js';
12+
import {
13+
compilePath,
14+
resolvePrefixedPath,
15+
validatePathPattern,
16+
} from '../../../src/rest/utils.js';
1317
import type {
1418
Middleware,
1519
Path,
@@ -567,4 +571,18 @@ describe('Path Utilities', () => {
567571
expect(result).toBeUndefined();
568572
});
569573
});
574+
575+
describe('resolvePrefixedPath', () => {
576+
it.each([
577+
{ path: '/test', prefix: '/prefix', expected: '/prefix/test' },
578+
{ path: '/', prefix: '/prefix', expected: '/prefix' },
579+
{ path: '/test', expected: '/test' },
580+
])('resolves prefixed path', ({ path, prefix, expected }) => {
581+
// Prepare & Act
582+
const resolvedPath = resolvePrefixedPath(path as Path, prefix as Path);
583+
584+
// Assert
585+
expect(resolvedPath).toBe(expected);
586+
});
587+
});
570588
});

0 commit comments

Comments
 (0)