Skip to content

Commit 2848f9a

Browse files
authored
Merge pull request #92 from Ecube-Labs/fix-method-all-catch-all
fix: register method: 'all' route specs as catch-all
2 parents 37bb854 + 8802a2d commit 2848f9a

4 files changed

Lines changed: 129 additions & 3 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,25 @@ app.listen(3000, () => {
121121
You can also implement your custom adapter by implementing the `XRouterAdaptor` interface.
122122
This allows you to use your preferred validation library for route validation.
123123

124+
### Catch-all routes (`method: 'all'`)
125+
126+
Use `method: 'all'` (case-insensitive) to register a catch-all route that matches every
127+
HTTP verb the router supports, mirroring `@koa/router`'s `router.all()` and keeping
128+
compatibility with `koa-joi-router`. This is handy for proxy or fallback handlers:
129+
130+
```ts
131+
router.add({
132+
method: 'all',
133+
path: '/hubspot/:path(.*)',
134+
handler: async (ctx) => {
135+
// handles GET, POST, PUT, PATCH, DELETE, ... on /hubspot/*
136+
},
137+
});
138+
```
139+
140+
In the generated OpenAPI spec, an `'all'` route is expanded into one operation per
141+
supported verb.
142+
124143
### CommonJS
125144

126145
```js

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "koa-x-router",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "`koa-x-router` is a library that extends the functionality of `koa-router` by providing validation and automatic API documentation features. It simplifies the process of defining routes, validating request data, and generating API documentation.",
55
"type": "module",
66
"main": "./dist/cjs/index.js",

src/libs/Router.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,3 +1120,104 @@ describe('koa-x-router API work with Joi', () => {
11201120
});
11211121
});
11221122
});
1123+
1124+
describe("koa-x-router API work with method: 'all'", () => {
1125+
it('should register a catch-all route that matches every HTTP verb', async () => {
1126+
const app = getApp();
1127+
const router = new Router();
1128+
app.use(router.routes());
1129+
1130+
router.add({
1131+
method: 'all',
1132+
path: '/proxy/:path(.*)',
1133+
handler: async (ctx) => {
1134+
ctx.body = `matched ${ctx.method}`;
1135+
},
1136+
});
1137+
1138+
const getResponse = await request(app.callback()).get('/proxy/foo');
1139+
expect(getResponse.status).toBe(200);
1140+
expect(getResponse.text).toBe('matched GET');
1141+
1142+
const postResponse = await request(app.callback()).post('/proxy/foo');
1143+
expect(postResponse.status).toBe(200);
1144+
expect(postResponse.text).toBe('matched POST');
1145+
1146+
const deleteResponse = await request(app.callback()).delete('/proxy/foo/bar');
1147+
expect(deleteResponse.status).toBe(200);
1148+
expect(deleteResponse.text).toBe('matched DELETE');
1149+
});
1150+
1151+
it("should treat method: 'ALL' (uppercase) the same as 'all'", async () => {
1152+
const app = getApp();
1153+
const router = new Router();
1154+
app.use(router.routes());
1155+
1156+
router.add({
1157+
method: 'ALL',
1158+
path: '/any/:path(.*)',
1159+
handler: async (ctx) => {
1160+
ctx.body = 'ok';
1161+
},
1162+
});
1163+
1164+
const putResponse = await request(app.callback()).put('/any/thing');
1165+
expect(putResponse.status).toBe(200);
1166+
expect(putResponse.text).toBe('ok');
1167+
});
1168+
1169+
it('should run the middleware chain for a catch-all route', async () => {
1170+
const app = getApp();
1171+
const router = new Router();
1172+
app.use(router.routes());
1173+
1174+
let middlewareRan = false;
1175+
router.use('/guarded/:path(.*)', async (_ctx, next) => {
1176+
middlewareRan = true;
1177+
await next();
1178+
});
1179+
1180+
router.add({
1181+
method: 'all',
1182+
path: '/guarded/:path(.*)',
1183+
handler: async (ctx) => {
1184+
ctx.body = 'guarded';
1185+
},
1186+
});
1187+
1188+
const response = await request(app.callback()).get('/guarded/foo');
1189+
expect(response.status).toBe(200);
1190+
expect(response.text).toBe('guarded');
1191+
expect(middlewareRan).toBe(true);
1192+
});
1193+
1194+
it('should expand a catch-all route into per-verb operations in the OpenAPI spec', () => {
1195+
const router = new Router();
1196+
1197+
router.add({
1198+
method: 'all',
1199+
path: '/proxy',
1200+
meta: {
1201+
document: {
1202+
summary: 'Proxy everything',
1203+
},
1204+
},
1205+
handler: async (ctx) => {
1206+
ctx.body = {};
1207+
},
1208+
});
1209+
1210+
const spec = JSON.parse(
1211+
router.generateOpenApiSpecJson({
1212+
info: {
1213+
title: 'koa-x-router',
1214+
version: '1.0.0',
1215+
},
1216+
}),
1217+
);
1218+
1219+
const operations = Object.keys(spec.paths['/proxy']);
1220+
expect(operations).toEqual(expect.arrayContaining(['get', 'post', 'put', 'patch', 'delete', 'options']));
1221+
expect(spec.paths['/proxy'].get.summary).toBe('Proxy everything');
1222+
});
1223+
});

src/libs/Router.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export interface SchemaLike {}
4343

4444
type SupportMethod = 'get' | 'post' | 'delete' | 'put' | 'options' | 'head' | 'patch' | 'trace';
4545

46+
type AllMethod = 'all' | 'ALL';
47+
4648
type SchemaMetadata = {
4749
summary: string;
4850
description?: string;
@@ -52,7 +54,7 @@ type SchemaMetadata = {
5254
};
5355

5456
export interface RouteLayerSpec<StateT = any, CustomT = {}> {
55-
method: SupportMethod | Uppercase<SupportMethod>;
57+
method: SupportMethod | Uppercase<SupportMethod> | AllMethod;
5658
path: string;
5759
meta?: {
5860
// compatible for `koa-joi-router-docs`
@@ -93,7 +95,11 @@ export class Router<StateT = any, CustomT = {}> extends KoaRouter<StateT, Custom
9395
specs.forEach((spec) => {
9496
const { validate, meta } = spec;
9597

96-
const layer = super.register(spec.path, [spec.method], [spec.handler], {
98+
// `method: 'all'`(대소문자 무관)은 koa-joi-router의 `router.all()`처럼
99+
// 라우터가 지원하는 모든 verb에 매칭되는 catch-all로 등록한다.
100+
const methods = String(spec.method).toLowerCase() === 'all' ? this.methods : [spec.method];
101+
102+
const layer = super.register(spec.path, methods, [spec.handler], {
97103
name: spec.path,
98104
});
99105

0 commit comments

Comments
 (0)