@dangao/bun-server 1.0.0 → 1.0.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 (200) hide show
  1. package/package.json +4 -2
  2. package/readme.md +163 -2
  3. package/src/auth/controller.ts +148 -0
  4. package/src/auth/decorators.ts +81 -0
  5. package/src/auth/index.ts +12 -0
  6. package/src/auth/jwt.ts +169 -0
  7. package/src/auth/oauth2.ts +244 -0
  8. package/src/auth/types.ts +248 -0
  9. package/src/cache/cache-module.ts +67 -0
  10. package/src/cache/decorators.ts +202 -0
  11. package/src/cache/index.ts +27 -0
  12. package/src/cache/service.ts +151 -0
  13. package/src/cache/types.ts +420 -0
  14. package/src/config/config-module.ts +76 -0
  15. package/src/config/index.ts +8 -0
  16. package/src/config/service.ts +93 -0
  17. package/src/config/types.ts +27 -0
  18. package/src/controller/controller.ts +251 -0
  19. package/src/controller/decorators.ts +84 -0
  20. package/src/controller/index.ts +7 -0
  21. package/src/controller/metadata.ts +27 -0
  22. package/src/controller/param-binder.ts +157 -0
  23. package/src/core/application.ts +233 -0
  24. package/src/core/context.ts +228 -0
  25. package/src/core/index.ts +4 -0
  26. package/src/core/server.ts +128 -0
  27. package/src/core/types.ts +2 -0
  28. package/src/database/connection-manager.ts +239 -0
  29. package/src/database/connection-pool.ts +322 -0
  30. package/src/database/database-extension.ts +62 -0
  31. package/src/database/database-module.ts +115 -0
  32. package/src/database/health-indicator.ts +51 -0
  33. package/src/database/index.ts +47 -0
  34. package/src/database/orm/decorators.ts +155 -0
  35. package/src/database/orm/drizzle-repository.ts +39 -0
  36. package/src/database/orm/index.ts +23 -0
  37. package/src/database/orm/repository-decorator.ts +39 -0
  38. package/src/database/orm/repository.ts +103 -0
  39. package/src/database/orm/service.ts +49 -0
  40. package/src/database/orm/transaction-decorator.ts +45 -0
  41. package/src/database/orm/transaction-interceptor.ts +243 -0
  42. package/src/database/orm/transaction-manager.ts +276 -0
  43. package/src/database/orm/transaction-types.ts +140 -0
  44. package/src/database/orm/types.ts +99 -0
  45. package/src/database/service.ts +221 -0
  46. package/src/database/types.ts +171 -0
  47. package/src/di/container.ts +398 -0
  48. package/src/di/decorators.ts +228 -0
  49. package/src/di/index.ts +4 -0
  50. package/src/di/module-registry.ts +188 -0
  51. package/src/di/module.ts +65 -0
  52. package/src/di/types.ts +67 -0
  53. package/src/error/error-codes.ts +222 -0
  54. package/src/error/filter.ts +43 -0
  55. package/src/error/handler.ts +66 -0
  56. package/src/error/http-exception.ts +115 -0
  57. package/src/error/i18n.ts +217 -0
  58. package/src/error/index.ts +16 -0
  59. package/src/extensions/index.ts +5 -0
  60. package/src/extensions/logger-extension.ts +31 -0
  61. package/src/extensions/logger-module.ts +69 -0
  62. package/src/extensions/types.ts +14 -0
  63. package/src/files/index.ts +5 -0
  64. package/src/files/static-middleware.ts +53 -0
  65. package/src/files/storage.ts +67 -0
  66. package/src/files/types.ts +33 -0
  67. package/src/files/upload-middleware.ts +45 -0
  68. package/src/health/controller.ts +76 -0
  69. package/src/health/health-module.ts +51 -0
  70. package/src/health/index.ts +12 -0
  71. package/src/health/types.ts +28 -0
  72. package/src/index.ts +270 -0
  73. package/src/metrics/collector.ts +209 -0
  74. package/src/metrics/controller.ts +40 -0
  75. package/src/metrics/index.ts +15 -0
  76. package/src/metrics/metrics-module.ts +58 -0
  77. package/src/metrics/middleware.ts +46 -0
  78. package/src/metrics/prometheus.ts +79 -0
  79. package/src/metrics/types.ts +103 -0
  80. package/src/middleware/builtin/cors.ts +60 -0
  81. package/src/middleware/builtin/error-handler.ts +90 -0
  82. package/src/middleware/builtin/file-upload.ts +42 -0
  83. package/src/middleware/builtin/index.ts +14 -0
  84. package/src/middleware/builtin/logger.ts +91 -0
  85. package/src/middleware/builtin/rate-limit.ts +252 -0
  86. package/src/middleware/builtin/static-file.ts +88 -0
  87. package/src/middleware/decorators.ts +91 -0
  88. package/src/middleware/index.ts +11 -0
  89. package/src/middleware/middleware.ts +13 -0
  90. package/src/middleware/pipeline.ts +93 -0
  91. package/src/queue/decorators.ts +110 -0
  92. package/src/queue/index.ts +26 -0
  93. package/src/queue/queue-module.ts +64 -0
  94. package/src/queue/service.ts +302 -0
  95. package/src/queue/types.ts +341 -0
  96. package/src/request/body-parser.ts +133 -0
  97. package/src/request/file-handler.ts +46 -0
  98. package/src/request/index.ts +5 -0
  99. package/src/request/request.ts +107 -0
  100. package/src/request/response.ts +150 -0
  101. package/src/router/decorators.ts +122 -0
  102. package/src/router/index.ts +6 -0
  103. package/src/router/registry.ts +98 -0
  104. package/src/router/route.ts +140 -0
  105. package/src/router/router.ts +241 -0
  106. package/src/router/types.ts +27 -0
  107. package/src/security/access-decision-manager.ts +34 -0
  108. package/src/security/authentication-manager.ts +47 -0
  109. package/src/security/context.ts +92 -0
  110. package/src/security/filter.ts +162 -0
  111. package/src/security/index.ts +8 -0
  112. package/src/security/providers/index.ts +3 -0
  113. package/src/security/providers/jwt-provider.ts +60 -0
  114. package/src/security/providers/oauth2-provider.ts +70 -0
  115. package/src/security/security-module.ts +145 -0
  116. package/src/security/types.ts +165 -0
  117. package/src/session/decorators.ts +45 -0
  118. package/src/session/index.ts +19 -0
  119. package/src/session/middleware.ts +143 -0
  120. package/src/session/service.ts +218 -0
  121. package/src/session/session-module.ts +69 -0
  122. package/src/session/types.ts +373 -0
  123. package/src/swagger/decorators.ts +133 -0
  124. package/src/swagger/generator.ts +234 -0
  125. package/src/swagger/index.ts +7 -0
  126. package/src/swagger/swagger-extension.ts +41 -0
  127. package/src/swagger/swagger-module.ts +83 -0
  128. package/src/swagger/types.ts +188 -0
  129. package/src/swagger/ui.ts +98 -0
  130. package/src/testing/harness.ts +96 -0
  131. package/src/validation/decorators.ts +95 -0
  132. package/src/validation/errors.ts +28 -0
  133. package/src/validation/index.ts +14 -0
  134. package/src/validation/types.ts +35 -0
  135. package/src/validation/validator.ts +63 -0
  136. package/src/websocket/decorators.ts +51 -0
  137. package/src/websocket/index.ts +12 -0
  138. package/src/websocket/registry.ts +133 -0
  139. package/tests/cache/cache-module.test.ts +212 -0
  140. package/tests/config/config-module.test.ts +151 -0
  141. package/tests/controller/controller.test.ts +189 -0
  142. package/tests/core/application.test.ts +57 -0
  143. package/tests/core/context-body.test.ts +44 -0
  144. package/tests/core/context.test.ts +86 -0
  145. package/tests/core/edge-cases.test.ts +432 -0
  146. package/tests/database/database-module.test.ts +385 -0
  147. package/tests/database/orm.test.ts +164 -0
  148. package/tests/database/postgres-mysql-integration.test.ts +395 -0
  149. package/tests/database/transaction.test.ts +238 -0
  150. package/tests/di/container.test.ts +264 -0
  151. package/tests/di/module.test.ts +128 -0
  152. package/tests/error/error-codes.test.ts +121 -0
  153. package/tests/error/error-handler.test.ts +68 -0
  154. package/tests/error/error-handling.test.ts +254 -0
  155. package/tests/error/http-exception.test.ts +37 -0
  156. package/tests/error/i18n-integration.test.ts +175 -0
  157. package/tests/extensions/logger-extension.test.ts +40 -0
  158. package/tests/files/static-middleware.test.ts +67 -0
  159. package/tests/files/upload-middleware.test.ts +43 -0
  160. package/tests/health/health-module.test.ts +116 -0
  161. package/tests/integration/application-router.test.ts +85 -0
  162. package/tests/integration/body-parsing.test.ts +88 -0
  163. package/tests/integration/cache-e2e.test.ts +114 -0
  164. package/tests/integration/oauth2-e2e.test.ts +615 -0
  165. package/tests/integration/session-e2e.test.ts +207 -0
  166. package/tests/metrics/metrics-module.test.ts +178 -0
  167. package/tests/middleware/builtin.test.ts +206 -0
  168. package/tests/middleware/file-upload.test.ts +41 -0
  169. package/tests/middleware/middleware.test.ts +120 -0
  170. package/tests/middleware/pipeline.test.ts +72 -0
  171. package/tests/middleware/rate-limit.test.ts +314 -0
  172. package/tests/middleware/static-file.test.ts +62 -0
  173. package/tests/perf/harness.test.ts +48 -0
  174. package/tests/perf/optimization.test.ts +183 -0
  175. package/tests/perf/regression.test.ts +120 -0
  176. package/tests/queue/queue-module.test.ts +217 -0
  177. package/tests/request/body-parser.test.ts +96 -0
  178. package/tests/request/response.test.ts +99 -0
  179. package/tests/router/decorators.test.ts +48 -0
  180. package/tests/router/registry.test.ts +51 -0
  181. package/tests/router/route.test.ts +71 -0
  182. package/tests/router/router-normalization.test.ts +106 -0
  183. package/tests/router/router.test.ts +133 -0
  184. package/tests/security/access-decision-manager.test.ts +84 -0
  185. package/tests/security/authentication-manager.test.ts +81 -0
  186. package/tests/security/context.test.ts +302 -0
  187. package/tests/security/filter.test.ts +225 -0
  188. package/tests/security/jwt-provider.test.ts +106 -0
  189. package/tests/security/oauth2-provider.test.ts +269 -0
  190. package/tests/security/security-module.test.ts +143 -0
  191. package/tests/session/session-module.test.ts +307 -0
  192. package/tests/stress/di-stress.test.ts +30 -0
  193. package/tests/swagger/decorators.test.ts +153 -0
  194. package/tests/swagger/generator.test.ts +202 -0
  195. package/tests/swagger/swagger-extension.test.ts +72 -0
  196. package/tests/swagger/swagger-module.test.ts +79 -0
  197. package/tests/utils/test-port.ts +10 -0
  198. package/tests/validation/controller-validation.test.ts +64 -0
  199. package/tests/validation/validation.test.ts +42 -0
  200. package/tests/websocket/gateway.test.ts +68 -0
@@ -0,0 +1,115 @@
1
+ import { ErrorCode, ERROR_CODE_MESSAGES, ERROR_CODE_TO_STATUS } from './error-codes';
2
+ import type { MessageParams } from './i18n';
3
+
4
+ /**
5
+ * HTTP 异常基类
6
+ */
7
+ export class HttpException extends Error {
8
+ public readonly status: number;
9
+ public readonly code?: ErrorCode;
10
+ public readonly details?: unknown;
11
+ public readonly messageParams?: MessageParams;
12
+
13
+ public constructor(
14
+ status: number,
15
+ message: string,
16
+ details?: unknown,
17
+ code?: ErrorCode,
18
+ messageParams?: MessageParams,
19
+ ) {
20
+ super(message);
21
+ this.name = this.constructor.name;
22
+ this.status = status;
23
+ this.code = code;
24
+ this.details = details;
25
+ this.messageParams = messageParams;
26
+ }
27
+
28
+ /**
29
+ * 创建带错误码的异常
30
+ * @param code - 错误码
31
+ * @param message - 自定义错误消息(可选,如果不提供则使用默认消息)
32
+ * @param details - 错误详情(可选)
33
+ * @param messageParams - 消息模板参数(可选,用于替换消息模板中的占位符)
34
+ * @returns HttpException 实例
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * // 基本用法
39
+ * HttpException.withCode(ErrorCode.RESOURCE_NOT_FOUND);
40
+ *
41
+ * // 带自定义消息
42
+ * HttpException.withCode(ErrorCode.RESOURCE_NOT_FOUND, 'User not found');
43
+ *
44
+ * // 带消息模板参数(如果消息模板支持)
45
+ * HttpException.withCode(ErrorCode.RESOURCE_NOT_FOUND, undefined, undefined, { resource: 'User' });
46
+ * ```
47
+ */
48
+ public static withCode(
49
+ code: ErrorCode,
50
+ message?: string,
51
+ details?: unknown,
52
+ messageParams?: MessageParams,
53
+ ): HttpException {
54
+ const status = ERROR_CODE_TO_STATUS[code] || 500;
55
+ const finalMessage = message || ERROR_CODE_MESSAGES[code] || 'Internal Server Error';
56
+ return new HttpException(status, finalMessage, details, code, messageParams);
57
+ }
58
+ }
59
+
60
+ export class BadRequestException extends HttpException {
61
+ public constructor(
62
+ message: string = 'Bad Request',
63
+ details?: unknown,
64
+ code?: ErrorCode,
65
+ messageParams?: MessageParams,
66
+ ) {
67
+ super(400, message, details, code, messageParams);
68
+ }
69
+ }
70
+
71
+ export class UnauthorizedException extends HttpException {
72
+ public constructor(
73
+ message: string = 'Unauthorized',
74
+ details?: unknown,
75
+ code?: ErrorCode,
76
+ messageParams?: MessageParams,
77
+ ) {
78
+ super(401, message, details, code, messageParams);
79
+ }
80
+ }
81
+
82
+ export class ForbiddenException extends HttpException {
83
+ public constructor(
84
+ message: string = 'Forbidden',
85
+ details?: unknown,
86
+ code?: ErrorCode,
87
+ messageParams?: MessageParams,
88
+ ) {
89
+ super(403, message, details, code, messageParams);
90
+ }
91
+ }
92
+
93
+ export class NotFoundException extends HttpException {
94
+ public constructor(
95
+ message: string = 'Not Found',
96
+ details?: unknown,
97
+ code?: ErrorCode,
98
+ messageParams?: MessageParams,
99
+ ) {
100
+ super(404, message, details, code, messageParams);
101
+ }
102
+ }
103
+
104
+ export class InternalServerErrorException extends HttpException {
105
+ public constructor(
106
+ message: string = 'Internal Server Error',
107
+ details?: unknown,
108
+ code?: ErrorCode,
109
+ messageParams?: MessageParams,
110
+ ) {
111
+ super(500, message, details, code, messageParams);
112
+ }
113
+ }
114
+
115
+
@@ -0,0 +1,217 @@
1
+ import { ErrorCode, ERROR_CODE_MESSAGES } from './error-codes';
2
+
3
+ /**
4
+ * 支持的语言
5
+ */
6
+ export type SupportedLanguage = 'en' | 'zh-CN' | 'ja' | 'ko';
7
+
8
+ /**
9
+ * 消息模板参数
10
+ */
11
+ export interface MessageParams {
12
+ [key: string]: string | number | boolean | undefined;
13
+ }
14
+
15
+ /**
16
+ * 错误消息国际化映射
17
+ * 支持消息模板,使用 {key} 作为占位符
18
+ * 例如:'资源 {resource} 未找到',可以通过参数替换 {resource}
19
+ */
20
+ const ERROR_MESSAGES_I18N: Record<SupportedLanguage, Partial<Record<ErrorCode, string>>> = {
21
+ 'en': {
22
+ // 使用默认英文消息
23
+ ...ERROR_CODE_MESSAGES,
24
+ },
25
+ 'zh-CN': {
26
+ // 通用错误
27
+ [ErrorCode.INTERNAL_ERROR]: '服务器内部错误',
28
+ [ErrorCode.INVALID_REQUEST]: '无效的请求',
29
+ [ErrorCode.RESOURCE_NOT_FOUND]: '资源未找到',
30
+ [ErrorCode.METHOD_NOT_ALLOWED]: '方法不允许',
31
+ [ErrorCode.RATE_LIMIT_EXCEEDED]: '请求频率超限',
32
+ [ErrorCode.SERVICE_UNAVAILABLE]: '服务不可用',
33
+ [ErrorCode.TIMEOUT]: '请求超时',
34
+
35
+ // 认证和授权错误
36
+ [ErrorCode.AUTH_REQUIRED]: '需要认证',
37
+ [ErrorCode.AUTH_INVALID_TOKEN]: '无效的认证令牌',
38
+ [ErrorCode.AUTH_TOKEN_EXPIRED]: '认证令牌已过期',
39
+ [ErrorCode.AUTH_INSUFFICIENT_PERMISSIONS]: '权限不足',
40
+ [ErrorCode.AUTH_INVALID_CREDENTIALS]: '无效的凭据',
41
+ [ErrorCode.AUTH_ACCOUNT_LOCKED]: '账户已锁定',
42
+ [ErrorCode.AUTH_ACCOUNT_DISABLED]: '账户已禁用',
43
+
44
+ // 验证错误
45
+ [ErrorCode.VALIDATION_FAILED]: '验证失败',
46
+ [ErrorCode.VALIDATION_REQUIRED_FIELD]: '必填字段缺失',
47
+ [ErrorCode.VALIDATION_INVALID_FORMAT]: '格式无效',
48
+ [ErrorCode.VALIDATION_OUT_OF_RANGE]: '值超出范围',
49
+ [ErrorCode.VALIDATION_TYPE_MISMATCH]: '类型不匹配',
50
+ [ErrorCode.VALIDATION_CONSTRAINT_VIOLATION]: '约束违反',
51
+
52
+ // OAuth2 错误
53
+ [ErrorCode.OAUTH2_INVALID_CLIENT]: '无效的客户端',
54
+ [ErrorCode.OAUTH2_INVALID_GRANT]: '无效的授权',
55
+ [ErrorCode.OAUTH2_INVALID_SCOPE]: '无效的作用域',
56
+ [ErrorCode.OAUTH2_INVALID_REDIRECT_URI]: '无效的重定向 URI',
57
+ [ErrorCode.OAUTH2_UNSUPPORTED_GRANT_TYPE]: '不支持的授权类型',
58
+ [ErrorCode.OAUTH2_ACCESS_DENIED]: '访问被拒绝',
59
+ [ErrorCode.OAUTH2_SERVER_ERROR]: 'OAuth2 服务器错误',
60
+
61
+ // 数据库错误
62
+ [ErrorCode.DATABASE_CONNECTION_FAILED]: '数据库连接失败',
63
+ [ErrorCode.DATABASE_QUERY_FAILED]: '数据库查询失败',
64
+ [ErrorCode.DATABASE_TRANSACTION_FAILED]: '数据库事务失败',
65
+ [ErrorCode.DATABASE_CONSTRAINT_VIOLATION]: '数据库约束违反',
66
+ [ErrorCode.DATABASE_TIMEOUT]: '数据库超时',
67
+ [ErrorCode.DATABASE_POOL_EXHAUSTED]: '数据库连接池耗尽',
68
+ [ErrorCode.DATABASE_MIGRATION_FAILED]: '数据库迁移失败',
69
+
70
+ // 文件操作错误
71
+ [ErrorCode.FILE_NOT_FOUND]: '文件未找到',
72
+ [ErrorCode.FILE_UPLOAD_FAILED]: '文件上传失败',
73
+ [ErrorCode.FILE_DOWNLOAD_FAILED]: '文件下载失败',
74
+ [ErrorCode.FILE_SIZE_EXCEEDED]: '文件大小超限',
75
+ [ErrorCode.FILE_TYPE_NOT_ALLOWED]: '不允许的文件类型',
76
+ [ErrorCode.FILE_ACCESS_DENIED]: '文件访问被拒绝',
77
+ [ErrorCode.FILE_PATH_TRAVERSAL]: '检测到路径遍历攻击',
78
+
79
+ // 中间件错误
80
+ [ErrorCode.MIDDLEWARE_EXECUTION_FAILED]: '中间件执行失败',
81
+ [ErrorCode.MIDDLEWARE_TIMEOUT]: '中间件超时',
82
+ [ErrorCode.CORS_NOT_ALLOWED]: 'CORS 不允许',
83
+
84
+ // 配置错误
85
+ [ErrorCode.CONFIG_INVALID]: '无效的配置',
86
+ [ErrorCode.CONFIG_MISSING]: '缺少配置',
87
+ [ErrorCode.CONFIG_VALIDATION_FAILED]: '配置验证失败',
88
+ },
89
+ 'ja': {
90
+ // 日语翻译(部分常用错误码)
91
+ [ErrorCode.INTERNAL_ERROR]: 'サーバー内部エラー',
92
+ [ErrorCode.INVALID_REQUEST]: '無効なリクエスト',
93
+ [ErrorCode.RESOURCE_NOT_FOUND]: 'リソースが見つかりません',
94
+ [ErrorCode.AUTH_REQUIRED]: '認証が必要です',
95
+ [ErrorCode.AUTH_INVALID_TOKEN]: '無効な認証トークン',
96
+ [ErrorCode.VALIDATION_FAILED]: '検証に失敗しました',
97
+ },
98
+ 'ko': {
99
+ // 韩语翻译(部分常用错误码)
100
+ [ErrorCode.INTERNAL_ERROR]: '서버 내부 오류',
101
+ [ErrorCode.INVALID_REQUEST]: '잘못된 요청',
102
+ [ErrorCode.RESOURCE_NOT_FOUND]: '리소스를 찾을 수 없습니다',
103
+ [ErrorCode.AUTH_REQUIRED]: '인증이 필요합니다',
104
+ [ErrorCode.AUTH_INVALID_TOKEN]: '잘못된 인증 토큰',
105
+ [ErrorCode.VALIDATION_FAILED]: '유효성 검사 실패',
106
+ },
107
+ };
108
+
109
+ /**
110
+ * 错误消息国际化服务
111
+ * 支持消息模板和参数替换
112
+ */
113
+ export class ErrorMessageI18n {
114
+ private static currentLanguage: SupportedLanguage = 'en';
115
+
116
+ /**
117
+ * 设置当前语言
118
+ */
119
+ public static setLanguage(language: SupportedLanguage): void {
120
+ this.currentLanguage = language;
121
+ }
122
+
123
+ /**
124
+ * 获取当前语言
125
+ */
126
+ public static getLanguage(): SupportedLanguage {
127
+ return this.currentLanguage;
128
+ }
129
+
130
+ /**
131
+ * 获取错误消息(国际化)
132
+ * @param code - 错误码
133
+ * @param language - 语言(可选,默认使用当前语言)
134
+ * @param params - 消息模板参数(可选)
135
+ * @returns 国际化后的错误消息
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * // 基本用法
140
+ * ErrorMessageI18n.getMessage(ErrorCode.RESOURCE_NOT_FOUND);
141
+ *
142
+ * // 带参数的消息模板
143
+ * ErrorMessageI18n.getMessage(ErrorCode.RESOURCE_NOT_FOUND, 'en', { resource: 'User' });
144
+ * // 如果消息模板是 'Resource {resource} not found',则返回 'Resource User not found'
145
+ * ```
146
+ */
147
+ public static getMessage(
148
+ code: ErrorCode,
149
+ language?: SupportedLanguage,
150
+ params?: MessageParams,
151
+ ): string {
152
+ const lang = language || this.currentLanguage;
153
+ const messages = ERROR_MESSAGES_I18N[lang];
154
+ let message = messages?.[code] || ERROR_CODE_MESSAGES[code] || 'Internal Server Error';
155
+
156
+ // 如果提供了参数,替换消息模板中的占位符
157
+ if (params) {
158
+ message = this.replaceTemplate(message, params);
159
+ }
160
+
161
+ return message;
162
+ }
163
+
164
+ /**
165
+ * 替换消息模板中的占位符
166
+ * @param template - 消息模板,使用 {key} 作为占位符
167
+ * @param params - 参数对象
168
+ * @returns 替换后的消息
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * replaceTemplate('Resource {resource} not found', { resource: 'User' });
173
+ * // 返回: 'Resource User not found'
174
+ * ```
175
+ */
176
+ private static replaceTemplate(template: string, params: MessageParams): string {
177
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
178
+ const value = params[key];
179
+ return value !== undefined ? String(value) : match;
180
+ });
181
+ }
182
+
183
+ /**
184
+ * 从 Accept-Language 头解析语言
185
+ * 支持的语言:en, zh-CN, ja, ko
186
+ *
187
+ * @param acceptLanguage - Accept-Language HTTP 头值
188
+ * @returns 解析后的语言代码
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * parseLanguageFromHeader('zh-CN,zh;q=0.9,en;q=0.8');
193
+ * // 返回: 'zh-CN'
194
+ * ```
195
+ */
196
+ public static parseLanguageFromHeader(acceptLanguage?: string | null): SupportedLanguage {
197
+ if (!acceptLanguage) {
198
+ return 'en';
199
+ }
200
+
201
+ const lowerAcceptLanguage = acceptLanguage.toLowerCase();
202
+
203
+ // 按优先级检查语言
204
+ if (lowerAcceptLanguage.includes('zh-cn') || lowerAcceptLanguage.includes('zh')) {
205
+ return 'zh-CN';
206
+ }
207
+ if (lowerAcceptLanguage.includes('ja')) {
208
+ return 'ja';
209
+ }
210
+ if (lowerAcceptLanguage.includes('ko')) {
211
+ return 'ko';
212
+ }
213
+
214
+ return 'en';
215
+ }
216
+ }
217
+
@@ -0,0 +1,16 @@
1
+ export {
2
+ HttpException,
3
+ BadRequestException,
4
+ UnauthorizedException,
5
+ ForbiddenException,
6
+ NotFoundException,
7
+ InternalServerErrorException,
8
+ } from './http-exception';
9
+ export type { ExceptionFilter } from './filter';
10
+ export { ExceptionFilterRegistry } from './filter';
11
+ export { handleError } from './handler';
12
+ export { ErrorCode, ERROR_CODE_MESSAGES, ERROR_CODE_TO_STATUS } from './error-codes';
13
+ export { ErrorMessageI18n } from './i18n';
14
+ export type { SupportedLanguage, MessageParams } from './i18n';
15
+
16
+
@@ -0,0 +1,5 @@
1
+ export type { ApplicationExtension } from "./types";
2
+ export { LOGGER_TOKEN, LoggerExtension } from "./logger-extension";
3
+ export { LoggerModule, type LoggerModuleOptions } from "./logger-module";
4
+ export { LoggerManager, LogLevel, SimpleLogger } from "@dangao/logsmith";
5
+ export type { LogEntry, Logger, LoggerOptions } from "@dangao/logsmith";
@@ -0,0 +1,31 @@
1
+ import type { Container } from "../di/container";
2
+ import type { ApplicationExtension } from "./types";
3
+ import {
4
+ LoggerManager,
5
+ type LoggerOptions,
6
+ LogLevel,
7
+ SimpleLogger,
8
+ } from "@dangao/logsmith";
9
+
10
+ export const LOGGER_TOKEN = Symbol("@dangao/bun-server:logger");
11
+
12
+ /**
13
+ * Bun Server Logger Provider
14
+ */
15
+ export class LoggerExtension implements ApplicationExtension {
16
+ private readonly options: LoggerOptions;
17
+
18
+ public constructor(options: LoggerOptions = {}) {
19
+ this.options = {
20
+ level: LogLevel.INFO,
21
+ ...options,
22
+ };
23
+ }
24
+
25
+ public register(container: Container): void {
26
+ const logger = new SimpleLogger(this.options);
27
+ LoggerManager.setLogger(logger);
28
+ container.registerInstance(SimpleLogger, logger);
29
+ container.registerInstance(LOGGER_TOKEN, logger);
30
+ }
31
+ }
@@ -0,0 +1,69 @@
1
+ import { Module, MODULE_METADATA_KEY } from '../di/module';
2
+ import { LoggerExtension } from './logger-extension';
3
+ import { createLoggerMiddleware } from '../middleware/builtin';
4
+ import type { LoggerOptions } from '@dangao/logsmith';
5
+
6
+ /**
7
+ * Logger 模块配置
8
+ */
9
+ export interface LoggerModuleOptions {
10
+ /**
11
+ * Logger 选项
12
+ */
13
+ logger?: LoggerOptions;
14
+ /**
15
+ * 是否启用请求日志中间件
16
+ */
17
+ enableRequestLogging?: boolean;
18
+ /**
19
+ * 请求日志中间件前缀
20
+ */
21
+ requestLoggingPrefix?: string;
22
+ }
23
+
24
+ /**
25
+ * Logger 模块
26
+ * 提供日志功能和请求日志中间件
27
+ */
28
+ @Module({
29
+ extensions: [
30
+ // 将在运行时根据配置创建
31
+ ],
32
+ middlewares: [
33
+ // 将在运行时根据配置创建
34
+ ],
35
+ })
36
+ export class LoggerModule {
37
+ /**
38
+ * 创建 Logger 模块
39
+ * @param options - 模块配置
40
+ */
41
+ public static forRoot(options: LoggerModuleOptions = {}): typeof LoggerModule {
42
+ const extensions: any[] = [];
43
+ const middlewares: any[] = [];
44
+
45
+ // 创建 Logger 扩展
46
+ const loggerExtension = new LoggerExtension(options.logger);
47
+ extensions.push(loggerExtension);
48
+
49
+ // 如果启用请求日志,添加中间件
50
+ if (options.enableRequestLogging !== false) {
51
+ const requestLoggingMiddleware = createLoggerMiddleware({
52
+ prefix: options.requestLoggingPrefix,
53
+ });
54
+ middlewares.push(requestLoggingMiddleware);
55
+ }
56
+
57
+ // 动态更新模块元数据
58
+ const existingMetadata = Reflect.getMetadata(MODULE_METADATA_KEY, LoggerModule) || {};
59
+ const metadata = {
60
+ ...existingMetadata,
61
+ extensions,
62
+ middlewares,
63
+ };
64
+ Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, LoggerModule);
65
+
66
+ return LoggerModule;
67
+ }
68
+ }
69
+
@@ -0,0 +1,14 @@
1
+ import type { Container } from '../di/container';
2
+
3
+ /**
4
+ * 应用扩展接口
5
+ */
6
+ export interface ApplicationExtension {
7
+ /**
8
+ * 注册扩展
9
+ * @param container - 应用 DI 容器
10
+ */
11
+ register(container: Container): void;
12
+ }
13
+
14
+
@@ -0,0 +1,5 @@
1
+ export type { UploadedFileInfo } from './types';
2
+ export { FileStorage } from './storage';
3
+ export { createFileUploadMiddleware } from './upload-middleware';
4
+ export { createStaticFileMiddleware } from './static-middleware';
5
+
@@ -0,0 +1,53 @@
1
+ import { join, normalize } from 'node:path';
2
+ import { stat } from 'node:fs/promises';
3
+
4
+ import type { Middleware } from '../middleware';
5
+
6
+ export interface StaticFileOptions {
7
+ root: string;
8
+ prefix?: string;
9
+ fallthrough?: boolean;
10
+ }
11
+
12
+ function safeResolve(root: string, requestPath: string): string {
13
+ const normalized = normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, '');
14
+ return join(root, normalized);
15
+ }
16
+
17
+ export function createStaticFileMiddleware(options: StaticFileOptions): Middleware {
18
+ const prefix = options.prefix?.replace(/\/$/, '') || '';
19
+ const fallthrough = options.fallthrough ?? true;
20
+
21
+ return async (context, next) => {
22
+ if (context.method !== 'GET' && context.method !== 'HEAD') {
23
+ return await next();
24
+ }
25
+
26
+ if (prefix && !context.path.startsWith(prefix)) {
27
+ return await next();
28
+ }
29
+
30
+ const relativePath = prefix ? context.path.slice(prefix.length) || '/' : context.path;
31
+ const filePath = safeResolve(options.root, relativePath);
32
+ try {
33
+ const stats = await stat(filePath);
34
+ if (!stats.isFile()) {
35
+ return await next();
36
+ }
37
+
38
+ const file = Bun.file(filePath);
39
+ return new Response(file, {
40
+ headers: {
41
+ 'Content-Type': file.type || 'application/octet-stream',
42
+ },
43
+ });
44
+ } catch {
45
+ if (fallthrough) {
46
+ return await next();
47
+ }
48
+ return context.createResponse({ error: 'File Not Found' }, { status: 404 });
49
+ }
50
+ };
51
+ }
52
+
53
+
@@ -0,0 +1,67 @@
1
+ import { access, mkdir } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import type { UploadedFileInfo } from './types';
6
+
7
+ export interface SaveFileOptions {
8
+ dest: string;
9
+ filename?: string;
10
+ overwrite?: boolean;
11
+ }
12
+
13
+ async function ensureDirectory(dir: string): Promise<void> {
14
+ await mkdir(dir, { recursive: true });
15
+ }
16
+
17
+ async function fileExists(path: string): Promise<boolean> {
18
+ try {
19
+ await access(path, constants.F_OK);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function sanitizeFilename(value: string): string {
27
+ return value.replace(/[^a-zA-Z0-9.-]/g, '_');
28
+ }
29
+
30
+ export class FileStorage {
31
+ public static async saveFile(
32
+ file: File,
33
+ options: SaveFileOptions,
34
+ fieldName: string,
35
+ ): Promise<UploadedFileInfo> {
36
+ await ensureDirectory(options.dest);
37
+
38
+ const baseName = sanitizeFilename(options.filename ?? file.name ?? `file-${Date.now()}`);
39
+ let targetName = baseName || `file-${Date.now()}`;
40
+ let targetPath = join(options.dest, targetName);
41
+
42
+ if (!options.overwrite) {
43
+ let counter = 1;
44
+ const dotIndex = targetName.lastIndexOf('.');
45
+ const name = dotIndex > 0 ? targetName.slice(0, dotIndex) : targetName;
46
+ const extension = dotIndex > 0 ? targetName.slice(dotIndex) : '';
47
+ while (await fileExists(targetPath)) {
48
+ targetName = `${name}-${counter}${extension}`;
49
+ targetPath = join(options.dest, targetName);
50
+ counter += 1;
51
+ }
52
+ }
53
+
54
+ await Bun.write(targetPath, file);
55
+
56
+ return {
57
+ fieldName,
58
+ filename: targetName,
59
+ originalName: file.name,
60
+ mimeType: file.type || 'application/octet-stream',
61
+ size: file.size,
62
+ path: targetPath,
63
+ };
64
+ }
65
+ }
66
+
67
+
@@ -0,0 +1,33 @@
1
+ export interface UploadedFileInfo {
2
+ /**
3
+ * 表单字段名
4
+ */
5
+ fieldName: string;
6
+
7
+ /**
8
+ * 保存后的文件名
9
+ */
10
+ filename: string;
11
+
12
+ /**
13
+ * 原始文件名
14
+ */
15
+ originalName?: string;
16
+
17
+ /**
18
+ * MIME 类型
19
+ */
20
+ mimeType: string;
21
+
22
+ /**
23
+ * 文件大小(字节)
24
+ */
25
+ size: number;
26
+
27
+ /**
28
+ * 文件保存路径
29
+ */
30
+ path: string;
31
+ }
32
+
33
+
@@ -0,0 +1,45 @@
1
+ import type { Middleware } from '../middleware';
2
+ import type { UploadedFileInfo } from './types';
3
+ import { FileStorage, type SaveFileOptions } from './storage';
4
+
5
+ export interface FileUploadOptions extends Omit<SaveFileOptions, 'filename'> {
6
+ filename?: (fieldName: string, file: File) => string | undefined;
7
+ }
8
+
9
+ export function createFileUploadMiddleware(options: FileUploadOptions): Middleware {
10
+ const dest = options.dest;
11
+
12
+ return async (context, next) => {
13
+ const body = await context.getBody();
14
+ if (!(body instanceof FormData)) {
15
+ return await next();
16
+ }
17
+
18
+ const savedFiles: UploadedFileInfo[] = [];
19
+
20
+ for (const [field, value] of body.entries()) {
21
+ const candidate = value as unknown;
22
+ if (candidate instanceof File) {
23
+ const filename = options.filename?.(field, candidate);
24
+ const info = await FileStorage.saveFile(
25
+ candidate,
26
+ {
27
+ dest,
28
+ filename,
29
+ overwrite: options.overwrite,
30
+ },
31
+ field,
32
+ );
33
+ savedFiles.push(info);
34
+ }
35
+ }
36
+
37
+ if (savedFiles.length > 0) {
38
+ context.files = savedFiles;
39
+ }
40
+
41
+ return await next();
42
+ };
43
+ }
44
+
45
+