@dangao/bun-server 3.0.5 → 3.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.
Files changed (42) hide show
  1. package/dist/database/connection-manager.d.ts +2 -2
  2. package/dist/database/connection-manager.d.ts.map +1 -1
  3. package/dist/database/connection-pool.d.ts +4 -4
  4. package/dist/database/connection-pool.d.ts.map +1 -1
  5. package/dist/database/database-module.d.ts.map +1 -1
  6. package/dist/database/db-proxy.d.ts.map +1 -1
  7. package/dist/database/driver.d.ts +83 -0
  8. package/dist/database/driver.d.ts.map +1 -0
  9. package/dist/database/index.d.ts +2 -1
  10. package/dist/database/index.d.ts.map +1 -1
  11. package/dist/database/service.d.ts +0 -10
  12. package/dist/database/service.d.ts.map +1 -1
  13. package/dist/database/sql-manager.d.ts.map +1 -1
  14. package/dist/database/types.d.ts +26 -0
  15. package/dist/database/types.d.ts.map +1 -1
  16. package/dist/di/module-registry.d.ts.map +1 -1
  17. package/dist/di/module.d.ts +9 -1
  18. package/dist/di/module.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5898 -5792
  22. package/dist/index.node.mjs +260 -114
  23. package/docs/database.md +44 -0
  24. package/docs/zh/database.md +44 -0
  25. package/package.json +1 -1
  26. package/src/database/connection-manager.ts +5 -46
  27. package/src/database/connection-pool.ts +26 -49
  28. package/src/database/database-module.ts +6 -0
  29. package/src/database/db-proxy.ts +3 -2
  30. package/src/database/driver.ts +368 -0
  31. package/src/database/index.ts +8 -0
  32. package/src/database/service.ts +3 -74
  33. package/src/database/sql-manager.ts +38 -24
  34. package/src/database/types.ts +27 -2
  35. package/src/di/module-registry.ts +15 -0
  36. package/src/di/module.ts +15 -1
  37. package/src/index.ts +1 -0
  38. package/tests/database/database-module.test.ts +4 -0
  39. package/tests/database/driver-mysql2.test.ts +95 -0
  40. package/tests/database/driver.test.ts +234 -0
  41. package/tests/database/orm.test.ts +4 -0
  42. package/tests/di/module.test.ts +74 -0
@@ -1,5 +1,6 @@
1
1
  import type { BunSQLConfig } from './types';
2
2
  import { getRuntime } from '../platform/runtime';
3
+ import { resolveDriver, tagConnection } from './driver';
3
4
 
4
5
  /**
5
6
  * SQL 连接管理器
@@ -17,39 +18,52 @@ export class BunSQLManager {
17
18
  }
18
19
 
19
20
  const pool = config.pool ?? {};
21
+ const driver = resolveDriver(config.type, config.driver, getRuntime().engine);
20
22
  let sql: unknown;
21
23
 
22
- if (getRuntime().engine === 'bun') {
24
+ if (driver === 'bun-sql') {
23
25
  const { SQL } = require('bun') as typeof import('bun');
24
- sql = new SQL(config.url, {
25
- max: pool.max ?? 10,
26
- idleTimeout: pool.idleTimeout ?? 30,
27
- maxLifetime: pool.maxLifetime ?? 0,
28
- connectionTimeout: pool.connectionTimeout ?? 30000,
29
- });
30
- } else {
31
- // Node.js: detect dialect from URL
32
- const url = config.url.toLowerCase();
33
- if (url.startsWith('mysql://') || url.startsWith('mysql2://')) {
34
- const mysql2 = require('mysql2/promise') as typeof import('mysql2/promise');
35
- // Create a pool in Node.js
36
- sql = mysql2.createPool({
37
- uri: config.url,
38
- connectionLimit: pool.max ?? 10,
39
- waitForConnections: true,
40
- });
26
+ if (config.type === 'mysql') {
27
+ // options-object 形式而非连接字符串,绕开 oven-sh/bun#26648
28
+ const parsed = new URL(config.url);
29
+ sql = new SQL({
30
+ adapter: 'mysql',
31
+ hostname: parsed.hostname,
32
+ port: parsed.port ? Number(parsed.port) : 3306,
33
+ username: decodeURIComponent(parsed.username),
34
+ password: decodeURIComponent(parsed.password),
35
+ database: parsed.pathname.replace(/^\//, ''),
36
+ max: pool.max ?? 10,
37
+ idleTimeout: pool.idleTimeout ?? 30,
38
+ maxLifetime: pool.maxLifetime ?? 0,
39
+ connectionTimeout: pool.connectionTimeout ?? 30000,
40
+ } as unknown as string);
41
41
  } else {
42
- // postgres (default)
43
- const postgres = require('postgres') as typeof import('postgres');
44
- sql = postgres(config.url, {
42
+ sql = new SQL(config.url, {
45
43
  max: pool.max ?? 10,
46
- idle_timeout: pool.idleTimeout ?? 30,
47
- max_lifetime: pool.maxLifetime ?? 0,
48
- connect_timeout: (pool.connectionTimeout ?? 30000) / 1000,
44
+ idleTimeout: pool.idleTimeout ?? 30,
45
+ maxLifetime: pool.maxLifetime ?? 0,
46
+ connectionTimeout: pool.connectionTimeout ?? 30000,
49
47
  });
50
48
  }
49
+ } else if (driver === 'mysql2') {
50
+ const mysql2 = require('mysql2/promise') as typeof import('mysql2/promise');
51
+ sql = mysql2.createPool({
52
+ uri: config.url,
53
+ connectionLimit: pool.max ?? 10,
54
+ waitForConnections: true,
55
+ });
56
+ } else {
57
+ const postgres = require('postgres') as typeof import('postgres');
58
+ sql = postgres(config.url, {
59
+ max: pool.max ?? 10,
60
+ idle_timeout: pool.idleTimeout ?? 30,
61
+ max_lifetime: pool.maxLifetime ?? 0,
62
+ connect_timeout: (pool.connectionTimeout ?? 30000) / 1000,
63
+ });
51
64
  }
52
65
 
66
+ tagConnection(sql, driver);
53
67
  this.instances.set(tenantId, sql);
54
68
  return sql;
55
69
  }
@@ -3,6 +3,19 @@
3
3
  */
4
4
  export type DatabaseType = 'sqlite' | 'postgres' | 'mysql';
5
5
 
6
+ /**
7
+ * SQL 驱动选择
8
+ *
9
+ * 与运行时平台(fs/crypto/http server 等)解耦,仅决定 Postgres/MySQL 连接走哪套底层驱动。
10
+ *
11
+ * - `'auto'`(默认):Bun 运行时使用内建 `Bun.SQL`,Node.js 运行时使用 `mysql2` / `postgres` 纯 JS 驱动(保持历史行为)。
12
+ * - `'mysql2'`:无论运行时是 Bun 还是 Node,都使用 `mysql2`(仅适用于 `type: 'mysql'`)。
13
+ * 适合 `bun build --compile` 场景,绕开编译二进制内焊死的 `Bun.SQL` MySQL 适配 bug。
14
+ * - `'postgres'`:无论运行时如何,都使用 `postgres` 纯 JS 驱动(仅适用于 `type: 'postgres'`)。
15
+ * - `'bun-sql'`:强制使用 `Bun.SQL`(仅在 Bun 运行时合法,Node.js 下会抛出清晰错误)。
16
+ */
17
+ export type DatabaseDriver = 'auto' | 'bun-sql' | 'mysql2' | 'postgres';
18
+
6
19
  /**
7
20
  * SQLite 配置
8
21
  */
@@ -72,11 +85,13 @@ export interface MysqlConfig {
72
85
 
73
86
  /**
74
87
  * 数据库配置(联合类型)
88
+ *
89
+ * Postgres/MySQL 支持可选的 `driver` 字段,用于显式选择底层驱动(与运行时平台解耦)。
75
90
  */
76
91
  export type DatabaseConfig =
77
92
  | { type: 'sqlite'; config: SqliteConfig }
78
- | { type: 'postgres'; config: PostgresConfig }
79
- | { type: 'mysql'; config: MysqlConfig };
93
+ | { type: 'postgres'; config: PostgresConfig; driver?: DatabaseDriver }
94
+ | { type: 'mysql'; config: MysqlConfig; driver?: DatabaseDriver };
80
95
 
81
96
  /**
82
97
  * Bun.SQL 连接池配置
@@ -111,6 +126,11 @@ export interface BunSQLConfig {
111
126
  type: 'postgres' | 'mysql';
112
127
  url: string;
113
128
  pool?: BunSQLPoolOptions;
129
+ /**
130
+ * 显式选择底层驱动(与运行时平台解耦)。
131
+ * @default 'auto'
132
+ */
133
+ driver?: DatabaseDriver;
114
134
  }
115
135
 
116
136
  /**
@@ -183,6 +203,11 @@ export interface DatabaseModuleOptions {
183
203
  * 单租户:数据库类型(V2)
184
204
  */
185
205
  type?: DatabaseType;
206
+ /**
207
+ * 单租户:显式选择底层驱动(V2,与运行时平台解耦)
208
+ * @default 'auto'
209
+ */
210
+ driver?: DatabaseDriver;
186
211
  /**
187
212
  * 单租户:Postgres/MySQL URL(V2)
188
213
  */
@@ -165,6 +165,15 @@ export class ModuleRegistry {
165
165
  continue;
166
166
  }
167
167
 
168
+ if ('useExisting' in provider) {
169
+ container.register(provider.provide, {
170
+ lifecycle: provider.lifecycle ?? Lifecycle.Singleton,
171
+ factory: () =>
172
+ container.resolve(provider.useExisting as Constructor<unknown>),
173
+ });
174
+ continue;
175
+ }
176
+
168
177
  if ('useClass' in provider) {
169
178
  const token = provider.provide ?? provider.useClass;
170
179
  container.register(token, {
@@ -259,6 +268,12 @@ export class ModuleRegistry {
259
268
  seen.add(instance);
260
269
  instances.push(instance);
261
270
  }
271
+ } else if ('useExisting' in provider) {
272
+ const instance = ref.container.resolve(provider.provide as Constructor<unknown>);
273
+ if (!seen.has(instance)) {
274
+ seen.add(instance);
275
+ instances.push(instance);
276
+ }
262
277
  }
263
278
  } catch (_error) {
264
279
  // skip providers that can't be resolved (e.g. pending async providers)
package/src/di/module.ts CHANGED
@@ -26,7 +26,21 @@ export interface FactoryProvider {
26
26
  lifecycle?: Lifecycle;
27
27
  }
28
28
 
29
- export type ModuleProvider = Constructor<unknown> | ClassProvider | ValueProvider | FactoryProvider;
29
+ /**
30
+ * NestJS 风格别名:`provide` 解析为已注册的 `useExisting` 同一实例(不新建)。
31
+ */
32
+ export interface ExistingProvider {
33
+ provide: ProviderToken;
34
+ useExisting: ProviderToken;
35
+ lifecycle?: Lifecycle;
36
+ }
37
+
38
+ export type ModuleProvider =
39
+ | Constructor<unknown>
40
+ | ClassProvider
41
+ | ValueProvider
42
+ | FactoryProvider
43
+ | ExistingProvider;
30
44
 
31
45
  export interface ModuleMetadata {
32
46
  imports?: ModuleClass[];
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export {
47
47
  type ModuleMetadata,
48
48
  type ModuleProvider,
49
49
  type ModuleClass,
50
+ type ExistingProvider,
50
51
  } from './di/module';
51
52
  export { ModuleRegistry } from './di/module-registry';
52
53
  export { AsyncProviderRegistry, type AsyncModuleOptions } from './di/async-module';
@@ -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
+ });
@@ -0,0 +1,234 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ closeViaDriver,
5
+ getConnectionDriver,
6
+ healthCheckViaDriver,
7
+ queryViaDriver,
8
+ resolveDriver,
9
+ tagConnection,
10
+ templateQueryViaDriver,
11
+ } from '../../src/database/driver';
12
+
13
+ describe('resolveDriver', () => {
14
+ test("'auto' on bun resolves to bun-sql for mysql and postgres", () => {
15
+ expect(resolveDriver('mysql', 'auto', 'bun')).toBe('bun-sql');
16
+ expect(resolveDriver('postgres', 'auto', 'bun')).toBe('bun-sql');
17
+ expect(resolveDriver('mysql', undefined, 'bun')).toBe('bun-sql');
18
+ });
19
+
20
+ test("'auto' on node resolves to js drivers", () => {
21
+ expect(resolveDriver('mysql', 'auto', 'node')).toBe('mysql2');
22
+ expect(resolveDriver('postgres', 'auto', 'node')).toBe('postgres');
23
+ });
24
+
25
+ test("'mysql2' forces mysql2 regardless of engine", () => {
26
+ expect(resolveDriver('mysql', 'mysql2', 'bun')).toBe('mysql2');
27
+ expect(resolveDriver('mysql', 'mysql2', 'node')).toBe('mysql2');
28
+ });
29
+
30
+ test("'postgres' forces postgres regardless of engine", () => {
31
+ expect(resolveDriver('postgres', 'postgres', 'bun')).toBe('postgres');
32
+ expect(resolveDriver('postgres', 'postgres', 'node')).toBe('postgres');
33
+ });
34
+
35
+ test("'bun-sql' is valid on bun, throws on node", () => {
36
+ expect(resolveDriver('mysql', 'bun-sql', 'bun')).toBe('bun-sql');
37
+ expect(() => resolveDriver('mysql', 'bun-sql', 'node')).toThrow();
38
+ });
39
+
40
+ test("'mysql2' on postgres type throws", () => {
41
+ expect(() => resolveDriver('postgres', 'mysql2', 'bun')).toThrow();
42
+ });
43
+
44
+ test("'postgres' on mysql type throws", () => {
45
+ expect(() => resolveDriver('mysql', 'postgres', 'bun')).toThrow();
46
+ });
47
+ });
48
+
49
+ describe('connection tagging', () => {
50
+ test('tag and read back driver on object connection', () => {
51
+ const conn = { query: () => undefined };
52
+ tagConnection(conn, 'mysql2');
53
+ expect(getConnectionDriver(conn)).toBe('mysql2');
54
+ });
55
+
56
+ test('tag preserves identity', () => {
57
+ const conn = {};
58
+ expect(tagConnection(conn, 'mysql2')).toBe(conn);
59
+ });
60
+
61
+ test('tag is non-enumerable', () => {
62
+ const conn = { query: () => undefined };
63
+ tagConnection(conn, 'mysql2');
64
+ expect(Object.keys(conn)).toEqual(['query']);
65
+ });
66
+
67
+ test('callable connection without tag falls back to bun-sql', () => {
68
+ const conn = (() => undefined) as unknown;
69
+ expect(getConnectionDriver(conn)).toBe('bun-sql');
70
+ });
71
+
72
+ test('untagged plain object returns undefined', () => {
73
+ expect(getConnectionDriver({ query: () => undefined })).toBeUndefined();
74
+ });
75
+ });
76
+
77
+ describe('queryViaDriver', () => {
78
+ test('mysql2 connection uses .query and returns rows (not [rows, fields])', async () => {
79
+ const calls: Array<{ sql: string; params: unknown[] }> = [];
80
+ const conn = tagConnection(
81
+ {
82
+ query: async (sql: string, params: unknown[]) => {
83
+ calls.push({ sql, params });
84
+ return [[{ id: 1 }], [{ name: 'id' }]];
85
+ },
86
+ },
87
+ 'mysql2',
88
+ );
89
+
90
+ const rows = await queryViaDriver(conn, 'SELECT * FROM t WHERE id = ?', [1]);
91
+ expect(rows).toEqual([{ id: 1 }]);
92
+ expect(calls).toEqual([{ sql: 'SELECT * FROM t WHERE id = ?', params: [1] }]);
93
+ });
94
+
95
+ test('postgres connection uses .unsafe and returns rows', async () => {
96
+ const calls: Array<{ sql: string; params: unknown[] }> = [];
97
+ const conn = tagConnection(
98
+ {
99
+ unsafe: async (sql: string, params: unknown[]) => {
100
+ calls.push({ sql, params });
101
+ return [{ ok: true }];
102
+ },
103
+ },
104
+ 'postgres',
105
+ );
106
+
107
+ const rows = await queryViaDriver(conn, 'SELECT 1 WHERE x = ?', ['y']);
108
+ expect(rows).toEqual([{ ok: true }]);
109
+ expect(calls).toEqual([{ sql: 'SELECT 1 WHERE x = ?', params: ['y'] }]);
110
+ });
111
+
112
+ test('bun-sql connection uses template string path', async () => {
113
+ let received: { strings: string[]; values: unknown[] } | null = null;
114
+ const conn = tagConnection(
115
+ Object.assign(
116
+ async (strings: TemplateStringsArray, ...values: unknown[]) => {
117
+ received = { strings: Array.from(strings), values };
118
+ return [{ via: 'bun-sql' }];
119
+ },
120
+ ),
121
+ 'bun-sql',
122
+ );
123
+
124
+ const rows = await queryViaDriver(conn, 'SELECT * FROM t WHERE id = ?', [42]);
125
+ expect(rows).toEqual([{ via: 'bun-sql' }]);
126
+ expect(received!.strings).toEqual(['SELECT * FROM t WHERE id = ', '']);
127
+ expect(received!.values).toEqual([42]);
128
+ });
129
+ });
130
+
131
+ describe('templateQueryViaDriver', () => {
132
+ test('mysql2 transforms template into ? placeholders', async () => {
133
+ const calls: Array<{ sql: string; params: unknown[] }> = [];
134
+ const conn = tagConnection(
135
+ {
136
+ query: async (sql: string, params: unknown[]) => {
137
+ calls.push({ sql, params });
138
+ return [[{ id: 7 }], []];
139
+ },
140
+ },
141
+ 'mysql2',
142
+ );
143
+
144
+ const strings = Object.assign(['SELECT * FROM t WHERE id = ', ''], {
145
+ raw: ['SELECT * FROM t WHERE id = ', ''],
146
+ }) as unknown as TemplateStringsArray;
147
+
148
+ const rows = await templateQueryViaDriver(conn, strings, [7]);
149
+ expect(rows).toEqual([{ id: 7 }]);
150
+ expect(calls).toEqual([
151
+ { sql: 'SELECT * FROM t WHERE id = ?', params: [7] },
152
+ ]);
153
+ });
154
+
155
+ test('callable connection invoked directly as tagged template', async () => {
156
+ let received: unknown[] = [];
157
+ const conn = tagConnection(
158
+ Object.assign(
159
+ async (_strings: TemplateStringsArray, ...values: unknown[]) => {
160
+ received = values;
161
+ return [{ ok: 1 }];
162
+ },
163
+ ),
164
+ 'bun-sql',
165
+ );
166
+
167
+ const strings = Object.assign(['SELECT ', ''], {
168
+ raw: ['SELECT ', ''],
169
+ }) as unknown as TemplateStringsArray;
170
+
171
+ const rows = await templateQueryViaDriver(conn, strings, [99]);
172
+ expect(rows).toEqual([{ ok: 1 }]);
173
+ expect(received).toEqual([99]);
174
+ });
175
+ });
176
+
177
+ describe('healthCheckViaDriver', () => {
178
+ test('mysql2 health check runs SELECT 1', async () => {
179
+ const conn = tagConnection(
180
+ {
181
+ query: async () => [[{ '1': 1 }], []],
182
+ },
183
+ 'mysql2',
184
+ );
185
+ expect(await healthCheckViaDriver(conn)).toBe(true);
186
+ });
187
+
188
+ test('returns false when query throws', async () => {
189
+ const conn = tagConnection(
190
+ {
191
+ query: async () => {
192
+ throw new Error('down');
193
+ },
194
+ },
195
+ 'mysql2',
196
+ );
197
+ expect(await healthCheckViaDriver(conn)).toBe(false);
198
+ });
199
+ });
200
+
201
+ describe('closeViaDriver', () => {
202
+ test('mysql2 connection closed via .end()', async () => {
203
+ let ended = false;
204
+ let closed = false;
205
+ const conn = tagConnection(
206
+ {
207
+ end: async () => {
208
+ ended = true;
209
+ },
210
+ close: async () => {
211
+ closed = true;
212
+ },
213
+ },
214
+ 'mysql2',
215
+ );
216
+ await closeViaDriver(conn);
217
+ expect(ended).toBe(true);
218
+ expect(closed).toBe(false);
219
+ });
220
+
221
+ test('bun-sql connection closed via .close()', async () => {
222
+ let closed = false;
223
+ const conn = tagConnection(
224
+ {
225
+ close: async () => {
226
+ closed = true;
227
+ },
228
+ },
229
+ 'bun-sql',
230
+ );
231
+ await closeViaDriver(conn);
232
+ expect(closed).toBe(true);
233
+ });
234
+ });
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../src/database/orm';
12
12
  import { DatabaseService, DATABASE_SERVICE_TOKEN } from '../../src/database';
13
13
  import { Container } from '../../src/di/container';
14
+ import { initRuntime } from '../../src/platform/runtime';
14
15
 
15
16
  // 测试实体
16
17
  @Entity('test_users')
@@ -71,6 +72,9 @@ describe('BaseRepository', () => {
71
72
  let databaseService: DatabaseService;
72
73
 
73
74
  beforeEach(async () => {
75
+ // DatabaseService.initialize() 会读取 getRuntime(),确保运行时已初始化
76
+ // (避免依赖其他测试文件的执行顺序)
77
+ initRuntime('bun');
74
78
  container = new Container();
75
79
  databaseService = new DatabaseService({
76
80
  database: {
@@ -103,6 +103,80 @@ describe('ModuleRegistry', () => {
103
103
  expect(featureService.shared).toBe(sharedFromModule);
104
104
  });
105
105
 
106
+ test('useExisting: alias token resolves to same instance as useExisting target', () => {
107
+ const ALIAS = Symbol.for('test.useExisting.alias');
108
+
109
+ @Injectable()
110
+ class SomeService {
111
+ public readonly n = 42;
112
+ }
113
+
114
+ @Module({
115
+ providers: [SomeService, { provide: ALIAS, useExisting: SomeService }],
116
+ exports: [SomeService, ALIAS],
117
+ })
118
+ class TestModule {}
119
+
120
+ const app = new Application();
121
+ app.registerModule(TestModule);
122
+
123
+ const ref = ModuleRegistry.getInstance().getModuleRef(TestModule)!;
124
+ const byClass = ref.container.resolve(SomeService);
125
+ const byAlias = ref.container.resolve(ALIAS);
126
+ expect(Object.is(byAlias, byClass)).toBe(true);
127
+ });
128
+
129
+ test('useExisting: exported Symbol alias can be resolved from importing module container', () => {
130
+ const TOKEN = Symbol.for('test.useExisting.export');
131
+
132
+ @Injectable()
133
+ class CoreService {
134
+ public ping(): string {
135
+ return 'pong';
136
+ }
137
+ }
138
+
139
+ @Module({
140
+ providers: [CoreService, { provide: TOKEN, useExisting: CoreService }],
141
+ exports: [TOKEN, CoreService],
142
+ })
143
+ class CoreModule {}
144
+
145
+ @Injectable()
146
+ class Consumer {
147
+ public constructor(@Inject(TOKEN) public readonly core: CoreService) {}
148
+ }
149
+
150
+ @Module({
151
+ imports: [CoreModule],
152
+ providers: [Consumer],
153
+ })
154
+ class AppModule {}
155
+
156
+ const app = new Application();
157
+ app.registerModule(AppModule);
158
+
159
+ const appRef = ModuleRegistry.getInstance().getModuleRef(AppModule)!;
160
+ const consumer = appRef.container.resolve(Consumer);
161
+ expect(consumer.core.ping()).toBe('pong');
162
+ });
163
+
164
+ test('useExisting: when target token is not registered, resolve throws', () => {
165
+ const ALIAS = Symbol.for('test.useExisting.orphan');
166
+ const MISSING = Symbol.for('test.useExisting.missing');
167
+
168
+ @Module({
169
+ providers: [{ provide: ALIAS, useExisting: MISSING }],
170
+ })
171
+ class BadModule {}
172
+
173
+ const app = new Application();
174
+ app.registerModule(BadModule);
175
+
176
+ const ref = ModuleRegistry.getInstance().getModuleRef(BadModule)!;
177
+ expect(() => ref.container.resolve(ALIAS)).toThrow(/Provider not found/);
178
+ });
179
+
106
180
  test('should throw error for circular module dependencies', () => {
107
181
  @Module({
108
182
  imports: [],