@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.
@@ -1,8 +1,15 @@
1
1
  import type { Context } from '../core/context';
2
- import { getParamMetadata, ParamType, type ParamMetadata } from './decorators';
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
 
@@ -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
- // 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
152
- // 这样可以确保 Request.body 流只读取一次
153
- if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
154
- await context.getBody();
155
- }
155
+ // 使用 AsyncLocalStorage 包裹请求处理,确保所有中间件和控制器都在请求上下文中执行
156
+ return await contextStore.run(context, async () => {
157
+ // 对于 POSTPUTPATCH 请求,提前解析 body 并缓存
158
+ // 这样可以确保 Request.body 流只读取一次
159
+ if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
160
+ await context.getBody();
161
+ }
156
162
 
157
- // 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
158
- const registry = RouteRegistry.getInstance();
159
- const router = registry.getRouter();
163
+ // 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
164
+ const registry = RouteRegistry.getInstance();
165
+ const router = registry.getRouter();
160
166
 
161
- // 预解析路由,仅设置上下文信息,不执行处理器
162
- await router.preHandle(context);
167
+ // 预解析路由,仅设置上下文信息,不执行处理器
168
+ await router.preHandle(context);
163
169
 
164
- // 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
165
- // 最后再由路由真正执行控制器方法
166
- return await this.middlewarePipeline.run(context, async () => {
167
- const response = await router.handle(context);
168
- if (response) {
169
- return response;
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
- context.setStatus(404);
173
- return context.createResponse({ error: 'Not Found' });
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
@@ -1,4 +1,5 @@
1
1
  export { Application, type ApplicationOptions } from './application';
2
2
  export { BunServer, type ServerOptions } from './server';
3
3
  export { Context } from './context';
4
+ export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './context-service';
4
5
 
@@ -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.factory) {
218
- const instance = provider.factory();
219
- if (provider.lifecycle === Lifecycle.Singleton) {
220
- this.singletons.set(token, instance);
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
- if (
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
- const instance = this.instantiate(provider.implementation);
229
- if (provider.lifecycle === Lifecycle.Singleton) {
230
- this.singletons.set(token, instance);
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
- throw new Error(
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 { Body, Query, Param, Header, ParamBinder, Controller, ControllerRegistry } from './controller';
9
- export type { ParamMetadata, ControllerMetadata } from './controller';
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
+