@dangao/bun-server 1.1.4 → 1.3.0
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/decorators.d.ts +46 -1
- package/dist/controller/decorators.d.ts.map +1 -1
- package/dist/controller/index.d.ts +2 -2
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/param-binder.d.ts +12 -0
- package/dist/controller/param-binder.d.ts.map +1 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/context-service.d.ts +83 -0
- package/dist/core/context-service.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/di/container.d.ts +6 -0
- package/dist/di/container.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +312 -125
- package/package.json +1 -1
- package/src/controller/decorators.ts +74 -0
- package/src/controller/index.ts +16 -2
- package/src/controller/param-binder.ts +91 -1
- package/src/core/application.ts +26 -19
- package/src/core/context-service.ts +134 -0
- package/src/core/index.ts +1 -0
- package/src/di/container.ts +81 -16
- package/src/index.ts +19 -2
- package/tests/controller/context-decorator.test.ts +183 -0
- package/tests/controller/param-map.test.ts +237 -0
- package/tests/core/context-service.test.ts +191 -0
- package/tests/di/scoped-lifecycle.test.ts +223 -0
- package/tests/middleware/rate-limit.test.ts +7 -2
- package/tests/utils/test-port.ts +21 -2
- /package/{readme.md → README.md} +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Context } from '../core/context';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getParamMetadata,
|
|
4
|
+
ParamType,
|
|
5
|
+
type ParamMetadata,
|
|
6
|
+
type QueryMapOptions,
|
|
7
|
+
type HeaderMapOptions,
|
|
8
|
+
} from './decorators';
|
|
3
9
|
import { Container } from '../di/container';
|
|
4
10
|
import { SessionService } from '../session/service';
|
|
5
11
|
import { SESSION_SERVICE_TOKEN } from '../session/types';
|
|
12
|
+
import { contextStore } from '../core/context-service';
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* 参数绑定器
|
|
@@ -102,6 +109,13 @@ export class ParamBinder {
|
|
|
102
109
|
// SessionService 未注册,返回 undefined
|
|
103
110
|
}
|
|
104
111
|
return undefined;
|
|
112
|
+
case ParamType.CONTEXT:
|
|
113
|
+
// 从 AsyncLocalStorage 获取当前请求的 Context
|
|
114
|
+
return contextStore.getStore() ?? context;
|
|
115
|
+
case ParamType.QUERY_MAP:
|
|
116
|
+
return await this.getQueryMapValue(meta.options as QueryMapOptions, context);
|
|
117
|
+
case ParamType.HEADER_MAP:
|
|
118
|
+
return await this.getHeaderMapValue(meta.options as HeaderMapOptions, context);
|
|
105
119
|
default:
|
|
106
120
|
return undefined;
|
|
107
121
|
}
|
|
@@ -153,5 +167,81 @@ export class ParamBinder {
|
|
|
153
167
|
private static getHeaderValue(key: string, context: Context): string | null {
|
|
154
168
|
return context.getHeader(key);
|
|
155
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 获取 QueryMap 值
|
|
173
|
+
* @param options - 装饰器选项
|
|
174
|
+
* @param context - 请求上下文
|
|
175
|
+
*/
|
|
176
|
+
private static async getQueryMapValue(
|
|
177
|
+
options: QueryMapOptions | undefined,
|
|
178
|
+
context: Context,
|
|
179
|
+
): Promise<unknown> {
|
|
180
|
+
const result: Record<string, string | string[]> = {};
|
|
181
|
+
const searchParams = context.query;
|
|
182
|
+
// 收集所有键,处理重复 key -> string[]
|
|
183
|
+
for (const key of searchParams.keys()) {
|
|
184
|
+
const values = searchParams.getAll(key);
|
|
185
|
+
if (values.length === 1) {
|
|
186
|
+
result[key] = values[0];
|
|
187
|
+
} else {
|
|
188
|
+
result[key] = values;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let output: unknown = result;
|
|
193
|
+
if (options?.transform) {
|
|
194
|
+
output = await options.transform(result);
|
|
195
|
+
}
|
|
196
|
+
if (options?.validate) {
|
|
197
|
+
await options.validate(output as never);
|
|
198
|
+
}
|
|
199
|
+
return output;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 获取 HeaderMap 值
|
|
204
|
+
* @param options - 装饰器选项
|
|
205
|
+
* @param context - 请求上下文
|
|
206
|
+
*/
|
|
207
|
+
private static async getHeaderMapValue(
|
|
208
|
+
options: HeaderMapOptions | undefined,
|
|
209
|
+
context: Context,
|
|
210
|
+
): Promise<unknown> {
|
|
211
|
+
const normalize = options?.normalize ?? true;
|
|
212
|
+
// Headers API 总是将 header 名称规范化为小写,所以 pick 数组也应该总是小写化
|
|
213
|
+
const pick = options?.pick?.map((key) => key.toLowerCase());
|
|
214
|
+
const headers = context.headers;
|
|
215
|
+
const result: Record<string, string | string[]> = {};
|
|
216
|
+
|
|
217
|
+
headers.forEach((value, rawKey) => {
|
|
218
|
+
// Headers API 总是返回小写的 key,所以 rawKey 已经是小写
|
|
219
|
+
// normalize 选项决定结果中的 key 格式(虽然实际上总是小写)
|
|
220
|
+
const key = normalize ? rawKey.toLowerCase() : rawKey;
|
|
221
|
+
if (pick && !pick.includes(key)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// 处理可能的多值(逗号分隔),统一 trim
|
|
225
|
+
const parts = value
|
|
226
|
+
.split(',')
|
|
227
|
+
.map((item) => item.trim())
|
|
228
|
+
.filter((item) => item.length > 0);
|
|
229
|
+
if (parts.length <= 1) {
|
|
230
|
+
// 单值也保持 trim 后的形态
|
|
231
|
+
result[key] = parts[0] ?? '';
|
|
232
|
+
} else {
|
|
233
|
+
result[key] = parts;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
let output: unknown = result;
|
|
238
|
+
if (options?.transform) {
|
|
239
|
+
output = await options.transform(result);
|
|
240
|
+
}
|
|
241
|
+
if (options?.validate) {
|
|
242
|
+
await options.validate(output as never);
|
|
243
|
+
}
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
156
246
|
}
|
|
157
247
|
|
package/src/core/application.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BunServer, type ServerOptions } from './server';
|
|
2
2
|
import { Context } from './context';
|
|
3
|
+
import { contextStore, ContextService, CONTEXT_SERVICE_TOKEN } from './context-service';
|
|
3
4
|
import { RouteRegistry } from '../router/registry';
|
|
4
5
|
import { ControllerRegistry } from '../controller/controller';
|
|
5
6
|
import { MiddlewarePipeline } from '../middleware/pipeline';
|
|
@@ -54,6 +55,9 @@ export class Application {
|
|
|
54
55
|
const interceptorRegistry = new InterceptorRegistry();
|
|
55
56
|
container.registerInstance(INTERCEPTOR_REGISTRY_TOKEN, interceptorRegistry);
|
|
56
57
|
|
|
58
|
+
// 自动注册 ContextService
|
|
59
|
+
container.registerInstance(CONTEXT_SERVICE_TOKEN, new ContextService());
|
|
60
|
+
|
|
57
61
|
// 默认注册 Logger(如果通过模块注册,会被覆盖)
|
|
58
62
|
this.registerExtension(new LoggerExtension());
|
|
59
63
|
}
|
|
@@ -148,29 +152,32 @@ export class Application {
|
|
|
148
152
|
* @returns 响应对象
|
|
149
153
|
*/
|
|
150
154
|
private async handleRequest(context: Context): Promise<Response> {
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
// 使用 AsyncLocalStorage 包裹请求处理,确保所有中间件和控制器都在请求上下文中执行
|
|
156
|
+
return await contextStore.run(context, async () => {
|
|
157
|
+
// 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
|
|
158
|
+
// 这样可以确保 Request.body 流只读取一次
|
|
159
|
+
if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
|
|
160
|
+
await context.getBody();
|
|
161
|
+
}
|
|
156
162
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
|
|
164
|
+
const registry = RouteRegistry.getInstance();
|
|
165
|
+
const router = registry.getRouter();
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
// 预解析路由,仅设置上下文信息,不执行处理器
|
|
168
|
+
await router.preHandle(context);
|
|
163
169
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
// 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
|
|
171
|
+
// 最后再由路由真正执行控制器方法
|
|
172
|
+
return await this.middlewarePipeline.run(context, async () => {
|
|
173
|
+
const response = await router.handle(context);
|
|
174
|
+
if (response) {
|
|
175
|
+
return response;
|
|
176
|
+
}
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
context.setStatus(404);
|
|
179
|
+
return context.createResponse({ error: 'Not Found' });
|
|
180
|
+
});
|
|
174
181
|
});
|
|
175
182
|
}
|
|
176
183
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { Context } from './context';
|
|
3
|
+
import { Injectable } from '../di/decorators';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context 存储(AsyncLocalStorage)
|
|
7
|
+
* 用于在请求级别存储和访问 Context
|
|
8
|
+
*/
|
|
9
|
+
export const contextStore = new AsyncLocalStorage<Context>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ContextService Token
|
|
13
|
+
*/
|
|
14
|
+
export const CONTEXT_SERVICE_TOKEN = Symbol('ContextService');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 上下文服务
|
|
18
|
+
* 提供统一的上下文访问服务,通过依赖注入方式访问请求上下文
|
|
19
|
+
*/
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class ContextService {
|
|
22
|
+
/**
|
|
23
|
+
* 获取当前请求的 Context
|
|
24
|
+
* 使用 AsyncLocalStorage 实现请求级别隔离
|
|
25
|
+
* @returns 当前请求的 Context,如果不在请求上下文中则返回 undefined
|
|
26
|
+
*/
|
|
27
|
+
public getContext(): Context | undefined {
|
|
28
|
+
return contextStore.getStore();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取请求头
|
|
33
|
+
* @param key - 请求头键名
|
|
34
|
+
* @returns 请求头值,如果不存在则返回 null
|
|
35
|
+
*/
|
|
36
|
+
public getHeader(key: string): string | null {
|
|
37
|
+
const context = this.getContext();
|
|
38
|
+
return context?.getHeader(key) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取查询参数
|
|
43
|
+
* @param key - 查询参数键名
|
|
44
|
+
* @returns 查询参数值,如果不存在则返回 null
|
|
45
|
+
*/
|
|
46
|
+
public getQuery(key: string): string | null {
|
|
47
|
+
const context = this.getContext();
|
|
48
|
+
return context?.getQuery(key) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 获取所有查询参数
|
|
53
|
+
* @returns 查询参数对象
|
|
54
|
+
*/
|
|
55
|
+
public getQueryAll(): Record<string, string> {
|
|
56
|
+
const context = this.getContext();
|
|
57
|
+
return context?.getQueryAll() ?? {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取路径参数
|
|
62
|
+
* @param key - 路径参数键名
|
|
63
|
+
* @returns 路径参数值,如果不存在则返回 undefined
|
|
64
|
+
*/
|
|
65
|
+
public getParam(key: string): string | undefined {
|
|
66
|
+
const context = this.getContext();
|
|
67
|
+
return context?.getParam(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取请求体
|
|
72
|
+
* @returns 请求体(已解析的)
|
|
73
|
+
*/
|
|
74
|
+
public getBody(): unknown {
|
|
75
|
+
const context = this.getContext();
|
|
76
|
+
return context?.body ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取请求方法
|
|
81
|
+
* @returns HTTP 方法
|
|
82
|
+
*/
|
|
83
|
+
public getMethod(): string {
|
|
84
|
+
const context = this.getContext();
|
|
85
|
+
return context?.method ?? '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取请求路径
|
|
90
|
+
* @returns 请求路径
|
|
91
|
+
*/
|
|
92
|
+
public getPath(): string {
|
|
93
|
+
const context = this.getContext();
|
|
94
|
+
return context?.path ?? '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 获取请求 URL
|
|
99
|
+
* @returns 请求 URL
|
|
100
|
+
*/
|
|
101
|
+
public getUrl(): URL | undefined {
|
|
102
|
+
const context = this.getContext();
|
|
103
|
+
return context?.url;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取客户端 IP 地址
|
|
108
|
+
* @returns 客户端 IP 地址
|
|
109
|
+
*/
|
|
110
|
+
public getClientIp(): string {
|
|
111
|
+
const context = this.getContext();
|
|
112
|
+
return context?.getClientIp() ?? 'unknown';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 设置响应头
|
|
117
|
+
* @param key - 响应头键名
|
|
118
|
+
* @param value - 响应头值
|
|
119
|
+
*/
|
|
120
|
+
public setHeader(key: string, value: string): void {
|
|
121
|
+
const context = this.getContext();
|
|
122
|
+
context?.setHeader(key, value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 设置状态码
|
|
127
|
+
* @param code - HTTP 状态码
|
|
128
|
+
*/
|
|
129
|
+
public setStatus(code: number): void {
|
|
130
|
+
const context = this.getContext();
|
|
131
|
+
context?.setStatus(code);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
package/src/core/index.ts
CHANGED
package/src/di/container.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./decorators";
|
|
12
12
|
import { LoggerManager } from "@dangao/logsmith";
|
|
13
13
|
import type { Constructor } from "@/core/types";
|
|
14
|
+
import { contextStore } from "../core/context-service";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 依赖注入容器
|
|
@@ -36,6 +37,12 @@ export class Container {
|
|
|
36
37
|
*/
|
|
37
38
|
private readonly singletons = new Map<string | symbol, unknown>();
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 请求作用域实例缓存(使用 WeakMap 存储,key 为 Context)
|
|
42
|
+
* 每个请求都有独立的实例映射
|
|
43
|
+
*/
|
|
44
|
+
private readonly scopedInstances = new WeakMap<object, Map<string | symbol, unknown>>();
|
|
45
|
+
|
|
39
46
|
/**
|
|
40
47
|
* 类型到 token 的映射(用于接口注入)
|
|
41
48
|
*/
|
|
@@ -144,6 +151,22 @@ export class Container {
|
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
// 检查请求作用域缓存
|
|
155
|
+
if (provider.lifecycle === Lifecycle.Scoped) {
|
|
156
|
+
const context = contextStore.getStore();
|
|
157
|
+
if (context) {
|
|
158
|
+
let scopedMap = this.scopedInstances.get(context);
|
|
159
|
+
if (!scopedMap) {
|
|
160
|
+
scopedMap = new Map();
|
|
161
|
+
this.scopedInstances.set(context, scopedMap);
|
|
162
|
+
}
|
|
163
|
+
const scopedInstance = scopedMap.get(tokenKey);
|
|
164
|
+
if (scopedInstance) {
|
|
165
|
+
return scopedInstance as T;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
// 使用工厂函数或实例化
|
|
148
171
|
let instance: T;
|
|
149
172
|
if (provider.factory) {
|
|
@@ -167,6 +190,19 @@ export class Container {
|
|
|
167
190
|
this.singletons.set(tokenKey, instance);
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
// 缓存请求作用域实例
|
|
194
|
+
if (provider.lifecycle === Lifecycle.Scoped) {
|
|
195
|
+
const context = contextStore.getStore();
|
|
196
|
+
if (context) {
|
|
197
|
+
let scopedMap = this.scopedInstances.get(context);
|
|
198
|
+
if (!scopedMap) {
|
|
199
|
+
scopedMap = new Map();
|
|
200
|
+
this.scopedInstances.set(context, scopedMap);
|
|
201
|
+
}
|
|
202
|
+
scopedMap.set(tokenKey, instance);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
170
206
|
return instance;
|
|
171
207
|
}
|
|
172
208
|
|
|
@@ -213,30 +249,57 @@ export class Container {
|
|
|
213
249
|
}
|
|
214
250
|
}
|
|
215
251
|
|
|
216
|
-
//
|
|
217
|
-
if (provider.
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
this.
|
|
252
|
+
// 检查请求作用域缓存
|
|
253
|
+
if (provider.lifecycle === Lifecycle.Scoped) {
|
|
254
|
+
const context = contextStore.getStore();
|
|
255
|
+
if (context) {
|
|
256
|
+
let scopedMap = this.scopedInstances.get(context);
|
|
257
|
+
if (!scopedMap) {
|
|
258
|
+
scopedMap = new Map();
|
|
259
|
+
this.scopedInstances.set(context, scopedMap);
|
|
260
|
+
}
|
|
261
|
+
const scopedInstance = scopedMap.get(token);
|
|
262
|
+
if (scopedInstance) {
|
|
263
|
+
return scopedInstance;
|
|
264
|
+
}
|
|
221
265
|
}
|
|
222
|
-
return instance;
|
|
223
266
|
}
|
|
224
267
|
|
|
225
|
-
|
|
268
|
+
// 使用工厂函数或实现类
|
|
269
|
+
let instance: unknown;
|
|
270
|
+
if (provider.factory) {
|
|
271
|
+
instance = provider.factory();
|
|
272
|
+
} else if (
|
|
226
273
|
provider.implementation && typeof provider.implementation === "function"
|
|
227
274
|
) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
instance = this.instantiate(provider.implementation);
|
|
276
|
+
} else {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Cannot instantiate token: ${
|
|
279
|
+
String(token)
|
|
280
|
+
}. Factory function required.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 缓存单例
|
|
285
|
+
if (provider.lifecycle === Lifecycle.Singleton) {
|
|
286
|
+
this.singletons.set(token, instance);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 缓存请求作用域实例
|
|
290
|
+
if (provider.lifecycle === Lifecycle.Scoped) {
|
|
291
|
+
const context = contextStore.getStore();
|
|
292
|
+
if (context) {
|
|
293
|
+
let scopedMap = this.scopedInstances.get(context);
|
|
294
|
+
if (!scopedMap) {
|
|
295
|
+
scopedMap = new Map();
|
|
296
|
+
this.scopedInstances.set(context, scopedMap);
|
|
297
|
+
}
|
|
298
|
+
scopedMap.set(token, instance);
|
|
231
299
|
}
|
|
232
|
-
return instance;
|
|
233
300
|
}
|
|
234
301
|
|
|
235
|
-
|
|
236
|
-
`Cannot instantiate token: ${
|
|
237
|
-
String(token)
|
|
238
|
-
}. Factory function required.`,
|
|
239
|
-
);
|
|
302
|
+
return instance;
|
|
240
303
|
}
|
|
241
304
|
|
|
242
305
|
// 对于构造函数类型,使用公共 resolve 方法
|
|
@@ -286,12 +349,14 @@ export class Container {
|
|
|
286
349
|
|
|
287
350
|
/**
|
|
288
351
|
* 清除所有注册(主要用于测试)
|
|
352
|
+
* 注意:scopedInstances 使用 WeakMap,会自动清理,无需手动清除
|
|
289
353
|
*/
|
|
290
354
|
public clear(): void {
|
|
291
355
|
this.providers.clear();
|
|
292
356
|
this.singletons.clear();
|
|
293
357
|
this.typeToToken.clear();
|
|
294
358
|
this.dependencyPlans.clear();
|
|
359
|
+
// scopedInstances 使用 WeakMap,当 Context 对象被 GC 时会自动清理
|
|
295
360
|
}
|
|
296
361
|
|
|
297
362
|
/**
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
export { Application, type ApplicationOptions } from './core/application';
|
|
2
2
|
export { BunServer, type ServerOptions } from './core/server';
|
|
3
3
|
export { Context } from './core/context';
|
|
4
|
+
export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './core/context-service';
|
|
4
5
|
export { Route, Router, RouteRegistry } from './router';
|
|
5
6
|
export { GET, POST, PUT, DELETE, PATCH } from './router/decorators';
|
|
6
7
|
export type { HttpMethod, RouteHandler, RouteMatch } from './router/types';
|
|
7
8
|
export { BodyParser, RequestWrapper, ResponseBuilder } from './request';
|
|
8
|
-
export {
|
|
9
|
-
|
|
9
|
+
export {
|
|
10
|
+
Body,
|
|
11
|
+
Query,
|
|
12
|
+
QueryMap,
|
|
13
|
+
Param,
|
|
14
|
+
Header,
|
|
15
|
+
HeaderMap,
|
|
16
|
+
ParamBinder,
|
|
17
|
+
Controller,
|
|
18
|
+
ControllerRegistry,
|
|
19
|
+
} from './controller';
|
|
20
|
+
export { Context as ContextParam } from './controller';
|
|
21
|
+
export type {
|
|
22
|
+
ParamMetadata,
|
|
23
|
+
QueryMapOptions,
|
|
24
|
+
HeaderMapOptions,
|
|
25
|
+
ControllerMetadata,
|
|
26
|
+
} from './controller';
|
|
10
27
|
export { Container } from './di/container';
|
|
11
28
|
export { Injectable, Inject } from './di/decorators';
|
|
12
29
|
export { Lifecycle, type ProviderConfig, type DependencyMetadata } from './di/types';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { Controller, Context, Param } from '../../src/controller';
|
|
4
|
+
import { GET } from '../../src/router';
|
|
5
|
+
import { contextStore } from '../../src/core/context-service';
|
|
6
|
+
import { Context as ContextType } from '../../src/core/context';
|
|
7
|
+
import { getTestPort } from '../utils/test-port';
|
|
8
|
+
|
|
9
|
+
describe('@Context() Decorator', () => {
|
|
10
|
+
let app: Application;
|
|
11
|
+
let port: number;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
port = getTestPort();
|
|
15
|
+
app = new Application({ port });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (app) {
|
|
20
|
+
await app.stop();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should inject Context into controller method', async () => {
|
|
25
|
+
@Controller('/api/test')
|
|
26
|
+
class TestController {
|
|
27
|
+
@GET('/context')
|
|
28
|
+
public async getContext(@Context() context: ContextType) {
|
|
29
|
+
expect(context).toBeDefined();
|
|
30
|
+
expect(context.path).toBe('/api/test/context');
|
|
31
|
+
return { path: context.path, method: context.method };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
app.registerController(TestController);
|
|
36
|
+
await app.listen();
|
|
37
|
+
|
|
38
|
+
const response = await fetch(`http://localhost:${port}/api/test/context`);
|
|
39
|
+
expect(response.status).toBe(200);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
expect(data.path).toBe('/api/test/context');
|
|
42
|
+
expect(data.method).toBe('GET');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should inject Context with other parameters', async () => {
|
|
46
|
+
@Controller('/api/test')
|
|
47
|
+
class TestController {
|
|
48
|
+
@GET('/users/:id')
|
|
49
|
+
public async getUser(
|
|
50
|
+
@Context() context: ContextType,
|
|
51
|
+
@Param('id') id: string,
|
|
52
|
+
) {
|
|
53
|
+
expect(context).toBeDefined();
|
|
54
|
+
expect(id).toBe('123');
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
path: context.path,
|
|
58
|
+
method: context.method,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
app.registerController(TestController);
|
|
64
|
+
await app.listen();
|
|
65
|
+
|
|
66
|
+
const response = await fetch(`http://localhost:${port}/api/test/users/123`);
|
|
67
|
+
expect(response.status).toBe(200);
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
expect(data.id).toBe('123');
|
|
70
|
+
expect(data.path).toBe('/api/test/users/123');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should access headers from injected Context', async () => {
|
|
74
|
+
@Controller('/api/test')
|
|
75
|
+
class TestController {
|
|
76
|
+
@GET('/headers')
|
|
77
|
+
public async getHeaders(@Context() context: ContextType) {
|
|
78
|
+
const authHeader = context.getHeader('Authorization');
|
|
79
|
+
return { authorization: authHeader };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
app.registerController(TestController);
|
|
84
|
+
await app.listen();
|
|
85
|
+
|
|
86
|
+
const response = await fetch(`http://localhost:${port}/api/test/headers`, {
|
|
87
|
+
headers: { 'Authorization': 'Bearer token123' },
|
|
88
|
+
});
|
|
89
|
+
expect(response.status).toBe(200);
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
expect(data.authorization).toBe('Bearer token123');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should access query params from injected Context', async () => {
|
|
95
|
+
@Controller('/api/test')
|
|
96
|
+
class TestController {
|
|
97
|
+
@GET('/query')
|
|
98
|
+
public async getQuery(@Context() context: ContextType) {
|
|
99
|
+
const name = context.getQuery('name');
|
|
100
|
+
const age = context.getQuery('age');
|
|
101
|
+
return { name, age };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
app.registerController(TestController);
|
|
106
|
+
await app.listen();
|
|
107
|
+
|
|
108
|
+
const response = await fetch(`http://localhost:${port}/api/test/query?name=John&age=30`);
|
|
109
|
+
expect(response.status).toBe(200);
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
expect(data.name).toBe('John');
|
|
112
|
+
expect(data.age).toBe('30');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should access path params from injected Context', async () => {
|
|
116
|
+
@Controller('/api/test')
|
|
117
|
+
class TestController {
|
|
118
|
+
@GET('/users/:id/posts/:postId')
|
|
119
|
+
public async getPost(@Context() context: ContextType) {
|
|
120
|
+
const userId = context.getParam('id');
|
|
121
|
+
const postId = context.getParam('postId');
|
|
122
|
+
return { userId, postId };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
app.registerController(TestController);
|
|
127
|
+
await app.listen();
|
|
128
|
+
|
|
129
|
+
const response = await fetch(`http://localhost:${port}/api/test/users/123/posts/456`);
|
|
130
|
+
expect(response.status).toBe(200);
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
expect(data.userId).toBe('123');
|
|
133
|
+
expect(data.postId).toBe('456');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should set response headers via injected Context', async () => {
|
|
137
|
+
@Controller('/api/test')
|
|
138
|
+
class TestController {
|
|
139
|
+
@GET('/custom-header')
|
|
140
|
+
public async setHeader(@Context() context: ContextType) {
|
|
141
|
+
context.setHeader('X-Custom-Header', 'custom-value');
|
|
142
|
+
return { message: 'ok' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
app.registerController(TestController);
|
|
147
|
+
await app.listen();
|
|
148
|
+
|
|
149
|
+
const response = await fetch(`http://localhost:${port}/api/test/custom-header`);
|
|
150
|
+
expect(response.status).toBe(200);
|
|
151
|
+
expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should handle Context injection in service layer', async () => {
|
|
155
|
+
// 这个测试验证 ContextService 可以在服务层使用
|
|
156
|
+
const { ContextService, CONTEXT_SERVICE_TOKEN } = await import('../../src/core/context-service');
|
|
157
|
+
const container = app.getContainer();
|
|
158
|
+
const contextService = container.resolve<ContextService>(CONTEXT_SERVICE_TOKEN);
|
|
159
|
+
|
|
160
|
+
@Controller('/api/test')
|
|
161
|
+
class TestController {
|
|
162
|
+
@GET('/service-context')
|
|
163
|
+
public async getServiceContext() {
|
|
164
|
+
// 在控制器中,ContextService 应该能够访问当前请求的 Context
|
|
165
|
+
const context = contextService.getContext();
|
|
166
|
+
if (!context) {
|
|
167
|
+
return { error: 'No context' };
|
|
168
|
+
}
|
|
169
|
+
return { path: context.path, method: context.method };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
app.registerController(TestController);
|
|
174
|
+
await app.listen();
|
|
175
|
+
|
|
176
|
+
const response = await fetch(`http://localhost:${port}/api/test/service-context`);
|
|
177
|
+
expect(response.status).toBe(200);
|
|
178
|
+
const data = await response.json();
|
|
179
|
+
expect(data.path).toBe('/api/test/service-context');
|
|
180
|
+
expect(data.method).toBe('GET');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|