@dangao/bun-server 1.8.1 → 1.8.3

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.
Files changed (35) hide show
  1. package/dist/controller/controller.d.ts.map +1 -1
  2. package/dist/core/application.d.ts.map +1 -1
  3. package/dist/error/handler.d.ts.map +1 -1
  4. package/dist/index.js +98 -21
  5. package/dist/middleware/builtin/error-handler.d.ts.map +1 -1
  6. package/dist/middleware/builtin/logger.d.ts.map +1 -1
  7. package/dist/router/decorators.d.ts +10 -10
  8. package/dist/router/decorators.d.ts.map +1 -1
  9. package/dist/router/router.d.ts.map +1 -1
  10. package/docs/api.md +194 -81
  11. package/docs/extensions.md +53 -0
  12. package/docs/guide.md +243 -1
  13. package/docs/microservice-config-center.md +73 -74
  14. package/docs/microservice-nacos.md +89 -90
  15. package/docs/microservice-service-registry.md +85 -86
  16. package/docs/microservice.md +142 -137
  17. package/docs/request-lifecycle.md +45 -4
  18. package/docs/symbol-interface-pattern.md +106 -106
  19. package/docs/zh/api.md +458 -18
  20. package/docs/zh/extensions.md +53 -0
  21. package/docs/zh/guide.md +251 -4
  22. package/docs/zh/microservice-config-center.md +258 -0
  23. package/docs/zh/microservice-nacos.md +346 -0
  24. package/docs/zh/microservice-service-registry.md +306 -0
  25. package/docs/zh/microservice.md +680 -0
  26. package/docs/zh/request-lifecycle.md +43 -5
  27. package/package.json +1 -1
  28. package/src/controller/controller.ts +10 -2
  29. package/src/core/application.ts +11 -0
  30. package/src/error/handler.ts +17 -0
  31. package/src/middleware/builtin/error-handler.ts +10 -0
  32. package/src/middleware/builtin/logger.ts +17 -0
  33. package/src/router/decorators.ts +15 -15
  34. package/src/router/router.ts +18 -1
  35. package/tests/controller/path-combination.test.ts +135 -0
@@ -20,6 +20,10 @@ HTTP Request
20
20
  └─────────────────────────────────────┘
21
21
 
22
22
  ┌─────────────────────────────────────┐
23
+ │ 守卫 │
24
+ └─────────────────────────────────────┘
25
+
26
+ ┌─────────────────────────────────────┐
23
27
  │ 拦截器(前置) │
24
28
  └─────────────────────────────────────┘
25
29
 
@@ -167,7 +171,39 @@ class UserController {
167
171
  }
168
172
  ```
169
173
 
170
- ## 4. 拦截器(前置处理)
174
+ ## 4. 守卫
175
+
176
+ 守卫在路由匹配之后、拦截器之前执行,提供细粒度的访问控制。它们可以访问 `ExecutionContext`,提供关于当前请求的丰富信息。
177
+
178
+ ### 执行顺序
179
+
180
+ 1. **全局守卫** - 通过 `SecurityModule.forRoot({ globalGuards: [...] })` 注册
181
+ 2. **控制器守卫** - 通过 `@UseGuards()` 应用于控制器类
182
+ 3. **方法守卫** - 通过 `@UseGuards()` 应用于方法
183
+
184
+ ### 内置守卫
185
+
186
+ - `AuthGuard`:要求认证
187
+ - `OptionalAuthGuard`:可选认证
188
+ - `RolesGuard`:基于角色的授权(与 `@Roles()` 装饰器一起使用)
189
+
190
+ ### 示例
191
+
192
+ ```typescript
193
+ @Controller('/api/admin')
194
+ @UseGuards(AuthGuard, RolesGuard)
195
+ class AdminController {
196
+ @GET('/dashboard')
197
+ @Roles('admin')
198
+ public dashboard() {
199
+ return { message: '管理员仪表板' };
200
+ }
201
+ }
202
+ ```
203
+
204
+ 详细文档请参阅 [守卫](./guards.md)。
205
+
206
+ ## 5. 拦截器(前置处理)
171
207
 
172
208
  拦截器在控制器方法之前和之后运行。前置拦截器按顺序执行:
173
209
 
@@ -200,7 +236,7 @@ class ApiController {}
200
236
  - 响应转换
201
237
  - 性能监控
202
238
 
203
- ## 5. 参数绑定和验证
239
+ ## 6. 参数绑定和验证
204
240
 
205
241
  ### 参数装饰器
206
242
 
@@ -208,9 +244,11 @@ class ApiController {}
208
244
  |--------|------|------|
209
245
  | `@Param(name)` | URL 路径参数 | `/users/:id` → `@Param('id')` |
210
246
  | `@Query(name)` | 查询字符串 | `?page=1` → `@Query('page')` |
247
+ | `@QueryMap()` | 所有查询参数 | `?page=1&limit=10` → `@QueryMap()` 返回 `{ page: '1', limit: '10' }` |
211
248
  | `@Body()` | 请求体 | JSON 请求体 |
212
249
  | `@Body(name)` | 请求体属性 | `body.name` → `@Body('name')` |
213
250
  | `@Header(name)` | 请求头 | `@Header('Authorization')` |
251
+ | `@HeaderMap()` | 所有请求头 | `@HeaderMap()` 返回所有请求头作为对象 |
214
252
  | `@Context()` | 完整上下文 | 请求上下文对象 |
215
253
  | `@Session()` | 会话数据 | 会话对象 |
216
254
 
@@ -262,7 +300,7 @@ public createUser(
262
300
  }
263
301
  ```
264
302
 
265
- ## 6. 控制器方法执行
303
+ ## 7. 控制器方法执行
266
304
 
267
305
  验证通过后,使用已解析的依赖和绑定的参数调用控制器方法。
268
306
 
@@ -294,7 +332,7 @@ class UserController {
294
332
  - **void** - 空响应(204)
295
333
  - **Promise** - 异步操作
296
334
 
297
- ## 7. 拦截器(后置处理)
335
+ ## 8. 拦截器(后置处理)
298
336
 
299
337
  处理器执行后,后置拦截器按相反顺序运行:
300
338
 
@@ -318,7 +356,7 @@ class TransformInterceptor implements Interceptor {
318
356
  }
319
357
  ```
320
358
 
321
- ## 8. 异常过滤器
359
+ ## 9. 异常过滤器
322
360
 
323
361
  如果在请求生命周期中抛出任何异常,它会被异常过滤器捕获。
324
362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dangao/bun-server",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,6 +15,7 @@ import {
15
15
  InterceptorChain,
16
16
  scanInterceptorMetadata,
17
17
  } from '../interceptor';
18
+ import { LoggerManager } from '@dangao/logsmith';
18
19
 
19
20
  /**
20
21
  * 控制器元数据键
@@ -212,6 +213,13 @@ export class ControllerRegistry {
212
213
  // 创建响应
213
214
  return context.createResponse(responseData);
214
215
  } catch (error) {
216
+ LoggerManager.getLogger().debug('Controller handler error', {
217
+ controller: controllerClass.name,
218
+ method: propertyKey,
219
+ path: context.path,
220
+ errorMessage: error instanceof Error ? error.message : String(error),
221
+ stack: error instanceof Error ? error.stack : undefined,
222
+ });
215
223
  // 使用全局错误处理器,确保错误码和国际化正确应用
216
224
  const { handleError } = await import('../error/handler');
217
225
  return await handleError(error, context);
@@ -258,8 +266,8 @@ export class ControllerRegistry {
258
266
  base = '/';
259
267
  }
260
268
 
261
- // 规范化 methodPath:移除前导斜杠
262
- const method = methodPath.replace(/^\/+/, '');
269
+ // 规范化 methodPath:移除前导斜杠(支持 undefined,如 @GET() 无参)
270
+ const method = (methodPath ?? '').replace(/^\/+/, '');
263
271
 
264
272
  if (!method) {
265
273
  // 如果方法路径为空,返回基础路径
@@ -242,6 +242,13 @@ export class Application {
242
242
  * @returns 响应对象
243
243
  */
244
244
  private async handleRequest(context: Context): Promise<Response> {
245
+ const logger = LoggerManager.getLogger();
246
+ logger.debug('[Request] Incoming', {
247
+ method: context.method,
248
+ path: context.path,
249
+ url: context.url?.href,
250
+ });
251
+
245
252
  // 使用 AsyncLocalStorage 包裹请求处理,确保所有中间件和控制器都在请求上下文中执行
246
253
  return await contextStore.run(context, async () => {
247
254
  // 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
@@ -265,6 +272,10 @@ export class Application {
265
272
  return response;
266
273
  }
267
274
 
275
+ logger.debug('[Router] No route matched', {
276
+ method: context.method,
277
+ path: context.path,
278
+ });
268
279
  context.setStatus(404);
269
280
  return context.createResponse({ error: 'Not Found' });
270
281
  });
@@ -3,6 +3,7 @@ import { HttpException } from './http-exception';
3
3
  import { ExceptionFilterRegistry } from './filter';
4
4
  import { ValidationError } from '../validation';
5
5
  import { ErrorMessageI18n } from './i18n';
6
+ import { LoggerManager } from '@dangao/logsmith';
6
7
 
7
8
  /**
8
9
  * 全局错误处理
@@ -16,6 +17,15 @@ export async function handleError(error: unknown, context: Context): Promise<Res
16
17
 
17
18
  if (error instanceof HttpException) {
18
19
  context.setStatus(error.status);
20
+ if (error.status >= 400) {
21
+ LoggerManager.getLogger().debug('HttpException', {
22
+ method: context.method,
23
+ path: context.path,
24
+ status: error.status,
25
+ code: error.code,
26
+ message: error.message,
27
+ });
28
+ }
19
29
 
20
30
  // 如果异常有错误码,尝试国际化消息
21
31
  let errorMessage = error.message;
@@ -56,7 +66,14 @@ export async function handleError(error: unknown, context: Context): Promise<Res
56
66
  }
57
67
 
58
68
  const message = error instanceof Error ? error.message : String(error);
69
+ const stack = error instanceof Error ? error.stack : undefined;
59
70
  context.setStatus(500);
71
+ LoggerManager.getLogger().debug('Internal error (500)', {
72
+ method: context.method,
73
+ path: context.path,
74
+ message,
75
+ stack,
76
+ });
60
77
  return context.createResponse({
61
78
  error: 'Internal Server Error',
62
79
  details: process.env.NODE_ENV === 'production' ? undefined : message,
@@ -41,12 +41,22 @@ export function createErrorHandlingMiddleware(
41
41
  try {
42
42
  return await next();
43
43
  } catch (error) {
44
+ const routeHandler = (context as { routeHandler?: { controller?: { name?: string }; method?: string } }).routeHandler;
44
45
  log(error, { method: context.method, path: context.path });
45
46
  logger.error("Unhandled error", {
46
47
  method: context.method,
47
48
  path: context.path,
48
49
  error,
49
50
  });
51
+ logger.debug("Unhandled error details", {
52
+ method: context.method,
53
+ path: context.path,
54
+ routeHandler: routeHandler
55
+ ? `${routeHandler.controller?.name ?? 'unknown'}.${routeHandler.method ?? 'unknown'}`
56
+ : undefined,
57
+ errorMessage: error instanceof Error ? error.message : String(error),
58
+ stack: error instanceof Error ? error.stack : undefined,
59
+ });
50
60
 
51
61
  if (error instanceof Response) {
52
62
  return error as Response;
@@ -38,6 +38,14 @@ export function createLoggerMiddleware(
38
38
  } finally {
39
39
  const status = response?.status ?? context.statusCode ?? 200;
40
40
  log(`${prefix} ${context.method} ${context.path} ${status}`);
41
+ if (status >= 400) {
42
+ const logger = LoggerManager.getLogger();
43
+ logger.debug(`${prefix} Error response`, {
44
+ method: context.method,
45
+ path: context.path,
46
+ statusCode: status,
47
+ });
48
+ }
41
49
  }
42
50
  };
43
51
  }
@@ -85,6 +93,15 @@ export function createRequestLoggingMiddleware(
85
93
  }ms`,
86
94
  error instanceof Error ? { error: error.message } : undefined,
87
95
  );
96
+ if (error instanceof Error) {
97
+ LoggerManager.getLogger().debug(`${prefix} Request error details`, {
98
+ method: context.method,
99
+ path: context.path,
100
+ durationMs: duration.toFixed(2),
101
+ message: error.message,
102
+ stack: error.stack,
103
+ });
104
+ }
88
105
  throw error;
89
106
  }
90
107
  };
@@ -25,10 +25,10 @@ export interface RouteMetadata {
25
25
  /**
26
26
  * 路由装饰器工厂
27
27
  * @param method - HTTP 方法
28
- * @param path - 路由路径
28
+ * @param path - 路由路径(可选,默认 '',即 @GET() 等价于根路径)
29
29
  * @returns 方法装饰器
30
30
  */
31
- function createRouteDecorator(method: HttpMethod, path: string) {
31
+ function createRouteDecorator(method: HttpMethod, path: string = '') {
32
32
  return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
33
33
  // 注意:装饰器应用顺序问题
34
34
  // 方法装饰器(@GET)在类装饰器(@Controller)之前应用
@@ -72,9 +72,9 @@ function createRouteDecorator(method: HttpMethod, path: string) {
72
72
  }
73
73
  }
74
74
 
75
- existingRoutes.push({
76
- method,
77
- path,
75
+ existingRoutes.push({
76
+ method,
77
+ path: path ?? '',
78
78
  handler: descriptor.value as RouteHandler,
79
79
  propertyKey: propertyKeyStr || undefined,
80
80
  });
@@ -84,40 +84,40 @@ function createRouteDecorator(method: HttpMethod, path: string) {
84
84
 
85
85
  /**
86
86
  * GET 路由装饰器
87
- * @param path - 路由路径
87
+ * @param path - 路由路径(可选,默认 '',即 @GET() 映射到控制器基础路径或 /)
88
88
  */
89
- export function GET(path: string) {
89
+ export function GET(path: string = '') {
90
90
  return createRouteDecorator('GET', path);
91
91
  }
92
92
 
93
93
  /**
94
94
  * POST 路由装饰器
95
- * @param path - 路由路径
95
+ * @param path - 路由路径(可选,默认 '')
96
96
  */
97
- export function POST(path: string) {
97
+ export function POST(path: string = '') {
98
98
  return createRouteDecorator('POST', path);
99
99
  }
100
100
 
101
101
  /**
102
102
  * PUT 路由装饰器
103
- * @param path - 路由路径
103
+ * @param path - 路由路径(可选,默认 '')
104
104
  */
105
- export function PUT(path: string) {
105
+ export function PUT(path: string = '') {
106
106
  return createRouteDecorator('PUT', path);
107
107
  }
108
108
 
109
109
  /**
110
110
  * DELETE 路由装饰器
111
- * @param path - 路由路径
111
+ * @param path - 路由路径(可选,默认 '')
112
112
  */
113
- export function DELETE(path: string) {
113
+ export function DELETE(path: string = '') {
114
114
  return createRouteDecorator('DELETE', path);
115
115
  }
116
116
 
117
117
  /**
118
118
  * PATCH 路由装饰器
119
- * @param path - 路由路径
119
+ * @param path - 路由路径(可选,默认 '')
120
120
  */
121
- export function PATCH(path: string) {
121
+ export function PATCH(path: string = '') {
122
122
  return createRouteDecorator('PATCH', path);
123
123
  }
@@ -3,6 +3,7 @@ import type { Constructor } from '../core/types';
3
3
  import { Route } from './route';
4
4
  import type { HttpMethod, RouteHandler, RouteMatch } from './types';
5
5
  import type { Middleware } from '../middleware';
6
+ import { LoggerManager } from '@dangao/logsmith';
6
7
 
7
8
  /**
8
9
  * 路由器类
@@ -168,14 +169,23 @@ export class Router {
168
169
  public async preHandle(context: Context): Promise<void> {
169
170
  const method = context.method as HttpMethod;
170
171
  const path = this.normalizePath(context.path);
172
+ const logger = LoggerManager.getLogger();
171
173
 
172
174
  // 使用 findRouteWithMatch 避免重复匹配
173
175
  const result = this.findRouteWithMatch(method, path);
174
176
  if (!result) {
177
+ logger.debug('[Router] Route not matched', { method, path });
175
178
  return;
176
179
  }
177
180
 
178
181
  const { route, match } = result;
182
+ logger.debug('[Router] Route matched', {
183
+ method,
184
+ path,
185
+ routePath: route.path,
186
+ controller: route.controllerClass?.name,
187
+ methodName: route.methodName,
188
+ });
179
189
  if (match.matched) {
180
190
  context.params = match.params;
181
191
  }
@@ -196,6 +206,7 @@ export class Router {
196
206
  public async handle(context: Context): Promise<Response | undefined> {
197
207
  const method = context.method as HttpMethod;
198
208
  const path = this.normalizePath(context.path);
209
+ const logger = LoggerManager.getLogger();
199
210
 
200
211
  // 使用 findRouteWithMatch 获取路由和匹配结果
201
212
  const result = this.findRouteWithMatch(method, path);
@@ -204,7 +215,13 @@ export class Router {
204
215
  }
205
216
 
206
217
  const { route, match } = result;
207
-
218
+
219
+ logger.debug('[Router] Executing handler', {
220
+ path: route.path,
221
+ controller: route.controllerClass?.name,
222
+ methodName: route.methodName,
223
+ });
224
+
208
225
  // 设置路径参数
209
226
  if (match.matched) {
210
227
  context.params = match.params;
@@ -483,6 +483,141 @@ describe('Controller Path Normalization', () => {
483
483
  expect(data.success).toBe(true);
484
484
  });
485
485
 
486
+ // 场景 a: @Controller() + @GET('test') / @POST('test') -> /test
487
+ test('scenario a: @Controller() with @GET("test") and @POST("test") should match /test', async () => {
488
+ @Controller()
489
+ class TestController {
490
+ @GET('test')
491
+ public getTest() {
492
+ return { method: 'GET', path: 'test' };
493
+ }
494
+
495
+ @POST('test')
496
+ public postTest() {
497
+ return { method: 'POST', path: 'test' };
498
+ }
499
+ }
500
+
501
+ app.registerController(TestController);
502
+ await app.listen();
503
+
504
+ const getRes = await fetch(`http://localhost:${port}/test`);
505
+ expect(getRes.status).toBe(200);
506
+ expect((await getRes.json()).method).toBe('GET');
507
+
508
+ const postRes = await fetch(`http://localhost:${port}/test`, { method: 'POST' });
509
+ expect(postRes.status).toBe(200);
510
+ expect((await postRes.json()).method).toBe('POST');
511
+ });
512
+
513
+ // 场景 b: @Controller() + @GET('/test') / @POST('/test') -> /test
514
+ test('scenario b: @Controller() with @GET("/test") and @POST("/test") should match /test', async () => {
515
+ @Controller()
516
+ class TestController {
517
+ @GET('/test')
518
+ public getTest() {
519
+ return { method: 'GET', path: 'test' };
520
+ }
521
+
522
+ @POST('/test')
523
+ public postTest() {
524
+ return { method: 'POST', path: 'test' };
525
+ }
526
+ }
527
+
528
+ app.registerController(TestController);
529
+ await app.listen();
530
+
531
+ const getRes = await fetch(`http://localhost:${port}/test`);
532
+ expect(getRes.status).toBe(200);
533
+ expect((await getRes.json()).method).toBe('GET');
534
+
535
+ const postRes = await fetch(`http://localhost:${port}/test`, { method: 'POST' });
536
+ expect(postRes.status).toBe(200);
537
+ expect((await postRes.json()).method).toBe('POST');
538
+ });
539
+
540
+ // 场景 c: @Controller('') + @GET() / @POST() -> /
541
+ test('scenario c: @Controller("") with @GET() and @POST() should match /', async () => {
542
+ @Controller('')
543
+ class TestController {
544
+ @GET()
545
+ public getRoot() {
546
+ return { method: 'GET', path: '/' };
547
+ }
548
+
549
+ @POST()
550
+ public postRoot() {
551
+ return { method: 'POST', path: '/' };
552
+ }
553
+ }
554
+
555
+ app.registerController(TestController);
556
+ await app.listen();
557
+
558
+ const getRes = await fetch(`http://localhost:${port}/`);
559
+ expect(getRes.status).toBe(200);
560
+ expect((await getRes.json()).method).toBe('GET');
561
+
562
+ const postRes = await fetch(`http://localhost:${port}/`, { method: 'POST' });
563
+ expect(postRes.status).toBe(200);
564
+ expect((await postRes.json()).method).toBe('POST');
565
+ });
566
+
567
+ // 场景 d: @Controller() + @GET() / @POST() -> /
568
+ test('scenario d: @Controller() with @GET() and @POST() should match /', async () => {
569
+ @Controller()
570
+ class TestController {
571
+ @GET()
572
+ public getRoot() {
573
+ return { method: 'GET', path: '/' };
574
+ }
575
+
576
+ @POST()
577
+ public postRoot() {
578
+ return { method: 'POST', path: '/' };
579
+ }
580
+ }
581
+
582
+ app.registerController(TestController);
583
+ await app.listen();
584
+
585
+ const getRes = await fetch(`http://localhost:${port}/`);
586
+ expect(getRes.status).toBe(200);
587
+ expect((await getRes.json()).method).toBe('GET');
588
+
589
+ const postRes = await fetch(`http://localhost:${port}/`, { method: 'POST' });
590
+ expect(postRes.status).toBe(200);
591
+ expect((await postRes.json()).method).toBe('POST');
592
+ });
593
+
594
+ // 场景 e: @Controller() + @GET('/') / @POST('/') -> /
595
+ test('scenario e: @Controller() with @GET("/") and @POST("/") should match /', async () => {
596
+ @Controller()
597
+ class TestController {
598
+ @GET('/')
599
+ public getRoot() {
600
+ return { method: 'GET', path: '/' };
601
+ }
602
+
603
+ @POST('/')
604
+ public postRoot() {
605
+ return { method: 'POST', path: '/' };
606
+ }
607
+ }
608
+
609
+ app.registerController(TestController);
610
+ await app.listen();
611
+
612
+ const getRes = await fetch(`http://localhost:${port}/`);
613
+ expect(getRes.status).toBe(200);
614
+ expect((await getRes.json()).method).toBe('GET');
615
+
616
+ const postRes = await fetch(`http://localhost:${port}/`, { method: 'POST' });
617
+ expect(postRes.status).toBe(200);
618
+ expect((await postRes.json()).method).toBe('POST');
619
+ });
620
+
486
621
  // 测试所有场景在同一应用中同时工作
487
622
  test('should handle all path normalization scenarios simultaneously', async () => {
488
623
  @Controller('/')