@dangao/bun-server 3.0.0 → 3.0.1
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/dist/database/connection-manager.d.ts.map +1 -1
- package/dist/database/connection-pool.d.ts.map +1 -1
- package/dist/database/service.d.ts +2 -1
- package/dist/database/service.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.d.ts +2 -2
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/index.js +64 -795
- package/dist/index.node.mjs +51 -17
- package/package.json +3 -4
- package/src/database/connection-manager.ts +19 -12
- package/src/database/connection-pool.ts +4 -3
- package/src/database/service.ts +21 -5
- package/src/database/sqlite-adapter.ts +19 -11
- package/tests/platform/shared/database.cases.ts +9 -9
- package/docs/design/query-interceptor-design.md +0 -381
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
# ORM 查询拦截器设计方案
|
|
2
|
-
|
|
3
|
-
> 日期:2026-03-17
|
|
4
|
-
> 状态:方案评估阶段
|
|
5
|
-
|
|
6
|
-
## 一、现状分析
|
|
7
|
-
|
|
8
|
-
### 1.1 当前 SQL 执行链路
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
Repository.findAll() / create() / update() / delete()
|
|
12
|
-
→ 手动拼接 SQL 字符串
|
|
13
|
-
→ BaseRepository.executeQuery(sql, params)
|
|
14
|
-
→ DatabaseService.query(sql, params)
|
|
15
|
-
→ getCurrentSession() 获取连接
|
|
16
|
-
→ 直接执行 SQL(无任何拦截点)
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
三条独立的 SQL 执行路径:
|
|
20
|
-
|
|
21
|
-
| 路径 | 入口 | 使用者 |
|
|
22
|
-
|---|---|---|
|
|
23
|
-
| 路径 A | `BaseRepository.executeQuery()` → `DatabaseService.query()` | Repository 模式用户 |
|
|
24
|
-
| 路径 B | `DatabaseService.query()` 直接调用 | Service 层手写 SQL 用户 |
|
|
25
|
-
| 路径 C | `` db`SELECT ...` `` → `BunSQLManager` | db proxy 用户 |
|
|
26
|
-
|
|
27
|
-
### 1.2 关键代码位置
|
|
28
|
-
|
|
29
|
-
| 组件 | 文件路径 |
|
|
30
|
-
|---|---|
|
|
31
|
-
| DatabaseService | `src/database/service.ts` |
|
|
32
|
-
| BaseRepository | `src/database/orm/repository.ts` |
|
|
33
|
-
| DrizzleBaseRepository | `src/database/orm/drizzle-repository.ts` |
|
|
34
|
-
| db proxy | `src/database/db-proxy.ts` |
|
|
35
|
-
| DatabaseModule | `src/database/database-module.ts` |
|
|
36
|
-
| OrmService | `src/database/orm/service.ts` |
|
|
37
|
-
| Entity/Column 装饰器 | `src/database/orm/decorators.ts` |
|
|
38
|
-
| Repository 装饰器 | `src/database/orm/repository-decorator.ts` |
|
|
39
|
-
| TransactionInterceptor | `src/database/orm/transaction-interceptor.ts` |
|
|
40
|
-
| TransactionManager | `src/database/orm/transaction-manager.ts` |
|
|
41
|
-
| BunSQLManager | `src/database/sql-manager.ts` |
|
|
42
|
-
| SqliteAdapter/Manager | `src/database/sqlite-adapter.ts` |
|
|
43
|
-
| DatabaseSession 上下文 | `src/database/database-context.ts` |
|
|
44
|
-
|
|
45
|
-
### 1.3 缺失的扩展点
|
|
46
|
-
|
|
47
|
-
| 缺失能力 | 说明 |
|
|
48
|
-
|---|---|
|
|
49
|
-
| SQL 拦截器链 | `DatabaseService.query()` 前后无 `beforeQuery` / `afterQuery` 钩子 |
|
|
50
|
-
| Repository 层钩子 | `BaseRepository.executeQuery()` 直接调用 service,无法注入自定义逻辑 |
|
|
51
|
-
| 查询构建管道 | SQL 全部手写字符串拼接,无 QueryBuilder 或 AST 抽象层 |
|
|
52
|
-
| 插件注册机制 | ORM 层无插件 API,无法通过 `DatabaseModule.forRoot` 注册拦截器 |
|
|
53
|
-
|
|
54
|
-
### 1.4 Drizzle 集成现状
|
|
55
|
-
|
|
56
|
-
Drizzle 在当前项目中**几乎没有实际集成**:
|
|
57
|
-
|
|
58
|
-
- `DrizzleBaseRepository` 全是 abstract 方法,零实现
|
|
59
|
-
- `OrmModuleOptions.drizzle` 类型是 `unknown`
|
|
60
|
-
- `package.json` 没有 drizzle 依赖
|
|
61
|
-
- `OrmService` 只是一个 drizzle 实例的容器,无查询逻辑
|
|
62
|
-
|
|
63
|
-
结论:**"与 Drizzle 重叠"是伪问题**,不存在真正需要兼容的 Drizzle 代码。
|
|
64
|
-
|
|
65
|
-
### 1.5 多租户现状
|
|
66
|
-
|
|
67
|
-
当前多租户策略是**连接级隔离**(每个租户一个独立数据库连接),不支持**行级隔离**(同库通过 `WHERE tenant_id = ?` 过滤)。
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## 二、初筛方案(已淘汰)
|
|
72
|
-
|
|
73
|
-
### 方案 X1:DatabaseService.query() 拦截器链
|
|
74
|
-
|
|
75
|
-
在 `DatabaseService.query()` 中引入 `QueryInterceptor` 链。
|
|
76
|
-
|
|
77
|
-
- 覆盖路径 A + B,不覆盖路径 C(db proxy)
|
|
78
|
-
- 改动量小(~50 行)
|
|
79
|
-
- SQL 是原始字符串,复杂改写不安全
|
|
80
|
-
- 同步/异步混合签名增加复杂度
|
|
81
|
-
|
|
82
|
-
**淘汰原因**:字符串级操作不安全,路径 C 未覆盖。
|
|
83
|
-
|
|
84
|
-
### 方案 X2:BaseRepository.executeQuery() 钩子
|
|
85
|
-
|
|
86
|
-
在 `BaseRepository` 中暴露 `beforeExecute` / `afterExecute` 钩子。
|
|
87
|
-
|
|
88
|
-
- 仅覆盖路径 A
|
|
89
|
-
- 改动量最小(~40 行)
|
|
90
|
-
- SQL 仍是字符串
|
|
91
|
-
- 多 Repository 需逐个继承
|
|
92
|
-
|
|
93
|
-
**淘汰原因**:覆盖面最窄,仅 Repository 模式受益。
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 三、候选方案(升级方案,不考虑向下兼容)
|
|
98
|
-
|
|
99
|
-
### 方案 A:Kysely 接管查询层(推荐)
|
|
100
|
-
|
|
101
|
-
#### 核心思路
|
|
102
|
-
|
|
103
|
-
引入 Kysely 作为查询引擎,利用其原生 Plugin 系统实现拦截器。
|
|
104
|
-
|
|
105
|
-
#### 解决 db proxy 兼容
|
|
106
|
-
|
|
107
|
-
**路径 1 — 替代**:Kysely 自带 `sql` 模板标签,功能等价于当前 db proxy:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// 旧 API
|
|
111
|
-
const users = await db`SELECT * FROM users WHERE id = ${id}`;
|
|
112
|
-
|
|
113
|
-
// 新 API(Kysely raw SQL)
|
|
114
|
-
const users = await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);
|
|
115
|
-
|
|
116
|
-
// 新 API(QueryBuilder)
|
|
117
|
-
const users = await db.selectFrom('users').where('id', '=', id).execute();
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**路径 2 — 融合**:保留 `db` 的模板字符串调用风格,底层替换为 Kysely 实例执行:
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// 用户代码不变
|
|
124
|
-
const users = await db`SELECT * FROM users`;
|
|
125
|
-
|
|
126
|
-
// 内部: db proxy → Kysely.executeRaw(sql, params) → Plugin chain → 执行
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
#### 解决 Drizzle 重叠
|
|
130
|
-
|
|
131
|
-
- 移除 `DrizzleBaseRepository`(空壳,无实际用户)
|
|
132
|
-
- 移除 `OrmService` 的 drizzle 相关代码
|
|
133
|
-
- Kysely 完全覆盖查询构建能力
|
|
134
|
-
- 用户如需 schema migration,可独立使用 Drizzle Kit CLI
|
|
135
|
-
|
|
136
|
-
#### 拦截器实现
|
|
137
|
-
|
|
138
|
-
Kysely 原生 `KyselyPlugin` 接口:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
interface KyselyPlugin {
|
|
142
|
-
transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
|
|
143
|
-
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
多租户拦截器示例:
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
class TenantPlugin implements KyselyPlugin {
|
|
151
|
-
transformQuery({ node }): RootOperationNode {
|
|
152
|
-
// 在 AST 层面往所有 SELECT/UPDATE/DELETE 追加 WHERE tenant_id = ?
|
|
153
|
-
// 在 INSERT 中自动注入 tenant_id 列
|
|
154
|
-
return addTenantFilter(node, getCurrentTenantId());
|
|
155
|
-
}
|
|
156
|
-
transformResult({ result }) { return result; }
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
操作的是 AST 节点而非 SQL 字符串,安全可靠。
|
|
161
|
-
|
|
162
|
-
#### 需要的额外工作
|
|
163
|
-
|
|
164
|
-
- 编写 `BunSQLDialect`(适配 `Bun.SQL`)
|
|
165
|
-
- 编写 `BunSQLiteDialect`(适配 `bun:sqlite`)
|
|
166
|
-
- 重写 `BaseRepository` 使用 Kysely 查询
|
|
167
|
-
|
|
168
|
-
#### 评估
|
|
169
|
-
|
|
170
|
-
| 维度 | 评价 |
|
|
171
|
-
|---|---|
|
|
172
|
-
| 拦截器能力 | **最强** — 原生 AST 级插件,非字符串操作 |
|
|
173
|
-
| 开发量 | 中等(~3 天) |
|
|
174
|
-
| 新依赖 | 1 个(kysely,轻量) |
|
|
175
|
-
| 类型安全 | 强 — Kysely 天生类型安全 |
|
|
176
|
-
| 风险 | 低 — Kysely 成熟稳定,社区活跃 |
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
### 方案 B:Drizzle 深度集成
|
|
181
|
-
|
|
182
|
-
#### 核心思路
|
|
183
|
-
|
|
184
|
-
将 Drizzle 从 `unknown` 占位符升级为核心引擎,自建拦截层包裹 Drizzle 查询执行。
|
|
185
|
-
|
|
186
|
-
#### 解决 db proxy 兼容
|
|
187
|
-
|
|
188
|
-
`db` proxy 重定义为 Drizzle 实例的包装:
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// Drizzle QueryBuilder 风格
|
|
192
|
-
const users = await db.select().from(usersTable).where(eq(usersTable.id, 1));
|
|
193
|
-
|
|
194
|
-
// 原始 SQL 风格
|
|
195
|
-
const users = await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
#### 解决 Drizzle 重叠
|
|
199
|
-
|
|
200
|
-
不存在重叠 — Drizzle 就是唯一的查询层。`@Entity` / `@Column` 可保留为装饰器语法糖,也可废弃转向 Drizzle 原生 schema。
|
|
201
|
-
|
|
202
|
-
#### 拦截器实现
|
|
203
|
-
|
|
204
|
-
Drizzle 无原生插件系统,有两个技术路线:
|
|
205
|
-
|
|
206
|
-
**路线 1 — 自定义 Driver**:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
class InterceptableDriver implements Driver {
|
|
210
|
-
async execute(query: Query): Promise<Result> {
|
|
211
|
-
let { sql, params } = query;
|
|
212
|
-
for (const interceptor of this.interceptors) {
|
|
213
|
-
({ sql, params } = interceptor.beforeQuery(sql, params, context));
|
|
214
|
-
}
|
|
215
|
-
return this.realDriver.execute({ sql, params });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
**路线 2 — Proxy 包裹 Drizzle 实例**:用 Proxy 拦截所有查询方法。
|
|
221
|
-
|
|
222
|
-
两种路线的问题:Drizzle 在 Driver 层已编译为 SQL 字符串,拦截器仍然是字符串级操作。
|
|
223
|
-
|
|
224
|
-
#### 评估
|
|
225
|
-
|
|
226
|
-
| 维度 | 评价 |
|
|
227
|
-
|---|---|
|
|
228
|
-
| 拦截器能力 | 中等 — Driver 层拦截是字符串级别,非 AST |
|
|
229
|
-
| 开发量 | 中大(~4 天) |
|
|
230
|
-
| 新依赖 | 1 个(drizzle-orm,较重) |
|
|
231
|
-
| 类型安全 | 最强 — Drizzle 类型推导业界领先 |
|
|
232
|
-
| 风险 | 中 — 拦截器实现依赖 Drizzle 内部结构 |
|
|
233
|
-
| 额外收益 | migration / studio / schema push 生态 |
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
### 方案 C:零依赖自研查询协议
|
|
238
|
-
|
|
239
|
-
#### 核心思路
|
|
240
|
-
|
|
241
|
-
定义框架自有的 `QueryIntent` 结构化查询描述,拦截器在对象层面修改查询,最终编译为 SQL。
|
|
242
|
-
|
|
243
|
-
#### 核心设计
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
interface QueryIntent {
|
|
247
|
-
type: 'select' | 'insert' | 'update' | 'delete';
|
|
248
|
-
table: string;
|
|
249
|
-
columns?: string[];
|
|
250
|
-
where?: WhereClause[];
|
|
251
|
-
joins?: JoinClause[];
|
|
252
|
-
values?: Record<string, unknown>;
|
|
253
|
-
orderBy?: OrderByClause[];
|
|
254
|
-
limit?: number;
|
|
255
|
-
offset?: number;
|
|
256
|
-
raw?: { sql: string; params: unknown[] }; // 原始 SQL 逃生舱
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
interface QueryInterceptor {
|
|
260
|
-
intercept(intent: QueryIntent, context: QueryContext): QueryIntent;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### 解决 db proxy 兼容
|
|
265
|
-
|
|
266
|
-
`db` proxy 模板字符串调用产出 `raw` 类型的 QueryIntent:
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// 用户写法不变
|
|
270
|
-
const users = await db`SELECT * FROM users WHERE id = ${id}`;
|
|
271
|
-
|
|
272
|
-
// 内部流程:
|
|
273
|
-
// 1. 解析模板字符串 → QueryIntent { raw: { sql, params } }
|
|
274
|
-
// 2. 过拦截器链
|
|
275
|
-
// 3. 编译执行
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
同时提供结构化 API:
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
const users = await db.from('users').where('id', '=', id).select();
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
#### 解决 Drizzle 重叠
|
|
285
|
-
|
|
286
|
-
完全移除 Drizzle 相关代码,框架自主可控。
|
|
287
|
-
|
|
288
|
-
#### 拦截器实现
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
class TenantInterceptor implements QueryInterceptor {
|
|
292
|
-
intercept(intent: QueryIntent, ctx: QueryContext): QueryIntent {
|
|
293
|
-
if (intent.raw) {
|
|
294
|
-
return { ...intent, raw: appendTenantFilter(intent.raw, ctx.tenantId) };
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
...intent,
|
|
298
|
-
where: [...(intent.where ?? []), { column: 'tenant_id', op: '=', value: ctx.tenantId }],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
#### 评估
|
|
305
|
-
|
|
306
|
-
| 维度 | 评价 |
|
|
307
|
-
|---|---|
|
|
308
|
-
| 拦截器能力 | 高 — 结构化查询是 AST 级;raw SQL 退化为字符串级 |
|
|
309
|
-
| 开发量 | 大(~5-7 天) |
|
|
310
|
-
| 新依赖 | 0 |
|
|
311
|
-
| 类型安全 | 中 — 需额外投入 |
|
|
312
|
-
| 风险 | 中 — QueryBuilder 完备性需打磨 |
|
|
313
|
-
| 额外收益 | 架构完全自主,未来可对接任意后端 |
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
## 四、三方案横向对比
|
|
318
|
-
|
|
319
|
-
| 维度 | A. Kysely | B. Drizzle | C. 自研 |
|
|
320
|
-
|---|---|---|---|
|
|
321
|
-
| **拦截器级别** | AST 原生 | SQL 字符串(Driver 层) | 结构化对象 + raw 退化 |
|
|
322
|
-
| **db proxy 方案** | 替代或融合均可 | 重定义为 Drizzle 包装 | 保留风格,内部走协议 |
|
|
323
|
-
| **Drizzle 处理** | 移除(空壳) | 升级为核心 | 移除 |
|
|
324
|
-
| **新增依赖** | kysely(轻量) | drizzle-orm(较重) | 无 |
|
|
325
|
-
| **类型安全** | 强 | 最强 | 需额外投入 |
|
|
326
|
-
| **拦截器实现难度** | 低(原生支持) | 中(需 hack) | 中(需自建) |
|
|
327
|
-
| **生态/附加能力** | 纯查询 | migration/studio | 无 |
|
|
328
|
-
| **总开发量** | ~3 天 | ~4 天 | ~5-7 天 |
|
|
329
|
-
| **长期维护成本** | 低 | 中 | 高 |
|
|
330
|
-
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
## 五、待清理的遗留代码
|
|
334
|
-
|
|
335
|
-
无论选择哪个方案,以下代码应当移除或重构:
|
|
336
|
-
|
|
337
|
-
| 文件 | 处理方式 |
|
|
338
|
-
|---|---|
|
|
339
|
-
| `src/database/orm/drizzle-repository.ts` | 移除(空壳,全 abstract) |
|
|
340
|
-
| `src/database/orm/service.ts` | 移除或重构(仅是 drizzle 实例容器) |
|
|
341
|
-
| `src/database/orm/types.ts` 中 `OrmModuleOptions.drizzle` | 移除 |
|
|
342
|
-
| `src/database/service.ts` 中 `DatabaseService.query()` | 重构为走新查询引擎 |
|
|
343
|
-
| `src/database/db-proxy.ts` | 重构底层执行路径 |
|
|
344
|
-
| `src/database/orm/repository.ts` 中手写 SQL | 重构为查询构建器 |
|
|
345
|
-
|
|
346
|
-
---
|
|
347
|
-
|
|
348
|
-
## 六、实施建议
|
|
349
|
-
|
|
350
|
-
### 推荐:方案 A(Kysely)
|
|
351
|
-
|
|
352
|
-
理由:
|
|
353
|
-
1. 原生 AST 级 Plugin 是为拦截器场景设计的,不需要任何 hack
|
|
354
|
-
2. Kysely 足够轻量(纯查询构建器),与框架"Bun 原生、轻量"定位一致
|
|
355
|
-
3. 开发量适中,风险低
|
|
356
|
-
4. 社区活跃,Bun 兼容性好
|
|
357
|
-
|
|
358
|
-
### 实施步骤(草案)
|
|
359
|
-
|
|
360
|
-
1. **Phase 1 — 基础设施**
|
|
361
|
-
- 引入 kysely 依赖
|
|
362
|
-
- 编写 BunSQLDialect(适配 Bun.SQL)
|
|
363
|
-
- 编写 BunSQLiteDialect(适配 bun:sqlite)
|
|
364
|
-
- 在 DatabaseModule 中创建 Kysely 实例
|
|
365
|
-
|
|
366
|
-
2. **Phase 2 — 查询层迁移**
|
|
367
|
-
- 重写 BaseRepository 使用 Kysely 查询
|
|
368
|
-
- 重构 db proxy 底层走 Kysely 执行
|
|
369
|
-
- 移除 DrizzleBaseRepository、OrmService 的 drizzle 代码
|
|
370
|
-
|
|
371
|
-
3. **Phase 3 — 拦截器体系**
|
|
372
|
-
- 定义 QueryPlugin 接口(包装 KyselyPlugin)
|
|
373
|
-
- 实现 TenantPlugin(多租户行级过滤)
|
|
374
|
-
- 在 DatabaseModule.forRoot() 中支持 plugins 配置
|
|
375
|
-
- 编写示例和文档
|
|
376
|
-
|
|
377
|
-
4. **Phase 4 — 清理和测试**
|
|
378
|
-
- 移除遗留代码
|
|
379
|
-
- 编写单元测试
|
|
380
|
-
- 更新示例应用
|
|
381
|
-
- 更新文档
|