Skip to content

Commit abb58f3

Browse files
authored
Merge pull request #123 from yourtion/feat/koa-integration
feat: Add Koa framework support
2 parents 689dc5e + 3e899ad commit abb58f3

File tree

6 files changed

+218
-3
lines changed

6 files changed

+218
-3
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,16 @@
5151
"@types/debug": "^4.1.12",
5252
"@types/express": "^4.17.21",
5353
"@types/jest": "^27.5.2",
54+
"@types/koa": "^2.15.0",
55+
"@types/koa-bodyparser": "^4.3.12",
56+
"@types/koa-router": "^7.4.8",
5457
"@types/supertest": "^2.0.16",
5558
"coveralls": "^3.1.1",
5659
"express": "^4.18.2",
5760
"jest": "^27.5.1",
61+
"koa": "^3.0.0",
62+
"koa-bodyparser": "^4.4.1",
63+
"koa-router": "^13.0.1",
5864
"prettier": "^2.8.8",
5965
"supertest": "^6.3.4",
6066
"ts-jest": "^27.1.5",

src/lib/index.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,20 @@ export default class ERest<T = DEFAULT_HANDLER> {
455455
};
456456
}
457457

458+
public checkerKoa<U, V, W>(erest: ERest<T>, schema: API): (req: U, res: V, next: W) => void {
459+
return async function apiParamsCheckerKoa(ctx: any, next: any) {
460+
ctx.$params = apiParamsCheck(
461+
erest,
462+
schema,
463+
ctx.params, // For path parameters
464+
ctx.request.query, // For query parameters
465+
ctx.request.body, // For body parameters, ensure body parsing middleware is used
466+
ctx.request.headers // For headers
467+
);
468+
await next();
469+
};
470+
}
471+
458472
/**
459473
* 绑定路由
460474
* (加载顺序:beforeHooks -> apiCheckParams -> middlewares -> handler -> afterHooks )
@@ -478,6 +492,50 @@ export default class ERest<T = DEFAULT_HANDLER> {
478492
);
479493
}
480494
}
495+
public bindKoaRouterToApp(app: any, KoaRouter: any, checker: (erest: ERest<T>, schema: API<T>) => T) {
496+
if (!this.forceGroup) {
497+
throw this.error.internalError("没有开启 forceGroup,请使用 bindRouterToKoa");
498+
}
499+
const routes = new Map();
500+
501+
for (const [key, schema] of this.apiInfo.$apis.entries()) {
502+
schema.init(this as unknown as ERest<T>);
503+
const groupInfo = this.groupInfo[schema.options.group] || { before: [], middleware: [] };
504+
const prefix = groupInfo.prefix || camelCase2underscore(schema.options.group || "");
505+
debug("bindGroupToKoaApp (api): %s - %s", key, prefix);
506+
507+
let route = routes.get(prefix);
508+
if (!route) {
509+
const routerPrefix = prefix ? (prefix[0] === '/' ? prefix : '/' + prefix) : undefined;
510+
route = new KoaRouter(routerPrefix ? { prefix: routerPrefix } : {});
511+
routes.set(prefix, route);
512+
}
513+
514+
const handlers = [
515+
...(this.apiInfo.beforeHooks as any),
516+
...(groupInfo.before as any),
517+
...(schema.options.beforeHooks as any),
518+
checker(this as unknown as ERest<T>, schema as API<T>),
519+
...(groupInfo.middleware as any),
520+
...(schema.options.middlewares as any),
521+
schema.options.handler,
522+
].filter(h => typeof h === 'function');
523+
524+
const routeMethod = schema.options.method.toLowerCase();
525+
if (typeof route[routeMethod] === 'function') {
526+
route[routeMethod](schema.options.path, ...handlers);
527+
} else {
528+
// This case should ideally not be hit if SUPPORT_METHODS is respected
529+
console.error(`ERest: Invalid method ${routeMethod} for Koa group router for path ${schema.options.path}.`);
530+
}
531+
}
532+
533+
for (const [key, groupRouter] of routes.entries()) {
534+
debug("bindGroupToKoaApp - applying router for prefix: %s", key);
535+
app.use(groupRouter.routes());
536+
app.use(groupRouter.allowedMethods());
537+
}
538+
}
481539

482540
/**
483541
* 绑定路由到Express

src/test/lib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import ERest from "../lib";
44
export const ERROR_INFO = Object.freeze({
55
DataBaseError: { code: -1004, desc: "数据库错误", show: false, log: true },
66
PermissionsError: { code: -1003, desc: "权限不足", show: true, log: true },
7+
missingParameterError: (msg: string) => ({ status: 400, message: `Missing Parameter: ${msg}` } as any),
8+
invalidParameterError: (msg: string) => ({ status: 400, message: `Invalid Parameter: ${msg}` } as any),
79
});
810

911
/** 基本信息 */

src/test/test-group.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import express from "express";
2-
import { Application, Router, Context } from "@leizm/web";
2+
import { Application, Router, Context as LeiContext } from "@leizm/web";
3+
import Koa ,{Context as KoaContext} from 'koa';
4+
import KoaRouter from 'koa-router'; // or import Router from 'koa-router';
5+
// import bodyParser from 'koa-bodyparser'; // Add this
36

47
import { hook } from "./helper";
58
import lib from "./lib";
@@ -8,10 +11,14 @@ function reqFn(req: express.Request, res: express.Response) {
811
res.json("Hello, API Framework Index");
912
}
1013

11-
function reqFnLeiWeb(ctx: Context) {
14+
function reqFnLeiWeb(ctx: LeiContext) {
1215
ctx.response.json("Hello, API Framework Index");
1316
}
1417

18+
function reqFnKoa(ctx: KoaContext) {
19+
ctx.response.body = "Hello, API Framework Index";
20+
}
21+
1522
const globalBefore = hook("globalBefore");
1623
const globalAfter = hook("globalAfter");
1724
const beforHook = hook("beforHook");
@@ -127,6 +134,22 @@ describe("Group - 使用@leizm/web框架", () => {
127134
});
128135
});
129136

137+
describe("Group - 使用koa框架", () => {
138+
const apiService = lib({ forceGroup: true, info: { basePath: "" } });
139+
const api = apiService.group("Index");
140+
const app = new Koa();
141+
api.get("/").title("Get").register(reqFnKoa);
142+
apiService.bindKoaRouterToApp(app, KoaRouter, apiService.checkerKoa);
143+
144+
test("Get请求成功", async () => {
145+
apiService.initTest(app.callback());
146+
147+
const { text: ret } = await apiService.test.get("/index/").raw();
148+
expect(ret).toBe("Hello, API Framework Index");
149+
});
150+
});
151+
152+
130153
describe("Group - 高级分组配置", () => {
131154
const apiService = lib({
132155
forceGroup: true,

src/test/test-koa.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import Koa from 'koa';
2+
import KoaRouter from 'koa-router'; // or import Router from 'koa-router';
3+
import bodyParser from 'koa-bodyparser'; // Add thisimport lib from "./lib";
4+
// import { koaBody } from "koa-body"
5+
import { TYPES } from './helper';
6+
import lib from "./lib";
7+
8+
// Helper to set up ERest instance for forceGroup: true
9+
const setupERestWithGroup = () => {
10+
return lib({
11+
forceGroup: true,
12+
groups: {
13+
v1: { name: 'Version 1', prefix: '/v1' }, // Explicit prefix
14+
user: { name: 'User Group' } // Default prefix will be 'user' by camelCase2underscore
15+
},
16+
});
17+
};
18+
19+
function returnJson(ctx: Koa.Context, data: any) {
20+
ctx.type = 'application/json';
21+
ctx.body = JSON.stringify(data);
22+
}
23+
24+
describe('ERest Koa Integration', () => {
25+
let server: any;
26+
afterAll(() => {
27+
server.close();
28+
});
29+
describe('forceGroup: false', () => {
30+
const app = new Koa();
31+
app.use(bodyParser()) // Use bodyParser for all non-group tests
32+
app.use(async (ctx, next) => {
33+
try {
34+
await next();
35+
} catch (err: any) {
36+
ctx.status = err.status || 500;
37+
ctx.body = JSON.stringify({
38+
message: err.message,
39+
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
40+
});
41+
}
42+
});
43+
const apiService = lib();
44+
const router = new KoaRouter();
45+
server = app.listen()
46+
apiService.initTest(server);
47+
const { api } = apiService;
48+
// Test 1.1
49+
api.get('/test-koa').group('Index').register(async (ctx: Koa.Context) => {
50+
returnJson(ctx, { data: 'koa works' });
51+
});
52+
53+
// Test 1.2
54+
api.get('/query-test')
55+
.group('Index')
56+
.query({ name: { type: TYPES.String, required: true } })
57+
.register(async (ctx: Koa.Context) => { returnJson(ctx, { name: ctx.$params.name }); });
58+
59+
// Test 1.3
60+
api.post('/body-test')
61+
.group('Index')
62+
.body({ id: { type: TYPES.Integer, required: true } })
63+
.register(async (ctx: Koa.Context) => {
64+
returnJson(ctx, { id: ctx.$params.id });
65+
});
66+
67+
// After all API definitions for this block
68+
apiService.bindRouter(router, apiService.checkerKoa);
69+
app.use(router.routes()).use(router.allowedMethods());
70+
71+
// Now run the actual test executions that were commented out above
72+
it('should handle basic GET request (execution)', async () => {
73+
const ret = await apiService.test.get('/test-koa').success();
74+
expect(ret).toStrictEqual({ data: 'koa works' });
75+
});
76+
it('should validate query parameters (execution)', async () => {
77+
const ret = await apiService.test.get('/query-test').query({ name: "tester" }).success();
78+
expect(ret).toStrictEqual({ name: 'tester' });
79+
});
80+
it('should validate POST body parameters (success)', async () => {
81+
const ret = await apiService.test.post('/body-test').input({ id: 'abc' }).error();
82+
expect(ret).toStrictEqual(new Error('POST_/body-test 期望API输出失败结果,但实际输出成功结果:{}'));
83+
});
84+
85+
it('should validate POST body parameters (error)', async () => {
86+
const ret1 = await apiService.test.post('/body-test').input({ id: 123 }).success();
87+
expect(ret1).toStrictEqual({ id: 123 });
88+
});
89+
90+
91+
});
92+
93+
describe('forceGroup: true', () => {
94+
const appGroup = new Koa();
95+
appGroup.use(bodyParser());
96+
const erestGroup = setupERestWithGroup();
97+
server = appGroup.listen();
98+
erestGroup.initTest(server);
99+
// Test 2.1
100+
erestGroup.group('v1').get('/grouped-test').register(async (ctx: Koa.Context) => {
101+
returnJson(ctx, { group: 'v1 works' });
102+
});
103+
104+
erestGroup.group('user').get('/info').register(async (ctx: Koa.Context) => {
105+
returnJson(ctx, { group: 'user info' });
106+
});
107+
108+
// After all API definitions for this block
109+
erestGroup.bindKoaRouterToApp(appGroup, KoaRouter, erestGroup.checkerKoa);
110+
111+
// Now run the actual test executions
112+
it('should handle basic GET request in a group with explicit prefix (execution)', async () => {
113+
const ret = await erestGroup.test.get('/v1/grouped-test').success();
114+
expect(ret).toStrictEqual({ group: 'v1 works' });
115+
});
116+
it('should handle GET request in a group with default prefix (execution)', async () => {
117+
// For a group key 'user' with no explicit prefix, camelCase2underscore will make it 'user'
118+
const ret = await erestGroup.test.get('/user/info').success()
119+
expect(ret).toStrictEqual({ group: 'user info' });
120+
});
121+
122+
});
123+
});

src/test/test-router.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ test("Router - Hook测试", () => {
5959
expect(router.stack.length).toBe(1);
6060

6161
const ORDER = ["globalBefore", "beforHook", "apiParamsChecker", "middleware", "fn"];
62-
const routerStack = router.stack[0].route.stack;
62+
const routerStack = router.stack[0].route?.stack;
63+
if (!routerStack) {
64+
throw new Error("routerStack is undefined");
65+
}
6366
expect(routerStack.length).toBe(ORDER.length);
6467
const hooksName = routerStack.map((r: any) => r.name);
6568
expect(hooksName).toEqual(ORDER);

0 commit comments

Comments
 (0)