Skip to content

Commit ade02bc

Browse files
authored
feat(routing): add route groups (#147)
* chore: extract reusable modern routing internals * feat: add route group contract * feat: normalize grouped route sources * feat: apply route group overlays * feat: support route groups in app config * chore: cover grouped route boot paths * chore: add grouped route e2e coverage * fix: normalize root route group prefixes
1 parent 9497786 commit ade02bc

19 files changed

+1159
-81
lines changed

src/Config/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type HttpMiddleware } from '@/Http';
22
import { type StaticFilesOptions } from '@/Http/Files';
33
import { type EventSubscriber } from '@/Kernel';
4-
import type { RouteDefinition } from '@/routing/route-definition';
4+
import type { RouteSource } from '@/routing/route-source';
55

66
/**
77
* @deprecated Use function-first routes from `@koala-ts/framework/routing` with `KoalaConfig.routes` instead.
@@ -13,7 +13,7 @@ export interface KoalaConfig {
1313
* @deprecated Use `routes` with `Route` from `@koala-ts/framework/routing` instead.
1414
*/
1515
controllers: Controller[];
16-
routes?: RouteDefinition[];
16+
routes?: RouteSource[];
1717
globalMiddleware?: HttpMiddleware[];
1818
staticFiles?: StaticFilesOptions;
1919
eventSubscribers?: Record<string, EventSubscriber | EventSubscriber[]>;

src/application/create-application.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { create } from '@/application/create-application';
22
import { koalaDefaultConfig } from '@/Config';
3-
import { Route } from '@/routing';
3+
import { Get, Route, RouteGroup } from '@/routing';
44
import { exclusiveRoutingModeError } from '@/routing/verify-routing-mode';
55
import { expect, test } from 'vitest';
66

@@ -27,6 +27,26 @@ test('create app with explicit routes', () => {
2727
expect(app).toBeDefined();
2828
});
2929

30+
test('create app with grouped routes', () => {
31+
const app = create({
32+
...koalaDefaultConfig,
33+
routes: [
34+
RouteGroup(
35+
{
36+
prefix: '/api',
37+
},
38+
() => [
39+
Get('/users', async scope => {
40+
scope.response.body = [];
41+
}),
42+
],
43+
),
44+
],
45+
});
46+
47+
expect(app).toBeDefined();
48+
});
49+
3050
test('it rejects mixing legacy controllers and explicit routes', () => {
3151
class LegacyController {}
3252

src/routing/create-route-definition.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { HttpMiddleware } from '@/Http';
22
import type { HttpMethod } from '@/routing/http-method';
33
import type { RouteOptions } from '@/routing/route-options';
4+
import { resolveRouteOptions } from '@/routing/resolve-route-options';
45
import type { RouterMethod } from '@/routing/router-method';
56
import type { RouteDefinition } from './route-definition';
67

@@ -21,14 +22,15 @@ export function createRouteDefinition({
2122
middleware = [],
2223
options = {},
2324
}: RouteDefinitionInput): RouteDefinition {
25+
const routeOptions = resolveRouteOptions(options);
26+
2427
return {
2528
name,
2629
path,
2730
methods: qualifyMethods(method),
2831
handler,
29-
parseBody: options.parseBody ?? true,
3032
middleware,
31-
bodyOptions: extractBodyOptions(options),
33+
...routeOptions,
3234
};
3335
}
3436

@@ -42,9 +44,3 @@ function qualifyMethods(method: HttpMethod | HttpMethod[]): RouterMethod[] {
4244

4345
return qualifiedMethods.includes('all') ? ['all'] : [...new Set<RouterMethod>(qualifiedMethods)];
4446
}
45-
46-
function extractBodyOptions(options: RouteOptions): RouteDefinition['bodyOptions'] {
47-
const { parseBody: _parseBody, ...bodyOptions } = options;
48-
49-
return bodyOptions as RouteDefinition['bodyOptions'];
50-
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, test, vi } from 'vitest';
2+
import { expandRouteDefinitions } from './expand-route-definitions';
3+
4+
describe('expand route definitions', () => {
5+
test('it expands route definitions into route registrations', () => {
6+
const handler = vi.fn(async () => undefined);
7+
8+
const registrations = expandRouteDefinitions([
9+
{
10+
path: '/users',
11+
methods: ['get', 'post'],
12+
handler,
13+
middleware: [],
14+
parseBody: false,
15+
bodyOptions: {},
16+
},
17+
]);
18+
19+
expect(registrations).toEqual([
20+
{
21+
method: 'get',
22+
path: '/users',
23+
middleware: [handler],
24+
},
25+
{
26+
method: 'post',
27+
path: '/users',
28+
middleware: [handler],
29+
},
30+
]);
31+
});
32+
33+
test('it prepends koa body middleware when body parsing is enabled', () => {
34+
const handler = vi.fn(async () => undefined);
35+
36+
const registrations = expandRouteDefinitions([
37+
{
38+
path: '/users',
39+
methods: ['post'],
40+
handler,
41+
middleware: [],
42+
parseBody: true,
43+
bodyOptions: { multipart: true },
44+
},
45+
]);
46+
47+
expect(registrations).toHaveLength(1);
48+
49+
const registration = registrations[0];
50+
expect(registration).toBeDefined();
51+
expect(registration?.middleware).toHaveLength(2);
52+
expect(registration?.middleware[1]).toBe(handler);
53+
});
54+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { koaBody } from 'koa-body';
2+
import type { RouteDefinition } from '@/routing/route-definition';
3+
import type { RouteRegistration } from '@/routing/route-registration';
4+
5+
export function expandRouteDefinitions(routes: RouteDefinition[]): RouteRegistration[] {
6+
const registrations: RouteRegistration[] = [];
7+
8+
for (const route of routes) {
9+
registrations.push(...expandRouteDefinition(route));
10+
}
11+
12+
return registrations;
13+
}
14+
15+
function expandRouteDefinition(route: RouteDefinition): RouteRegistration[] {
16+
const middleware = resolveRouteMiddleware(route);
17+
18+
return route.methods.map(method => ({
19+
method,
20+
path: route.path,
21+
middleware,
22+
}));
23+
}
24+
25+
function resolveRouteMiddleware(route: RouteDefinition): RouteRegistration['middleware'] {
26+
const middlewareStack = [...route.middleware, route.handler];
27+
28+
return route.parseBody ? [koaBody(route.bodyOptions), ...middlewareStack] : middlewareStack;
29+
}

src/routing/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export { Route } from './route';
2+
export { RouteGroup } from './route-group';
23
export { Any, Delete, Get, Head, Options, Patch, Post, Put } from './http-verb-helpers';
34
export type { RouteDeclaration } from './route';
5+
export type { RouteConfigOverlay, RouteGroupDefinition, RouteGroupOptions } from './route-group';
6+
export type { RouteSource } from './route-source';
47
export type { HttpMethod } from './http-method';
58
export type { RouteOptions } from './route-options';

0 commit comments

Comments
 (0)