@zhin.js/database 1.0.4 → 1.0.6
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/CHANGELOG.md +12 -0
- package/README.md +1360 -34
- package/lib/base/database.d.ts +71 -13
- package/lib/base/database.d.ts.map +1 -1
- package/lib/base/database.js +128 -4
- package/lib/base/database.js.map +1 -1
- package/lib/base/dialect.d.ts +27 -10
- package/lib/base/dialect.d.ts.map +1 -1
- package/lib/base/dialect.js +32 -0
- package/lib/base/dialect.js.map +1 -1
- package/lib/base/index.d.ts +1 -0
- package/lib/base/index.d.ts.map +1 -1
- package/lib/base/index.js +1 -0
- package/lib/base/index.js.map +1 -1
- package/lib/base/model.d.ts +105 -12
- package/lib/base/model.d.ts.map +1 -1
- package/lib/base/model.js +224 -3
- package/lib/base/model.js.map +1 -1
- package/lib/base/query-classes.d.ts +204 -33
- package/lib/base/query-classes.d.ts.map +1 -1
- package/lib/base/query-classes.js +276 -0
- package/lib/base/query-classes.js.map +1 -1
- package/lib/base/thenable.d.ts +7 -7
- package/lib/base/thenable.d.ts.map +1 -1
- package/lib/base/thenable.js +5 -4
- package/lib/base/thenable.js.map +1 -1
- package/lib/base/transaction.d.ts +46 -0
- package/lib/base/transaction.d.ts.map +1 -0
- package/lib/base/transaction.js +186 -0
- package/lib/base/transaction.js.map +1 -0
- package/lib/dialects/memory.d.ts +12 -7
- package/lib/dialects/memory.d.ts.map +1 -1
- package/lib/dialects/memory.js +7 -4
- package/lib/dialects/memory.js.map +1 -1
- package/lib/dialects/mongodb.d.ts +11 -7
- package/lib/dialects/mongodb.d.ts.map +1 -1
- package/lib/dialects/mongodb.js +18 -15
- package/lib/dialects/mongodb.js.map +1 -1
- package/lib/dialects/mysql.d.ts +35 -6
- package/lib/dialects/mysql.d.ts.map +1 -1
- package/lib/dialects/mysql.js +137 -18
- package/lib/dialects/mysql.js.map +1 -1
- package/lib/dialects/pg.d.ts +35 -6
- package/lib/dialects/pg.d.ts.map +1 -1
- package/lib/dialects/pg.js +137 -18
- package/lib/dialects/pg.js.map +1 -1
- package/lib/dialects/redis.d.ts +11 -6
- package/lib/dialects/redis.d.ts.map +1 -1
- package/lib/dialects/redis.js +11 -8
- package/lib/dialects/redis.js.map +1 -1
- package/lib/dialects/sqlite.d.ts +19 -6
- package/lib/dialects/sqlite.d.ts.map +1 -1
- package/lib/dialects/sqlite.js +63 -10
- package/lib/dialects/sqlite.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/migration.d.ts +132 -0
- package/lib/migration.d.ts.map +1 -0
- package/lib/migration.js +475 -0
- package/lib/migration.js.map +1 -0
- package/lib/registry.d.ts +26 -23
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +1 -5
- package/lib/registry.js.map +1 -1
- package/lib/type/document/database.d.ts +11 -11
- package/lib/type/document/database.d.ts.map +1 -1
- package/lib/type/document/database.js.map +1 -1
- package/lib/type/document/model.d.ts +7 -7
- package/lib/type/document/model.d.ts.map +1 -1
- package/lib/type/document/model.js.map +1 -1
- package/lib/type/keyvalue/database.d.ts +11 -11
- package/lib/type/keyvalue/database.d.ts.map +1 -1
- package/lib/type/keyvalue/database.js.map +1 -1
- package/lib/type/keyvalue/model.d.ts +2 -2
- package/lib/type/keyvalue/model.d.ts.map +1 -1
- package/lib/type/keyvalue/model.js.map +1 -1
- package/lib/type/related/database.d.ts +48 -13
- package/lib/type/related/database.d.ts.map +1 -1
- package/lib/type/related/database.js +258 -27
- package/lib/type/related/database.js.map +1 -1
- package/lib/type/related/model.d.ts +251 -15
- package/lib/type/related/model.d.ts.map +1 -1
- package/lib/type/related/model.js +647 -22
- package/lib/type/related/model.js.map +1 -1
- package/lib/types.d.ts +475 -37
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +6 -0
- package/lib/types.js.map +1 -1
- package/package.json +14 -5
- package/src/base/database.ts +168 -24
- package/src/base/dialect.ts +49 -10
- package/src/base/index.ts +2 -1
- package/src/base/model.ts +258 -18
- package/src/base/query-classes.ts +471 -63
- package/src/base/thenable.ts +12 -11
- package/src/base/transaction.ts +213 -0
- package/src/dialects/memory.ts +14 -13
- package/src/dialects/mongodb.ts +40 -38
- package/src/dialects/mysql.ts +151 -22
- package/src/dialects/pg.ts +148 -21
- package/src/dialects/redis.ts +40 -38
- package/src/dialects/sqlite.ts +73 -15
- package/src/index.ts +1 -2
- package/src/migration.ts +544 -0
- package/src/registry.ts +33 -33
- package/src/type/document/database.ts +32 -32
- package/src/type/document/model.ts +14 -14
- package/src/type/keyvalue/database.ts +32 -32
- package/src/type/keyvalue/model.ts +18 -18
- package/src/type/related/database.ts +309 -34
- package/src/type/related/model.ts +800 -33
- package/src/types.ts +559 -44
- package/tests/database.test.ts +1738 -0
|
@@ -1,29 +1,516 @@
|
|
|
1
1
|
import { Model } from '../../base/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 关联查询构建器
|
|
4
|
+
* 支持链式调用预加载关联数据
|
|
5
|
+
*/
|
|
6
|
+
export class RelationQueryBuilder {
|
|
7
|
+
model;
|
|
8
|
+
relationNames;
|
|
9
|
+
conditions = {};
|
|
10
|
+
orderings = [];
|
|
11
|
+
limitCount;
|
|
12
|
+
offsetCount;
|
|
13
|
+
selectedFields;
|
|
14
|
+
constructor(model, relationNames) {
|
|
15
|
+
this.model = model;
|
|
16
|
+
this.relationNames = relationNames;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 选择字段
|
|
20
|
+
*/
|
|
21
|
+
select(...fields) {
|
|
22
|
+
this.selectedFields = fields;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 添加查询条件
|
|
27
|
+
*/
|
|
28
|
+
where(condition) {
|
|
29
|
+
this.conditions = { ...this.conditions, ...condition };
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 排序
|
|
34
|
+
*/
|
|
35
|
+
orderBy(field, direction = 'ASC') {
|
|
36
|
+
this.orderings.push({ field, direction });
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 限制数量
|
|
41
|
+
*/
|
|
42
|
+
limit(count) {
|
|
43
|
+
this.limitCount = count;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 偏移量
|
|
48
|
+
*/
|
|
49
|
+
offset(count) {
|
|
50
|
+
this.offsetCount = count;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 执行查询并加载关联
|
|
55
|
+
*/
|
|
56
|
+
async then(onfulfilled) {
|
|
57
|
+
// 构建主查询
|
|
58
|
+
let selection = this.model.select(...(this.selectedFields || []));
|
|
59
|
+
if (Object.keys(this.conditions).length > 0) {
|
|
60
|
+
selection = selection.where(this.conditions);
|
|
61
|
+
}
|
|
62
|
+
for (const { field, direction } of this.orderings) {
|
|
63
|
+
selection = selection.orderBy(field, direction);
|
|
64
|
+
}
|
|
65
|
+
if (this.limitCount !== undefined) {
|
|
66
|
+
selection = selection.limit(this.limitCount);
|
|
67
|
+
}
|
|
68
|
+
if (this.offsetCount !== undefined) {
|
|
69
|
+
selection = selection.offset(this.offsetCount);
|
|
70
|
+
}
|
|
71
|
+
// 执行主查询
|
|
72
|
+
const records = await selection;
|
|
73
|
+
// 加载关联
|
|
74
|
+
const result = await this.model.loadRelations(records, this.relationNames);
|
|
75
|
+
return onfulfilled ? onfulfilled(result) : result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
2
78
|
/**
|
|
3
79
|
* 关系型模型类
|
|
4
80
|
* 继承自 BaseModel,提供关系型数据库特有的操作
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* // 创建带软删除的模型
|
|
85
|
+
* const userModel = new RelatedModel(db, 'users', { softDelete: true });
|
|
86
|
+
*
|
|
87
|
+
* // 删除(实际执行 UPDATE SET deletedAt = NOW())
|
|
88
|
+
* await userModel.delete({ id: 1 });
|
|
89
|
+
*
|
|
90
|
+
* // 查询(自动排除已删除)
|
|
91
|
+
* await userModel.select('id', 'name');
|
|
92
|
+
*
|
|
93
|
+
* // 查询包含已删除
|
|
94
|
+
* await userModel.selectWithTrashed('id', 'name');
|
|
95
|
+
*
|
|
96
|
+
* // 恢复已删除
|
|
97
|
+
* await userModel.restore({ id: 1 });
|
|
98
|
+
* ```
|
|
5
99
|
*/
|
|
6
100
|
export class RelatedModel extends Model {
|
|
7
|
-
|
|
8
|
-
|
|
101
|
+
/** 关系定义存储 */
|
|
102
|
+
relations = new Map();
|
|
103
|
+
constructor(database, name, options) {
|
|
104
|
+
super(database, name, options);
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// 关系定义方法
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* 定义一对多关系
|
|
111
|
+
* @param targetModel 目标模型实例
|
|
112
|
+
* @param foreignKey 目标表中的外键字段
|
|
113
|
+
* @param localKey 本表的主键字段(默认 'id')
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const userModel = db.model('users');
|
|
117
|
+
* const orderModel = db.model('orders');
|
|
118
|
+
*
|
|
119
|
+
* // User hasMany Orders (orders.userId -> users.id)
|
|
120
|
+
* userModel.hasMany(orderModel, 'userId');
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
hasMany(targetModel, foreignKey, localKey = 'id') {
|
|
124
|
+
const relationName = String(targetModel.name);
|
|
125
|
+
this.relations.set(relationName, {
|
|
126
|
+
type: 'hasMany',
|
|
127
|
+
target: targetModel.name,
|
|
128
|
+
foreignKey: foreignKey,
|
|
129
|
+
localKey,
|
|
130
|
+
});
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 定义多对一关系
|
|
135
|
+
* @param targetModel 目标模型实例
|
|
136
|
+
* @param foreignKey 本表中的外键字段
|
|
137
|
+
* @param targetKey 目标表的主键字段(默认 'id')
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const userModel = db.model('users');
|
|
141
|
+
* const orderModel = db.model('orders');
|
|
142
|
+
*
|
|
143
|
+
* // Order belongsTo User (orders.userId -> users.id)
|
|
144
|
+
* orderModel.belongsTo(userModel, 'userId');
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
belongsTo(targetModel, foreignKey, targetKey = 'id') {
|
|
148
|
+
const relationName = String(targetModel.name);
|
|
149
|
+
this.relations.set(relationName, {
|
|
150
|
+
type: 'belongsTo',
|
|
151
|
+
target: targetModel.name,
|
|
152
|
+
foreignKey,
|
|
153
|
+
targetKey: targetKey,
|
|
154
|
+
});
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 定义一对一关系
|
|
159
|
+
* @param targetModel 目标模型实例
|
|
160
|
+
* @param foreignKey 目标表中的外键字段
|
|
161
|
+
* @param localKey 本表的主键字段(默认 'id')
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* const userModel = db.model('users');
|
|
165
|
+
* const profileModel = db.model('profiles');
|
|
166
|
+
*
|
|
167
|
+
* // User hasOne Profile (profiles.userId -> users.id)
|
|
168
|
+
* userModel.hasOne(profileModel, 'userId');
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
hasOne(targetModel, foreignKey, localKey = 'id') {
|
|
172
|
+
const relationName = String(targetModel.name);
|
|
173
|
+
this.relations.set(relationName, {
|
|
174
|
+
type: 'hasOne',
|
|
175
|
+
target: targetModel.name,
|
|
176
|
+
foreignKey: foreignKey,
|
|
177
|
+
localKey,
|
|
178
|
+
});
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 定义多对多关系
|
|
183
|
+
* @param targetModel 目标模型实例
|
|
184
|
+
* @param pivotTable 中间表名
|
|
185
|
+
* @param foreignPivotKey 中间表中指向本表的外键
|
|
186
|
+
* @param relatedPivotKey 中间表中指向目标表的外键
|
|
187
|
+
* @param localKey 本表的主键字段(默认 'id')
|
|
188
|
+
* @param relatedKey 目标表的主键字段(默认 'id')
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* const userModel = db.model('users');
|
|
192
|
+
* const roleModel = db.model('roles');
|
|
193
|
+
*
|
|
194
|
+
* // User belongsToMany Roles (通过 user_roles 中间表)
|
|
195
|
+
* userModel.belongsToMany(roleModel, 'user_roles', 'user_id', 'role_id');
|
|
196
|
+
*
|
|
197
|
+
* // 双向关系
|
|
198
|
+
* roleModel.belongsToMany(userModel, 'user_roles', 'role_id', 'user_id');
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
belongsToMany(targetModel, pivotTable, foreignPivotKey, relatedPivotKey, localKey = 'id', relatedKey = 'id', pivotFields) {
|
|
202
|
+
const relationName = String(targetModel.name);
|
|
203
|
+
this.relations.set(relationName, {
|
|
204
|
+
type: 'belongsToMany',
|
|
205
|
+
target: targetModel.name,
|
|
206
|
+
foreignKey: localKey,
|
|
207
|
+
targetKey: relatedKey,
|
|
208
|
+
localKey: localKey,
|
|
209
|
+
pivot: {
|
|
210
|
+
table: pivotTable,
|
|
211
|
+
foreignPivotKey,
|
|
212
|
+
relatedPivotKey,
|
|
213
|
+
pivotFields,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 获取关系定义
|
|
220
|
+
*/
|
|
221
|
+
getRelation(name) {
|
|
222
|
+
return this.relations.get(name);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 获取所有关系名称
|
|
226
|
+
*/
|
|
227
|
+
getRelationNames() {
|
|
228
|
+
return Array.from(this.relations.keys());
|
|
9
229
|
}
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// 关系查询方法
|
|
232
|
+
// ============================================================================
|
|
10
233
|
/**
|
|
11
|
-
*
|
|
234
|
+
* 加载单条记录的关联数据
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const user = await userModel.selectById(1);
|
|
238
|
+
* const userWithPosts = await userModel.loadRelation(user, 'posts');
|
|
239
|
+
* // userWithPosts.posts = [{ id: 1, title: '...' }, ...]
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
async loadRelation(record, relationName) {
|
|
243
|
+
const relation = this.relations.get(relationName);
|
|
244
|
+
if (!relation) {
|
|
245
|
+
throw new Error(`Relation "${relationName}" not defined on model "${String(this.name)}"`);
|
|
246
|
+
}
|
|
247
|
+
const relatedData = await this.fetchRelatedData(record, relation);
|
|
248
|
+
return {
|
|
249
|
+
...record,
|
|
250
|
+
[relationName]: relatedData,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 批量加载关联数据(预加载)
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const users = await userModel.select('id', 'name');
|
|
258
|
+
* const usersWithPosts = await userModel.loadRelations(users, ['posts']);
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
async loadRelations(records, relationNames) {
|
|
262
|
+
if (records.length === 0)
|
|
263
|
+
return [];
|
|
264
|
+
const result = [...records];
|
|
265
|
+
for (const relationName of relationNames) {
|
|
266
|
+
const relation = this.relations.get(relationName);
|
|
267
|
+
if (!relation) {
|
|
268
|
+
throw new Error(`Relation "${relationName}" not defined on model "${String(this.name)}"`);
|
|
269
|
+
}
|
|
270
|
+
await this.batchLoadRelation(result, relationName, relation);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 带关联的查询(链式调用入口)
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const users = await userModel.with('posts', 'profile')
|
|
279
|
+
* .where({ status: 'active' });
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
with(...relationNames) {
|
|
283
|
+
return new RelationQueryBuilder(this, relationNames);
|
|
284
|
+
}
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// 内部方法
|
|
287
|
+
// ============================================================================
|
|
288
|
+
/**
|
|
289
|
+
* 获取单条记录的关联数据
|
|
290
|
+
*/
|
|
291
|
+
async fetchRelatedData(record, relation) {
|
|
292
|
+
const targetDb = this.database;
|
|
293
|
+
switch (relation.type) {
|
|
294
|
+
case 'hasMany': {
|
|
295
|
+
const localValue = record[relation.localKey || 'id'];
|
|
296
|
+
const results = await targetDb.select(relation.target, [])
|
|
297
|
+
.where({ [relation.foreignKey]: localValue });
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
case 'hasOne': {
|
|
301
|
+
const localValue = record[relation.localKey || 'id'];
|
|
302
|
+
const results = await targetDb.select(relation.target, [])
|
|
303
|
+
.where({ [relation.foreignKey]: localValue })
|
|
304
|
+
.limit(1);
|
|
305
|
+
return results.length > 0 ? results[0] : null;
|
|
306
|
+
}
|
|
307
|
+
case 'belongsTo': {
|
|
308
|
+
const foreignValue = record[relation.foreignKey];
|
|
309
|
+
if (foreignValue === null || foreignValue === undefined) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const results = await targetDb.select(relation.target, [])
|
|
313
|
+
.where({ [relation.targetKey || 'id']: foreignValue })
|
|
314
|
+
.limit(1);
|
|
315
|
+
return results.length > 0 ? results[0] : null;
|
|
316
|
+
}
|
|
317
|
+
case 'belongsToMany': {
|
|
318
|
+
if (!relation.pivot) {
|
|
319
|
+
throw new Error('belongsToMany relation requires pivot configuration');
|
|
320
|
+
}
|
|
321
|
+
const localKey = relation.localKey || 'id';
|
|
322
|
+
const localValue = record[localKey];
|
|
323
|
+
const { table: pivotTable, foreignPivotKey, relatedPivotKey, pivotFields } = relation.pivot;
|
|
324
|
+
const targetKey = relation.targetKey || 'id';
|
|
325
|
+
// 查询中间表获取关联的目标ID
|
|
326
|
+
const pivotRecords = await targetDb.query(`SELECT * FROM "${pivotTable}" WHERE "${foreignPivotKey}" = ?`, [localValue]);
|
|
327
|
+
if (pivotRecords.length === 0) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
// 获取目标表数据
|
|
331
|
+
const relatedIds = pivotRecords.map(p => p[relatedPivotKey]);
|
|
332
|
+
const relatedRecords = await targetDb.select(relation.target, [])
|
|
333
|
+
.where({ [targetKey]: { $in: relatedIds } });
|
|
334
|
+
// 如果需要包含 pivot 数据,将其附加到每条记录
|
|
335
|
+
if (pivotFields && pivotFields.length > 0) {
|
|
336
|
+
const pivotMap = new Map();
|
|
337
|
+
pivotRecords.forEach(p => pivotMap.set(p[relatedPivotKey], p));
|
|
338
|
+
return relatedRecords.map(r => ({
|
|
339
|
+
...r,
|
|
340
|
+
pivot: pivotMap.get(r[targetKey]) || {}
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
return relatedRecords;
|
|
344
|
+
}
|
|
345
|
+
default:
|
|
346
|
+
throw new Error(`Unknown relation type: ${relation.type}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 批量加载关联(优化 N+1 问题)
|
|
351
|
+
*/
|
|
352
|
+
async batchLoadRelation(records, relationName, relation) {
|
|
353
|
+
const targetDb = this.database;
|
|
354
|
+
switch (relation.type) {
|
|
355
|
+
case 'hasMany':
|
|
356
|
+
case 'hasOne': {
|
|
357
|
+
// 收集所有本地主键值
|
|
358
|
+
const localKey = relation.localKey || 'id';
|
|
359
|
+
const localValues = records.map(r => r[localKey]).filter(v => v != null);
|
|
360
|
+
if (localValues.length === 0) {
|
|
361
|
+
records.forEach(r => r[relationName] = relation.type === 'hasMany' ? [] : null);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// 一次性查询所有关联数据
|
|
365
|
+
const relatedRecords = await targetDb.select(relation.target, [])
|
|
366
|
+
.where({ [relation.foreignKey]: { $in: localValues } });
|
|
367
|
+
// 按外键分组
|
|
368
|
+
const grouped = new Map();
|
|
369
|
+
for (const related of relatedRecords) {
|
|
370
|
+
const fkValue = related[relation.foreignKey];
|
|
371
|
+
if (!grouped.has(fkValue)) {
|
|
372
|
+
grouped.set(fkValue, []);
|
|
373
|
+
}
|
|
374
|
+
grouped.get(fkValue).push(related);
|
|
375
|
+
}
|
|
376
|
+
// 分配给每条记录
|
|
377
|
+
for (const record of records) {
|
|
378
|
+
const localValue = record[localKey];
|
|
379
|
+
const related = grouped.get(localValue) || [];
|
|
380
|
+
record[relationName] = relation.type === 'hasMany' ? related : (related[0] || null);
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case 'belongsTo': {
|
|
385
|
+
// 收集所有外键值
|
|
386
|
+
const foreignValues = records
|
|
387
|
+
.map(r => r[relation.foreignKey])
|
|
388
|
+
.filter(v => v != null);
|
|
389
|
+
if (foreignValues.length === 0) {
|
|
390
|
+
records.forEach(r => r[relationName] = null);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// 一次性查询所有关联数据
|
|
394
|
+
const targetKey = relation.targetKey || 'id';
|
|
395
|
+
const relatedRecords = await targetDb.select(relation.target, [])
|
|
396
|
+
.where({ [targetKey]: { $in: foreignValues } });
|
|
397
|
+
// 按主键索引
|
|
398
|
+
const indexed = new Map();
|
|
399
|
+
for (const related of relatedRecords) {
|
|
400
|
+
indexed.set(related[targetKey], related);
|
|
401
|
+
}
|
|
402
|
+
// 分配给每条记录
|
|
403
|
+
for (const record of records) {
|
|
404
|
+
const fkValue = record[relation.foreignKey];
|
|
405
|
+
record[relationName] = indexed.get(fkValue) || null;
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case 'belongsToMany': {
|
|
410
|
+
if (!relation.pivot) {
|
|
411
|
+
throw new Error('belongsToMany relation requires pivot configuration');
|
|
412
|
+
}
|
|
413
|
+
const localKey = relation.localKey || 'id';
|
|
414
|
+
const targetKey = relation.targetKey || 'id';
|
|
415
|
+
const { table: pivotTable, foreignPivotKey, relatedPivotKey, pivotFields } = relation.pivot;
|
|
416
|
+
// 收集所有本地主键值
|
|
417
|
+
const localValues = records.map(r => r[localKey]).filter(v => v != null);
|
|
418
|
+
if (localValues.length === 0) {
|
|
419
|
+
records.forEach(r => r[relationName] = []);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// 批量查询中间表
|
|
423
|
+
const placeholders = localValues.map(() => '?').join(', ');
|
|
424
|
+
const pivotRecords = await targetDb.query(`SELECT * FROM "${pivotTable}" WHERE "${foreignPivotKey}" IN (${placeholders})`, localValues);
|
|
425
|
+
if (pivotRecords.length === 0) {
|
|
426
|
+
records.forEach(r => r[relationName] = []);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// 获取所有相关的目标ID
|
|
430
|
+
const allRelatedIds = [...new Set(pivotRecords.map(p => p[relatedPivotKey]))];
|
|
431
|
+
// 批量查询目标表
|
|
432
|
+
const relatedRecords = await targetDb.select(relation.target, [])
|
|
433
|
+
.where({ [targetKey]: { $in: allRelatedIds } });
|
|
434
|
+
// 建立目标记录索引
|
|
435
|
+
const relatedIndex = new Map();
|
|
436
|
+
for (const related of relatedRecords) {
|
|
437
|
+
relatedIndex.set(related[targetKey], related);
|
|
438
|
+
}
|
|
439
|
+
// 按源ID分组中间表记录
|
|
440
|
+
const pivotGrouped = new Map();
|
|
441
|
+
for (const pivot of pivotRecords) {
|
|
442
|
+
const srcId = pivot[foreignPivotKey];
|
|
443
|
+
if (!pivotGrouped.has(srcId)) {
|
|
444
|
+
pivotGrouped.set(srcId, []);
|
|
445
|
+
}
|
|
446
|
+
pivotGrouped.get(srcId).push(pivot);
|
|
447
|
+
}
|
|
448
|
+
// 分配给每条记录
|
|
449
|
+
for (const record of records) {
|
|
450
|
+
const localValue = record[localKey];
|
|
451
|
+
const pivots = pivotGrouped.get(localValue) || [];
|
|
452
|
+
record[relationName] = pivots.map(pivot => {
|
|
453
|
+
const related = relatedIndex.get(pivot[relatedPivotKey]);
|
|
454
|
+
if (!related)
|
|
455
|
+
return null;
|
|
456
|
+
// 如果需要包含 pivot 数据
|
|
457
|
+
if (pivotFields && pivotFields.length > 0) {
|
|
458
|
+
const pivotData = {};
|
|
459
|
+
pivotFields.forEach(field => {
|
|
460
|
+
pivotData[field] = pivot[field];
|
|
461
|
+
});
|
|
462
|
+
return { ...related, pivot: pivotData };
|
|
463
|
+
}
|
|
464
|
+
return related;
|
|
465
|
+
}).filter(Boolean);
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// 带钩子的 CRUD 便捷方法
|
|
473
|
+
// ============================================================================
|
|
474
|
+
/**
|
|
475
|
+
* 创建数据(支持生命周期钩子)
|
|
476
|
+
* @example
|
|
477
|
+
* ```ts
|
|
478
|
+
* userModel.addHook('beforeCreate', (ctx) => {
|
|
479
|
+
* ctx.data.slug = slugify(ctx.data.name);
|
|
480
|
+
* });
|
|
481
|
+
* const user = await userModel.create({ name: 'John' });
|
|
482
|
+
* ```
|
|
12
483
|
*/
|
|
13
484
|
async create(data) {
|
|
14
485
|
if (!this.validateData(data)) {
|
|
15
486
|
throw new Error('Invalid data provided');
|
|
16
487
|
}
|
|
488
|
+
// 复制数据以避免修改原始对象
|
|
489
|
+
const inputData = { ...data };
|
|
490
|
+
// beforeCreate 钩子
|
|
491
|
+
const ctx = this.createHookContext({ data: inputData });
|
|
492
|
+
const shouldContinue = await this.runHooks('beforeCreate', ctx);
|
|
493
|
+
if (!shouldContinue) {
|
|
494
|
+
return null; // 钩子取消了操作
|
|
495
|
+
}
|
|
17
496
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
497
|
+
// 获取 insert 方法添加的时间戳
|
|
498
|
+
const insertData = { ...ctx.data };
|
|
499
|
+
await this.insert(insertData);
|
|
500
|
+
// 返回插入的数据(包括时间戳)
|
|
501
|
+
// 注意:SQLite INSERT 不返回实际数据,所以我们返回传入的数据
|
|
502
|
+
const result = insertData;
|
|
503
|
+
// afterCreate 钩子
|
|
504
|
+
ctx.result = result;
|
|
505
|
+
await this.runHooks('afterCreate', ctx);
|
|
506
|
+
return ctx.result;
|
|
20
507
|
}
|
|
21
508
|
catch (error) {
|
|
22
509
|
this.handleError(error, 'create');
|
|
23
510
|
}
|
|
24
511
|
}
|
|
25
512
|
/**
|
|
26
|
-
*
|
|
513
|
+
* 批量创建数据(每条数据都会触发钩子)
|
|
27
514
|
*/
|
|
28
515
|
async createMany(data) {
|
|
29
516
|
if (!Array.isArray(data) || data.length === 0) {
|
|
@@ -33,7 +520,9 @@ export class RelatedModel extends Model {
|
|
|
33
520
|
const results = [];
|
|
34
521
|
for (const item of data) {
|
|
35
522
|
const result = await this.create(item);
|
|
36
|
-
|
|
523
|
+
if (result) {
|
|
524
|
+
results.push(result);
|
|
525
|
+
}
|
|
37
526
|
}
|
|
38
527
|
return results;
|
|
39
528
|
}
|
|
@@ -42,39 +531,118 @@ export class RelatedModel extends Model {
|
|
|
42
531
|
}
|
|
43
532
|
}
|
|
44
533
|
/**
|
|
45
|
-
*
|
|
534
|
+
* 查找单个数据(支持生命周期钩子)
|
|
535
|
+
* @example
|
|
536
|
+
* ```ts
|
|
537
|
+
* userModel.addHook('afterFind', (ctx) => {
|
|
538
|
+
* if (ctx.result) {
|
|
539
|
+
* ctx.result.fullName = ctx.result.firstName + ' ' + ctx.result.lastName;
|
|
540
|
+
* }
|
|
541
|
+
* });
|
|
542
|
+
* const user = await userModel.findOne({ id: 1 });
|
|
543
|
+
* ```
|
|
46
544
|
*/
|
|
47
|
-
async
|
|
545
|
+
async findOne(query) {
|
|
546
|
+
// beforeFind 钩子
|
|
547
|
+
const ctx = this.createHookContext({ where: query });
|
|
548
|
+
const shouldContinue = await this.runHooks('beforeFind', ctx);
|
|
549
|
+
if (!shouldContinue) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
48
552
|
try {
|
|
49
553
|
const selection = this.select();
|
|
50
|
-
if (
|
|
51
|
-
selection.where(
|
|
554
|
+
if (ctx.where) {
|
|
555
|
+
selection.where(ctx.where);
|
|
52
556
|
}
|
|
53
557
|
const results = await selection.limit(1);
|
|
54
|
-
|
|
558
|
+
const result = results.length > 0 ? results[0] : null;
|
|
559
|
+
// afterFind 钩子
|
|
560
|
+
ctx.result = result ?? undefined;
|
|
561
|
+
await this.runHooks('afterFind', ctx);
|
|
562
|
+
return ctx.result ?? null;
|
|
55
563
|
}
|
|
56
564
|
catch (error) {
|
|
57
|
-
this.handleError(error, '
|
|
565
|
+
this.handleError(error, 'findOne');
|
|
58
566
|
}
|
|
59
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* 查找多条数据(支持生命周期钩子)
|
|
570
|
+
*/
|
|
571
|
+
async findAll(query) {
|
|
572
|
+
// beforeFind 钩子
|
|
573
|
+
const ctx = this.createHookContext({ where: query });
|
|
574
|
+
const shouldContinue = await this.runHooks('beforeFind', ctx);
|
|
575
|
+
if (!shouldContinue) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const selection = this.select();
|
|
580
|
+
if (ctx.where) {
|
|
581
|
+
selection.where(ctx.where);
|
|
582
|
+
}
|
|
583
|
+
const results = await selection;
|
|
584
|
+
// afterFind 钩子
|
|
585
|
+
ctx.result = results;
|
|
586
|
+
await this.runHooks('afterFind', ctx);
|
|
587
|
+
return ctx.result ?? [];
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
this.handleError(error, 'findAll');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* selectOne 的别名(向后兼容)
|
|
595
|
+
*/
|
|
596
|
+
async selectOne(query) {
|
|
597
|
+
return this.findOne(query);
|
|
598
|
+
}
|
|
60
599
|
/**
|
|
61
600
|
* 根据ID查找
|
|
62
601
|
*/
|
|
63
602
|
async selectById(id) {
|
|
64
|
-
return this.
|
|
603
|
+
return this.findOne({ id });
|
|
65
604
|
}
|
|
66
605
|
/**
|
|
67
|
-
*
|
|
606
|
+
* findById 别名
|
|
68
607
|
*/
|
|
69
|
-
async
|
|
608
|
+
async findById(id) {
|
|
609
|
+
return this.findOne({ id });
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 更新数据(支持生命周期钩子)
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* userModel.addHook('beforeUpdate', (ctx) => {
|
|
616
|
+
* ctx.data.updatedAt = new Date();
|
|
617
|
+
* });
|
|
618
|
+
* await userModel.updateWhere({ role: 'guest' }, { role: 'member' });
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
async updateWhere(query, data) {
|
|
622
|
+
// beforeUpdate 钩子
|
|
623
|
+
const ctx = this.createHookContext({ where: query, data });
|
|
624
|
+
const shouldContinue = await this.runHooks('beforeUpdate', ctx);
|
|
625
|
+
if (!shouldContinue) {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
70
628
|
try {
|
|
71
|
-
const result = await this.update(data).where(
|
|
72
|
-
|
|
629
|
+
const result = await this.update(ctx.data).where(ctx.where);
|
|
630
|
+
// afterUpdate 钩子
|
|
631
|
+
ctx.result = result;
|
|
632
|
+
await this.runHooks('afterUpdate', ctx);
|
|
633
|
+
return result;
|
|
73
634
|
}
|
|
74
635
|
catch (error) {
|
|
75
|
-
this.handleError(error, '
|
|
636
|
+
this.handleError(error, 'updateWhere');
|
|
76
637
|
}
|
|
77
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* 更新单个数据(向后兼容)
|
|
641
|
+
*/
|
|
642
|
+
async updateOne(query, data) {
|
|
643
|
+
const result = await this.updateWhere(query, data);
|
|
644
|
+
return result > 0;
|
|
645
|
+
}
|
|
78
646
|
/**
|
|
79
647
|
* 根据ID更新
|
|
80
648
|
*/
|
|
@@ -82,10 +650,67 @@ export class RelatedModel extends Model {
|
|
|
82
650
|
return this.updateOne({ id }, data);
|
|
83
651
|
}
|
|
84
652
|
/**
|
|
85
|
-
*
|
|
653
|
+
* 删除数据(支持生命周期钩子和软删除)
|
|
654
|
+
* @example
|
|
655
|
+
* ```ts
|
|
656
|
+
* userModel.addHook('beforeDelete', async (ctx) => {
|
|
657
|
+
* // 删除前检查
|
|
658
|
+
* const user = await userModel.findOne(ctx.where);
|
|
659
|
+
* if (user?.role === 'admin') return false; // 取消删除
|
|
660
|
+
* });
|
|
661
|
+
* await userModel.deleteWhere({ status: 'inactive' });
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
async deleteWhere(query) {
|
|
665
|
+
// beforeDelete 钩子
|
|
666
|
+
const ctx = this.createHookContext({ where: query });
|
|
667
|
+
const shouldContinue = await this.runHooks('beforeDelete', ctx);
|
|
668
|
+
if (!shouldContinue) {
|
|
669
|
+
return this.isSoftDelete ? 0 : [];
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const result = await this.delete(ctx.where);
|
|
673
|
+
// afterDelete 钩子
|
|
674
|
+
ctx.result = result;
|
|
675
|
+
await this.runHooks('afterDelete', ctx);
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
this.handleError(error, 'deleteWhere');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 根据ID删除(支持软删除和钩子)
|
|
86
684
|
*/
|
|
87
685
|
async deleteById(id) {
|
|
88
|
-
const result = await this.
|
|
686
|
+
const result = await this.deleteWhere({ id });
|
|
687
|
+
if (typeof result === 'number') {
|
|
688
|
+
return result > 0;
|
|
689
|
+
}
|
|
690
|
+
return result.length > 0;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* 根据ID强制删除(物理删除,忽略软删除设置,但仍触发钩子)
|
|
694
|
+
*/
|
|
695
|
+
async forceDeleteById(id) {
|
|
696
|
+
const query = { id };
|
|
697
|
+
// beforeDelete 钩子
|
|
698
|
+
const ctx = this.createHookContext({ where: query });
|
|
699
|
+
const shouldContinue = await this.runHooks('beforeDelete', ctx);
|
|
700
|
+
if (!shouldContinue) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
const result = await this.forceDelete(ctx.where);
|
|
704
|
+
// afterDelete 钩子
|
|
705
|
+
ctx.result = result;
|
|
706
|
+
await this.runHooks('afterDelete', ctx);
|
|
707
|
+
return result.length > 0;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* 根据ID恢复软删除的记录
|
|
711
|
+
*/
|
|
712
|
+
async restoreById(id) {
|
|
713
|
+
const result = await this.restore({ id });
|
|
89
714
|
return result > 0;
|
|
90
715
|
}
|
|
91
716
|
/**
|
|
@@ -93,7 +718,7 @@ export class RelatedModel extends Model {
|
|
|
93
718
|
*/
|
|
94
719
|
async count(query) {
|
|
95
720
|
try {
|
|
96
|
-
const selection = this.select(
|
|
721
|
+
const selection = this.select();
|
|
97
722
|
if (query) {
|
|
98
723
|
selection.where(query);
|
|
99
724
|
}
|