@dangao/bun-server 3.1.0 → 3.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/ai/types.d.ts +4 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/auth/types.d.ts +4 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/cache/interceptors.d.ts +3 -3
- package/dist/cache/interceptors.d.ts.map +1 -1
- package/dist/cache/service.d.ts +6 -6
- package/dist/cache/service.d.ts.map +1 -1
- package/dist/cache/types.d.ts +12 -12
- package/dist/cache/types.d.ts.map +1 -1
- package/dist/config/service.d.ts +6 -4
- package/dist/config/service.d.ts.map +1 -1
- package/dist/core/application.d.ts +4 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/context.d.ts +4 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/database/connection-manager.d.ts +2 -2
- package/dist/database/connection-manager.d.ts.map +1 -1
- package/dist/database/connection-pool.d.ts +4 -4
- package/dist/database/connection-pool.d.ts.map +1 -1
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/database/db-proxy.d.ts.map +1 -1
- package/dist/database/driver.d.ts +83 -0
- package/dist/database/driver.d.ts.map +1 -0
- package/dist/database/index.d.ts +2 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/service.d.ts +0 -10
- package/dist/database/service.d.ts.map +1 -1
- package/dist/database/sql-manager.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.d.ts +4 -2
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/database/types.d.ts +26 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/di/container.d.ts +2 -0
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/di/module.d.ts +11 -1
- package/dist/di/module.d.ts.map +1 -1
- package/dist/di/types.d.ts +1 -1
- package/dist/di/types.d.ts.map +1 -1
- package/dist/error/handler.d.ts.map +1 -1
- package/dist/error/http-exception.d.ts +8 -8
- package/dist/error/http-exception.d.ts.map +1 -1
- package/dist/error/index.d.ts +1 -0
- package/dist/error/index.d.ts.map +1 -1
- package/dist/events/service.d.ts +2 -2
- package/dist/events/service.d.ts.map +1 -1
- package/dist/events/types.d.ts +12 -3
- package/dist/events/types.d.ts.map +1 -1
- package/dist/index.js +5951 -5820
- package/dist/index.node.mjs +310 -139
- package/dist/interceptor/base-interceptor.d.ts +3 -3
- package/dist/interceptor/base-interceptor.d.ts.map +1 -1
- package/dist/interceptor/builtin/log-interceptor.d.ts +1 -1
- package/dist/interceptor/builtin/log-interceptor.d.ts.map +1 -1
- package/dist/interceptor/builtin/permission-interceptor.d.ts +1 -1
- package/dist/interceptor/builtin/permission-interceptor.d.ts.map +1 -1
- package/dist/interceptor/interceptor-chain.d.ts +1 -1
- package/dist/interceptor/interceptor-chain.d.ts.map +1 -1
- package/dist/interceptor/interceptor-registry.d.ts +3 -1
- package/dist/interceptor/interceptor-registry.d.ts.map +1 -1
- package/dist/interceptor/types.d.ts +6 -1
- package/dist/interceptor/types.d.ts.map +1 -1
- package/dist/microservice/service-client/types.d.ts +1 -0
- package/dist/microservice/service-client/types.d.ts.map +1 -1
- package/dist/microservice/tracing/tracer.d.ts +1 -0
- package/dist/microservice/tracing/tracer.d.ts.map +1 -1
- package/dist/middleware/builtin/file-upload.d.ts +2 -0
- package/dist/middleware/builtin/file-upload.d.ts.map +1 -1
- package/dist/middleware/builtin/rate-limit.d.ts +9 -1
- package/dist/middleware/builtin/rate-limit.d.ts.map +1 -1
- package/dist/queue/service.d.ts +2 -2
- package/dist/queue/service.d.ts.map +1 -1
- package/dist/queue/types.d.ts +25 -1
- package/dist/queue/types.d.ts.map +1 -1
- package/dist/router/decorators.d.ts +1 -2
- package/dist/router/decorators.d.ts.map +1 -1
- package/dist/security/guards/types.d.ts +1 -0
- package/dist/security/guards/types.d.ts.map +1 -1
- package/dist/security/types.d.ts +1 -1
- package/dist/security/types.d.ts.map +1 -1
- package/dist/session/types.d.ts +8 -0
- package/dist/session/types.d.ts.map +1 -1
- package/dist/swagger/decorators.d.ts +1 -1
- package/dist/swagger/decorators.d.ts.map +1 -1
- package/dist/swagger/types.d.ts +1 -1
- package/dist/swagger/types.d.ts.map +1 -1
- package/dist/testing/harness.d.ts +1 -1
- package/dist/testing/harness.d.ts.map +1 -1
- package/dist/testing/test-client.d.ts +1 -1
- package/dist/testing/test-client.d.ts.map +1 -1
- package/dist/testing/testing-module.d.ts +2 -2
- package/dist/testing/testing-module.d.ts.map +1 -1
- package/dist/validation/errors.d.ts +5 -1
- package/dist/validation/errors.d.ts.map +1 -1
- package/docs/database.md +44 -0
- package/docs/zh/database.md +44 -0
- package/package.json +3 -3
- package/src/ai/types.ts +5 -0
- package/src/auth/types.ts +4 -1
- package/src/cache/interceptors.ts +6 -6
- package/src/cache/service-proxy.ts +2 -2
- package/src/cache/service.ts +17 -8
- package/src/cache/types.ts +12 -12
- package/src/config/service.ts +8 -6
- package/src/core/application.ts +7 -1
- package/src/core/context.ts +6 -3
- package/src/database/connection-manager.ts +5 -46
- package/src/database/connection-pool.ts +26 -49
- package/src/database/database-module.ts +6 -0
- package/src/database/db-proxy.ts +3 -2
- package/src/database/driver.ts +368 -0
- package/src/database/index.ts +8 -0
- package/src/database/service.ts +3 -74
- package/src/database/sql-manager.ts +38 -24
- package/src/database/sqlite-adapter.ts +4 -3
- package/src/database/types.ts +27 -2
- package/src/di/container.ts +13 -0
- package/src/di/module-registry.ts +2 -3
- package/src/di/module.ts +21 -2
- package/src/di/types.ts +1 -2
- package/src/error/handler.ts +3 -4
- package/src/error/http-exception.ts +11 -11
- package/src/error/index.ts +1 -1
- package/src/events/service.ts +4 -2
- package/src/events/types.ts +14 -3
- package/src/interceptor/base-interceptor.ts +5 -6
- package/src/interceptor/builtin/log-interceptor.ts +2 -3
- package/src/interceptor/builtin/permission-interceptor.ts +2 -3
- package/src/interceptor/interceptor-chain.ts +6 -7
- package/src/interceptor/interceptor-registry.ts +5 -3
- package/src/interceptor/types.ts +9 -4
- package/src/microservice/service-client/types.ts +1 -1
- package/src/microservice/tracing/tracer.ts +15 -3
- package/src/middleware/builtin/file-upload.ts +3 -2
- package/src/middleware/builtin/rate-limit.ts +22 -5
- package/src/queue/service.ts +1 -1
- package/src/queue/types.ts +40 -1
- package/src/router/decorators.ts +2 -3
- package/src/security/guards/types.ts +2 -1
- package/src/security/types.ts +1 -2
- package/src/session/service.ts +1 -1
- package/src/session/types.ts +10 -0
- package/src/swagger/decorators.ts +15 -4
- package/src/swagger/generator.ts +2 -3
- package/src/swagger/types.ts +1 -2
- package/src/testing/harness.ts +1 -2
- package/src/testing/test-client.ts +2 -2
- package/src/testing/testing-module.ts +5 -5
- package/src/validation/errors.ts +6 -2
- package/tests/bun-test-shim.d.ts +11 -0
- package/tests/controller/context-decorator.test.ts +1 -2
- package/tests/database/database-module.test.ts +4 -0
- package/tests/database/driver-mysql2.test.ts +95 -0
- package/tests/database/driver.test.ts +234 -0
- package/tests/database/orm.test.ts +4 -0
- package/tests/di/module.test.ts +199 -1
- package/tests/events/event-emitter.test.ts +2 -2
- package/tests/global.d.ts +30 -0
- package/tests/queue/queue-service.test.ts +14 -0
- package/tests/testing/testing-module.test.ts +20 -0
|
@@ -88,7 +88,12 @@ export interface RateLimitOptions {
|
|
|
88
88
|
/**
|
|
89
89
|
* 时间窗口内的最大请求数
|
|
90
90
|
*/
|
|
91
|
-
max
|
|
91
|
+
max?: number;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 时间窗口内的最大请求数别名,兼容装饰器早期写法。
|
|
95
|
+
*/
|
|
96
|
+
limit?: number;
|
|
92
97
|
|
|
93
98
|
/**
|
|
94
99
|
* 时间窗口(毫秒)
|
|
@@ -108,6 +113,11 @@ export interface RateLimitOptions {
|
|
|
108
113
|
*/
|
|
109
114
|
keyGenerator?: (context: Context) => string | Promise<string>;
|
|
110
115
|
|
|
116
|
+
/**
|
|
117
|
+
* 返回 true 时跳过限流。
|
|
118
|
+
*/
|
|
119
|
+
skip?: (context: Context) => boolean | Promise<boolean>;
|
|
120
|
+
|
|
111
121
|
/**
|
|
112
122
|
* 是否跳过成功响应(只对错误响应计数)
|
|
113
123
|
* @default false
|
|
@@ -157,9 +167,11 @@ function defaultKeyGenerator(context: Context): string {
|
|
|
157
167
|
export function createRateLimitMiddleware(options: RateLimitOptions): Middleware {
|
|
158
168
|
const {
|
|
159
169
|
max,
|
|
170
|
+
limit,
|
|
160
171
|
windowMs = 60000, // 默认 1 分钟
|
|
161
172
|
store = new MemoryRateLimitStore(),
|
|
162
173
|
keyGenerator = defaultKeyGenerator,
|
|
174
|
+
skip,
|
|
163
175
|
skipSuccessfulRequests = false,
|
|
164
176
|
skipFailedRequests = false,
|
|
165
177
|
message = 'Too Many Requests',
|
|
@@ -167,31 +179,36 @@ export function createRateLimitMiddleware(options: RateLimitOptions): Middleware
|
|
|
167
179
|
standardHeaders = true,
|
|
168
180
|
legacyHeaders = true,
|
|
169
181
|
} = options;
|
|
182
|
+
const requestLimit = limit ?? max ?? 100;
|
|
170
183
|
|
|
171
184
|
return async (context: Context, next) => {
|
|
185
|
+
if (skip && await skip(context)) {
|
|
186
|
+
return await next();
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
// 生成限流键
|
|
173
190
|
const key = await keyGenerator(context);
|
|
174
191
|
const currentCount = await store.increment(key, windowMs);
|
|
175
192
|
|
|
176
193
|
// 计算剩余请求数和重置时间
|
|
177
|
-
const remaining = Math.max(0,
|
|
194
|
+
const remaining = Math.max(0, requestLimit - currentCount);
|
|
178
195
|
const resetTime = Date.now() + windowMs;
|
|
179
196
|
|
|
180
197
|
// 设置响应头
|
|
181
198
|
if (standardHeaders) {
|
|
182
|
-
context.setHeader('RateLimit-Limit',
|
|
199
|
+
context.setHeader('RateLimit-Limit', requestLimit.toString());
|
|
183
200
|
context.setHeader('RateLimit-Remaining', remaining.toString());
|
|
184
201
|
context.setHeader('RateLimit-Reset', Math.ceil(resetTime / 1000).toString());
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
if (legacyHeaders) {
|
|
188
|
-
context.setHeader('X-RateLimit-Limit',
|
|
205
|
+
context.setHeader('X-RateLimit-Limit', requestLimit.toString());
|
|
189
206
|
context.setHeader('X-RateLimit-Remaining', remaining.toString());
|
|
190
207
|
context.setHeader('X-RateLimit-Reset', Math.ceil(resetTime / 1000).toString());
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
// 检查是否超过限制
|
|
194
|
-
if (currentCount >
|
|
211
|
+
if (currentCount > requestLimit) {
|
|
195
212
|
context.setStatus(statusCode);
|
|
196
213
|
return context.createErrorResponse({
|
|
197
214
|
error: message,
|
package/src/queue/service.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { getRuntime } from '../platform/runtime';
|
|
|
17
17
|
*/
|
|
18
18
|
@Injectable()
|
|
19
19
|
export class QueueService {
|
|
20
|
-
|
|
20
|
+
public readonly store: QueueStore;
|
|
21
21
|
private defaultQueue: string;
|
|
22
22
|
private enableWorker: boolean;
|
|
23
23
|
private concurrency: number;
|
package/src/queue/types.ts
CHANGED
|
@@ -24,6 +24,11 @@ export interface JobOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
attempts?: number;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 任务重试次数别名,兼容早期 API。
|
|
29
|
+
*/
|
|
30
|
+
retries?: number;
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* 任务重试延迟(毫秒)
|
|
29
34
|
*/
|
|
@@ -90,7 +95,22 @@ export interface Job<T = JobData> {
|
|
|
90
95
|
/**
|
|
91
96
|
* 更新时间
|
|
92
97
|
*/
|
|
93
|
-
updatedAt
|
|
98
|
+
updatedAt?: number;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 完成结果
|
|
102
|
+
*/
|
|
103
|
+
result?: unknown;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 完成时间
|
|
107
|
+
*/
|
|
108
|
+
completedAt?: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 失败错误信息
|
|
112
|
+
*/
|
|
113
|
+
error?: string;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
/**
|
|
@@ -141,6 +161,25 @@ export interface QueueStore {
|
|
|
141
161
|
status: Job['status'],
|
|
142
162
|
): Promise<boolean>;
|
|
143
163
|
|
|
164
|
+
complete?(
|
|
165
|
+
queueName: string,
|
|
166
|
+
jobId: string,
|
|
167
|
+
result?: unknown,
|
|
168
|
+
): Promise<boolean>;
|
|
169
|
+
|
|
170
|
+
fail?(
|
|
171
|
+
queueName: string,
|
|
172
|
+
jobId: string,
|
|
173
|
+
error: Error,
|
|
174
|
+
): Promise<boolean>;
|
|
175
|
+
|
|
176
|
+
getStats?(queueName: string): Promise<{
|
|
177
|
+
waiting: number;
|
|
178
|
+
active: number;
|
|
179
|
+
completed: number;
|
|
180
|
+
failed: number;
|
|
181
|
+
}>;
|
|
182
|
+
|
|
144
183
|
/**
|
|
145
184
|
* 删除任务
|
|
146
185
|
* @param queueName - 队列名称
|
package/src/router/decorators.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import type { RouteHandler } from './types';
|
|
3
2
|
import { RouteRegistry } from './registry';
|
|
4
3
|
import type { HttpMethod } from './types';
|
|
5
4
|
import { CONTROLLER_METADATA_KEY } from '../controller/controller';
|
|
@@ -15,7 +14,7 @@ export const ROUTE_METADATA_KEY = Symbol('route');
|
|
|
15
14
|
export interface RouteMetadata {
|
|
16
15
|
method: HttpMethod;
|
|
17
16
|
path: string;
|
|
18
|
-
handler:
|
|
17
|
+
handler: (...args: any[]) => unknown;
|
|
19
18
|
/**
|
|
20
19
|
* 属性键(用于控制器方法)
|
|
21
20
|
*/
|
|
@@ -75,7 +74,7 @@ function createRouteDecorator(method: HttpMethod, path: string = '') {
|
|
|
75
74
|
existingRoutes.push({
|
|
76
75
|
method,
|
|
77
76
|
path: path ?? '',
|
|
78
|
-
handler: descriptor.value as
|
|
77
|
+
handler: descriptor.value as (...args: any[]) => unknown,
|
|
79
78
|
propertyKey: propertyKeyStr || undefined,
|
|
80
79
|
});
|
|
81
80
|
Reflect.defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target);
|
|
@@ -16,6 +16,8 @@ export interface CanActivate {
|
|
|
16
16
|
canActivate(context: ExecutionContext): boolean | Promise<boolean>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type Guard = CanActivate;
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* HTTP 参数主机接口
|
|
21
23
|
* 提供 HTTP 请求相关的上下文信息
|
|
@@ -141,4 +143,3 @@ export const GUARD_REGISTRY_TOKEN = Symbol('@dangao/bun-server:guard-registry');
|
|
|
141
143
|
* Roles 元数据键
|
|
142
144
|
*/
|
|
143
145
|
export const ROLES_METADATA_KEY = Symbol('@dangao/bun-server:roles');
|
|
144
|
-
|
package/src/security/types.ts
CHANGED
package/src/session/service.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class SessionService {
|
|
|
25
25
|
) {
|
|
26
26
|
this.store = options.store!;
|
|
27
27
|
this.name = options.name ?? 'sessionId';
|
|
28
|
-
this.maxAge = options.maxAge ?? 86400000; // 24 小时
|
|
28
|
+
this.maxAge = options.maxAge ?? options.ttl ?? 86400000; // 24 小时
|
|
29
29
|
this.rolling = options.rolling ?? true;
|
|
30
30
|
this.cookieOptions = {
|
|
31
31
|
secure: options.cookie?.secure ?? false,
|
package/src/session/types.ts
CHANGED
|
@@ -317,6 +317,11 @@ export interface SessionModuleOptions {
|
|
|
317
317
|
*/
|
|
318
318
|
maxAge?: number;
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Session 最大存活时间别名。
|
|
322
|
+
*/
|
|
323
|
+
ttl?: number;
|
|
324
|
+
|
|
320
325
|
/**
|
|
321
326
|
* 是否在每次访问时更新过期时间
|
|
322
327
|
* @default true
|
|
@@ -327,6 +332,11 @@ export interface SessionModuleOptions {
|
|
|
327
332
|
* Cookie 选项
|
|
328
333
|
*/
|
|
329
334
|
cookie?: {
|
|
335
|
+
/**
|
|
336
|
+
* Cookie 名称别名,优先使用顶层 name。
|
|
337
|
+
*/
|
|
338
|
+
name?: string;
|
|
339
|
+
|
|
330
340
|
/**
|
|
331
341
|
* 是否只在 HTTPS 下发送
|
|
332
342
|
* @default false
|
|
@@ -50,13 +50,25 @@ export function ApiOperation(metadata: ApiOperationMetadata): MethodDecorator {
|
|
|
50
50
|
* 用于描述 API 参数
|
|
51
51
|
* @param metadata - 参数元数据
|
|
52
52
|
*/
|
|
53
|
-
export function ApiParam(metadata: ApiParamMetadata): ParameterDecorator {
|
|
54
|
-
|
|
53
|
+
export function ApiParam(metadata: ApiParamMetadata): ParameterDecorator & MethodDecorator {
|
|
54
|
+
const decorator = (
|
|
55
|
+
target: Object,
|
|
56
|
+
propertyKey: string | symbol | undefined,
|
|
57
|
+
descriptorOrParameterIndex: TypedPropertyDescriptor<any> | number,
|
|
58
|
+
): void => {
|
|
59
|
+
const paramMetadata: ApiParamMetadata = {
|
|
60
|
+
in: 'path',
|
|
61
|
+
...metadata,
|
|
62
|
+
};
|
|
55
63
|
const existingParams: Array<{ index: number; metadata: ApiParamMetadata }> =
|
|
56
64
|
Reflect.getMetadata(API_PARAM_METADATA_KEY, target as Object, propertyKey!) || [];
|
|
57
|
-
|
|
65
|
+
const index = typeof descriptorOrParameterIndex === 'number'
|
|
66
|
+
? descriptorOrParameterIndex
|
|
67
|
+
: -1;
|
|
68
|
+
existingParams.push({ index, metadata: paramMetadata });
|
|
58
69
|
Reflect.defineMetadata(API_PARAM_METADATA_KEY, existingParams, target as Object, propertyKey!);
|
|
59
70
|
};
|
|
71
|
+
return decorator as ParameterDecorator & MethodDecorator;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/**
|
|
@@ -130,4 +142,3 @@ export function getApiResponses(
|
|
|
130
142
|
): ApiResponseMetadata[] {
|
|
131
143
|
return Reflect.getMetadata(API_RESPONSE_METADATA_KEY, target as Object, propertyKey) || [];
|
|
132
144
|
}
|
|
133
|
-
|
package/src/swagger/generator.ts
CHANGED
|
@@ -139,7 +139,7 @@ export class SwaggerGenerator {
|
|
|
139
139
|
for (const match of pathParamMatches) {
|
|
140
140
|
const paramName = match[1];
|
|
141
141
|
// 检查是否已经有手动定义的参数
|
|
142
|
-
const existingParam = params.find((p) => p.metadata.name === paramName && p.metadata.in === 'path');
|
|
142
|
+
const existingParam = params.find((p) => p.metadata.name === paramName && (p.metadata.in ?? 'path') === 'path');
|
|
143
143
|
if (!existingParam) {
|
|
144
144
|
// 自动添加路径参数
|
|
145
145
|
pathParams.push({
|
|
@@ -155,7 +155,7 @@ export class SwaggerGenerator {
|
|
|
155
155
|
for (const param of params) {
|
|
156
156
|
pathParams.push({
|
|
157
157
|
name: param.metadata.name,
|
|
158
|
-
in: param.metadata.in,
|
|
158
|
+
in: param.metadata.in ?? 'path',
|
|
159
159
|
description: param.metadata.description,
|
|
160
160
|
required: param.metadata.required,
|
|
161
161
|
schema: param.metadata.schema,
|
|
@@ -332,4 +332,3 @@ export class SwaggerGenerator {
|
|
|
332
332
|
return path;
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
|
-
|
package/src/swagger/types.ts
CHANGED
|
@@ -60,7 +60,7 @@ export interface ApiParamMetadata {
|
|
|
60
60
|
enum?: unknown[];
|
|
61
61
|
default?: unknown;
|
|
62
62
|
};
|
|
63
|
-
in
|
|
63
|
+
in?: 'query' | 'path' | 'header' | 'cookie';
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
@@ -185,4 +185,3 @@ export interface SwaggerDocument {
|
|
|
185
185
|
patch?: SwaggerPathItem;
|
|
186
186
|
}>;
|
|
187
187
|
}
|
|
188
|
-
|
package/src/testing/harness.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class PerformanceHarness {
|
|
|
22
22
|
public static async benchmark(
|
|
23
23
|
name: string,
|
|
24
24
|
iterations: number,
|
|
25
|
-
runner: (iteration: number) =>
|
|
25
|
+
runner: (iteration: number) => unknown | Promise<unknown>,
|
|
26
26
|
): Promise<BenchmarkResult> {
|
|
27
27
|
if (iterations <= 0) {
|
|
28
28
|
throw new Error('iterations must be greater than 0');
|
|
@@ -93,4 +93,3 @@ export class StressTester {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
|
|
@@ -9,7 +9,7 @@ interface RequestOptions {
|
|
|
9
9
|
interface TestResponse {
|
|
10
10
|
status: number;
|
|
11
11
|
headers: Headers;
|
|
12
|
-
body:
|
|
12
|
+
body: any;
|
|
13
13
|
text: string;
|
|
14
14
|
ok: boolean;
|
|
15
15
|
}
|
|
@@ -94,7 +94,7 @@ export class TestHttpClient {
|
|
|
94
94
|
const response = await fetch(url, fetchOptions);
|
|
95
95
|
const text = await response.text();
|
|
96
96
|
|
|
97
|
-
let body:
|
|
97
|
+
let body: any;
|
|
98
98
|
try {
|
|
99
99
|
body = JSON.parse(text);
|
|
100
100
|
} catch (_error) {
|
|
@@ -5,7 +5,7 @@ import { Container } from '../di/container';
|
|
|
5
5
|
import { ModuleRegistry } from '../di/module-registry';
|
|
6
6
|
import { ControllerRegistry } from '../controller/controller';
|
|
7
7
|
import { RouteRegistry } from '../router/registry';
|
|
8
|
-
import { MODULE_METADATA_KEY, type ModuleMetadata, type ModuleClass, type ModuleProvider, type ProviderToken } from '../di/module';
|
|
8
|
+
import { MODULE_METADATA_KEY, invokeFactoryProvider, type ModuleMetadata, type ModuleClass, type ModuleProvider, type ProviderToken } from '../di/module';
|
|
9
9
|
import type { Constructor } from '../core/types';
|
|
10
10
|
import { TestHttpClient } from './test-client';
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ interface ProviderOverride {
|
|
|
13
13
|
token: ProviderToken;
|
|
14
14
|
useValue?: unknown;
|
|
15
15
|
useClass?: Constructor<unknown>;
|
|
16
|
-
useFactory?: () => unknown;
|
|
16
|
+
useFactory?: (...args: any[]) => unknown;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -75,7 +75,7 @@ export class TestingModuleBuilder {
|
|
|
75
75
|
} else if (override.useClass) {
|
|
76
76
|
result.push({ provide: override.token, useClass: override.useClass });
|
|
77
77
|
} else if (override.useFactory) {
|
|
78
|
-
result.push({ provide: override.token, useFactory: override.useFactory
|
|
78
|
+
result.push({ provide: override.token, useFactory: override.useFactory });
|
|
79
79
|
}
|
|
80
80
|
overrideMap.delete(key as string | symbol);
|
|
81
81
|
} else {
|
|
@@ -121,7 +121,7 @@ class ProviderOverrideBuilder {
|
|
|
121
121
|
/**
|
|
122
122
|
* 使用工厂函数覆盖
|
|
123
123
|
*/
|
|
124
|
-
public useFactory(factory: () => unknown): TestingModuleBuilder {
|
|
124
|
+
public useFactory(factory: (...args: any[]) => unknown): TestingModuleBuilder {
|
|
125
125
|
this.builder.addOverride({ token: this.token, useFactory: factory });
|
|
126
126
|
return this.builder;
|
|
127
127
|
}
|
|
@@ -177,7 +177,7 @@ export class TestingModule {
|
|
|
177
177
|
container.registerInstance(provider.provide, provider.useValue);
|
|
178
178
|
} else if ('useFactory' in provider && provider.provide) {
|
|
179
179
|
container.register(provider.provide as Constructor<unknown>, {
|
|
180
|
-
factory: () => (provider
|
|
180
|
+
factory: () => invokeFactoryProvider(provider, container),
|
|
181
181
|
});
|
|
182
182
|
} else if ('useClass' in provider) {
|
|
183
183
|
const token = (provider as { provide?: ProviderToken }).provide ?? (provider as { useClass: Constructor<unknown> }).useClass;
|
package/src/validation/errors.ts
CHANGED
|
@@ -9,10 +9,15 @@ export interface ValidationIssue {
|
|
|
9
9
|
*/
|
|
10
10
|
property?: string;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 属性路径别名,兼容早期错误对象形状。
|
|
14
|
+
*/
|
|
15
|
+
path?: string;
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* 失败的规则名称
|
|
14
19
|
*/
|
|
15
|
-
rule
|
|
20
|
+
rule?: string;
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* 错误信息
|
|
@@ -73,4 +78,3 @@ export class ValidationError extends Error {
|
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module 'bun:test' {
|
|
2
|
+
export const describe: any;
|
|
3
|
+
export const test: any;
|
|
4
|
+
export const it: any;
|
|
5
|
+
export const expect: any;
|
|
6
|
+
export const beforeEach: any;
|
|
7
|
+
export const afterEach: any;
|
|
8
|
+
export const beforeAll: any;
|
|
9
|
+
export const afterAll: any;
|
|
10
|
+
export const mock: any;
|
|
11
|
+
}
|
|
@@ -155,7 +155,7 @@ describe('@Context() Decorator', () => {
|
|
|
155
155
|
// 这个测试验证 ContextService 可以在服务层使用
|
|
156
156
|
const { ContextService, CONTEXT_SERVICE_TOKEN } = await import('../../src/core/context-service');
|
|
157
157
|
const container = app.getContainer();
|
|
158
|
-
const contextService = container.resolve<ContextService
|
|
158
|
+
const contextService = container.resolve<InstanceType<typeof ContextService>>(CONTEXT_SERVICE_TOKEN);
|
|
159
159
|
|
|
160
160
|
@Controller('/api/test')
|
|
161
161
|
class TestController {
|
|
@@ -180,4 +180,3 @@ describe('@Context() Decorator', () => {
|
|
|
180
180
|
expect(data.method).toBe('GET');
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
|
-
|
|
@@ -9,9 +9,13 @@ import {
|
|
|
9
9
|
SQLITE_MANAGER_TOKEN,
|
|
10
10
|
} from '../../src/database';
|
|
11
11
|
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
12
|
+
import { initRuntime } from '../../src/platform/runtime';
|
|
12
13
|
|
|
13
14
|
describe('DatabaseModule V2', () => {
|
|
14
15
|
beforeEach(() => {
|
|
16
|
+
// forRoot() 会在创建 sqlite/sql 管理器时读取 getRuntime(),确保运行时已初始化
|
|
17
|
+
// (避免依赖其他测试文件的执行顺序)
|
|
18
|
+
initRuntime('bun');
|
|
15
19
|
Reflect.deleteMetadata(MODULE_METADATA_KEY, DatabaseModule);
|
|
16
20
|
});
|
|
17
21
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeAll } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { initRuntime, getRuntime } from '../../src/platform/runtime';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 验证:在 Bun 运行时下显式选用 driver: 'mysql2' 时,
|
|
7
|
+
* 连接创建 / 参数化查询 / health check / close 全部走 mysql2(而非 Bun.SQL)。
|
|
8
|
+
*
|
|
9
|
+
* 通过 mock 'mysql2/promise' 模块注入假连接,无需真实 MySQL。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const createdConnections: Array<Record<string, unknown>> = [];
|
|
13
|
+
const queryCalls: Array<{ sql: string; params: unknown[] }> = [];
|
|
14
|
+
let endCount = 0;
|
|
15
|
+
|
|
16
|
+
function makeFakeConnection() {
|
|
17
|
+
const conn = {
|
|
18
|
+
query: async (sql: string, params: unknown[]) => {
|
|
19
|
+
queryCalls.push({ sql, params });
|
|
20
|
+
// mysql2 返回 [rows, fields]
|
|
21
|
+
if (/select\s+1/i.test(sql)) {
|
|
22
|
+
return [[{ '1': 1 }], []];
|
|
23
|
+
}
|
|
24
|
+
return [[{ id: 1, name: 'alice' }], [{ name: 'id' }, { name: 'name' }]];
|
|
25
|
+
},
|
|
26
|
+
end: async () => {
|
|
27
|
+
endCount += 1;
|
|
28
|
+
},
|
|
29
|
+
close: async () => {
|
|
30
|
+
throw new Error('mysql2 connection should be closed via end(), not close()');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
createdConnections.push(conn);
|
|
34
|
+
return conn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
mock.module('mysql2/promise', () => {
|
|
38
|
+
const createConnection = async (_opts: unknown) => makeFakeConnection();
|
|
39
|
+
const createPool = (_opts: unknown) => makeFakeConnection();
|
|
40
|
+
return {
|
|
41
|
+
default: { createConnection, createPool },
|
|
42
|
+
createConnection,
|
|
43
|
+
createPool,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('DatabaseService with driver: mysql2 on Bun runtime', () => {
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
initRuntime('bun');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('runs the full lifecycle (create/query/health/close) through mysql2', async () => {
|
|
53
|
+
// 前置确认:当前进程运行在 Bun,但我们强制 mysql2
|
|
54
|
+
expect(getRuntime().engine).toBe('bun');
|
|
55
|
+
|
|
56
|
+
const { DatabaseService } = await import('../../src/database/service');
|
|
57
|
+
|
|
58
|
+
const svc = new DatabaseService({
|
|
59
|
+
database: {
|
|
60
|
+
type: 'mysql',
|
|
61
|
+
driver: 'mysql2',
|
|
62
|
+
config: {
|
|
63
|
+
host: 'localhost',
|
|
64
|
+
port: 3306,
|
|
65
|
+
database: 'testdb',
|
|
66
|
+
user: 'root',
|
|
67
|
+
password: 'secret',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
enableHealthCheck: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await svc.initialize();
|
|
74
|
+
|
|
75
|
+
// 连接由 mysql2.createConnection 创建
|
|
76
|
+
expect(createdConnections.length).toBeGreaterThan(0);
|
|
77
|
+
|
|
78
|
+
// 参数化查询走 mysql2.query(sql, params),并返回 rows(而非 [rows, fields])
|
|
79
|
+
const rows = await svc.query<{ id: number; name: string }>(
|
|
80
|
+
'SELECT * FROM users WHERE id = ?',
|
|
81
|
+
[1],
|
|
82
|
+
);
|
|
83
|
+
expect(rows).toEqual([{ id: 1, name: 'alice' }]);
|
|
84
|
+
expect(queryCalls.some((c) => c.sql === 'SELECT * FROM users WHERE id = ?' && c.params[0] === 1)).toBe(true);
|
|
85
|
+
|
|
86
|
+
// health check 走 mysql2.query('SELECT 1')
|
|
87
|
+
const healthy = await svc.healthCheck();
|
|
88
|
+
expect(healthy).toBe(true);
|
|
89
|
+
expect(queryCalls.some((c) => /select\s+1/i.test(c.sql))).toBe(true);
|
|
90
|
+
|
|
91
|
+
// close 走 mysql2 的 end()
|
|
92
|
+
await svc.closePool();
|
|
93
|
+
expect(endCount).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
});
|