@dangao/bun-server 1.0.3 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller/controller.d.ts +1 -1
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/database/database-extension.d.ts.map +1 -1
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/database/orm/transaction-decorator.d.ts +1 -0
- package/dist/database/orm/transaction-decorator.d.ts.map +1 -1
- package/dist/database/orm/transaction-interceptor.d.ts +12 -3
- package/dist/database/orm/transaction-interceptor.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +678 -310
- package/dist/interceptor/base-interceptor.d.ts +94 -0
- package/dist/interceptor/base-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/cache-interceptor.d.ts +69 -0
- package/dist/interceptor/builtin/cache-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/index.d.ts +4 -0
- package/dist/interceptor/builtin/index.d.ts.map +1 -0
- package/dist/interceptor/builtin/log-interceptor.d.ts +56 -0
- package/dist/interceptor/builtin/log-interceptor.d.ts.map +1 -0
- package/dist/interceptor/builtin/permission-interceptor.d.ts +70 -0
- package/dist/interceptor/builtin/permission-interceptor.d.ts.map +1 -0
- package/dist/interceptor/index.d.ts +7 -0
- package/dist/interceptor/index.d.ts.map +1 -0
- package/dist/interceptor/interceptor-chain.d.ts +22 -0
- package/dist/interceptor/interceptor-chain.d.ts.map +1 -0
- package/dist/interceptor/interceptor-registry.d.ts +59 -0
- package/dist/interceptor/interceptor-registry.d.ts.map +1 -0
- package/dist/interceptor/metadata.d.ts +12 -0
- package/dist/interceptor/metadata.d.ts.map +1 -0
- package/dist/interceptor/types.d.ts +42 -0
- package/dist/interceptor/types.d.ts.map +1 -0
- package/dist/middleware/decorators.d.ts +2 -1
- package/dist/middleware/decorators.d.ts.map +1 -1
- package/dist/router/decorators.d.ts.map +1 -1
- package/dist/router/registry.d.ts +2 -1
- package/dist/router/registry.d.ts.map +1 -1
- package/dist/router/route.d.ts +3 -2
- package/dist/router/route.d.ts.map +1 -1
- package/dist/router/router.d.ts +2 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/websocket/decorators.d.ts +2 -1
- package/dist/websocket/decorators.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/controller/controller.ts +41 -14
- package/src/core/application.ts +7 -1
- package/src/database/database-extension.ts +23 -2
- package/src/database/database-module.ts +6 -0
- package/src/database/orm/transaction-decorator.ts +33 -2
- package/src/database/orm/transaction-interceptor.ts +31 -11
- package/src/index.ts +22 -0
- package/src/interceptor/base-interceptor.ts +203 -0
- package/src/interceptor/builtin/cache-interceptor.ts +169 -0
- package/src/interceptor/builtin/index.ts +28 -0
- package/src/interceptor/builtin/log-interceptor.ts +178 -0
- package/src/interceptor/builtin/permission-interceptor.ts +173 -0
- package/src/interceptor/index.ts +26 -0
- package/src/interceptor/interceptor-chain.ts +79 -0
- package/src/interceptor/interceptor-registry.ts +132 -0
- package/src/interceptor/metadata.ts +40 -0
- package/src/interceptor/types.ts +52 -0
- package/src/middleware/decorators.ts +2 -1
- package/src/router/decorators.ts +44 -43
- package/src/router/registry.ts +2 -1
- package/src/router/route.ts +3 -2
- package/src/router/router.ts +2 -1
- package/src/websocket/decorators.ts +3 -1
- package/tests/controller/path-combination.test.ts +207 -0
- package/tests/interceptor/builtin/cache-interceptor.test.ts +137 -0
- package/tests/interceptor/builtin/permission-interceptor.test.ts +182 -0
- package/tests/interceptor/interceptor-advanced-integration.test.ts +592 -0
- package/tests/interceptor/interceptor-arg-modification.test.ts +76 -0
- package/tests/interceptor/interceptor-chain.test.ts +199 -0
- package/tests/interceptor/interceptor-integration.test.ts +230 -0
- package/tests/interceptor/interceptor-registry.test.ts +200 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +341 -0
- package/tests/router/decorators.test.ts +13 -15
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Interceptor, InterceptorMetadata } from './types';
|
|
2
|
+
import { INTERCEPTOR_REGISTRY_TOKEN } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 拦截器注册表
|
|
6
|
+
* 管理所有注册的拦截器,支持按优先级排序
|
|
7
|
+
*/
|
|
8
|
+
export class InterceptorRegistry {
|
|
9
|
+
/**
|
|
10
|
+
* 拦截器存储
|
|
11
|
+
* key: 元数据键(Symbol)
|
|
12
|
+
* value: 拦截器元数据列表
|
|
13
|
+
*/
|
|
14
|
+
private readonly interceptors = new Map<symbol, InterceptorMetadata[]>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 注册拦截器
|
|
18
|
+
* @param metadataKey - 元数据键(用于匹配装饰器)
|
|
19
|
+
* @param interceptor - 拦截器实例
|
|
20
|
+
* @param priority - 优先级(数字越小优先级越高,默认 100)
|
|
21
|
+
*/
|
|
22
|
+
public register(
|
|
23
|
+
metadataKey: symbol,
|
|
24
|
+
interceptor: Interceptor,
|
|
25
|
+
priority: number = 100,
|
|
26
|
+
): void {
|
|
27
|
+
if (!this.interceptors.has(metadataKey)) {
|
|
28
|
+
this.interceptors.set(metadataKey, []);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const metadataList = this.interceptors.get(metadataKey)!;
|
|
32
|
+
|
|
33
|
+
// 检查是否已注册相同的拦截器
|
|
34
|
+
const exists = metadataList.some(
|
|
35
|
+
(meta) => meta.interceptor === interceptor,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!exists) {
|
|
39
|
+
metadataList.push({
|
|
40
|
+
metadataKey,
|
|
41
|
+
interceptor,
|
|
42
|
+
priority,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 按优先级排序(数字越小优先级越高)
|
|
46
|
+
metadataList.sort((a, b) => a.priority - b.priority);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取拦截器元数据列表(按优先级排序)
|
|
52
|
+
* @param metadataKey - 元数据键
|
|
53
|
+
* @returns 拦截器元数据列表
|
|
54
|
+
*/
|
|
55
|
+
public getInterceptorMetadata(metadataKey: symbol): InterceptorMetadata[] {
|
|
56
|
+
const metadataList = this.interceptors.get(metadataKey);
|
|
57
|
+
if (!metadataList || metadataList.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 返回已排序的拦截器元数据列表(包括优先级信息)
|
|
62
|
+
return [...metadataList];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 获取拦截器列表(按优先级排序)
|
|
67
|
+
* @param metadataKey - 元数据键
|
|
68
|
+
* @returns 拦截器列表
|
|
69
|
+
*/
|
|
70
|
+
public getInterceptors(metadataKey: symbol): Interceptor[] {
|
|
71
|
+
const metadataList = this.interceptors.get(metadataKey);
|
|
72
|
+
if (!metadataList || metadataList.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 返回已排序的拦截器列表
|
|
77
|
+
return metadataList.map((meta) => meta.interceptor);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 检查是否有拦截器
|
|
82
|
+
* @param metadataKey - 元数据键
|
|
83
|
+
* @returns 是否有拦截器
|
|
84
|
+
*/
|
|
85
|
+
public hasInterceptor(metadataKey: symbol): boolean {
|
|
86
|
+
const metadataList = this.interceptors.get(metadataKey);
|
|
87
|
+
return metadataList !== undefined && metadataList.length > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 获取所有已注册的元数据键
|
|
92
|
+
* @returns 元数据键迭代器
|
|
93
|
+
*/
|
|
94
|
+
public getAllMetadataKeys(): IterableIterator<symbol> {
|
|
95
|
+
return this.interceptors.keys();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 清除所有拦截器
|
|
100
|
+
*/
|
|
101
|
+
public clear(): void {
|
|
102
|
+
this.interceptors.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 移除指定元数据键的所有拦截器
|
|
107
|
+
* @param metadataKey - 元数据键
|
|
108
|
+
*/
|
|
109
|
+
public remove(metadataKey: symbol): void {
|
|
110
|
+
this.interceptors.delete(metadataKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 获取拦截器数量
|
|
115
|
+
* @param metadataKey - 元数据键(可选)
|
|
116
|
+
* @returns 拦截器数量
|
|
117
|
+
*/
|
|
118
|
+
public count(metadataKey?: symbol): number {
|
|
119
|
+
if (metadataKey) {
|
|
120
|
+
const metadataList = this.interceptors.get(metadataKey);
|
|
121
|
+
return metadataList ? metadataList.length : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 返回所有拦截器的总数
|
|
125
|
+
let total = 0;
|
|
126
|
+
for (const metadataList of this.interceptors.values()) {
|
|
127
|
+
total += metadataList.length;
|
|
128
|
+
}
|
|
129
|
+
return total;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import type { InterceptorRegistry } from './interceptor-registry';
|
|
3
|
+
import type { Interceptor, InterceptorMetadata } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 扫描方法上的所有拦截器元数据
|
|
7
|
+
* @param target - 目标对象(控制器实例的原型)
|
|
8
|
+
* @param propertyKey - 方法名
|
|
9
|
+
* @param registry - 拦截器注册表
|
|
10
|
+
* @returns 匹配的拦截器列表(按优先级排序)
|
|
11
|
+
*/
|
|
12
|
+
export function scanInterceptorMetadata(
|
|
13
|
+
target: object,
|
|
14
|
+
propertyKey: string | symbol,
|
|
15
|
+
registry: InterceptorRegistry,
|
|
16
|
+
): Interceptor[] {
|
|
17
|
+
const interceptorMetadataList: InterceptorMetadata[] = [];
|
|
18
|
+
|
|
19
|
+
// 扫描所有已注册的元数据键
|
|
20
|
+
for (const metadataKey of registry.getAllMetadataKeys()) {
|
|
21
|
+
// 检查方法上是否有该元数据
|
|
22
|
+
const metadata = Reflect.getMetadata(metadataKey, target, propertyKey);
|
|
23
|
+
|
|
24
|
+
if (metadata !== undefined && metadata !== null) {
|
|
25
|
+
// 找到匹配的元数据,获取对应的拦截器元数据
|
|
26
|
+
// 使用公共方法获取完整的元数据(包括优先级)
|
|
27
|
+
const metadataList = registry.getInterceptorMetadata(metadataKey);
|
|
28
|
+
if (metadataList.length > 0) {
|
|
29
|
+
interceptorMetadataList.push(...metadataList);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 按优先级排序(数字越小优先级越高)
|
|
35
|
+
interceptorMetadataList.sort((a, b) => a.priority - b.priority);
|
|
36
|
+
|
|
37
|
+
// 返回排序后的拦截器列表
|
|
38
|
+
return interceptorMetadataList.map((meta) => meta.interceptor);
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Container } from '../di/container';
|
|
2
|
+
import type { Context } from '../core/context';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 拦截器接口
|
|
6
|
+
* 定义拦截器的核心执行方法
|
|
7
|
+
*/
|
|
8
|
+
export interface Interceptor {
|
|
9
|
+
/**
|
|
10
|
+
* 执行拦截器逻辑
|
|
11
|
+
* @param target - 目标对象(控制器实例的原型)
|
|
12
|
+
* @param propertyKey - 方法名
|
|
13
|
+
* @param originalMethod - 原始方法
|
|
14
|
+
* @param args - 方法参数
|
|
15
|
+
* @param container - DI 容器
|
|
16
|
+
* @param context - 请求上下文(可选)
|
|
17
|
+
* @returns 方法执行结果
|
|
18
|
+
*/
|
|
19
|
+
execute<T>(
|
|
20
|
+
target: unknown,
|
|
21
|
+
propertyKey: string | symbol,
|
|
22
|
+
originalMethod: (...args: unknown[]) => T | Promise<T>,
|
|
23
|
+
args: unknown[],
|
|
24
|
+
container: Container,
|
|
25
|
+
context?: Context,
|
|
26
|
+
): Promise<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 拦截器元数据
|
|
31
|
+
* 用于存储拦截器的注册信息
|
|
32
|
+
*/
|
|
33
|
+
export interface InterceptorMetadata {
|
|
34
|
+
/**
|
|
35
|
+
* 元数据键(用于匹配装饰器)
|
|
36
|
+
*/
|
|
37
|
+
metadataKey: symbol;
|
|
38
|
+
/**
|
|
39
|
+
* 拦截器实例
|
|
40
|
+
*/
|
|
41
|
+
interceptor: Interceptor;
|
|
42
|
+
/**
|
|
43
|
+
* 优先级(数字越小优先级越高,默认 100)
|
|
44
|
+
*/
|
|
45
|
+
priority: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 拦截器注册表 Token
|
|
50
|
+
*/
|
|
51
|
+
export const INTERCEPTOR_REGISTRY_TOKEN = Symbol('@dangao/bun-server:interceptor-registry');
|
|
52
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
+
import type { Constructor } from '../core/types';
|
|
2
3
|
import type { Middleware } from './middleware';
|
|
3
4
|
import { createRateLimitMiddleware, type RateLimitOptions } from './builtin/rate-limit';
|
|
4
5
|
|
|
@@ -68,7 +69,7 @@ export function RateLimit(options: RateLimitOptions): MethodDecorator {
|
|
|
68
69
|
* @param constructor - 控制器构造函数
|
|
69
70
|
* @returns 中间件列表
|
|
70
71
|
*/
|
|
71
|
-
export function getClassMiddlewares(constructor:
|
|
72
|
+
export function getClassMiddlewares(constructor: Constructor<unknown>): Middleware[] {
|
|
72
73
|
return (
|
|
73
74
|
(Reflect.getMetadata(CLASS_MIDDLEWARE_METADATA_KEY, constructor) as Middleware[]) || []
|
|
74
75
|
);
|
package/src/router/decorators.ts
CHANGED
|
@@ -30,53 +30,55 @@ export interface RouteMetadata {
|
|
|
30
30
|
*/
|
|
31
31
|
function createRouteDecorator(method: HttpMethod, path: string) {
|
|
32
32
|
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// 注意:装饰器应用顺序问题
|
|
34
|
+
// 方法装饰器(@GET)在类装饰器(@Controller)之前应用
|
|
35
|
+
// 因此这里无法立即检查 @Controller,需要在 ControllerRegistry.register 时验证
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} else {
|
|
46
|
-
// 从函数名获取(可能不可靠,但作为后备方案)
|
|
47
|
-
propertyKeyStr = descriptor.value.name || '';
|
|
37
|
+
// 尝试检查类是否已经有 @Controller 装饰器(如果装饰器已经应用)
|
|
38
|
+
// 这可以捕获一些早期错误,但不是 100% 可靠(因为装饰器应用顺序)
|
|
39
|
+
const constructor = target.constructor;
|
|
40
|
+
if (constructor && typeof constructor === 'function') {
|
|
41
|
+
const hasController = Reflect.getMetadata(CONTROLLER_METADATA_KEY, constructor);
|
|
42
|
+
if (hasController === undefined) {
|
|
43
|
+
// 类装饰器可能还没有应用,这是正常的
|
|
44
|
+
// 如果类装饰器已经应用但没有 @Controller,这里会捕获到
|
|
45
|
+
// 但这种情况很少见,因为通常 @Controller 会在 @GET 之前应用
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 保存元数据
|
|
50
|
+
// 注意:即使类没有 @Controller,元数据也会被保存
|
|
51
|
+
// 但 ControllerRegistry.register 会验证并抛出错误,所以不会造成问题
|
|
52
|
+
const existingRoutes: RouteMetadata[] = Reflect.getMetadata(ROUTE_METADATA_KEY, target) || [];
|
|
53
|
+
|
|
54
|
+
// 获取方法名:优先使用 propertyKey,如果不可用则从函数名获取
|
|
55
|
+
let propertyKeyStr: string;
|
|
56
|
+
if (propertyKey && propertyKey !== '') {
|
|
57
|
+
propertyKeyStr = typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
58
|
+
} else {
|
|
59
|
+
// 从函数名获取(可能不可靠,但作为后备方案)
|
|
60
|
+
propertyKeyStr = descriptor.value.name || '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 如果仍然没有方法名,尝试从原型中查找
|
|
64
|
+
if (!propertyKeyStr) {
|
|
65
|
+
const propertyNames = Object.getOwnPropertyNames(target);
|
|
66
|
+
for (const key of propertyNames) {
|
|
67
|
+
const targetDescriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
68
|
+
if (targetDescriptor?.value === descriptor.value) {
|
|
69
|
+
propertyKeyStr = key;
|
|
70
|
+
break;
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
|
-
|
|
62
|
-
existingRoutes.push({
|
|
63
|
-
method,
|
|
64
|
-
path,
|
|
65
|
-
handler: descriptor.value as RouteHandler,
|
|
66
|
-
propertyKey: propertyKeyStr || undefined,
|
|
67
|
-
});
|
|
68
|
-
Reflect.defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target);
|
|
69
|
-
} else {
|
|
70
|
-
// 普通函数:直接注册路由
|
|
71
|
-
const handler = descriptor.value as RouteHandler;
|
|
72
|
-
const registry = RouteRegistry.getInstance();
|
|
73
|
-
registry.register(method, path, handler);
|
|
74
|
-
|
|
75
|
-
// 保存元数据(用于兼容性)
|
|
76
|
-
const existingRoutes: RouteMetadata[] = Reflect.getMetadata(ROUTE_METADATA_KEY, target) || [];
|
|
77
|
-
existingRoutes.push({ method, path, handler });
|
|
78
|
-
Reflect.defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target);
|
|
79
73
|
}
|
|
74
|
+
|
|
75
|
+
existingRoutes.push({
|
|
76
|
+
method,
|
|
77
|
+
path,
|
|
78
|
+
handler: descriptor.value as RouteHandler,
|
|
79
|
+
propertyKey: propertyKeyStr || undefined,
|
|
80
|
+
});
|
|
81
|
+
Reflect.defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target);
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -119,4 +121,3 @@ export function DELETE(path: string) {
|
|
|
119
121
|
export function PATCH(path: string) {
|
|
120
122
|
return createRouteDecorator('PATCH', path);
|
|
121
123
|
}
|
|
122
|
-
|
package/src/router/registry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Router } from './router';
|
|
2
|
+
import type { Constructor } from '../core/types';
|
|
2
3
|
import type { HttpMethod, RouteHandler } from './types';
|
|
3
4
|
import type { Middleware } from '../middleware';
|
|
4
5
|
|
|
@@ -39,7 +40,7 @@ export class RouteRegistry {
|
|
|
39
40
|
path: string,
|
|
40
41
|
handler: RouteHandler,
|
|
41
42
|
middlewares: Middleware[] = [],
|
|
42
|
-
controllerClass?:
|
|
43
|
+
controllerClass?: Constructor<unknown>,
|
|
43
44
|
methodName?: string,
|
|
44
45
|
): void {
|
|
45
46
|
this.router.register(method, path, handler, middlewares, controllerClass, methodName);
|
package/src/router/route.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context } from '../core/context';
|
|
2
|
+
import type { Constructor } from '../core/types';
|
|
2
3
|
import type { HttpMethod, RouteHandler, RouteMatch } from './types';
|
|
3
4
|
import { MiddlewarePipeline } from '../middleware/pipeline';
|
|
4
5
|
import type { Middleware } from '../middleware';
|
|
@@ -26,7 +27,7 @@ export class Route {
|
|
|
26
27
|
/**
|
|
27
28
|
* 控制器类(可选,用于控制器路由)
|
|
28
29
|
*/
|
|
29
|
-
public readonly controllerClass?:
|
|
30
|
+
public readonly controllerClass?: Constructor<unknown>;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* 方法名(可选,用于控制器路由)
|
|
@@ -52,7 +53,7 @@ export class Route {
|
|
|
52
53
|
path: string,
|
|
53
54
|
handler: RouteHandler,
|
|
54
55
|
middlewares: Middleware[] = [],
|
|
55
|
-
controllerClass?:
|
|
56
|
+
controllerClass?: Constructor<unknown>,
|
|
56
57
|
methodName?: string,
|
|
57
58
|
) {
|
|
58
59
|
this.method = method;
|
package/src/router/router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context } from '../core/context';
|
|
2
|
+
import type { Constructor } from '../core/types';
|
|
2
3
|
import { Route } from './route';
|
|
3
4
|
import type { HttpMethod, RouteHandler, RouteMatch } from './types';
|
|
4
5
|
import type { Middleware } from '../middleware';
|
|
@@ -45,7 +46,7 @@ export class Router {
|
|
|
45
46
|
path: string,
|
|
46
47
|
handler: RouteHandler,
|
|
47
48
|
middlewares: Middleware[] = [],
|
|
48
|
-
controllerClass?:
|
|
49
|
+
controllerClass?: Constructor<unknown>,
|
|
49
50
|
methodName?: string,
|
|
50
51
|
): void {
|
|
51
52
|
// 规范化路径
|
|
@@ -36,8 +36,10 @@ export const OnOpen = createHandlerDecorator('open');
|
|
|
36
36
|
export const OnMessage = createHandlerDecorator('message');
|
|
37
37
|
export const OnClose = createHandlerDecorator('close');
|
|
38
38
|
|
|
39
|
+
import type { Constructor } from '../core/types';
|
|
40
|
+
|
|
39
41
|
export function getGatewayMetadata(
|
|
40
|
-
constructor:
|
|
42
|
+
constructor: Constructor<unknown>,
|
|
41
43
|
): WebSocketGatewayMetadata | undefined {
|
|
42
44
|
return Reflect.getMetadata(GATEWAY_METADATA_KEY, constructor);
|
|
43
45
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
3
|
+
import { Application } from '../../src/core/application';
|
|
4
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
5
|
+
import { GET, POST } from '../../src/router/decorators';
|
|
6
|
+
import { Param } from '../../src/controller/decorators';
|
|
7
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
8
|
+
import { getTestPort } from '../utils/test-port';
|
|
9
|
+
|
|
10
|
+
describe('Controller Path Combination', () => {
|
|
11
|
+
let app: Application;
|
|
12
|
+
let port: number;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
port = getTestPort();
|
|
16
|
+
app = new Application({ port });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
if (app) {
|
|
21
|
+
await app.stop();
|
|
22
|
+
}
|
|
23
|
+
RouteRegistry.getInstance().clear();
|
|
24
|
+
ControllerRegistry.getInstance().clear();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should correctly combine base path with method path starting with /', async () => {
|
|
28
|
+
@Controller('/api/products')
|
|
29
|
+
class ProductController {
|
|
30
|
+
@GET('/')
|
|
31
|
+
public listProducts() {
|
|
32
|
+
return { products: ['product1', 'product2'] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@GET('/:id')
|
|
36
|
+
public getProduct(@Param('id') id: string) {
|
|
37
|
+
return { id, name: `Product ${id}` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
app.registerController(ProductController);
|
|
42
|
+
await app.listen();
|
|
43
|
+
|
|
44
|
+
// 应该访问 /api/products,而不是 /
|
|
45
|
+
const response = await fetch(`http://localhost:${port}/api/products`);
|
|
46
|
+
expect(response.status).toBe(200);
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
expect(data.products).toBeDefined();
|
|
49
|
+
|
|
50
|
+
// 访问 / 应该返回 404,而不是访问到 listProducts
|
|
51
|
+
const rootResponse = await fetch(`http://localhost:${port}/`);
|
|
52
|
+
expect(rootResponse.status).toBe(404);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should correctly combine base path with empty method path', async () => {
|
|
56
|
+
@Controller('/api/users')
|
|
57
|
+
class UserController {
|
|
58
|
+
@GET('')
|
|
59
|
+
public listUsers() {
|
|
60
|
+
return { users: ['user1', 'user2'] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
app.registerController(UserController);
|
|
65
|
+
await app.listen();
|
|
66
|
+
|
|
67
|
+
const response = await fetch(`http://localhost:${port}/api/users`);
|
|
68
|
+
expect(response.status).toBe(200);
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
expect(data.users).toBeDefined();
|
|
71
|
+
|
|
72
|
+
// 访问 / 应该返回 404
|
|
73
|
+
const rootResponse = await fetch(`http://localhost:${port}/`);
|
|
74
|
+
expect(rootResponse.status).toBe(404);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should correctly combine base path with method path without leading /', async () => {
|
|
78
|
+
@Controller('/api/orders')
|
|
79
|
+
class OrderController {
|
|
80
|
+
@GET('list')
|
|
81
|
+
public listOrders() {
|
|
82
|
+
return { orders: ['order1', 'order2'] };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
app.registerController(OrderController);
|
|
87
|
+
await app.listen();
|
|
88
|
+
|
|
89
|
+
const response = await fetch(`http://localhost:${port}/api/orders/list`);
|
|
90
|
+
expect(response.status).toBe(200);
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
expect(data.orders).toBeDefined();
|
|
93
|
+
|
|
94
|
+
// 访问 /api/orders 应该返回 404
|
|
95
|
+
const baseResponse = await fetch(`http://localhost:${port}/api/orders`);
|
|
96
|
+
expect(baseResponse.status).toBe(404);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should handle root controller with method path /', async () => {
|
|
100
|
+
@Controller('')
|
|
101
|
+
class RootController {
|
|
102
|
+
@GET('/')
|
|
103
|
+
public root() {
|
|
104
|
+
return { message: 'root' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
app.registerController(RootController);
|
|
109
|
+
await app.listen();
|
|
110
|
+
|
|
111
|
+
const response = await fetch(`http://localhost:${port}/`);
|
|
112
|
+
expect(response.status).toBe(200);
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
expect(data.message).toBe('root');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should handle root controller with empty method path', async () => {
|
|
118
|
+
@Controller('/')
|
|
119
|
+
class RootController {
|
|
120
|
+
@GET('')
|
|
121
|
+
public root() {
|
|
122
|
+
return { message: 'root' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
app.registerController(RootController);
|
|
127
|
+
await app.listen();
|
|
128
|
+
|
|
129
|
+
const response = await fetch(`http://localhost:${port}/`);
|
|
130
|
+
expect(response.status).toBe(200);
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
expect(data.message).toBe('root');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should not allow / to match when controller has base path', async () => {
|
|
136
|
+
@Controller('/api/products')
|
|
137
|
+
class ProductController {
|
|
138
|
+
@GET('/')
|
|
139
|
+
public listProducts() {
|
|
140
|
+
return { products: ['product1'] };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Controller('/')
|
|
145
|
+
class RootController {
|
|
146
|
+
@GET('/')
|
|
147
|
+
public root() {
|
|
148
|
+
return { message: 'root' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
app.registerController(ProductController);
|
|
153
|
+
app.registerController(RootController);
|
|
154
|
+
await app.listen();
|
|
155
|
+
|
|
156
|
+
// /api/products 应该访问 ProductController
|
|
157
|
+
const productsResponse = await fetch(`http://localhost:${port}/api/products`);
|
|
158
|
+
expect(productsResponse.status).toBe(200);
|
|
159
|
+
const productsData = await productsResponse.json();
|
|
160
|
+
expect(productsData.products).toBeDefined();
|
|
161
|
+
|
|
162
|
+
// / 应该访问 RootController
|
|
163
|
+
const rootResponse = await fetch(`http://localhost:${port}/`);
|
|
164
|
+
expect(rootResponse.status).toBe(200);
|
|
165
|
+
const rootData = await rootResponse.json();
|
|
166
|
+
expect(rootData.message).toBe('root');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should handle multiple controllers with different base paths', async () => {
|
|
170
|
+
@Controller('/api/products')
|
|
171
|
+
class ProductController {
|
|
172
|
+
@GET('/')
|
|
173
|
+
public listProducts() {
|
|
174
|
+
return { source: 'products' };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Controller('/api/users')
|
|
179
|
+
class UserController {
|
|
180
|
+
@GET('/')
|
|
181
|
+
public listUsers() {
|
|
182
|
+
return { source: 'users' };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
app.registerController(ProductController);
|
|
187
|
+
app.registerController(UserController);
|
|
188
|
+
await app.listen();
|
|
189
|
+
|
|
190
|
+
// 访问 /api/products 应该返回 products
|
|
191
|
+
const productsResponse = await fetch(`http://localhost:${port}/api/products`);
|
|
192
|
+
expect(productsResponse.status).toBe(200);
|
|
193
|
+
const productsData = await productsResponse.json();
|
|
194
|
+
expect(productsData.source).toBe('products');
|
|
195
|
+
|
|
196
|
+
// 访问 /api/users 应该返回 users
|
|
197
|
+
const usersResponse = await fetch(`http://localhost:${port}/api/users`);
|
|
198
|
+
expect(usersResponse.status).toBe(200);
|
|
199
|
+
const usersData = await usersResponse.json();
|
|
200
|
+
expect(usersData.source).toBe('users');
|
|
201
|
+
|
|
202
|
+
// 访问 / 应该返回 404
|
|
203
|
+
const rootResponse = await fetch(`http://localhost:${port}/`);
|
|
204
|
+
expect(rootResponse.status).toBe(404);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|