Skip to content

Commit 264320f

Browse files
authored
Merge branch 'main' into ci/fix_linting
2 parents 3566aa2 + 0ab1bd6 commit 264320f

File tree

7 files changed

+181
-9
lines changed

7 files changed

+181
-9
lines changed

.github/workflows/ossf_scorecard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ jobs:
4343

4444
# Upload the results to GitHub's code scanning dashboard.
4545
- name: "Upload to code-scanning"
46-
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5
46+
uses: github/codeql-action/upload-sarif@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.29.5
4747
with:
4848
sarif_file: results.sarif

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,21 @@ export class ErrorHandlerRegistry {
7171

7272
return null;
7373
}
74+
75+
/**
76+
* Merges another {@link ErrorHandlerRegistry | `ErrorHandlerRegistry`} instance into the current instance.
77+
* It takes the handlers from the provided registry and adds them to the current registry.
78+
*
79+
* 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.
80+
*
81+
* @param errorHandlerRegistry - The registry instance to merge with the current instance
82+
*/
83+
public merge(errorHandlerRegistry: ErrorHandlerRegistry): void {
84+
for (const [
85+
errorConstructor,
86+
errorHandler,
87+
] of errorHandlerRegistry.#handlers) {
88+
this.register(errorConstructor, errorHandler);
89+
}
90+
}
7491
}

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import type {
88
ValidationResult,
99
} from '../types/rest.js';
1010
import { ParameterValidationError } from './errors.js';
11-
import type { Route } from './Route.js';
12-
import { compilePath, validatePathPattern } from './utils.js';
11+
import { Route } from './Route.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();
@@ -193,6 +197,36 @@ class RouteHandlerRegistry {
193197

194198
return null;
195199
}
200+
201+
/**
202+
* Merges another {@link RouteHandlerRegistry | `RouteHandlerRegistry`} instance into the current instance.
203+
* It takes the static and dynamic routes from the provided registry and adds them to the current registry.
204+
*
205+
* 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.
206+
*
207+
* @param routeHandlerRegistry - The registry instance to merge with the current instance
208+
* @param options - Configuration options for merging the router
209+
* @param options.prefix - An optional prefix to be added to the paths defined in the router
210+
*/
211+
public merge(
212+
routeHandlerRegistry: RouteHandlerRegistry,
213+
options?: { prefix: Path }
214+
): void {
215+
const routes = [
216+
...routeHandlerRegistry.#staticRoutes.values(),
217+
...routeHandlerRegistry.#dynamicRoutes,
218+
];
219+
for (const route of routes) {
220+
this.register(
221+
new Route(
222+
route.method as HttpMethod,
223+
resolvePrefixedPath(route.path, options?.prefix),
224+
route.handler,
225+
route.middleware
226+
)
227+
);
228+
}
229+
}
196230
}
197231

198232
export { RouteHandlerRegistry };

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

Lines changed: 46 additions & 4 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(
@@ -551,6 +549,50 @@ class Router {
551549
handler
552550
);
553551
}
552+
553+
/**
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).
561+
*
562+
* @example
563+
* ```typescript
564+
* import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
565+
*
566+
* const todosRouter = new Router();
567+
*
568+
* todosRouter.get('/todos', async () => {
569+
* // List API
570+
* });
571+
*
572+
* todosRouter.get('/todos/{todoId}', async () => {
573+
* // Get API
574+
* });
575+
*
576+
* const app = new Router();
577+
* app.includeRouter(todosRouter);
578+
*
579+
* export const handler = async (event: unknown, context: Context) => {
580+
* return app.resolve(event, context);
581+
* };
582+
* ```
583+
* @param router - The `Router` from which to merge the routes, context and middleware
584+
* @param options - Configuration options for merging the router
585+
* @param options.prefix - An optional prefix to be added to the paths defined in the router
586+
*/
587+
public includeRouter(router: Router, options?: { prefix: Path }): void {
588+
this.context = {
589+
...this.context,
590+
...router.context,
591+
};
592+
this.routeRegistry.merge(router.routeRegistry, options);
593+
this.errorHandlerRegistry.merge(router.errorHandlerRegistry);
594+
this.middleware.push(...router.middleware);
595+
}
554596
}
555597

556598
export { Router };

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: 49 additions & 1 deletion
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,
@@ -144,4 +144,52 @@ describe('Class: Router - Basic Routing', () => {
144144
expect(JSON.parse(createResult.body).actualPath).toBe('/todos');
145145
expect(JSON.parse(getResult.body).actualPath).toBe('/todos/1');
146146
});
147+
148+
it('routes to the included router when using split routers', async () => {
149+
// Prepare
150+
const todoRouter = new Router({ logger: console });
151+
todoRouter.use(async ({ next }) => {
152+
console.log('todoRouter middleware');
153+
await next();
154+
});
155+
todoRouter.get('/', async () => ({ api: 'listTodos' }));
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');
163+
164+
const app = new Router();
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' }));
171+
app.includeRouter(todoRouter, { prefix: '/todos' });
172+
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+
// Assert
185+
expect(JSON.parse(rootResult.body).api).toEqual('root');
186+
expect(JSON.parse(listTodosResult.body).api).toEqual('listTodos');
187+
expect(JSON.parse(notFoundResult.body).error).toEqual('Route not found');
188+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, 'app middleware');
189+
expect(consoleLogSpy).toHaveBeenNthCalledWith(2, 'todoRouter middleware');
190+
expect(consoleWarnSpy).toHaveBeenNthCalledWith(
191+
1,
192+
'Handler for method: GET and path: /todos already exists. The previous handler will be replaced.'
193+
);
194+
});
147195
});

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)