@dangao/bun-server 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/ai/types.d.ts +4 -0
  2. package/dist/ai/types.d.ts.map +1 -1
  3. package/dist/auth/types.d.ts +4 -0
  4. package/dist/auth/types.d.ts.map +1 -1
  5. package/dist/cache/interceptors.d.ts +3 -3
  6. package/dist/cache/interceptors.d.ts.map +1 -1
  7. package/dist/cache/service.d.ts +6 -6
  8. package/dist/cache/service.d.ts.map +1 -1
  9. package/dist/cache/types.d.ts +12 -12
  10. package/dist/cache/types.d.ts.map +1 -1
  11. package/dist/config/service.d.ts +6 -4
  12. package/dist/config/service.d.ts.map +1 -1
  13. package/dist/core/application.d.ts +4 -0
  14. package/dist/core/application.d.ts.map +1 -1
  15. package/dist/core/context.d.ts +4 -2
  16. package/dist/core/context.d.ts.map +1 -1
  17. package/dist/database/connection-manager.d.ts +2 -2
  18. package/dist/database/connection-manager.d.ts.map +1 -1
  19. package/dist/database/connection-pool.d.ts +4 -4
  20. package/dist/database/connection-pool.d.ts.map +1 -1
  21. package/dist/database/database-module.d.ts.map +1 -1
  22. package/dist/database/db-proxy.d.ts.map +1 -1
  23. package/dist/database/driver.d.ts +83 -0
  24. package/dist/database/driver.d.ts.map +1 -0
  25. package/dist/database/index.d.ts +2 -1
  26. package/dist/database/index.d.ts.map +1 -1
  27. package/dist/database/service.d.ts +0 -10
  28. package/dist/database/service.d.ts.map +1 -1
  29. package/dist/database/sql-manager.d.ts.map +1 -1
  30. package/dist/database/sqlite-adapter.d.ts +4 -2
  31. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  32. package/dist/database/types.d.ts +26 -0
  33. package/dist/database/types.d.ts.map +1 -1
  34. package/dist/di/container.d.ts +2 -0
  35. package/dist/di/container.d.ts.map +1 -1
  36. package/dist/di/module-registry.d.ts.map +1 -1
  37. package/dist/di/module.d.ts +11 -1
  38. package/dist/di/module.d.ts.map +1 -1
  39. package/dist/di/types.d.ts +1 -1
  40. package/dist/di/types.d.ts.map +1 -1
  41. package/dist/error/handler.d.ts.map +1 -1
  42. package/dist/error/http-exception.d.ts +8 -8
  43. package/dist/error/http-exception.d.ts.map +1 -1
  44. package/dist/error/index.d.ts +1 -0
  45. package/dist/error/index.d.ts.map +1 -1
  46. package/dist/events/service.d.ts +2 -2
  47. package/dist/events/service.d.ts.map +1 -1
  48. package/dist/events/types.d.ts +12 -3
  49. package/dist/events/types.d.ts.map +1 -1
  50. package/dist/index.js +5951 -5820
  51. package/dist/index.node.mjs +310 -139
  52. package/dist/interceptor/base-interceptor.d.ts +3 -3
  53. package/dist/interceptor/base-interceptor.d.ts.map +1 -1
  54. package/dist/interceptor/builtin/log-interceptor.d.ts +1 -1
  55. package/dist/interceptor/builtin/log-interceptor.d.ts.map +1 -1
  56. package/dist/interceptor/builtin/permission-interceptor.d.ts +1 -1
  57. package/dist/interceptor/builtin/permission-interceptor.d.ts.map +1 -1
  58. package/dist/interceptor/interceptor-chain.d.ts +1 -1
  59. package/dist/interceptor/interceptor-chain.d.ts.map +1 -1
  60. package/dist/interceptor/interceptor-registry.d.ts +3 -1
  61. package/dist/interceptor/interceptor-registry.d.ts.map +1 -1
  62. package/dist/interceptor/types.d.ts +6 -1
  63. package/dist/interceptor/types.d.ts.map +1 -1
  64. package/dist/microservice/service-client/types.d.ts +1 -0
  65. package/dist/microservice/service-client/types.d.ts.map +1 -1
  66. package/dist/microservice/tracing/tracer.d.ts +1 -0
  67. package/dist/microservice/tracing/tracer.d.ts.map +1 -1
  68. package/dist/middleware/builtin/file-upload.d.ts +2 -0
  69. package/dist/middleware/builtin/file-upload.d.ts.map +1 -1
  70. package/dist/middleware/builtin/rate-limit.d.ts +9 -1
  71. package/dist/middleware/builtin/rate-limit.d.ts.map +1 -1
  72. package/dist/queue/service.d.ts +2 -2
  73. package/dist/queue/service.d.ts.map +1 -1
  74. package/dist/queue/types.d.ts +25 -1
  75. package/dist/queue/types.d.ts.map +1 -1
  76. package/dist/router/decorators.d.ts +1 -2
  77. package/dist/router/decorators.d.ts.map +1 -1
  78. package/dist/security/guards/types.d.ts +1 -0
  79. package/dist/security/guards/types.d.ts.map +1 -1
  80. package/dist/security/types.d.ts +1 -1
  81. package/dist/security/types.d.ts.map +1 -1
  82. package/dist/session/types.d.ts +8 -0
  83. package/dist/session/types.d.ts.map +1 -1
  84. package/dist/swagger/decorators.d.ts +1 -1
  85. package/dist/swagger/decorators.d.ts.map +1 -1
  86. package/dist/swagger/types.d.ts +1 -1
  87. package/dist/swagger/types.d.ts.map +1 -1
  88. package/dist/testing/harness.d.ts +1 -1
  89. package/dist/testing/harness.d.ts.map +1 -1
  90. package/dist/testing/test-client.d.ts +1 -1
  91. package/dist/testing/test-client.d.ts.map +1 -1
  92. package/dist/testing/testing-module.d.ts +2 -2
  93. package/dist/testing/testing-module.d.ts.map +1 -1
  94. package/dist/validation/errors.d.ts +5 -1
  95. package/dist/validation/errors.d.ts.map +1 -1
  96. package/docs/database.md +44 -0
  97. package/docs/zh/database.md +44 -0
  98. package/package.json +3 -3
  99. package/src/ai/types.ts +5 -0
  100. package/src/auth/types.ts +4 -1
  101. package/src/cache/interceptors.ts +6 -6
  102. package/src/cache/service-proxy.ts +2 -2
  103. package/src/cache/service.ts +17 -8
  104. package/src/cache/types.ts +12 -12
  105. package/src/config/service.ts +8 -6
  106. package/src/core/application.ts +7 -1
  107. package/src/core/context.ts +6 -3
  108. package/src/database/connection-manager.ts +5 -46
  109. package/src/database/connection-pool.ts +26 -49
  110. package/src/database/database-module.ts +6 -0
  111. package/src/database/db-proxy.ts +3 -2
  112. package/src/database/driver.ts +368 -0
  113. package/src/database/index.ts +8 -0
  114. package/src/database/service.ts +3 -74
  115. package/src/database/sql-manager.ts +38 -24
  116. package/src/database/sqlite-adapter.ts +4 -3
  117. package/src/database/types.ts +27 -2
  118. package/src/di/container.ts +13 -0
  119. package/src/di/module-registry.ts +2 -3
  120. package/src/di/module.ts +21 -2
  121. package/src/di/types.ts +1 -2
  122. package/src/error/handler.ts +3 -4
  123. package/src/error/http-exception.ts +11 -11
  124. package/src/error/index.ts +1 -1
  125. package/src/events/service.ts +4 -2
  126. package/src/events/types.ts +14 -3
  127. package/src/interceptor/base-interceptor.ts +5 -6
  128. package/src/interceptor/builtin/log-interceptor.ts +2 -3
  129. package/src/interceptor/builtin/permission-interceptor.ts +2 -3
  130. package/src/interceptor/interceptor-chain.ts +6 -7
  131. package/src/interceptor/interceptor-registry.ts +5 -3
  132. package/src/interceptor/types.ts +9 -4
  133. package/src/microservice/service-client/types.ts +1 -1
  134. package/src/microservice/tracing/tracer.ts +15 -3
  135. package/src/middleware/builtin/file-upload.ts +3 -2
  136. package/src/middleware/builtin/rate-limit.ts +22 -5
  137. package/src/queue/service.ts +1 -1
  138. package/src/queue/types.ts +40 -1
  139. package/src/router/decorators.ts +2 -3
  140. package/src/security/guards/types.ts +2 -1
  141. package/src/security/types.ts +1 -2
  142. package/src/session/service.ts +1 -1
  143. package/src/session/types.ts +10 -0
  144. package/src/swagger/decorators.ts +15 -4
  145. package/src/swagger/generator.ts +2 -3
  146. package/src/swagger/types.ts +1 -2
  147. package/src/testing/harness.ts +1 -2
  148. package/src/testing/test-client.ts +2 -2
  149. package/src/testing/testing-module.ts +5 -5
  150. package/src/validation/errors.ts +6 -2
  151. package/tests/bun-test-shim.d.ts +11 -0
  152. package/tests/controller/context-decorator.test.ts +1 -2
  153. package/tests/database/database-module.test.ts +4 -0
  154. package/tests/database/driver-mysql2.test.ts +95 -0
  155. package/tests/database/driver.test.ts +234 -0
  156. package/tests/database/orm.test.ts +4 -0
  157. package/tests/di/module.test.ts +199 -1
  158. package/tests/events/event-emitter.test.ts +2 -2
  159. package/tests/global.d.ts +30 -0
  160. package/tests/queue/queue-service.test.ts +14 -0
  161. package/tests/testing/testing-module.test.ts +20 -0
@@ -76,6 +76,7 @@ export class DatabaseModule {
76
76
  type: db.type,
77
77
  url,
78
78
  pool: options.bunSqlPool,
79
+ driver: db.driver ?? options.driver,
79
80
  },
80
81
  },
81
82
  ];
@@ -103,6 +104,7 @@ export class DatabaseModule {
103
104
  type: options.type,
104
105
  url: options.url,
105
106
  pool: options.bunSqlPool,
107
+ driver: options.driver,
106
108
  },
107
109
  },
108
110
  ];
@@ -119,6 +121,7 @@ export class DatabaseModule {
119
121
  type: options.type,
120
122
  url,
121
123
  pool: options.bunSqlPool,
124
+ driver: options.driver,
122
125
  },
123
126
  },
124
127
  ];
@@ -245,6 +248,9 @@ export class DatabaseModule {
245
248
  user: options.username ?? 'root',
246
249
  password: options.password ?? '',
247
250
  },
251
+ driver:
252
+ (normalized[0]?.config as BunSQLConfig | undefined)?.driver ??
253
+ options.driver,
248
254
  },
249
255
  };
250
256
 
@@ -1,5 +1,6 @@
1
1
  import type { BunSQLManager } from './sql-manager';
2
2
  import { getCurrentSession } from './database-context';
3
+ import { templateQueryViaDriver } from './driver';
3
4
  import type { TransactionManager } from './orm/transaction-manager';
4
5
 
5
6
  type DbResult = Promise<unknown>;
@@ -49,10 +50,10 @@ const baseDb = async (
49
50
  if (tenantId) {
50
51
  const tenantSql = sqlManager.get(tenantId);
51
52
  if (tenantSql) {
52
- return await (tenantSql as any)(strings, ...values);
53
+ return await templateQueryViaDriver(tenantSql, strings, values);
53
54
  }
54
55
  }
55
- return await (sqlManager.getDefault() as any)(strings, ...values);
56
+ return await templateQueryViaDriver(sqlManager.getDefault(), strings, values);
56
57
  };
57
58
 
58
59
  function createDb(tenantId?: string): DbProxy {
@@ -0,0 +1,368 @@
1
+ import type { DatabaseDriver, MysqlConfig, PostgresConfig } from './types';
2
+
3
+ /**
4
+ * 已解析的具体驱动类型(连接级别 tag)
5
+ * - `'bun-sql'`:Bun 内建 `Bun.SQL`
6
+ * - `'mysql2'`:纯 JS `mysql2` 驱动
7
+ * - `'postgres'`:纯 JS `postgres` 驱动
8
+ */
9
+ export type ResolvedDriver = 'bun-sql' | 'mysql2' | 'postgres';
10
+
11
+ /**
12
+ * 运行时引擎(来自 platform/runtime)
13
+ */
14
+ export type RuntimeEngine = 'bun' | 'node';
15
+
16
+ /**
17
+ * 连接级别的驱动 tag,附加在连接对象/函数上,
18
+ * 让查询执行 / 健康检查 / 关闭逻辑统一按所选 driver 分流,
19
+ * 而不是按 `getRuntime().engine` 分流。
20
+ */
21
+ const DRIVER_TAG: unique symbol = Symbol.for('@dangao/bun-server:database:driver');
22
+
23
+ /**
24
+ * 将驱动选项(含 'auto')解析为具体驱动。
25
+ *
26
+ * - `'auto'`:Bun → `bun-sql`,Node → `mysql2`(MySQL)/ `postgres`(PostgreSQL),保持历史行为。
27
+ * - `'bun-sql'`:强制 `Bun.SQL`,仅 Bun 合法,Node 抛清晰错误。
28
+ * - `'mysql2'`:强制 `mysql2`,仅 `type: 'mysql'` 合法。
29
+ * - `'postgres'`:强制 `postgres`,仅 `type: 'postgres'` 合法。
30
+ *
31
+ * @param dbType 数据库类型(postgres / mysql)
32
+ * @param option 用户配置的 driver(默认 'auto')
33
+ * @param engine 当前运行时引擎
34
+ */
35
+ export function resolveDriver(
36
+ dbType: 'postgres' | 'mysql',
37
+ option: DatabaseDriver | undefined,
38
+ engine: RuntimeEngine,
39
+ ): ResolvedDriver {
40
+ const driver = option ?? 'auto';
41
+
42
+ switch (driver) {
43
+ case 'auto':
44
+ if (engine === 'bun') {
45
+ return 'bun-sql';
46
+ }
47
+ return dbType === 'mysql' ? 'mysql2' : 'postgres';
48
+
49
+ case 'bun-sql':
50
+ if (engine !== 'bun') {
51
+ throw new Error(
52
+ `[bun-server] driver 'bun-sql' requires the Bun runtime, but the current platform engine is '${engine}'. ` +
53
+ `Use driver 'auto' / 'mysql2' / 'postgres', or run on Bun.`,
54
+ );
55
+ }
56
+ return 'bun-sql';
57
+
58
+ case 'mysql2':
59
+ if (dbType !== 'mysql') {
60
+ throw new Error(
61
+ `[bun-server] driver 'mysql2' is only valid for type 'mysql', but got type '${dbType}'.`,
62
+ );
63
+ }
64
+ return 'mysql2';
65
+
66
+ case 'postgres':
67
+ if (dbType !== 'postgres') {
68
+ throw new Error(
69
+ `[bun-server] driver 'postgres' is only valid for type 'postgres', but got type '${dbType}'.`,
70
+ );
71
+ }
72
+ return 'postgres';
73
+
74
+ default:
75
+ throw new Error(`[bun-server] unknown driver '${String(driver)}'.`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 给连接对象/函数打上驱动 tag(返回同一引用,保证池内 identity 比较不被破坏)。
81
+ */
82
+ export function tagConnection<T>(connection: T, driver: ResolvedDriver): T {
83
+ if (connection && (typeof connection === 'object' || typeof connection === 'function')) {
84
+ try {
85
+ Object.defineProperty(connection as object, DRIVER_TAG, {
86
+ value: driver,
87
+ enumerable: false,
88
+ configurable: true,
89
+ writable: true,
90
+ });
91
+ } catch {
92
+ // 某些 frozen 连接无法附加 tag,回退到 heuristic 推断
93
+ }
94
+ }
95
+ return connection;
96
+ }
97
+
98
+ /**
99
+ * 读取连接 tag;若无显式 tag 则按结构启发式推断(向后兼容未打 tag 的连接,
100
+ * 例如 `sql.reserve()` 返回的 reserved 连接)。
101
+ */
102
+ export function getConnectionDriver(connection: unknown): ResolvedDriver | undefined {
103
+ if (!connection) {
104
+ return undefined;
105
+ }
106
+ if (typeof connection === 'object' || typeof connection === 'function') {
107
+ const tagged = (connection as Record<symbol, unknown>)[DRIVER_TAG];
108
+ if (tagged === 'bun-sql' || tagged === 'mysql2' || tagged === 'postgres') {
109
+ return tagged;
110
+ }
111
+ }
112
+ // heuristic 回退:可作为模板字符串调用的(Bun.SQL / postgres-js / reserved)按模板路径处理
113
+ if (typeof connection === 'function') {
114
+ return 'bun-sql';
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ /**
120
+ * 创建 PostgreSQL 连接(按所选 driver 分流,并打 tag)。
121
+ */
122
+ export async function createPostgresConnection(
123
+ config: PostgresConfig,
124
+ driver: ResolvedDriver,
125
+ ): Promise<unknown> {
126
+ const url = `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`;
127
+
128
+ if (driver === 'bun-sql') {
129
+ const { SQL } = await import('bun');
130
+ return tagConnection(
131
+ new SQL({
132
+ adapter: 'postgres',
133
+ hostname: config.host,
134
+ port: config.port,
135
+ username: config.user,
136
+ password: config.password,
137
+ database: config.database,
138
+ max: 1,
139
+ tls: config.ssl ?? false,
140
+ } as unknown as string),
141
+ 'bun-sql',
142
+ );
143
+ }
144
+
145
+ if (driver === 'postgres') {
146
+ const postgres = loadPostgres();
147
+ return tagConnection(
148
+ postgres(url, { max: 1, ssl: config.ssl ? 'require' : false }),
149
+ 'postgres',
150
+ );
151
+ }
152
+
153
+ throw new Error(`[bun-server] driver '${driver}' cannot create a postgres connection.`);
154
+ }
155
+
156
+ /**
157
+ * 创建 MySQL 连接(按所选 driver 分流,并打 tag)。
158
+ *
159
+ * Bun.SQL 走 options-object 形式而非连接字符串,绕开 oven-sh/bun#26648
160
+ * (MySQL 连接串被误判为 postgres)。
161
+ */
162
+ export async function createMysqlConnection(
163
+ config: MysqlConfig & { ssl?: boolean },
164
+ driver: ResolvedDriver,
165
+ ): Promise<unknown> {
166
+ if (driver === 'bun-sql') {
167
+ const { SQL } = await import('bun');
168
+ return tagConnection(
169
+ new SQL({
170
+ adapter: 'mysql',
171
+ hostname: config.host,
172
+ port: config.port,
173
+ username: config.user,
174
+ password: config.password,
175
+ database: config.database,
176
+ max: 1,
177
+ ssl: config.ssl ?? false,
178
+ } as unknown as string),
179
+ 'bun-sql',
180
+ );
181
+ }
182
+
183
+ if (driver === 'mysql2') {
184
+ const mysql2 = loadMysql2();
185
+ const conn = await mysql2.createConnection({
186
+ host: config.host,
187
+ port: config.port,
188
+ database: config.database,
189
+ user: config.user,
190
+ password: config.password,
191
+ });
192
+ return tagConnection(conn, 'mysql2');
193
+ }
194
+
195
+ throw new Error(`[bun-server] driver '${driver}' cannot create a mysql connection.`);
196
+ }
197
+
198
+ /**
199
+ * 执行参数化查询(按连接 tag 分流),统一返回行数组。
200
+ *
201
+ * - `bun-sql`:通过模板字符串调用,参数走 Bun.SQL 的 values 通道。
202
+ * - `mysql2`:`conn.query(sql, params)`,返回 `[rows, fields]`,取 rows。
203
+ * - `postgres`:`sql.unsafe(sql, params)`,直接返回行数组。
204
+ */
205
+ export async function queryViaDriver<T = unknown>(
206
+ connection: unknown,
207
+ sql: string,
208
+ params?: unknown[],
209
+ ): Promise<T[]> {
210
+ const driver = getConnectionDriver(connection);
211
+
212
+ if (driver === 'mysql2') {
213
+ const conn = connection as {
214
+ query: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
215
+ };
216
+ const [rows] = await conn.query(sql, params ?? []);
217
+ return (rows ?? []) as T[];
218
+ }
219
+
220
+ if (driver === 'postgres') {
221
+ const conn = connection as {
222
+ unsafe: (sql: string, params?: unknown[]) => Promise<unknown[]>;
223
+ };
224
+ const rows = await conn.unsafe(sql, params ?? []);
225
+ return (rows ?? []) as T[];
226
+ }
227
+
228
+ // bun-sql 及 heuristic 回退:模板字符串路径
229
+ if (typeof connection === 'function') {
230
+ const { strings, values } = buildTemplateFromSql(sql, params);
231
+ const template = Object.assign(strings.slice(), {
232
+ raw: strings.slice(),
233
+ }) as unknown as TemplateStringsArray;
234
+ const result = await (
235
+ connection as (
236
+ template: TemplateStringsArray,
237
+ ...values: unknown[]
238
+ ) => Promise<Array<Record<string, unknown>>>
239
+ )(template, ...values);
240
+ return result as T[];
241
+ }
242
+
243
+ // 对象但带 query 方法(兜底)
244
+ if (
245
+ connection &&
246
+ typeof connection === 'object' &&
247
+ 'query' in connection &&
248
+ typeof (connection as { query: unknown }).query === 'function'
249
+ ) {
250
+ const result = await (
251
+ connection as {
252
+ query: (sql: string, params?: unknown[]) => Promise<unknown>;
253
+ }
254
+ ).query(sql, params ?? []);
255
+ return (Array.isArray(result) ? result[0] : result) as T[];
256
+ }
257
+
258
+ throw new Error('[bun-server] invalid SQL connection for query.');
259
+ }
260
+
261
+ /**
262
+ * 通过 tagged template 执行查询(供 db proxy 使用),按连接 tag 分流。
263
+ *
264
+ * - `mysql2`:把模板片段拼成 `?` 占位 SQL,走 `conn.query(sql, values)`。
265
+ * - 其余(bun-sql / postgres / reserved):连接本身即可作为 tagged template 调用。
266
+ */
267
+ export async function templateQueryViaDriver(
268
+ connection: unknown,
269
+ strings: TemplateStringsArray,
270
+ values: unknown[],
271
+ ): Promise<unknown> {
272
+ const driver = getConnectionDriver(connection);
273
+
274
+ if (driver === 'mysql2') {
275
+ const sql = strings.join('?');
276
+ const conn = connection as {
277
+ query: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
278
+ };
279
+ const [rows] = await conn.query(sql, values);
280
+ return rows;
281
+ }
282
+
283
+ return await (
284
+ connection as (
285
+ strings: TemplateStringsArray,
286
+ ...values: unknown[]
287
+ ) => Promise<unknown>
288
+ )(strings, ...values);
289
+ }
290
+
291
+ /**
292
+ * 健康检查(按连接 tag 分流)。
293
+ */
294
+ export async function healthCheckViaDriver(connection: unknown): Promise<boolean> {
295
+ try {
296
+ const rows = await queryViaDriver(connection, 'SELECT 1');
297
+ return Array.isArray(rows);
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * 关闭连接(按连接 tag 分流)。
305
+ *
306
+ * - `bun-sql`:`.close()`
307
+ * - `mysql2` / `postgres`:`.end()`
308
+ * - 兜底:可用的 `.close()` 或 `.end()`
309
+ */
310
+ export async function closeViaDriver(connection: unknown): Promise<void> {
311
+ if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) {
312
+ return;
313
+ }
314
+
315
+ const driver = getConnectionDriver(connection);
316
+ const conn = connection as { close?: () => unknown; end?: () => unknown };
317
+
318
+ if (driver === 'mysql2' || driver === 'postgres') {
319
+ if (typeof conn.end === 'function') {
320
+ await conn.end();
321
+ return;
322
+ }
323
+ }
324
+
325
+ if (driver === 'bun-sql') {
326
+ if (typeof conn.close === 'function') {
327
+ await conn.close();
328
+ return;
329
+ }
330
+ }
331
+
332
+ // 兜底
333
+ if (typeof conn.close === 'function') {
334
+ await conn.close();
335
+ } else if (typeof conn.end === 'function') {
336
+ await conn.end();
337
+ }
338
+ }
339
+
340
+ /**
341
+ * 将 `SELECT ... ?` 占位符 SQL + 参数转换为模板字符串片段,
342
+ * 让参数通过 Bun.SQL 的 values 通道注入。
343
+ */
344
+ export function buildTemplateFromSql(
345
+ sql: string,
346
+ params?: unknown[],
347
+ ): { strings: string[]; values: unknown[] } {
348
+ if (!params || params.length === 0) {
349
+ return { strings: [sql], values: [] };
350
+ }
351
+
352
+ const strings = sql.split('?');
353
+ if (strings.length !== params.length + 1) {
354
+ throw new Error('SQL placeholders count does not match parameters count');
355
+ }
356
+
357
+ return { strings, values: params };
358
+ }
359
+
360
+ function loadMysql2(): typeof import('mysql2/promise') {
361
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
362
+ return require('mysql2/promise') as typeof import('mysql2/promise');
363
+ }
364
+
365
+ function loadPostgres(): typeof import('postgres') {
366
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
367
+ return require('postgres') as typeof import('postgres');
368
+ }
@@ -5,6 +5,13 @@ export { ConnectionPool } from './connection-pool';
5
5
  export { DatabaseHealthIndicator } from './health-indicator';
6
6
  export { DatabaseExtension } from './database-extension';
7
7
  export { BunSQLManager } from './sql-manager';
8
+ export {
9
+ resolveDriver,
10
+ tagConnection,
11
+ getConnectionDriver,
12
+ type ResolvedDriver,
13
+ type RuntimeEngine,
14
+ } from './driver';
8
15
  export { SqliteAdapter, SqliteManager, Semaphore } from './sqlite-adapter';
9
16
  export {
10
17
  db,
@@ -37,6 +44,7 @@ export {
37
44
  type ConnectionInfo,
38
45
  type ConnectionPoolOptions,
39
46
  type DatabaseConfig,
47
+ type DatabaseDriver,
40
48
  type DatabaseModuleOptions,
41
49
  type DatabaseType,
42
50
  type MysqlConfig,
@@ -2,6 +2,7 @@ import { Injectable } from '../di/decorators';
2
2
 
3
3
  import { DatabaseConnectionManager } from './connection-manager';
4
4
  import { getCurrentSession } from './database-context';
5
+ import { queryViaDriver } from './driver';
5
6
  import type {
6
7
  ConnectionInfo,
7
8
  DatabaseConfig,
@@ -131,7 +132,8 @@ export class DatabaseService {
131
132
  if (dbType === 'sqlite') {
132
133
  return this.querySqlite(connection, sql, params);
133
134
  } else if (dbType === 'postgres' || dbType === 'mysql') {
134
- return this.queryBunSQL(connection, sql, params);
135
+ // 按连接 driver tag 分流(bun-sql 模板 / mysql2 / postgres)
136
+ return queryViaDriver(connection, sql, params);
135
137
  }
136
138
 
137
139
  throw new Error(`Query not supported for database type: ${dbType}`);
@@ -184,77 +186,4 @@ export class DatabaseService {
184
186
 
185
187
  throw new Error('Invalid SQLite connection');
186
188
  }
187
-
188
- /**
189
- * Bun.SQL 查询实现(PostgreSQL/MySQL)
190
- * 通过模板字符串调用 Bun.SQL,确保参数走 Bun.SQL 转义逻辑
191
- */
192
- private async queryBunSQL<T = unknown>(
193
- connection: unknown,
194
- sql: string,
195
- params?: unknown[],
196
- ): Promise<T[]> {
197
- // Bun.SQL 对象可以作为函数调用(模板字符串)
198
- if (connection && typeof connection === 'function') {
199
- try {
200
- const { strings, values } = this.buildTemplateFromSql(sql, params);
201
- const template = Object.assign(strings, {
202
- raw: strings,
203
- }) as unknown as TemplateStringsArray;
204
- const result = await (connection as (
205
- template: TemplateStringsArray,
206
- ...values: unknown[]
207
- ) => Promise<Array<Record<string, unknown>>>)(template, ...values);
208
- return result as T[];
209
- } catch (error) {
210
- const errorMessage =
211
- error instanceof Error ? error.message : String(error);
212
- // 如果模板字符串方式失败,保留原始错误,便于排查参数/SQL 构造问题
213
- throw new Error(
214
- `Bun.SQL parameterized queries are not fully supported. Consider using template string queries. Original error: ${errorMessage}`,
215
- );
216
- }
217
- }
218
-
219
- // 尝试使用 query 方法(如果存在)
220
- if (
221
- connection &&
222
- typeof connection === 'object' &&
223
- 'query' in connection &&
224
- typeof connection.query === 'function'
225
- ) {
226
- const db = connection as {
227
- query: (
228
- sql: string,
229
- ...params: unknown[]
230
- ) => Promise<Array<Record<string, unknown>>>;
231
- };
232
- const result = await db.query(sql, ...(params ?? []));
233
- return result as T[];
234
- }
235
-
236
- throw new Error('Invalid Bun.SQL connection');
237
- }
238
-
239
- /**
240
- * 将 SQL 与 ? 占位符参数转换为模板字符串片段
241
- * 让参数通过 Bun.SQL 的 values 通道注入,避免手工拼接 SQL
242
- */
243
- private buildTemplateFromSql(
244
- sql: string,
245
- params?: unknown[],
246
- ): { strings: string[]; values: unknown[] } {
247
- if (!params || params.length === 0) {
248
- return { strings: [sql], values: [] };
249
- }
250
-
251
- const strings = sql.split('?');
252
- if (strings.length !== params.length + 1) {
253
- throw new Error(
254
- 'SQL placeholders count does not match parameters count',
255
- );
256
- }
257
-
258
- return { strings, values: params };
259
- }
260
189
  }
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import type { SqliteV2Config } from './types';
2
2
  import { getRuntime } from '../platform/runtime';
3
3
 
4
+ type SqliteV2InputConfig = SqliteV2Config | Omit<SqliteV2Config, 'type'>;
5
+
4
6
  export interface DisposableLock {
5
7
  [Symbol.dispose](): void;
6
8
  }
@@ -47,7 +49,7 @@ export class SqliteAdapter {
47
49
  public readonly semaphore: Semaphore;
48
50
  private readonly isBun: boolean;
49
51
 
50
- public constructor(config: SqliteV2Config) {
52
+ public constructor(config: SqliteV2InputConfig) {
51
53
  this.isBun = getRuntime().engine === 'bun';
52
54
 
53
55
  if (this.isBun) {
@@ -119,7 +121,7 @@ export class SqliteManager {
119
121
  private readonly instances = new Map<string, SqliteAdapter>();
120
122
  private defaultTenantId = 'default';
121
123
 
122
- public getOrCreate(tenantId: string, config: SqliteV2Config): SqliteAdapter {
124
+ public getOrCreate(tenantId: string, config: SqliteV2InputConfig): SqliteAdapter {
123
125
  const existing = this.instances.get(tenantId);
124
126
  if (existing) {
125
127
  return existing;
@@ -167,4 +169,3 @@ export class SqliteManager {
167
169
  }
168
170
  }
169
171
  }
170
-