@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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { Inject, Injectable } from '../../di/decorators';
|
|
2
|
+
import { getCurrentSession, runWithSession } from '../database-context';
|
|
3
|
+
import { BUN_SQL_MANAGER_TOKEN } from '../types';
|
|
4
|
+
import type { BunSQLManager } from '../sql-manager';
|
|
4
5
|
import {
|
|
5
|
-
Propagation,
|
|
6
6
|
IsolationLevel,
|
|
7
7
|
TransactionStatus,
|
|
8
8
|
type TransactionOptions,
|
|
@@ -15,262 +15,195 @@ import {
|
|
|
15
15
|
*/
|
|
16
16
|
@Injectable()
|
|
17
17
|
export class TransactionManager {
|
|
18
|
-
private readonly transactions = new Map<string, TransactionContext>();
|
|
19
|
-
private readonly connectionTransactions = new Map<unknown, string>(); // connection -> transactionId
|
|
20
|
-
|
|
21
18
|
public constructor(
|
|
22
|
-
@Inject(
|
|
23
|
-
private readonly
|
|
19
|
+
@Inject(BUN_SQL_MANAGER_TOKEN)
|
|
20
|
+
private readonly sqlManager: BunSQLManager,
|
|
24
21
|
) {}
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
|
-
*
|
|
24
|
+
* 在当前 session 中执行事务
|
|
28
25
|
*/
|
|
29
|
-
public async
|
|
26
|
+
public async runInTransaction<T>(
|
|
27
|
+
fn: () => Promise<T>,
|
|
30
28
|
options: TransactionOptions = {},
|
|
31
|
-
): Promise<
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
level: 0,
|
|
38
|
-
savepoints: [],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// 获取数据库连接
|
|
42
|
-
const connection = await this.databaseService.getConnection();
|
|
43
|
-
this.connectionTransactions.set(connection, transactionId);
|
|
44
|
-
this.transactions.set(transactionId, context);
|
|
45
|
-
|
|
46
|
-
// 开始事务(根据数据库类型)
|
|
47
|
-
await this.executeBegin(connection, options);
|
|
48
|
-
|
|
49
|
-
return context;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 提交事务
|
|
54
|
-
*/
|
|
55
|
-
public async commitTransaction(transactionId: string): Promise<void> {
|
|
56
|
-
const context = this.transactions.get(transactionId);
|
|
57
|
-
if (!context) {
|
|
58
|
-
throw new Error(`Transaction ${transactionId} not found`);
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
const session = getCurrentSession();
|
|
31
|
+
if (!session) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'[TransactionManager] No database session in current request context',
|
|
34
|
+
);
|
|
59
35
|
}
|
|
60
36
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
37
|
+
if (session.sqlite) {
|
|
38
|
+
return await this.runInSqliteTransaction(fn);
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
41
|
+
let reserved = session.reserved;
|
|
42
|
+
if (!reserved && session.lazyReserve) {
|
|
43
|
+
reserved = await session.lazyReserve();
|
|
44
|
+
}
|
|
45
|
+
if (!reserved) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'[TransactionManager] No reserved session in current context. Add @Session() or @DbStrategy("session").',
|
|
48
|
+
);
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
51
|
+
const transactionId = this.generateTransactionId();
|
|
52
|
+
session.transaction = {
|
|
53
|
+
id: transactionId,
|
|
54
|
+
status: TransactionStatus.ACTIVE,
|
|
55
|
+
level: 0,
|
|
56
|
+
savepoints: [],
|
|
57
|
+
};
|
|
77
58
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!context) {
|
|
84
|
-
throw new Error(`Transaction ${transactionId} not found`);
|
|
59
|
+
if (options.isolationLevel) {
|
|
60
|
+
const isolationLevel = this.getIsolationLevelSQL(options.isolationLevel);
|
|
61
|
+
if (isolationLevel) {
|
|
62
|
+
await reserved`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`;
|
|
63
|
+
}
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
66
|
+
try {
|
|
67
|
+
const result = await reserved.begin(fn);
|
|
68
|
+
if (session.transaction) {
|
|
69
|
+
session.transaction.status = TransactionStatus.COMMITTED;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (session.transaction) {
|
|
74
|
+
session.transaction.status = TransactionStatus.ROLLED_BACK;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
session.transaction = undefined;
|
|
91
79
|
}
|
|
92
|
-
|
|
93
|
-
// 回滚事务
|
|
94
|
-
await this.executeRollback(connection);
|
|
95
|
-
|
|
96
|
-
context.status = TransactionStatus.ROLLED_BACK;
|
|
97
|
-
this.cleanupTransaction(transactionId);
|
|
98
80
|
}
|
|
99
81
|
|
|
100
82
|
/**
|
|
101
|
-
*
|
|
83
|
+
* REQUIRES_NEW:使用独立连接开启新事务
|
|
102
84
|
*/
|
|
103
|
-
public async
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
85
|
+
public async runInNewTransaction<T>(
|
|
86
|
+
fn: () => Promise<T>,
|
|
87
|
+
options: TransactionOptions = {},
|
|
88
|
+
): Promise<T> {
|
|
89
|
+
const reserved = await this.sqlManager.getDefault().reserve();
|
|
90
|
+
try {
|
|
91
|
+
const tenantId = getCurrentSession()?.tenantId ?? 'default';
|
|
92
|
+
return await runWithSession(
|
|
93
|
+
{
|
|
94
|
+
tenantId,
|
|
95
|
+
reserved: reserved as any,
|
|
96
|
+
},
|
|
97
|
+
() => this.runInTransaction(fn, options),
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
await reserved.release();
|
|
117
101
|
}
|
|
118
|
-
|
|
119
|
-
return savepointName;
|
|
120
102
|
}
|
|
121
103
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
throw new Error(`Transaction ${transactionId} not found`);
|
|
104
|
+
public async runInNestedTransaction<T>(
|
|
105
|
+
fn: () => Promise<T>,
|
|
106
|
+
options: TransactionOptions = {},
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
const session = getCurrentSession();
|
|
109
|
+
if (!session?.transaction || !session.reserved) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'[TransactionManager] NESTED propagation requires an active transaction',
|
|
112
|
+
);
|
|
132
113
|
}
|
|
133
114
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
115
|
+
const savepointName = `sp_${session.transaction.level}_${Date.now()}`;
|
|
116
|
+
session.transaction.level += 1;
|
|
117
|
+
session.transaction.savepoints.push(savepointName);
|
|
138
118
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
119
|
+
await session.reserved`SAVEPOINT ${savepointName}`;
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (this.shouldRollback(error, options)) {
|
|
124
|
+
await session.reserved`ROLLBACK TO SAVEPOINT ${savepointName}`;
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
} finally {
|
|
128
|
+
session.transaction.level = Math.max(0, session.transaction.level - 1);
|
|
129
|
+
session.transaction.savepoints = session.transaction.savepoints.filter(
|
|
130
|
+
(item) => item !== savepointName,
|
|
131
|
+
);
|
|
144
132
|
}
|
|
145
133
|
}
|
|
146
134
|
|
|
147
|
-
/**
|
|
148
|
-
* 获取当前事务上下文
|
|
149
|
-
*/
|
|
150
135
|
public getCurrentTransaction(): TransactionContext | null {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (context.status === TransactionStatus.ACTIVE) {
|
|
155
|
-
return context;
|
|
156
|
-
}
|
|
136
|
+
const tx = getCurrentSession()?.transaction;
|
|
137
|
+
if (!tx) {
|
|
138
|
+
return null;
|
|
157
139
|
}
|
|
158
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
id: tx.id,
|
|
142
|
+
status: tx.status,
|
|
143
|
+
startTime: 0,
|
|
144
|
+
level: tx.level,
|
|
145
|
+
savepoints: tx.savepoints,
|
|
146
|
+
};
|
|
159
147
|
}
|
|
160
148
|
|
|
161
|
-
/**
|
|
162
|
-
* 检查是否有活动事务
|
|
163
|
-
*/
|
|
164
149
|
public hasActiveTransaction(): boolean {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 生成事务 ID
|
|
170
|
-
*/
|
|
171
|
-
private generateTransactionId(): string {
|
|
172
|
-
return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* 查找连接对应的事务 ID
|
|
177
|
-
*/
|
|
178
|
-
private findConnectionByTransactionId(transactionId: string): unknown {
|
|
179
|
-
for (const [connection, txId] of this.connectionTransactions.entries()) {
|
|
180
|
-
if (txId === transactionId) {
|
|
181
|
-
return connection;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
150
|
+
const tx = this.getCurrentTransaction();
|
|
151
|
+
return tx?.status === TransactionStatus.ACTIVE;
|
|
185
152
|
}
|
|
186
153
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for (const [connection, txId] of this.connectionTransactions.entries()) {
|
|
194
|
-
if (txId === transactionId) {
|
|
195
|
-
this.connectionTransactions.delete(connection);
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
154
|
+
public async runInSqliteTransaction<T>(fn: () => Promise<T>): Promise<T> {
|
|
155
|
+
const session = getCurrentSession();
|
|
156
|
+
if (!session?.sqlite) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'[TransactionManager] No sqlite adapter found in current request context',
|
|
159
|
+
);
|
|
198
160
|
}
|
|
199
|
-
}
|
|
200
161
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
} else if (dbType === 'postgres' || dbType === 'mysql') {
|
|
211
|
-
// PostgreSQL 和 MySQL 使用 Bun.SQL,需要设置隔离级别
|
|
212
|
-
let sql = 'START TRANSACTION';
|
|
213
|
-
if (options.isolationLevel) {
|
|
214
|
-
const isolation = this.getIsolationLevelSQL(options.isolationLevel);
|
|
215
|
-
sql += ` ${isolation}`;
|
|
216
|
-
}
|
|
217
|
-
if (options.readOnly) {
|
|
218
|
-
sql += ' READ ONLY';
|
|
219
|
-
}
|
|
220
|
-
await this.databaseService.query(sql);
|
|
162
|
+
using _lock = await session.sqlite.semaphore.acquire();
|
|
163
|
+
await session.sqlite.execute('BEGIN TRANSACTION');
|
|
164
|
+
try {
|
|
165
|
+
const result = await fn();
|
|
166
|
+
await session.sqlite.execute('COMMIT');
|
|
167
|
+
return result;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await session.sqlite.execute('ROLLBACK');
|
|
170
|
+
throw error;
|
|
221
171
|
}
|
|
222
172
|
}
|
|
223
173
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
*/
|
|
227
|
-
private async executeCommit(connection: unknown): Promise<void> {
|
|
228
|
-
await this.databaseService.query('COMMIT');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* 执行 ROLLBACK 语句
|
|
233
|
-
*/
|
|
234
|
-
private async executeRollback(connection: unknown): Promise<void> {
|
|
235
|
-
await this.databaseService.query('ROLLBACK');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 执行 SAVEPOINT 语句
|
|
240
|
-
*/
|
|
241
|
-
private async executeSavepoint(connection: unknown, savepointName: string): Promise<void> {
|
|
242
|
-
await this.databaseService.query(`SAVEPOINT ${savepointName}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* 执行 ROLLBACK TO SAVEPOINT 语句
|
|
247
|
-
*/
|
|
248
|
-
private async executeRollbackToSavepoint(
|
|
249
|
-
connection: unknown,
|
|
250
|
-
savepointName: string,
|
|
251
|
-
): Promise<void> {
|
|
252
|
-
await this.databaseService.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
174
|
+
private generateTransactionId(): string {
|
|
175
|
+
return `tx_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
253
176
|
}
|
|
254
177
|
|
|
255
|
-
/**
|
|
256
|
-
* 获取隔离级别的 SQL
|
|
257
|
-
*/
|
|
258
178
|
private getIsolationLevelSQL(level: IsolationLevel): string {
|
|
259
|
-
const dbType = this.databaseService['config'].database.type;
|
|
260
179
|
const levelMap: Record<IsolationLevel, string> = {
|
|
261
180
|
[IsolationLevel.READ_UNCOMMITTED]: 'READ UNCOMMITTED',
|
|
262
181
|
[IsolationLevel.READ_COMMITTED]: 'READ COMMITTED',
|
|
263
182
|
[IsolationLevel.REPEATABLE_READ]: 'REPEATABLE READ',
|
|
264
183
|
[IsolationLevel.SERIALIZABLE]: 'SERIALIZABLE',
|
|
265
184
|
};
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
185
|
+
return levelMap[level];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private shouldRollback(
|
|
189
|
+
error: unknown,
|
|
190
|
+
options: Pick<TransactionOptions, 'rollbackFor' | 'noRollbackFor'>,
|
|
191
|
+
): boolean {
|
|
192
|
+
if (options.noRollbackFor && options.noRollbackFor.length > 0) {
|
|
193
|
+
for (const ErrorClass of options.noRollbackFor) {
|
|
194
|
+
if (error instanceof ErrorClass) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
271
198
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
199
|
+
if (options.rollbackFor && options.rollbackFor.length > 0) {
|
|
200
|
+
for (const ErrorClass of options.rollbackFor) {
|
|
201
|
+
if (error instanceof ErrorClass) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
275
208
|
}
|
|
276
209
|
}
|
package/src/database/service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Injectable } from '../di/decorators';
|
|
2
2
|
|
|
3
3
|
import { DatabaseConnectionManager } from './connection-manager';
|
|
4
|
+
import { getCurrentSession } from './database-context';
|
|
4
5
|
import type {
|
|
5
6
|
ConnectionInfo,
|
|
6
7
|
DatabaseConfig,
|
|
@@ -18,8 +19,27 @@ export class DatabaseService {
|
|
|
18
19
|
|
|
19
20
|
public constructor(options: DatabaseModuleOptions) {
|
|
20
21
|
this.options = options;
|
|
22
|
+
const databaseConfig: DatabaseConfig =
|
|
23
|
+
options.database ??
|
|
24
|
+
(options.type === 'sqlite'
|
|
25
|
+
? {
|
|
26
|
+
type: 'sqlite',
|
|
27
|
+
config: {
|
|
28
|
+
path: options.databasePath ?? ':memory:',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
type: (options.type ?? 'postgres') as 'postgres' | 'mysql',
|
|
33
|
+
config: {
|
|
34
|
+
host: options.host ?? 'localhost',
|
|
35
|
+
port: options.port ?? 5432,
|
|
36
|
+
database: options.databasePath ?? 'default',
|
|
37
|
+
user: options.username ?? 'root',
|
|
38
|
+
password: options.password ?? '',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
21
41
|
this.connectionManager = new DatabaseConnectionManager(
|
|
22
|
-
|
|
42
|
+
databaseConfig,
|
|
23
43
|
options.pool,
|
|
24
44
|
);
|
|
25
45
|
}
|
|
@@ -96,7 +116,13 @@ export class DatabaseService {
|
|
|
96
116
|
* SQLite 返回同步结果,PostgreSQL/MySQL 返回异步结果
|
|
97
117
|
*/
|
|
98
118
|
public query<T = unknown>(sql: string, params?: unknown[]): T[] | Promise<T[]> {
|
|
99
|
-
const
|
|
119
|
+
const session = getCurrentSession();
|
|
120
|
+
if (session?.sqlite) {
|
|
121
|
+
return session.sqlite.query<T>(sql, (params ?? []) as any);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const perRequestConnection = session?.reserved;
|
|
125
|
+
const connection = perRequestConnection ?? this.getConnection();
|
|
100
126
|
if (!connection) {
|
|
101
127
|
throw new Error('Database connection is not established');
|
|
102
128
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SQL } from 'bun';
|
|
2
|
+
|
|
3
|
+
import type { BunSQLConfig } from './types';
|
|
4
|
+
|
|
5
|
+
export class BunSQLManager {
|
|
6
|
+
private readonly instances = new Map<string, SQL>();
|
|
7
|
+
private defaultTenantId = 'default';
|
|
8
|
+
|
|
9
|
+
public getOrCreate(tenantId: string, config: BunSQLConfig): SQL {
|
|
10
|
+
const existing = this.instances.get(tenantId);
|
|
11
|
+
if (existing) {
|
|
12
|
+
return existing;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pool = config.pool ?? {};
|
|
16
|
+
const sql = new SQL(config.url, {
|
|
17
|
+
max: pool.max ?? 10,
|
|
18
|
+
idleTimeout: pool.idleTimeout ?? 30,
|
|
19
|
+
maxLifetime: pool.maxLifetime ?? 0,
|
|
20
|
+
connectionTimeout: pool.connectionTimeout ?? 30000,
|
|
21
|
+
});
|
|
22
|
+
this.instances.set(tenantId, sql);
|
|
23
|
+
return sql;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public hasTenant(tenantId: string): boolean {
|
|
27
|
+
return this.instances.has(tenantId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public get(tenantId: string): SQL | undefined {
|
|
31
|
+
return this.instances.get(tenantId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public setDefaultTenant(tenantId: string): void {
|
|
35
|
+
this.defaultTenantId = tenantId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getDefault(): SQL {
|
|
39
|
+
const sql = this.instances.get(this.defaultTenantId);
|
|
40
|
+
if (!sql) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[BunSQLManager] default tenant '${this.defaultTenantId}' not initialized`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return sql;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async destroy(tenantId: string, timeout = 10): Promise<void> {
|
|
49
|
+
const sql = this.instances.get(tenantId);
|
|
50
|
+
if (!sql) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await sql.close({ timeout });
|
|
54
|
+
this.instances.delete(tenantId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async destroyAll(timeout = 10): Promise<void> {
|
|
58
|
+
const tenantIds = Array.from(this.instances.keys());
|
|
59
|
+
await Promise.all(tenantIds.map((tenantId) => this.destroy(tenantId, timeout)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Database, type SQLQueryBindings } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
import type { SqliteV2Config } from './types';
|
|
4
|
+
|
|
5
|
+
export interface DisposableLock {
|
|
6
|
+
[Symbol.dispose](): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Semaphore {
|
|
10
|
+
private active = 0;
|
|
11
|
+
private readonly queue: Array<() => void> = [];
|
|
12
|
+
|
|
13
|
+
public constructor(private readonly max: number) {}
|
|
14
|
+
|
|
15
|
+
public async acquire(): Promise<DisposableLock> {
|
|
16
|
+
if (this.active < this.max) {
|
|
17
|
+
this.active += 1;
|
|
18
|
+
return this.createDisposable();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await new Promise<void>((resolve) => this.queue.push(resolve));
|
|
22
|
+
this.active += 1;
|
|
23
|
+
return this.createDisposable();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private createDisposable(): DisposableLock {
|
|
27
|
+
let released = false;
|
|
28
|
+
return {
|
|
29
|
+
[Symbol.dispose]: () => {
|
|
30
|
+
if (released) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
released = true;
|
|
34
|
+
this.active = Math.max(0, this.active - 1);
|
|
35
|
+
const next = this.queue.shift();
|
|
36
|
+
next?.();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SqliteAdapter {
|
|
43
|
+
private readonly db: Database;
|
|
44
|
+
public readonly semaphore: Semaphore;
|
|
45
|
+
|
|
46
|
+
public constructor(config: SqliteV2Config) {
|
|
47
|
+
this.db = new Database(config.database);
|
|
48
|
+
if (config.wal !== false) {
|
|
49
|
+
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
50
|
+
}
|
|
51
|
+
this.semaphore = new Semaphore(config.maxWriteConcurrency ?? 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public query<T = unknown>(sql: string, params: SQLQueryBindings[] = []): T[] {
|
|
55
|
+
const stmt = this.db.query(sql);
|
|
56
|
+
return stmt.all(...params) as T[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public async execute(sql: string, params: SQLQueryBindings[] = []): Promise<void> {
|
|
60
|
+
const stmt = this.db.query(sql);
|
|
61
|
+
stmt.run(...params);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public close(): void {
|
|
65
|
+
this.db.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SqliteManager {
|
|
70
|
+
private readonly instances = new Map<string, SqliteAdapter>();
|
|
71
|
+
private defaultTenantId = 'default';
|
|
72
|
+
|
|
73
|
+
public getOrCreate(tenantId: string, config: SqliteV2Config): SqliteAdapter {
|
|
74
|
+
const existing = this.instances.get(tenantId);
|
|
75
|
+
if (existing) {
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
const adapter = new SqliteAdapter(config);
|
|
79
|
+
this.instances.set(tenantId, adapter);
|
|
80
|
+
return adapter;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public setDefaultTenant(tenantId: string): void {
|
|
84
|
+
this.defaultTenantId = tenantId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public getDefault(): SqliteAdapter {
|
|
88
|
+
const adapter = this.instances.get(this.defaultTenantId);
|
|
89
|
+
if (!adapter) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`[SqliteManager] default tenant '${this.defaultTenantId}' not initialized`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return adapter;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public getAdapter(tenantId: string): SqliteAdapter {
|
|
98
|
+
const adapter = this.instances.get(tenantId);
|
|
99
|
+
if (!adapter) {
|
|
100
|
+
throw new Error(`[SqliteManager] tenant '${tenantId}' not initialized`);
|
|
101
|
+
}
|
|
102
|
+
return adapter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public destroy(tenantId: string): void {
|
|
106
|
+
const adapter = this.instances.get(tenantId);
|
|
107
|
+
if (!adapter) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
adapter.close();
|
|
111
|
+
this.instances.delete(tenantId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public destroyAll(): void {
|
|
115
|
+
for (const [tenantId, adapter] of this.instances.entries()) {
|
|
116
|
+
adapter.close();
|
|
117
|
+
this.instances.delete(tenantId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
import type { Constructor } from '@/core/types';
|
|
4
|
+
|
|
5
|
+
export const DB_STRATEGY_KEY = Symbol('@dangao/bun-server:database:strategy');
|
|
6
|
+
|
|
7
|
+
export type DbStrategyType = 'pool' | 'session';
|
|
8
|
+
|
|
9
|
+
export function DbStrategy(
|
|
10
|
+
strategy: DbStrategyType,
|
|
11
|
+
): MethodDecorator & ClassDecorator {
|
|
12
|
+
return (target: object, propertyKey?: string | symbol) => {
|
|
13
|
+
if (propertyKey !== undefined) {
|
|
14
|
+
Reflect.defineMetadata(DB_STRATEGY_KEY, strategy, target, propertyKey);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
Reflect.defineMetadata(DB_STRATEGY_KEY, strategy, target);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Session(): MethodDecorator & ClassDecorator {
|
|
22
|
+
return DbStrategy('session');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getDbStrategy(
|
|
26
|
+
controllerClass: Constructor<unknown>,
|
|
27
|
+
methodName: string,
|
|
28
|
+
): DbStrategyType | undefined {
|
|
29
|
+
const methodStrategy = Reflect.getMetadata(
|
|
30
|
+
DB_STRATEGY_KEY,
|
|
31
|
+
controllerClass.prototype,
|
|
32
|
+
methodName,
|
|
33
|
+
) as DbStrategyType | undefined;
|
|
34
|
+
if (methodStrategy) {
|
|
35
|
+
return methodStrategy;
|
|
36
|
+
}
|
|
37
|
+
return Reflect.getMetadata(
|
|
38
|
+
DB_STRATEGY_KEY,
|
|
39
|
+
controllerClass,
|
|
40
|
+
) as DbStrategyType | undefined;
|
|
41
|
+
}
|
|
42
|
+
|