@dangao/bun-server 2.0.8 → 2.2.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/README.md +4 -0
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/core/application.d.ts +6 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/server.d.ts +5 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/database/database-context.d.ts +25 -0
- package/dist/database/database-context.d.ts.map +1 -0
- package/dist/database/database-extension.d.ts +8 -9
- package/dist/database/database-extension.d.ts.map +1 -1
- package/dist/database/database-module.d.ts +7 -1
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/database/db-proxy.d.ts +12 -0
- package/dist/database/db-proxy.d.ts.map +1 -0
- package/dist/database/index.d.ts +6 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/orm/transaction-interceptor.d.ts +0 -16
- package/dist/database/orm/transaction-interceptor.d.ts.map +1 -1
- package/dist/database/orm/transaction-manager.d.ts +10 -61
- package/dist/database/orm/transaction-manager.d.ts.map +1 -1
- package/dist/database/service.d.ts.map +1 -1
- package/dist/database/sql-manager.d.ts +14 -0
- package/dist/database/sql-manager.d.ts.map +1 -0
- package/dist/database/sqlite-adapter.d.ts +32 -0
- package/dist/database/sqlite-adapter.d.ts.map +1 -0
- package/dist/database/strategy-decorator.d.ts +8 -0
- package/dist/database/strategy-decorator.d.ts.map +1 -0
- package/dist/database/types.d.ts +122 -1
- package/dist/database/types.d.ts.map +1 -1
- package/dist/di/container.d.ts +16 -0
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/lifecycle.d.ts +48 -0
- package/dist/di/lifecycle.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +10 -6
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3267 -2620
- package/dist/microservice/service-registry/service-registry-module.d.ts +16 -0
- package/dist/microservice/service-registry/service-registry-module.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/registry.d.ts +1 -1
- package/dist/router/registry.d.ts.map +1 -1
- package/dist/router/route.d.ts +2 -1
- package/dist/router/route.d.ts.map +1 -1
- package/dist/router/router.d.ts +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/timeout-decorator.d.ts +6 -0
- package/dist/router/timeout-decorator.d.ts.map +1 -0
- package/docs/database.md +48 -15
- package/docs/idle-timeout.md +42 -0
- package/docs/lifecycle.md +80 -4
- package/docs/microservice-nacos.md +1 -0
- package/docs/microservice-service-registry.md +7 -0
- package/docs/zh/database.md +48 -15
- package/docs/zh/idle-timeout.md +41 -0
- package/docs/zh/lifecycle.md +49 -5
- package/docs/zh/microservice-nacos.md +1 -0
- package/docs/zh/microservice-service-registry.md +6 -0
- package/package.json +1 -1
- package/src/controller/controller.ts +11 -1
- package/src/core/application.ts +98 -26
- package/src/core/server.ts +10 -0
- package/src/database/database-context.ts +43 -0
- package/src/database/database-extension.ts +12 -45
- package/src/database/database-module.ts +254 -11
- package/src/database/db-proxy.ts +75 -0
- package/src/database/index.ts +29 -0
- package/src/database/orm/transaction-interceptor.ts +12 -149
- package/src/database/orm/transaction-manager.ts +143 -210
- package/src/database/service.ts +28 -2
- package/src/database/sql-manager.ts +62 -0
- package/src/database/sqlite-adapter.ts +121 -0
- package/src/database/strategy-decorator.ts +42 -0
- package/src/database/types.ts +133 -1
- package/src/di/container.ts +55 -1
- package/src/di/lifecycle.ts +114 -0
- package/src/di/module-registry.ts +78 -14
- package/src/index.ts +31 -1
- package/src/microservice/service-registry/service-registry-module.ts +25 -1
- package/src/router/index.ts +1 -0
- package/src/router/registry.ts +10 -1
- package/src/router/route.ts +31 -3
- package/src/router/router.ts +10 -1
- package/src/router/timeout-decorator.ts +35 -0
- package/tests/core/application.test.ts +10 -0
- package/tests/database/database-module.test.ts +91 -430
- package/tests/database/db-proxy.test.ts +93 -0
- package/tests/database/sql-manager.test.ts +43 -0
- package/tests/database/sqlite-adapter.test.ts +45 -0
- package/tests/database/strategy-decorator.test.ts +29 -0
- package/tests/database/transaction.test.ts +84 -222
- package/tests/di/lifecycle.test.ts +139 -1
- package/tests/di/scoped-lifecycle.test.ts +61 -0
- package/tests/microservice/service-registry.test.ts +15 -0
- package/tests/router/timeout-decorator.test.ts +48 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
scanInterceptorMetadata,
|
|
17
17
|
} from '../interceptor';
|
|
18
18
|
import { LoggerManager } from '@dangao/logsmith';
|
|
19
|
+
import { getIdleTimeout } from '../router/timeout-decorator';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* 控制器元数据键
|
|
@@ -233,7 +234,16 @@ export class ControllerRegistry {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
// 注册路由,传递控制器和方法信息
|
|
236
|
-
|
|
237
|
+
const timeout = getIdleTimeout(controllerClass, propertyKey);
|
|
238
|
+
registry.register(
|
|
239
|
+
route.method,
|
|
240
|
+
fullPath,
|
|
241
|
+
handler,
|
|
242
|
+
middlewares,
|
|
243
|
+
controllerClass,
|
|
244
|
+
propertyKey,
|
|
245
|
+
timeout,
|
|
246
|
+
);
|
|
237
247
|
}
|
|
238
248
|
}
|
|
239
249
|
|
package/src/core/application.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { WebSocketGatewayRegistry } from '../websocket/registry';
|
|
|
10
10
|
import type { ApplicationExtension } from '../extensions/types';
|
|
11
11
|
import { LoggerExtension } from '../extensions/logger-extension';
|
|
12
12
|
import { ModuleRegistry } from '../di/module-registry';
|
|
13
|
-
import type
|
|
13
|
+
import { MODULE_METADATA_KEY, type ModuleClass } from '../di/module';
|
|
14
14
|
import type { Constructor } from './types';
|
|
15
15
|
import { InterceptorRegistry, INTERCEPTOR_REGISTRY_TOKEN } from '../interceptor';
|
|
16
16
|
import { CONFIG_SERVICE_TOKEN } from '../config/types';
|
|
@@ -20,6 +20,7 @@ import { CacheModule, CACHE_POST_PROCESSOR_TOKEN } from '../cache';
|
|
|
20
20
|
import { LoggerManager } from '@dangao/logsmith';
|
|
21
21
|
import { EventModule } from '../events/event-module';
|
|
22
22
|
import { AsyncProviderRegistry } from '../di/async-module';
|
|
23
|
+
import { ServiceRegistryModule } from '../microservice/service-registry/service-registry-module';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* 应用配置选项
|
|
@@ -54,6 +55,12 @@ export interface ApplicationOptions {
|
|
|
54
55
|
* @default false
|
|
55
56
|
*/
|
|
56
57
|
reusePort?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 连接空闲超时时间(毫秒)
|
|
61
|
+
* 框架内部会自动转换为 Bun.serve 的秒单位
|
|
62
|
+
*/
|
|
63
|
+
idleTimeout?: number;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -77,6 +84,13 @@ export class Application {
|
|
|
77
84
|
RouteRegistry.getInstance().clear();
|
|
78
85
|
ControllerRegistry.getInstance().clear();
|
|
79
86
|
ModuleRegistry.getInstance().clear();
|
|
87
|
+
const serviceRegistryMetadata = Reflect.getMetadata(
|
|
88
|
+
MODULE_METADATA_KEY,
|
|
89
|
+
ServiceRegistryModule,
|
|
90
|
+
);
|
|
91
|
+
if (!serviceRegistryMetadata) {
|
|
92
|
+
ServiceRegistryModule.autoRegister = true;
|
|
93
|
+
}
|
|
80
94
|
|
|
81
95
|
// 注册 InterceptorRegistry 到 DI 容器
|
|
82
96
|
const container = ControllerRegistry.getInstance().getContainer();
|
|
@@ -135,6 +149,7 @@ export class Application {
|
|
|
135
149
|
port: finalPort,
|
|
136
150
|
hostname: finalHostname,
|
|
137
151
|
reusePort: this.options.reusePort,
|
|
152
|
+
idleTimeout: this.options.idleTimeout,
|
|
138
153
|
fetch: this.handleRequest.bind(this),
|
|
139
154
|
websocketRegistry: this.websocketRegistry,
|
|
140
155
|
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
|
|
@@ -309,6 +324,7 @@ export class Application {
|
|
|
309
324
|
*/
|
|
310
325
|
private async handleRequest(context: Context): Promise<Response> {
|
|
311
326
|
const logger = LoggerManager.getLogger();
|
|
327
|
+
const moduleRegistry = ModuleRegistry.getInstance();
|
|
312
328
|
logger.debug('[Request] Incoming', {
|
|
313
329
|
method: context.method,
|
|
314
330
|
path: context.path,
|
|
@@ -317,34 +333,46 @@ export class Application {
|
|
|
317
333
|
|
|
318
334
|
// 使用 AsyncLocalStorage 包裹请求处理,确保所有中间件和控制器都在请求上下文中执行
|
|
319
335
|
return await contextStore.run(context, async () => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
|
|
327
|
-
const registry = RouteRegistry.getInstance();
|
|
328
|
-
const router = registry.getRouter();
|
|
329
|
-
|
|
330
|
-
// 预解析路由,仅设置上下文信息,不执行处理器
|
|
331
|
-
await router.preHandle(context);
|
|
332
|
-
|
|
333
|
-
// 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
|
|
334
|
-
// 最后再由路由真正执行控制器方法
|
|
335
|
-
return await this.middlewarePipeline.run(context, async () => {
|
|
336
|
-
const response = await router.handle(context);
|
|
337
|
-
if (response) {
|
|
338
|
-
return response;
|
|
336
|
+
try {
|
|
337
|
+
// 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
|
|
338
|
+
// 这样可以确保 Request.body 流只读取一次
|
|
339
|
+
if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
|
|
340
|
+
await context.getBody();
|
|
339
341
|
}
|
|
340
342
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
// 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
|
|
344
|
+
const registry = RouteRegistry.getInstance();
|
|
345
|
+
const router = registry.getRouter();
|
|
346
|
+
|
|
347
|
+
// 预解析路由,仅设置上下文信息,不执行处理器
|
|
348
|
+
await router.preHandle(context);
|
|
349
|
+
|
|
350
|
+
// 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
|
|
351
|
+
// 最后再由路由真正执行控制器方法
|
|
352
|
+
return await this.middlewarePipeline.run(context, async () => {
|
|
353
|
+
const response = await router.handle(context);
|
|
354
|
+
if (response) {
|
|
355
|
+
return response;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
logger.debug('[Router] No route matched', {
|
|
359
|
+
method: context.method,
|
|
360
|
+
path: context.path,
|
|
361
|
+
});
|
|
362
|
+
context.setStatus(404);
|
|
363
|
+
return context.createErrorResponse({ error: 'Not Found' });
|
|
344
364
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
365
|
+
} finally {
|
|
366
|
+
try {
|
|
367
|
+
await moduleRegistry.disposeScopedInstances(context);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
logger.warn('[Application] Failed to dispose scoped instances', {
|
|
370
|
+
path: context.path,
|
|
371
|
+
method: context.method,
|
|
372
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
348
376
|
});
|
|
349
377
|
}
|
|
350
378
|
|
|
@@ -430,7 +458,27 @@ export class Application {
|
|
|
430
458
|
* 扫描所有使用 @ServiceRegistry 装饰器的控制器,自动注册服务
|
|
431
459
|
*/
|
|
432
460
|
private async registerServices(port: number, hostname?: string): Promise<void> {
|
|
461
|
+
if (ServiceRegistryModule.autoRegister === false) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
433
464
|
try {
|
|
465
|
+
const configuredService = ServiceRegistryModule.autoRegisterService;
|
|
466
|
+
if (configuredService) {
|
|
467
|
+
const { SERVICE_REGISTRY_TOKEN } = await import(
|
|
468
|
+
'../microservice/service-registry/types'
|
|
469
|
+
);
|
|
470
|
+
const container = this.getContainer();
|
|
471
|
+
if (container.isRegistered(SERVICE_REGISTRY_TOKEN)) {
|
|
472
|
+
const registry = container.resolve<any>(SERVICE_REGISTRY_TOKEN);
|
|
473
|
+
await registry.register({
|
|
474
|
+
...configuredService,
|
|
475
|
+
ip: configuredService.ip ?? hostname ?? '127.0.0.1',
|
|
476
|
+
port: configuredService.port ?? port,
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
434
482
|
// 动态导入服务注册装饰器(避免循环依赖)
|
|
435
483
|
const { registerServiceInstance } = await import(
|
|
436
484
|
'../microservice/service-registry/decorators'
|
|
@@ -456,7 +504,31 @@ export class Application {
|
|
|
456
504
|
* 注销所有使用 @ServiceRegistry 装饰器的服务
|
|
457
505
|
*/
|
|
458
506
|
private async deregisterServices(): Promise<void> {
|
|
507
|
+
if (ServiceRegistryModule.autoRegister === false) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
459
510
|
try {
|
|
511
|
+
const configuredService = ServiceRegistryModule.autoRegisterService;
|
|
512
|
+
if (configuredService) {
|
|
513
|
+
const { SERVICE_REGISTRY_TOKEN } = await import(
|
|
514
|
+
'../microservice/service-registry/types'
|
|
515
|
+
);
|
|
516
|
+
const container = this.getContainer();
|
|
517
|
+
if (container.isRegistered(SERVICE_REGISTRY_TOKEN)) {
|
|
518
|
+
const registry = container.resolve<any>(SERVICE_REGISTRY_TOKEN);
|
|
519
|
+
await registry.deregister({
|
|
520
|
+
serviceName: configuredService.serviceName,
|
|
521
|
+
ip:
|
|
522
|
+
configuredService.ip ??
|
|
523
|
+
this.server?.getHostname() ??
|
|
524
|
+
this.options.hostname ??
|
|
525
|
+
'127.0.0.1',
|
|
526
|
+
port: configuredService.port ?? this.server?.getPort() ?? this.options.port ?? 3000,
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
460
532
|
// 动态导入服务注册装饰器(避免循环依赖)
|
|
461
533
|
const { deregisterServiceInstance } = await import(
|
|
462
534
|
'../microservice/service-registry/decorators'
|
package/src/core/server.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface ServerOptions {
|
|
|
41
41
|
* @default false
|
|
42
42
|
*/
|
|
43
43
|
reusePort?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 连接空闲超时时间(毫秒)
|
|
47
|
+
* 框架内部会转换为 Bun.serve 所需的秒
|
|
48
|
+
*/
|
|
49
|
+
idleTimeout?: number;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
@@ -161,6 +167,10 @@ export class BunServer {
|
|
|
161
167
|
port: this.options.port ?? 3000,
|
|
162
168
|
hostname: this.options.hostname,
|
|
163
169
|
reusePort: this.options.reusePort,
|
|
170
|
+
idleTimeout:
|
|
171
|
+
typeof this.options.idleTimeout === 'number'
|
|
172
|
+
? Math.max(0, Math.ceil(this.options.idleTimeout / 1000))
|
|
173
|
+
: undefined,
|
|
164
174
|
fetch: fetchHandler,
|
|
165
175
|
websocket: websocketHandlers,
|
|
166
176
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
import type { TransactionStatus } from './orm/transaction-types';
|
|
4
|
+
import type { SqliteAdapter } from './sqlite-adapter';
|
|
5
|
+
|
|
6
|
+
export type SqlTemplateCall = (
|
|
7
|
+
strings: TemplateStringsArray,
|
|
8
|
+
...values: unknown[]
|
|
9
|
+
) => Promise<unknown>;
|
|
10
|
+
|
|
11
|
+
export interface ReservedSqlSession extends SqlTemplateCall {
|
|
12
|
+
begin<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
|
+
release(): Promise<void> | void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TransactionState {
|
|
17
|
+
id: string;
|
|
18
|
+
status: TransactionStatus;
|
|
19
|
+
level: number;
|
|
20
|
+
savepoints: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DatabaseSession {
|
|
24
|
+
reserved?: ReservedSqlSession;
|
|
25
|
+
sqlite?: SqliteAdapter;
|
|
26
|
+
tenantId: string;
|
|
27
|
+
transaction?: TransactionState;
|
|
28
|
+
lazyReserve?: () => Promise<ReservedSqlSession>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const databaseSessionStore = new AsyncLocalStorage<DatabaseSession>();
|
|
32
|
+
|
|
33
|
+
export function getCurrentSession(): DatabaseSession | undefined {
|
|
34
|
+
return databaseSessionStore.getStore();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function runWithSession<T>(
|
|
38
|
+
session: DatabaseSession,
|
|
39
|
+
fn: () => Promise<T>,
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
return databaseSessionStore.run(session, fn);
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -7,14 +7,19 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import { TransactionInterceptor } from './orm/transaction-interceptor';
|
|
9
9
|
import { TRANSACTION_METADATA_KEY } from './orm/transaction-decorator';
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
10
|
+
import type { BunSQLManager } from './sql-manager';
|
|
11
|
+
import type { SqliteManager } from './sqlite-adapter';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* 数据库扩展
|
|
15
|
-
*
|
|
15
|
+
* 注册事务拦截器,并在应用关闭时释放数据库资源
|
|
16
16
|
*/
|
|
17
17
|
export class DatabaseExtension implements ApplicationExtension {
|
|
18
|
+
public constructor(
|
|
19
|
+
private readonly sqlManager?: BunSQLManager,
|
|
20
|
+
private readonly sqliteManager?: SqliteManager,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
18
23
|
public register(container: Container): void {
|
|
19
24
|
// 注册事务拦截器到拦截器注册表
|
|
20
25
|
try {
|
|
@@ -36,48 +41,10 @@ export class DatabaseExtension implements ApplicationExtension {
|
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
40
|
-
* 应该在应用启动时调用
|
|
44
|
+
* 关闭数据库资源
|
|
41
45
|
*/
|
|
42
|
-
public async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DATABASE_SERVICE_TOKEN,
|
|
46
|
-
);
|
|
47
|
-
await databaseService.initialize();
|
|
48
|
-
} catch (error) {
|
|
49
|
-
// 如果 DatabaseService 未注册,忽略错误
|
|
50
|
-
// 这意味着用户可能没有使用 DatabaseModule
|
|
51
|
-
if (
|
|
52
|
-
error instanceof Error &&
|
|
53
|
-
error.message.includes('Provider not found')
|
|
54
|
-
) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 关闭数据库连接
|
|
63
|
-
* 应该在应用停止时调用
|
|
64
|
-
*/
|
|
65
|
-
public async close(container: Container): Promise<void> {
|
|
66
|
-
try {
|
|
67
|
-
const databaseService = container.resolve<DatabaseService>(
|
|
68
|
-
DATABASE_SERVICE_TOKEN,
|
|
69
|
-
);
|
|
70
|
-
// 关闭连接池(关闭所有连接)
|
|
71
|
-
await databaseService.closePool();
|
|
72
|
-
} catch (error) {
|
|
73
|
-
// 如果 DatabaseService 未注册,忽略错误
|
|
74
|
-
if (
|
|
75
|
-
error instanceof Error &&
|
|
76
|
-
error.message.includes('Provider not found')
|
|
77
|
-
) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
46
|
+
public async close(_container: Container): Promise<void> {
|
|
47
|
+
await this.sqlManager?.destroyAll(10);
|
|
48
|
+
this.sqliteManager?.destroyAll();
|
|
82
49
|
}
|
|
83
50
|
}
|
|
@@ -1,30 +1,207 @@
|
|
|
1
1
|
import { Module, MODULE_METADATA_KEY, type ModuleProvider } from '../di/module';
|
|
2
|
-
import type { ApplicationExtension } from '../extensions/types';
|
|
3
|
-
import {
|
|
4
|
-
InterceptorRegistry,
|
|
5
|
-
INTERCEPTOR_REGISTRY_TOKEN,
|
|
6
|
-
} from '../interceptor';
|
|
7
2
|
import { type AsyncModuleOptions, registerAsyncProviders } from '../di/async-module';
|
|
3
|
+
import type { Constructor } from '@/core/types';
|
|
4
|
+
import type { Middleware } from '../middleware';
|
|
8
5
|
|
|
9
6
|
import { DatabaseExtension } from './database-extension';
|
|
10
7
|
import { DatabaseHealthIndicator } from './health-indicator';
|
|
11
8
|
import { OrmService } from './orm/service';
|
|
12
9
|
import { TransactionManager } from './orm/transaction-manager';
|
|
13
|
-
import { TransactionInterceptor } from './orm/transaction-interceptor';
|
|
14
10
|
import { TRANSACTION_METADATA_KEY } from './orm/transaction-decorator';
|
|
15
11
|
import { DatabaseService } from './service';
|
|
16
12
|
import {
|
|
13
|
+
BUN_SQL_MANAGER_TOKEN,
|
|
14
|
+
DB_TOKEN,
|
|
17
15
|
DATABASE_OPTIONS_TOKEN,
|
|
18
16
|
DATABASE_SERVICE_TOKEN,
|
|
17
|
+
SQLITE_MANAGER_TOKEN,
|
|
18
|
+
type BunSQLConfig,
|
|
19
19
|
type DatabaseModuleOptions,
|
|
20
|
+
type SqliteV2Config,
|
|
20
21
|
} from './types';
|
|
21
22
|
import { ORM_SERVICE_TOKEN } from './orm/types';
|
|
22
23
|
import { TRANSACTION_SERVICE_TOKEN } from './orm/transaction-types';
|
|
24
|
+
import { BunSQLManager } from './sql-manager';
|
|
25
|
+
import { SqliteManager } from './sqlite-adapter';
|
|
26
|
+
import { db, initDbProxy } from './db-proxy';
|
|
27
|
+
import { getDbStrategy } from './strategy-decorator';
|
|
28
|
+
import { runWithSession, type DatabaseSession } from './database-context';
|
|
23
29
|
|
|
24
30
|
@Module({
|
|
25
31
|
providers: [],
|
|
26
32
|
})
|
|
27
33
|
export class DatabaseModule {
|
|
34
|
+
private static isBunSqlType(
|
|
35
|
+
type: DatabaseModuleOptions['type'] | undefined,
|
|
36
|
+
): type is 'postgres' | 'mysql' {
|
|
37
|
+
return type === 'postgres' || type === 'mysql';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static normalizeConfig(
|
|
41
|
+
options: DatabaseModuleOptions,
|
|
42
|
+
): Array<{ tenantId: string; config: BunSQLConfig | SqliteV2Config }> {
|
|
43
|
+
if (options.tenants && options.tenants.length > 0) {
|
|
44
|
+
return options.tenants.map((tenant) => ({
|
|
45
|
+
tenantId: tenant.id,
|
|
46
|
+
config: tenant.config,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (options.database?.type === 'sqlite') {
|
|
51
|
+
if (options.pool) {
|
|
52
|
+
console.warn('[DatabaseModule] pool options are ignored for SQLite');
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
tenantId: options.defaultTenant ?? 'default',
|
|
57
|
+
config: {
|
|
58
|
+
type: 'sqlite',
|
|
59
|
+
database: options.database.config.path,
|
|
60
|
+
wal: options.wal ?? true,
|
|
61
|
+
maxWriteConcurrency: options.maxWriteConcurrency ?? 1,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.database?.type === 'postgres' || options.database?.type === 'mysql') {
|
|
68
|
+
const db = options.database;
|
|
69
|
+
const protocol = db.type === 'mysql' ? 'mysql' : 'postgres';
|
|
70
|
+
const url =
|
|
71
|
+
`${protocol}://${db.config.user}:${db.config.password}@${db.config.host}:${db.config.port}/${db.config.database}`;
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
tenantId: options.defaultTenant ?? 'default',
|
|
75
|
+
config: {
|
|
76
|
+
type: db.type,
|
|
77
|
+
url,
|
|
78
|
+
pool: options.bunSqlPool,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.type === 'sqlite') {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
tenantId: options.defaultTenant ?? 'default',
|
|
88
|
+
config: {
|
|
89
|
+
type: 'sqlite',
|
|
90
|
+
database: options.databasePath ?? ':memory:',
|
|
91
|
+
wal: options.wal ?? true,
|
|
92
|
+
maxWriteConcurrency: options.maxWriteConcurrency ?? 1,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.url && DatabaseModule.isBunSqlType(options.type)) {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
tenantId: options.defaultTenant ?? 'default',
|
|
102
|
+
config: {
|
|
103
|
+
type: options.type,
|
|
104
|
+
url: options.url,
|
|
105
|
+
pool: options.bunSqlPool,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options.host && DatabaseModule.isBunSqlType(options.type)) {
|
|
112
|
+
const protocol = options.type === 'mysql' ? 'mysql' : 'postgres';
|
|
113
|
+
const url =
|
|
114
|
+
`${protocol}://${options.username}:${options.password}@${options.host}:${options.port ?? 5432}/${options.databasePath ?? ''}`;
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
tenantId: options.defaultTenant ?? 'default',
|
|
118
|
+
config: {
|
|
119
|
+
type: options.type,
|
|
120
|
+
url,
|
|
121
|
+
pool: options.bunSqlPool,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(
|
|
128
|
+
'[DatabaseModule] invalid configuration: specify tenants or single tenant connection options',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static createDatabaseMiddleware(
|
|
133
|
+
options: DatabaseModuleOptions,
|
|
134
|
+
normalized: Array<{ tenantId: string; config: BunSQLConfig | SqliteV2Config }>,
|
|
135
|
+
sqlManager: BunSQLManager,
|
|
136
|
+
sqliteManager: SqliteManager,
|
|
137
|
+
): Middleware {
|
|
138
|
+
const defaultStrategy = options.defaultStrategy ?? 'pool';
|
|
139
|
+
const defaultTenant = options.defaultTenant ?? normalized[0]?.tenantId ?? 'default';
|
|
140
|
+
|
|
141
|
+
return async (context, next) => {
|
|
142
|
+
const routeHandler = (context as any).routeHandler as
|
|
143
|
+
| { controller: Constructor<unknown>; method: string }
|
|
144
|
+
| undefined;
|
|
145
|
+
|
|
146
|
+
let strategy: 'pool' | 'session' = defaultStrategy;
|
|
147
|
+
if (routeHandler) {
|
|
148
|
+
const routeStrategy = getDbStrategy(
|
|
149
|
+
routeHandler.controller,
|
|
150
|
+
routeHandler.method,
|
|
151
|
+
);
|
|
152
|
+
const hasTx = Boolean(
|
|
153
|
+
Reflect.getMetadata(
|
|
154
|
+
TRANSACTION_METADATA_KEY,
|
|
155
|
+
routeHandler.controller.prototype,
|
|
156
|
+
routeHandler.method,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
strategy = routeStrategy ?? (hasTx ? 'session' : defaultStrategy);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (strategy !== 'session') {
|
|
163
|
+
return await next();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const selected = normalized.find((item) => item.tenantId === defaultTenant) ?? normalized[0];
|
|
167
|
+
if (!selected) {
|
|
168
|
+
return await next();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (selected.config.type === 'sqlite') {
|
|
172
|
+
const sqlite = sqliteManager.getAdapter(selected.tenantId);
|
|
173
|
+
return await runWithSession(
|
|
174
|
+
{
|
|
175
|
+
tenantId: selected.tenantId,
|
|
176
|
+
sqlite,
|
|
177
|
+
},
|
|
178
|
+
async () => await next(),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const sql = sqlManager.getOrCreate(selected.tenantId, selected.config);
|
|
183
|
+
let reserved: any;
|
|
184
|
+
const session: DatabaseSession = {
|
|
185
|
+
tenantId: selected.tenantId,
|
|
186
|
+
lazyReserve: async () => {
|
|
187
|
+
if (!reserved) {
|
|
188
|
+
reserved = await sql.reserve();
|
|
189
|
+
session.reserved = reserved;
|
|
190
|
+
}
|
|
191
|
+
return reserved;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
return await runWithSession(session, async () => await next());
|
|
197
|
+
} finally {
|
|
198
|
+
if (reserved) {
|
|
199
|
+
await reserved.release().catch(() => undefined);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
28
205
|
/**
|
|
29
206
|
* 创建数据库模块
|
|
30
207
|
* @param options - 模块配置
|
|
@@ -33,8 +210,47 @@ export class DatabaseModule {
|
|
|
33
210
|
options: DatabaseModuleOptions,
|
|
34
211
|
): typeof DatabaseModule {
|
|
35
212
|
const providers: ModuleProvider[] = [];
|
|
213
|
+
const normalized = DatabaseModule.normalizeConfig(options);
|
|
214
|
+
const sqlManager = new BunSQLManager();
|
|
215
|
+
const sqliteManager = new SqliteManager();
|
|
216
|
+
|
|
217
|
+
for (const item of normalized) {
|
|
218
|
+
if (item.config.type === 'sqlite') {
|
|
219
|
+
sqliteManager.getOrCreate(item.tenantId, item.config);
|
|
220
|
+
} else {
|
|
221
|
+
sqlManager.getOrCreate(item.tenantId, item.config);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
sqlManager.setDefaultTenant(options.defaultTenant ?? normalized[0]?.tenantId ?? 'default');
|
|
225
|
+
sqliteManager.setDefaultTenant(options.defaultTenant ?? normalized[0]?.tenantId ?? 'default');
|
|
226
|
+
|
|
227
|
+
const legacyOptions: DatabaseModuleOptions = options.database
|
|
228
|
+
? options
|
|
229
|
+
: {
|
|
230
|
+
...options,
|
|
231
|
+
database:
|
|
232
|
+
normalized[0]?.config.type === 'sqlite'
|
|
233
|
+
? {
|
|
234
|
+
type: 'sqlite',
|
|
235
|
+
config: {
|
|
236
|
+
path: (normalized[0].config as SqliteV2Config).database,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
: {
|
|
240
|
+
type: (normalized[0]?.config.type ?? 'postgres') as 'postgres' | 'mysql',
|
|
241
|
+
config: {
|
|
242
|
+
host: options.host ?? 'localhost',
|
|
243
|
+
port: options.port ?? (normalized[0]?.config.type === 'mysql' ? 3306 : 5432),
|
|
244
|
+
database: options.databasePath ?? 'default',
|
|
245
|
+
user: options.username ?? 'root',
|
|
246
|
+
password: options.password ?? '',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
36
250
|
|
|
37
|
-
const service = new DatabaseService(
|
|
251
|
+
const service = new DatabaseService(legacyOptions);
|
|
252
|
+
const transactionManager = new TransactionManager(sqlManager);
|
|
253
|
+
initDbProxy(sqlManager, transactionManager);
|
|
38
254
|
|
|
39
255
|
providers.push(
|
|
40
256
|
{
|
|
@@ -45,7 +261,22 @@ export class DatabaseModule {
|
|
|
45
261
|
provide: DATABASE_OPTIONS_TOKEN,
|
|
46
262
|
useValue: options,
|
|
47
263
|
},
|
|
48
|
-
|
|
264
|
+
{
|
|
265
|
+
provide: DatabaseService,
|
|
266
|
+
useValue: service,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
provide: BUN_SQL_MANAGER_TOKEN,
|
|
270
|
+
useValue: sqlManager,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
provide: SQLITE_MANAGER_TOKEN,
|
|
274
|
+
useValue: sqliteManager,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
provide: DB_TOKEN,
|
|
278
|
+
useValue: db,
|
|
279
|
+
},
|
|
49
280
|
);
|
|
50
281
|
|
|
51
282
|
// 如果启用了 ORM,注册 ORM 服务
|
|
@@ -65,13 +296,15 @@ export class DatabaseModule {
|
|
|
65
296
|
}
|
|
66
297
|
|
|
67
298
|
// 注册事务管理器(总是注册,即使 ORM 未启用)
|
|
68
|
-
const transactionManager = new TransactionManager(service);
|
|
69
299
|
providers.push(
|
|
70
300
|
{
|
|
71
301
|
provide: TRANSACTION_SERVICE_TOKEN,
|
|
72
302
|
useValue: transactionManager,
|
|
73
303
|
},
|
|
74
|
-
|
|
304
|
+
{
|
|
305
|
+
provide: TransactionManager,
|
|
306
|
+
useValue: transactionManager,
|
|
307
|
+
},
|
|
75
308
|
);
|
|
76
309
|
|
|
77
310
|
// 数据库健康检查指示器可以通过 DatabaseModule.createHealthIndicator() 方法获取
|
|
@@ -82,7 +315,13 @@ export class DatabaseModule {
|
|
|
82
315
|
Reflect.getMetadata(MODULE_METADATA_KEY, DatabaseModule) || {};
|
|
83
316
|
|
|
84
317
|
// 创建数据库扩展,用于在应用启动时初始化连接
|
|
85
|
-
const databaseExtension = new DatabaseExtension();
|
|
318
|
+
const databaseExtension = new DatabaseExtension(sqlManager, sqliteManager);
|
|
319
|
+
const middleware = DatabaseModule.createDatabaseMiddleware(
|
|
320
|
+
options,
|
|
321
|
+
normalized,
|
|
322
|
+
sqlManager,
|
|
323
|
+
sqliteManager,
|
|
324
|
+
);
|
|
86
325
|
|
|
87
326
|
const metadata = {
|
|
88
327
|
...existingMetadata,
|
|
@@ -102,6 +341,10 @@ export class DatabaseModule {
|
|
|
102
341
|
...(existingMetadata.extensions || []),
|
|
103
342
|
databaseExtension,
|
|
104
343
|
],
|
|
344
|
+
middlewares: [
|
|
345
|
+
...(existingMetadata.middlewares || []),
|
|
346
|
+
middleware,
|
|
347
|
+
],
|
|
105
348
|
};
|
|
106
349
|
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, DatabaseModule);
|
|
107
350
|
|