@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.
- package/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
DatabaseModule,
|
|
4
|
+
DatabaseService,
|
|
5
|
+
DATABASE_SERVICE_TOKEN,
|
|
6
|
+
} from '../../src/database';
|
|
7
|
+
import { Container } from '../../src/di/container';
|
|
8
|
+
import { ModuleRegistry } from '../../src/di/module-registry';
|
|
9
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
10
|
+
import 'reflect-metadata';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* PostgreSQL/MySQL 集成测试
|
|
14
|
+
*
|
|
15
|
+
* 这些测试需要实际的数据库连接,通过环境变量配置:
|
|
16
|
+
* - POSTGRES_URL: PostgreSQL 连接字符串(可选)
|
|
17
|
+
* - MYSQL_URL: MySQL 连接字符串(可选)
|
|
18
|
+
*
|
|
19
|
+
* 如果没有配置相应的环境变量,相关测试会被跳过。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function parseDatabaseUrl(url: string): {
|
|
23
|
+
type: 'postgres' | 'mysql';
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
26
|
+
database: string;
|
|
27
|
+
user: string;
|
|
28
|
+
password: string;
|
|
29
|
+
} {
|
|
30
|
+
const urlObj = new URL(url);
|
|
31
|
+
return {
|
|
32
|
+
type: urlObj.protocol === 'postgresql:' || urlObj.protocol === 'postgres:' ? 'postgres' : 'mysql',
|
|
33
|
+
host: urlObj.hostname,
|
|
34
|
+
port: Number(urlObj.port) || (urlObj.protocol.includes('postgres') ? 5432 : 3306),
|
|
35
|
+
database: urlObj.pathname.slice(1),
|
|
36
|
+
user: urlObj.username,
|
|
37
|
+
password: urlObj.password,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('PostgreSQL Integration Tests', () => {
|
|
42
|
+
const postgresUrl = process.env.POSTGRES_URL;
|
|
43
|
+
const hasPostgres = !!postgresUrl;
|
|
44
|
+
|
|
45
|
+
let container: Container;
|
|
46
|
+
let moduleRegistry: ModuleRegistry;
|
|
47
|
+
let dbService: DatabaseService | null = null;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
if (!hasPostgres) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, DatabaseModule);
|
|
55
|
+
container = new Container();
|
|
56
|
+
moduleRegistry = ModuleRegistry.getInstance();
|
|
57
|
+
moduleRegistry.clear();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
if (dbService) {
|
|
62
|
+
try {
|
|
63
|
+
await dbService.closePool();
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore errors during cleanup
|
|
66
|
+
}
|
|
67
|
+
dbService = null;
|
|
68
|
+
}
|
|
69
|
+
if (moduleRegistry) {
|
|
70
|
+
moduleRegistry.clear();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test.skipIf(!hasPostgres)('should connect to PostgreSQL database', async () => {
|
|
75
|
+
|
|
76
|
+
const config = parseDatabaseUrl(postgresUrl!);
|
|
77
|
+
|
|
78
|
+
DatabaseModule.forRoot({
|
|
79
|
+
database: {
|
|
80
|
+
type: 'postgres',
|
|
81
|
+
config: {
|
|
82
|
+
host: config.host,
|
|
83
|
+
port: config.port,
|
|
84
|
+
database: config.database,
|
|
85
|
+
user: config.user,
|
|
86
|
+
password: config.password,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
92
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
93
|
+
|
|
94
|
+
await dbService.initialize();
|
|
95
|
+
|
|
96
|
+
const info = dbService.getConnectionInfo();
|
|
97
|
+
expect(info.status).toBe('connected');
|
|
98
|
+
expect(info.type).toBe('postgres');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test.skipIf(!hasPostgres)('should execute PostgreSQL queries', async () => {
|
|
102
|
+
|
|
103
|
+
const config = parseDatabaseUrl(postgresUrl!);
|
|
104
|
+
|
|
105
|
+
DatabaseModule.forRoot({
|
|
106
|
+
database: {
|
|
107
|
+
type: 'postgres',
|
|
108
|
+
config: {
|
|
109
|
+
host: config.host,
|
|
110
|
+
port: config.port,
|
|
111
|
+
database: config.database,
|
|
112
|
+
user: config.user,
|
|
113
|
+
password: config.password,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
119
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
120
|
+
|
|
121
|
+
await dbService.initialize();
|
|
122
|
+
|
|
123
|
+
// 测试查询
|
|
124
|
+
const result = await dbService.query('SELECT version() as version');
|
|
125
|
+
expect(Array.isArray(result)).toBe(true);
|
|
126
|
+
expect(result.length).toBeGreaterThan(0);
|
|
127
|
+
expect(result[0]).toHaveProperty('version');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test.skipIf(!hasPostgres)('should handle PostgreSQL transactions', async () => {
|
|
131
|
+
|
|
132
|
+
const config = parseDatabaseUrl(postgresUrl!);
|
|
133
|
+
|
|
134
|
+
DatabaseModule.forRoot({
|
|
135
|
+
database: {
|
|
136
|
+
type: 'postgres',
|
|
137
|
+
config: {
|
|
138
|
+
host: config.host,
|
|
139
|
+
port: config.port,
|
|
140
|
+
database: config.database,
|
|
141
|
+
user: config.user,
|
|
142
|
+
password: config.password,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
148
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
149
|
+
|
|
150
|
+
await dbService.initialize();
|
|
151
|
+
|
|
152
|
+
// 创建测试表
|
|
153
|
+
await dbService.query(`
|
|
154
|
+
CREATE TABLE IF NOT EXISTS test_transaction_pg (
|
|
155
|
+
id SERIAL PRIMARY KEY,
|
|
156
|
+
value TEXT NOT NULL
|
|
157
|
+
)
|
|
158
|
+
`);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// 开始事务
|
|
162
|
+
await dbService.query('BEGIN');
|
|
163
|
+
|
|
164
|
+
// 插入数据
|
|
165
|
+
await dbService.query(
|
|
166
|
+
"INSERT INTO test_transaction_pg (value) VALUES ('test')",
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// 回滚事务
|
|
170
|
+
await dbService.query('ROLLBACK');
|
|
171
|
+
|
|
172
|
+
// 验证数据未插入
|
|
173
|
+
const result = await dbService.query<{ count: number | string }>(
|
|
174
|
+
"SELECT COUNT(*) as count FROM test_transaction_pg WHERE value = 'test'",
|
|
175
|
+
);
|
|
176
|
+
const countValue = Array.isArray(result) && result.length > 0
|
|
177
|
+
? result[0]?.count
|
|
178
|
+
: 0;
|
|
179
|
+
const count = typeof countValue === 'string' ? parseInt(countValue, 10) : (countValue || 0);
|
|
180
|
+
expect(count).toBe(0);
|
|
181
|
+
} finally {
|
|
182
|
+
// 清理测试表
|
|
183
|
+
try {
|
|
184
|
+
await dbService.query('DROP TABLE IF EXISTS test_transaction_pg');
|
|
185
|
+
} catch {
|
|
186
|
+
// Ignore cleanup errors
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test.skipIf(!hasPostgres)('should check PostgreSQL health status', async () => {
|
|
192
|
+
|
|
193
|
+
const config = parseDatabaseUrl(postgresUrl!);
|
|
194
|
+
|
|
195
|
+
DatabaseModule.forRoot({
|
|
196
|
+
database: {
|
|
197
|
+
type: 'postgres',
|
|
198
|
+
config: {
|
|
199
|
+
host: config.host,
|
|
200
|
+
port: config.port,
|
|
201
|
+
database: config.database,
|
|
202
|
+
user: config.user,
|
|
203
|
+
password: config.password,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
enableHealthCheck: true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
210
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
211
|
+
|
|
212
|
+
await dbService.initialize();
|
|
213
|
+
|
|
214
|
+
const isHealthy = await dbService.healthCheck();
|
|
215
|
+
expect(isHealthy).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('MySQL Integration Tests', () => {
|
|
220
|
+
const mysqlUrl = process.env.MYSQL_URL;
|
|
221
|
+
const hasMysql = !!mysqlUrl;
|
|
222
|
+
|
|
223
|
+
let container: Container;
|
|
224
|
+
let moduleRegistry: ModuleRegistry;
|
|
225
|
+
let dbService: DatabaseService | null = null;
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
if (!hasMysql) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, DatabaseModule);
|
|
233
|
+
container = new Container();
|
|
234
|
+
moduleRegistry = ModuleRegistry.getInstance();
|
|
235
|
+
moduleRegistry.clear();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterEach(async () => {
|
|
239
|
+
if (dbService) {
|
|
240
|
+
try {
|
|
241
|
+
await dbService.closePool();
|
|
242
|
+
} catch {
|
|
243
|
+
// Ignore errors during cleanup
|
|
244
|
+
}
|
|
245
|
+
dbService = null;
|
|
246
|
+
}
|
|
247
|
+
if (moduleRegistry) {
|
|
248
|
+
moduleRegistry.clear();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test.skipIf(!hasMysql)('should connect to MySQL database', async () => {
|
|
253
|
+
|
|
254
|
+
const config = parseDatabaseUrl(mysqlUrl!);
|
|
255
|
+
|
|
256
|
+
DatabaseModule.forRoot({
|
|
257
|
+
database: {
|
|
258
|
+
type: 'mysql',
|
|
259
|
+
config: {
|
|
260
|
+
host: config.host,
|
|
261
|
+
port: config.port,
|
|
262
|
+
database: config.database,
|
|
263
|
+
user: config.user,
|
|
264
|
+
password: config.password,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
270
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
271
|
+
|
|
272
|
+
await dbService.initialize();
|
|
273
|
+
|
|
274
|
+
const info = dbService.getConnectionInfo();
|
|
275
|
+
expect(info.status).toBe('connected');
|
|
276
|
+
expect(info.type).toBe('mysql');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test.skipIf(!hasMysql)('should execute MySQL queries', async () => {
|
|
280
|
+
|
|
281
|
+
const config = parseDatabaseUrl(mysqlUrl!);
|
|
282
|
+
|
|
283
|
+
DatabaseModule.forRoot({
|
|
284
|
+
database: {
|
|
285
|
+
type: 'mysql',
|
|
286
|
+
config: {
|
|
287
|
+
host: config.host,
|
|
288
|
+
port: config.port,
|
|
289
|
+
database: config.database,
|
|
290
|
+
user: config.user,
|
|
291
|
+
password: config.password,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
297
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
298
|
+
|
|
299
|
+
await dbService.initialize();
|
|
300
|
+
|
|
301
|
+
// 测试查询
|
|
302
|
+
const result = await dbService.query('SELECT VERSION() as version');
|
|
303
|
+
expect(Array.isArray(result)).toBe(true);
|
|
304
|
+
expect(result.length).toBeGreaterThan(0);
|
|
305
|
+
expect(result[0]).toHaveProperty('version');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test.skipIf(!hasMysql)('should handle MySQL transactions', async () => {
|
|
309
|
+
|
|
310
|
+
const config = parseDatabaseUrl(mysqlUrl!);
|
|
311
|
+
|
|
312
|
+
DatabaseModule.forRoot({
|
|
313
|
+
database: {
|
|
314
|
+
type: 'mysql',
|
|
315
|
+
config: {
|
|
316
|
+
host: config.host,
|
|
317
|
+
port: config.port,
|
|
318
|
+
database: config.database,
|
|
319
|
+
user: config.user,
|
|
320
|
+
password: config.password,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
326
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
327
|
+
|
|
328
|
+
await dbService.initialize();
|
|
329
|
+
|
|
330
|
+
// 创建测试表
|
|
331
|
+
await dbService.query(`
|
|
332
|
+
CREATE TABLE IF NOT EXISTS test_transaction_mysql (
|
|
333
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
334
|
+
value TEXT NOT NULL
|
|
335
|
+
)
|
|
336
|
+
`);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// 开始事务
|
|
340
|
+
await dbService.query('START TRANSACTION');
|
|
341
|
+
|
|
342
|
+
// 插入数据
|
|
343
|
+
await dbService.query(
|
|
344
|
+
"INSERT INTO test_transaction_mysql (value) VALUES ('test')",
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// 回滚事务
|
|
348
|
+
await dbService.query('ROLLBACK');
|
|
349
|
+
|
|
350
|
+
// 验证数据未插入
|
|
351
|
+
const result = await dbService.query<{ count: number | string }>(
|
|
352
|
+
"SELECT COUNT(*) as count FROM test_transaction_mysql WHERE value = 'test'",
|
|
353
|
+
);
|
|
354
|
+
const countValue = Array.isArray(result) && result.length > 0
|
|
355
|
+
? result[0]?.count
|
|
356
|
+
: 0;
|
|
357
|
+
const count = typeof countValue === 'string' ? parseInt(countValue, 10) : (countValue || 0);
|
|
358
|
+
expect(count).toBe(0);
|
|
359
|
+
} finally {
|
|
360
|
+
// 清理测试表
|
|
361
|
+
try {
|
|
362
|
+
await dbService.query('DROP TABLE IF EXISTS test_transaction_mysql');
|
|
363
|
+
} catch {
|
|
364
|
+
// Ignore cleanup errors
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test.skipIf(!hasMysql)('should check MySQL health status', async () => {
|
|
370
|
+
|
|
371
|
+
const config = parseDatabaseUrl(mysqlUrl!);
|
|
372
|
+
|
|
373
|
+
DatabaseModule.forRoot({
|
|
374
|
+
database: {
|
|
375
|
+
type: 'mysql',
|
|
376
|
+
config: {
|
|
377
|
+
host: config.host,
|
|
378
|
+
port: config.port,
|
|
379
|
+
database: config.database,
|
|
380
|
+
user: config.user,
|
|
381
|
+
password: config.password,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
enableHealthCheck: true,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
moduleRegistry.register(DatabaseModule, container);
|
|
388
|
+
dbService = container.resolve<DatabaseService>(DATABASE_SERVICE_TOKEN);
|
|
389
|
+
|
|
390
|
+
await dbService.initialize();
|
|
391
|
+
|
|
392
|
+
const isHealthy = await dbService.healthCheck();
|
|
393
|
+
expect(isHealthy).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Transactional,
|
|
4
|
+
TransactionManager,
|
|
5
|
+
Propagation,
|
|
6
|
+
IsolationLevel,
|
|
7
|
+
TransactionStatus,
|
|
8
|
+
TRANSACTION_SERVICE_TOKEN,
|
|
9
|
+
} from '../../src/database/orm';
|
|
10
|
+
import { DatabaseService, DATABASE_SERVICE_TOKEN } from '../../src/database';
|
|
11
|
+
import { Container } from '../../src/di/container';
|
|
12
|
+
import 'reflect-metadata';
|
|
13
|
+
|
|
14
|
+
describe('TransactionManager', () => {
|
|
15
|
+
let container: Container;
|
|
16
|
+
let databaseService: DatabaseService;
|
|
17
|
+
let transactionManager: TransactionManager;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
container = new Container();
|
|
21
|
+
databaseService = new DatabaseService({
|
|
22
|
+
database: {
|
|
23
|
+
type: 'sqlite',
|
|
24
|
+
config: {
|
|
25
|
+
path: ':memory:',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
await databaseService.initialize();
|
|
30
|
+
|
|
31
|
+
transactionManager = new TransactionManager(databaseService);
|
|
32
|
+
container.registerInstance(DATABASE_SERVICE_TOKEN, databaseService);
|
|
33
|
+
container.registerInstance(TRANSACTION_SERVICE_TOKEN, transactionManager);
|
|
34
|
+
|
|
35
|
+
// 创建测试表
|
|
36
|
+
databaseService.query(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS test_accounts (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
balance REAL NOT NULL DEFAULT 0
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await databaseService.closePool();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should begin and commit transaction', async () => {
|
|
50
|
+
const context = await transactionManager.beginTransaction();
|
|
51
|
+
expect(context.status).toBe(TransactionStatus.ACTIVE);
|
|
52
|
+
expect(context.id).toBeDefined();
|
|
53
|
+
|
|
54
|
+
// 插入数据
|
|
55
|
+
await databaseService.query(
|
|
56
|
+
'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
|
|
57
|
+
['Alice', 100],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await transactionManager.commitTransaction(context.id);
|
|
61
|
+
|
|
62
|
+
// 验证数据已提交
|
|
63
|
+
const result = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Alice']);
|
|
64
|
+
expect(Array.isArray(result) ? result.length : 0).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should rollback transaction', async () => {
|
|
68
|
+
const context = await transactionManager.beginTransaction();
|
|
69
|
+
|
|
70
|
+
// 插入数据
|
|
71
|
+
await databaseService.query(
|
|
72
|
+
'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
|
|
73
|
+
['Bob', 200],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await transactionManager.rollbackTransaction(context.id);
|
|
77
|
+
|
|
78
|
+
// 验证数据已回滚
|
|
79
|
+
const result = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Bob']);
|
|
80
|
+
expect(Array.isArray(result) ? result.length : 0).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should create and rollback to savepoint', async () => {
|
|
84
|
+
const context = await transactionManager.beginTransaction();
|
|
85
|
+
|
|
86
|
+
// 插入第一条数据
|
|
87
|
+
await databaseService.query(
|
|
88
|
+
'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
|
|
89
|
+
['Charlie', 300],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// 创建保存点
|
|
93
|
+
const savepoint = await transactionManager.createSavepoint(context.id);
|
|
94
|
+
expect(savepoint).toBeDefined();
|
|
95
|
+
expect(context.level).toBe(1);
|
|
96
|
+
|
|
97
|
+
// 插入第二条数据
|
|
98
|
+
await databaseService.query(
|
|
99
|
+
'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
|
|
100
|
+
['David', 400],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// 回滚到保存点
|
|
104
|
+
await transactionManager.rollbackToSavepoint(context.id, savepoint);
|
|
105
|
+
|
|
106
|
+
// 提交事务
|
|
107
|
+
await transactionManager.commitTransaction(context.id);
|
|
108
|
+
|
|
109
|
+
// 验证:Charlie 应该存在,David 应该不存在(已回滚)
|
|
110
|
+
const charlie = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Charlie']);
|
|
111
|
+
const david = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['David']);
|
|
112
|
+
|
|
113
|
+
expect(Array.isArray(charlie) ? charlie.length : 0).toBeGreaterThan(0);
|
|
114
|
+
expect(Array.isArray(david) ? david.length : 0).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should get current transaction', async () => {
|
|
118
|
+
expect(transactionManager.hasActiveTransaction()).toBe(false);
|
|
119
|
+
|
|
120
|
+
const context = await transactionManager.beginTransaction();
|
|
121
|
+
expect(transactionManager.hasActiveTransaction()).toBe(true);
|
|
122
|
+
|
|
123
|
+
const current = transactionManager.getCurrentTransaction();
|
|
124
|
+
expect(current).not.toBeNull();
|
|
125
|
+
expect(current?.id).toBe(context.id);
|
|
126
|
+
|
|
127
|
+
await transactionManager.commitTransaction(context.id);
|
|
128
|
+
expect(transactionManager.hasActiveTransaction()).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('@Transactional Decorator', () => {
|
|
133
|
+
let container: Container;
|
|
134
|
+
let databaseService: DatabaseService;
|
|
135
|
+
let transactionManager: TransactionManager;
|
|
136
|
+
|
|
137
|
+
beforeEach(async () => {
|
|
138
|
+
container = new Container();
|
|
139
|
+
databaseService = new DatabaseService({
|
|
140
|
+
database: {
|
|
141
|
+
type: 'sqlite',
|
|
142
|
+
config: {
|
|
143
|
+
path: ':memory:',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
await databaseService.initialize();
|
|
148
|
+
|
|
149
|
+
transactionManager = new TransactionManager(databaseService);
|
|
150
|
+
container.registerInstance(DATABASE_SERVICE_TOKEN, databaseService);
|
|
151
|
+
container.registerInstance(TRANSACTION_SERVICE_TOKEN, transactionManager);
|
|
152
|
+
|
|
153
|
+
// 创建测试表
|
|
154
|
+
databaseService.query(`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS test_users (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
name TEXT NOT NULL,
|
|
158
|
+
email TEXT NOT NULL UNIQUE
|
|
159
|
+
)
|
|
160
|
+
`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
afterEach(async () => {
|
|
164
|
+
await databaseService.closePool();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should apply transaction to method', async () => {
|
|
168
|
+
class UserService {
|
|
169
|
+
@Transactional()
|
|
170
|
+
public async createUser(name: string, email: string): Promise<void> {
|
|
171
|
+
await databaseService.query(
|
|
172
|
+
'INSERT INTO test_users (name, email) VALUES (?, ?)',
|
|
173
|
+
[name, email],
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Transactional({ propagation: Propagation.REQUIRES_NEW })
|
|
178
|
+
public async createUserInNewTransaction(name: string, email: string): Promise<void> {
|
|
179
|
+
await databaseService.query(
|
|
180
|
+
'INSERT INTO test_users (name, email) VALUES (?, ?)',
|
|
181
|
+
[name, email],
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const service = new UserService();
|
|
187
|
+
const prototype = UserService.prototype;
|
|
188
|
+
|
|
189
|
+
// 使用 TransactionInterceptor 执行方法
|
|
190
|
+
const { TransactionInterceptor } = await import('../../src/database/orm/transaction-interceptor');
|
|
191
|
+
await TransactionInterceptor.executeWithTransaction(
|
|
192
|
+
prototype,
|
|
193
|
+
'createUser',
|
|
194
|
+
service.createUser.bind(service),
|
|
195
|
+
['Alice', 'alice@example.com'],
|
|
196
|
+
container,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// 验证数据已提交
|
|
200
|
+
const result = databaseService.query('SELECT * FROM test_users WHERE name = ?', ['Alice']);
|
|
201
|
+
expect(Array.isArray(result) ? result.length : 0).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should rollback on error', async () => {
|
|
205
|
+
class UserService {
|
|
206
|
+
@Transactional()
|
|
207
|
+
public async createUserWithError(name: string, email: string): Promise<void> {
|
|
208
|
+
await databaseService.query(
|
|
209
|
+
'INSERT INTO test_users (name, email) VALUES (?, ?)',
|
|
210
|
+
[name, email],
|
|
211
|
+
);
|
|
212
|
+
throw new Error('Test error');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const service = new UserService();
|
|
217
|
+
const prototype = UserService.prototype;
|
|
218
|
+
|
|
219
|
+
const { TransactionInterceptor } = await import('../../src/database/orm/transaction-interceptor');
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await TransactionInterceptor.executeWithTransaction(
|
|
223
|
+
prototype,
|
|
224
|
+
'createUserWithError',
|
|
225
|
+
service.createUserWithError.bind(service),
|
|
226
|
+
['Bob', 'bob@example.com'],
|
|
227
|
+
container,
|
|
228
|
+
);
|
|
229
|
+
expect(true).toBe(false); // 不应该到达这里
|
|
230
|
+
} catch (error) {
|
|
231
|
+
expect(error).toBeInstanceOf(Error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 验证数据已回滚
|
|
235
|
+
const result = databaseService.query('SELECT * FROM test_users WHERE name = ?', ['Bob']);
|
|
236
|
+
expect(Array.isArray(result) ? result.length : 0).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
});
|