Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions packages/event-handler/src/rest/RouteHandlerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './utils.js';

class RouteHandlerRegistry {
readonly #regexRoutes: Map<string, DynamicRoute> = new Map();
readonly #staticRoutes: Map<string, Route> = new Map();
readonly #dynamicRoutesSet: Set<string> = new Set();
readonly #dynamicRoutes: DynamicRoute[] = [];
Expand Down Expand Up @@ -103,6 +104,18 @@ class RouteHandlerRegistry {

const compiled = compilePath(route.path);

if (!/^[\w+/:-]+$/.test(compiled.path)) {
if (this.#regexRoutes.has(route.id)) {
this.#logger.warn(
`Handler for method: ${route.method} and path: ${route.path} already exists. The previous handler will be replaced.`
);
}
this.#regexRoutes.set(route.id, {
...route,
...compiled,
});
return;
}
if (compiled.isDynamic) {
const dynamicRoute = {
...route,
Expand Down Expand Up @@ -171,28 +184,10 @@ class RouteHandlerRegistry {
};
}

for (const route of this.#dynamicRoutes) {
if (route.method !== method) continue;

const match = route.regex.exec(path);
if (match?.groups) {
const params = match.groups;

const processedParams = this.#processParams(params);

const validation = this.#validateParams(processedParams);

if (!validation.isValid) {
throw new ParameterValidationError(validation.issues);
}

return {
handler: route.handler,
params: processedParams,
rawParams: params,
middleware: route.middleware,
};
}
const routes = [...this.#dynamicRoutes, ...this.#regexRoutes.values()];
for (const route of routes) {
const result = this.#processRoute(route, method, path);
if (result) return result;
}

return null;
Expand All @@ -215,6 +210,7 @@ class RouteHandlerRegistry {
const routes = [
...routeHandlerRegistry.#staticRoutes.values(),
...routeHandlerRegistry.#dynamicRoutes,
...routeHandlerRegistry.#regexRoutes.values(),
];
for (const route of routes) {
this.register(
Expand All @@ -227,6 +223,28 @@ class RouteHandlerRegistry {
);
}
}

#processRoute(route: DynamicRoute, method: HttpMethod, path: Path) {
if (route.method !== method) return;

const match = route.regex.exec(path);
if (!match) return;

const params = match.groups || {};
const processedParams = this.#processParams(params);
const validation = this.#validateParams(processedParams);

if (!validation.isValid) {
throw new ParameterValidationError(validation.issues);
}

return {
handler: route.handler,
params: processedParams,
rawParams: params,
middleware: route.middleware,
};
}
}

export { RouteHandlerRegistry };
1 change: 1 addition & 0 deletions packages/event-handler/src/rest/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const composeMiddleware = (middleware: Middleware[]): Middleware => {
*/
export const resolvePrefixedPath = (path: Path, prefix?: Path): Path => {
if (prefix) {
if (!path.startsWith('/')) return `${prefix}/${path}`;
return path === '/' ? prefix : `${prefix}${path}`;
}
return path;
Expand Down
9 changes: 5 additions & 4 deletions packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type HttpMethod = keyof typeof HttpVerbs;

type HttpStatusCode = (typeof HttpStatusCodes)[keyof typeof HttpStatusCodes];

type Path = `/${string}`;
type Path = string;

type RestRouteHandlerOptions = {
handler: RouteHandler;
Expand All @@ -79,14 +79,15 @@ type RestRouteOptions = {
middleware?: Middleware[];
};

// biome-ignore lint/suspicious/noConfusingVoidType: To ensure next function is awaited
type NextFunction = () => Promise<HandlerResponse | void>;
type NextFunction =
() => // biome-ignore lint/suspicious/noConfusingVoidType: To ensure next function is awaited
Promise<HandlerResponse | void> | HandlerResponse | void;

type Middleware = (args: {
reqCtx: RequestContext;
next: NextFunction;
// biome-ignore lint/suspicious/noConfusingVoidType: To ensure next function is awaited
}) => Promise<HandlerResponse | void>;
}) => Promise<HandlerResponse | void> | HandlerResponse | void;

type RouteRegistryOptions = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,32 @@ describe('Class: Router - Basic Routing', () => {
'Handler for method: GET and path: /todos already exists. The previous handler will be replaced.'
);
});

it.each([
['/files/test', 'GET', 'serveFileOverride'],
['/api/v1/test', 'GET', 'apiVersioning'],
['/users/1/files/test', 'GET', 'dynamicRegex1'],
['/any-route', 'GET', 'getAnyRoute'],
['/no-matches', 'POST', 'catchAllUnmatched'],
])('routes %s %s to %s handler', async (path, method, expectedApi) => {
// Prepare
const app = new Router();
app.get('/files/.+', async () => ({ api: 'serveFile' }));
app.get('/files/.+', async () => ({ api: 'serveFileOverride' }));
app.get('/api/v\\d+/.*', async () => ({ api: 'apiVersioning' }));
app.get('/users/:userId/files/.+', async (reqCtx) => ({
api: `dynamicRegex${reqCtx.params.userId}`,
}));
app.get('.+', async () => ({ api: 'getAnyRoute' }));
app.route(async () => ({ api: 'catchAllUnmatched' }), {
path: '.*',
method: [HttpVerbs.GET, HttpVerbs.POST],
});

// Act
const result = await app.resolve(createTestEvent(path, method), context);

// Assess
expect(JSON.parse(result.body).api).toEqual(expectedApi);
});
});
1 change: 1 addition & 0 deletions packages/event-handler/tests/unit/rest/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ describe('Path Utilities', () => {
{ path: '/test', prefix: '/prefix', expected: '/prefix/test' },
{ path: '/', prefix: '/prefix', expected: '/prefix' },
{ path: '/test', expected: '/test' },
{ path: '.+', prefix: '/prefix', expected: '/prefix/.+' },
])('resolves prefixed path', ({ path, prefix, expected }) => {
// Prepare & Act
const resolvedPath = resolvePrefixedPath(path as Path, prefix as Path);
Expand Down