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