monsqlize 1.0.2 → 1.0.5
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 +122 -2
- package/README.md +117 -4
- package/index.d.ts +690 -3
- package/lib/index.js +55 -0
- package/lib/model/examples/test.js +222 -26
- package/lib/model/features/soft-delete.js +348 -0
- package/lib/model/features/version.js +156 -0
- package/lib/model/index.js +756 -0
- package/lib/mongodb/index.js +17 -0
- package/lib/mongodb/writes/common/batch-retry.js +64 -0
- package/lib/mongodb/writes/delete-batch.js +322 -0
- package/lib/mongodb/writes/increment-one.js +6 -0
- package/lib/mongodb/writes/index.js +4 -0
- package/lib/mongodb/writes/update-batch.js +358 -0
- package/package.json +8 -2
package/lib/index.js
CHANGED
|
@@ -458,4 +458,59 @@ module.exports = class {
|
|
|
458
458
|
|
|
459
459
|
return defaults;
|
|
460
460
|
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 获取 Model 实例
|
|
464
|
+
*
|
|
465
|
+
* @param {string} collectionName - 集合名称
|
|
466
|
+
* @returns {ModelInstance} Model 实例
|
|
467
|
+
* @throws {Error} Model 未定义
|
|
468
|
+
* @throws {Error} 数据库未连接
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* // 1. 定义 Model
|
|
472
|
+
* const { Model } = require('monsqlize');
|
|
473
|
+
* Model.define('users', {
|
|
474
|
+
* schema: (dsl) => dsl({ username: 'string!' })
|
|
475
|
+
* });
|
|
476
|
+
*
|
|
477
|
+
* // 2. 连接数据库并获取 Model 实例
|
|
478
|
+
* const msq = new MonSQLize({ ... });
|
|
479
|
+
* await msq.connect();
|
|
480
|
+
* const User = msq.model('users');
|
|
481
|
+
*
|
|
482
|
+
* // 3. 使用 Model
|
|
483
|
+
* const result = await User.find({ status: 'active' });
|
|
484
|
+
*/
|
|
485
|
+
model(collectionName) {
|
|
486
|
+
// 检查数据库是否已连接
|
|
487
|
+
if (!this.dbInstance) {
|
|
488
|
+
const err = new Error('Database is not connected. Call connect() before accessing models.');
|
|
489
|
+
err.code = 'NOT_CONNECTED';
|
|
490
|
+
throw err;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 检查 Model 是否已定义
|
|
494
|
+
const Model = require('./model');
|
|
495
|
+
if (!Model.has(collectionName)) {
|
|
496
|
+
const err = new Error(`Model '${collectionName}' is not defined. Call Model.define() first.`);
|
|
497
|
+
err.code = 'MODEL_NOT_DEFINED';
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 获取 Model 定义
|
|
502
|
+
const modelDef = Model.get(collectionName);
|
|
503
|
+
|
|
504
|
+
// 获取 collection 实例
|
|
505
|
+
const collection = this.dbInstance.collection(collectionName);
|
|
506
|
+
|
|
507
|
+
// 创建 ModelInstance
|
|
508
|
+
const ModelInstanceClass = require('./model').ModelInstance;
|
|
509
|
+
return new ModelInstanceClass(collection, modelDef.definition, this);
|
|
510
|
+
}
|
|
461
511
|
};
|
|
512
|
+
|
|
513
|
+
// ========== 导出 Model 类 ==========
|
|
514
|
+
module.exports.Model = require('./model');
|
|
515
|
+
|
|
516
|
+
|
|
@@ -1,39 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Model 定义示例模板
|
|
3
|
+
*
|
|
4
|
+
* 使用说明:
|
|
5
|
+
* 1. enums - 枚举配置可被外部代码直接访问
|
|
6
|
+
* 例如:UserModel.enums.role
|
|
7
|
+
*
|
|
8
|
+
* 2. schema - 由 schema-dsl 包提供验证能力
|
|
9
|
+
* 使用 function 定义时,this 自动绑定到当前模型定义对象
|
|
10
|
+
* 例如:UserModel.schema(dsl) 时,this === UserModel
|
|
11
|
+
*
|
|
12
|
+
* 3. methods/hooks - 接收 model 实例作为参数
|
|
13
|
+
* 可以调用 model 的所有查询方法
|
|
14
|
+
*
|
|
15
|
+
* 注意:这是 API 设计示例,展示:
|
|
16
|
+
* - schema 中 this.enums 引用同一对象内的枚举配置
|
|
17
|
+
* - methods 通过参数接收 model 实例
|
|
18
|
+
* - hooks 通过 ctx 上下文传递状态(如事务)
|
|
19
|
+
*/
|
|
2
20
|
|
|
3
21
|
module.exports = {
|
|
4
22
|
|
|
5
|
-
//
|
|
23
|
+
// 枚举配置(可被外部代码直接访问)
|
|
6
24
|
enums: {
|
|
7
|
-
role:
|
|
8
|
-
status:
|
|
25
|
+
role: 'admin|user',
|
|
26
|
+
status: 'active|inactive|banned'
|
|
9
27
|
},
|
|
10
28
|
|
|
11
|
-
// 定义
|
|
12
|
-
schema:(
|
|
13
|
-
return {
|
|
14
|
-
username:
|
|
15
|
-
password:
|
|
16
|
-
age:
|
|
17
|
-
role:
|
|
18
|
-
}
|
|
29
|
+
// 定义 schema(使用 function 时,this 自动绑定到当前对象)
|
|
30
|
+
schema: function(dsl) {
|
|
31
|
+
return dsl({
|
|
32
|
+
username: 'string:3-32!',
|
|
33
|
+
password: 'string!'.pattern(/^[a-zA-Z0-9]{6,30}$/),
|
|
34
|
+
age: 'number:0-18!',
|
|
35
|
+
role: this.enums.role.default('user'), // this 指向 module.exports
|
|
36
|
+
})
|
|
19
37
|
},
|
|
20
38
|
|
|
21
39
|
// 自定义方法
|
|
40
|
+
//
|
|
41
|
+
// 说明:
|
|
42
|
+
// - methods 接收 model 参数(ModelInstance 实例)
|
|
43
|
+
// - 返回对象必须包含 instance 和/或 static 两个固定分组
|
|
44
|
+
// - instance: 实例方法,注入到查询结果文档对象(this 指向文档)
|
|
45
|
+
// - static: 静态方法,挂载到 Model 实例(通过 model 参数操作)
|
|
46
|
+
//
|
|
47
|
+
// 设计原因:
|
|
48
|
+
// - 明确区分方法类型,避免混淆
|
|
49
|
+
// - 实例方法:操作具体文档数据(需要 this)
|
|
50
|
+
// - 静态方法:执行查询操作(不需要 this)
|
|
51
|
+
//
|
|
52
|
+
// 可选配置:
|
|
53
|
+
// - 只需要 instance:只写 instance 分组
|
|
54
|
+
// - 只需要 static:只写 static 分组
|
|
55
|
+
// - 都不需要:整个 methods 配置项可省略
|
|
56
|
+
//
|
|
22
57
|
methods: (model)=>{
|
|
23
58
|
return {
|
|
24
|
-
//
|
|
59
|
+
// 实例方法(注入到文档对象)
|
|
60
|
+
// 用法:const user = await User.findOne(...); user.checkPassword('123');
|
|
25
61
|
instance: {
|
|
26
62
|
checkPassword(password) {
|
|
27
|
-
return this.password === password;
|
|
63
|
+
return this.password === password; // this 指向文档对象
|
|
28
64
|
},
|
|
29
|
-
async getPosts() {
|
|
30
|
-
|
|
65
|
+
async getPosts(_id) {
|
|
66
|
+
return await model.find({ userId: _id }); // 可以调用 model 查询
|
|
31
67
|
}
|
|
32
68
|
},
|
|
33
|
-
|
|
69
|
+
|
|
70
|
+
// 静态方法(挂载到 Model 实例)
|
|
71
|
+
// 用法:const User = msq.model('users'); await User.findByName('test');
|
|
34
72
|
static: {
|
|
35
73
|
findByName(name) {
|
|
36
|
-
return
|
|
74
|
+
return model.find({ username: name }); // 使用 model 参数
|
|
37
75
|
}
|
|
38
76
|
}
|
|
39
77
|
};
|
|
@@ -101,14 +139,172 @@ module.exports = {
|
|
|
101
139
|
}
|
|
102
140
|
},
|
|
103
141
|
|
|
104
|
-
//
|
|
142
|
+
// 模型选项配置(核心功能)
|
|
105
143
|
options: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
// 1️⃣ 时间戳自动维护
|
|
145
|
+
// 默认值:{ enabled: false, createdAt: 'createdAt', updatedAt: 'updatedAt' }
|
|
146
|
+
timestamps: {
|
|
147
|
+
enabled: true, // 启用时间戳
|
|
148
|
+
createdAt: 'createdAt', // 创建时间字段名
|
|
149
|
+
updatedAt: 'updatedAt' // 更新时间字段名
|
|
150
|
+
},
|
|
151
|
+
// 简化配置方式:
|
|
152
|
+
// timestamps: true // 使用默认字段名(createdAt, updatedAt)
|
|
153
|
+
// timestamps: false // 禁用时间戳
|
|
154
|
+
|
|
155
|
+
// 2️⃣ 软删除配置
|
|
156
|
+
// 默认值:{ enabled: false, field: 'deletedAt', type: 'timestamp', ttl: null, index: true }
|
|
157
|
+
softDelete: {
|
|
158
|
+
enabled: true, // 启用软删除
|
|
159
|
+
field: 'deletedAt', // 软删除字段名
|
|
160
|
+
type: 'timestamp', // 类型:timestamp | boolean
|
|
161
|
+
ttl: 30 * 24 * 60 * 60 * 1000, // 30天后物理删除(null=永久保留)
|
|
162
|
+
index: true, // 自动创建索引
|
|
163
|
+
},
|
|
164
|
+
// 简化配置方式:
|
|
165
|
+
// softDelete: true // 使用默认值
|
|
166
|
+
// softDelete: false // 禁用软删除({ enabled: false })
|
|
167
|
+
|
|
168
|
+
// 3️⃣ 乐观锁版本控制(防止并发冲突)
|
|
169
|
+
// 默认值:{ enabled: false, field: 'version', strategy: 'increment' }
|
|
170
|
+
version: {
|
|
171
|
+
enabled: true, // 启用版本号
|
|
172
|
+
field: 'version', // 版本字段名
|
|
173
|
+
strategy: 'increment' // 策略:increment | timestamp
|
|
174
|
+
},
|
|
175
|
+
// 简化配置方式:
|
|
176
|
+
// version: true // 使用默认值
|
|
177
|
+
// version: false // 禁用版本控制({ enabled: false })
|
|
178
|
+
|
|
179
|
+
// 4️⃣ 索引自动同步
|
|
180
|
+
// 默认值:{ enabled: false, mode: 'safe', background: true }
|
|
181
|
+
sync: {
|
|
182
|
+
enabled: true, // 启用索引自动同步
|
|
183
|
+
mode: 'safe', // 模式:safe | force
|
|
184
|
+
background: true, // 后台创建索引
|
|
185
|
+
},
|
|
186
|
+
// 简化配置方式:
|
|
187
|
+
// sync: true // 使用默认值(safe 模式)
|
|
188
|
+
// sync: false // 禁用同步({ enabled: false })
|
|
189
|
+
},
|
|
111
190
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
191
|
+
// ========================================
|
|
192
|
+
// 📝 默认配置值速查表
|
|
193
|
+
// ========================================
|
|
194
|
+
// timestamps: { enabled: false, createdAt: 'createdAt', updatedAt: 'updatedAt' }
|
|
195
|
+
// softDelete: { enabled: false, field: 'deletedAt', type: 'timestamp', ttl: null, index: true }
|
|
196
|
+
// version: { enabled: false, field: 'version', strategy: 'increment' }
|
|
197
|
+
// sync: { enabled: false, mode: 'safe', background: true }
|
|
198
|
+
//
|
|
199
|
+
// 说明:
|
|
200
|
+
// - 默认所有功能都禁用(enabled: false),用户需要主动启用
|
|
201
|
+
// - ttl: null = 软删除数据永久保留
|
|
202
|
+
// - ttl: 数字 = 指定天数后物理删除
|
|
203
|
+
// - mode: 'safe' = 只创建缺失的索引(生产环境推荐)
|
|
204
|
+
// - mode: 'force' = 创建+删除索引,完全同步(开发环境)
|
|
205
|
+
//
|
|
206
|
+
// ========================================
|
|
207
|
+
// 📝 配置方式说明(三层递进)
|
|
208
|
+
// ========================================
|
|
209
|
+
//
|
|
210
|
+
// 【第1层】完全默认(什么都不改)
|
|
211
|
+
// options: {} // 所有功能禁用
|
|
212
|
+
//
|
|
213
|
+
// 【第2层】简化配置(快速启用功能)
|
|
214
|
+
// options: {
|
|
215
|
+
// timestamps: true,
|
|
216
|
+
// softDelete: true,
|
|
217
|
+
// version: true,
|
|
218
|
+
// sync: true
|
|
219
|
+
// }
|
|
220
|
+
//
|
|
221
|
+
// 【第3层】详细配置(精细控制)
|
|
222
|
+
// options: {
|
|
223
|
+
// timestamps: { enabled: true, createdAt: 'createdAt', updatedAt: 'updatedAt' },
|
|
224
|
+
// softDelete: { enabled: true, field: 'deletedAt', type: 'timestamp', ttl: ..., index: true },
|
|
225
|
+
// version: { enabled: true, field: 'version', strategy: 'increment' },
|
|
226
|
+
// sync: { enabled: true, mode: 'safe', background: true }
|
|
227
|
+
// }
|
|
228
|
+
//
|
|
229
|
+
// ========================================
|
|
230
|
+
// 📝 日常开发场景最佳实践
|
|
231
|
+
// ========================================
|
|
232
|
+
//
|
|
233
|
+
// 场景1: 用户/订单表(需要完整功能)
|
|
234
|
+
// options: {
|
|
235
|
+
// timestamps: true,
|
|
236
|
+
// softDelete: true, // 删除后保留30天便于恢复
|
|
237
|
+
// version: true, // 防止并发冲突
|
|
238
|
+
// sync: true
|
|
239
|
+
// }
|
|
240
|
+
//
|
|
241
|
+
// 场景2: 会话/缓存表(最小化配置)
|
|
242
|
+
// options: {
|
|
243
|
+
// timestamps: false, // 不需要时间戳
|
|
244
|
+
// softDelete: false, // 不需要软删除,过期自动删除
|
|
245
|
+
// version: false, // 不需要版本控制
|
|
246
|
+
// sync: true // 需要同步 TTL 索引
|
|
247
|
+
// }
|
|
248
|
+
// indexes: [
|
|
249
|
+
// { key: { expireAt: 1 }, expireAfterSeconds: 0 } // TTL 索引
|
|
250
|
+
// ]
|
|
251
|
+
//
|
|
252
|
+
// 场景3: 中间表(极简配置)
|
|
253
|
+
// options: {
|
|
254
|
+
// timestamps: false,
|
|
255
|
+
// softDelete: false,
|
|
256
|
+
// version: false,
|
|
257
|
+
// sync: true // 需要同步唯一索引
|
|
258
|
+
// }
|
|
259
|
+
//
|
|
260
|
+
// 场景4: 日志/事件表(无删除需求)
|
|
261
|
+
// options: {
|
|
262
|
+
// timestamps: true, // 记录事件时间
|
|
263
|
+
// softDelete: false, // 日志不删除
|
|
264
|
+
// version: false, // 日志不并发更新
|
|
265
|
+
// sync: true
|
|
266
|
+
// }
|
|
267
|
+
//
|
|
268
|
+
// 场景5: 商品/内容表(高频并发)
|
|
269
|
+
// options: {
|
|
270
|
+
// timestamps: true,
|
|
271
|
+
// softDelete: true, // 下架商品保留
|
|
272
|
+
// version: true, // 防止秒杀并发冲突
|
|
273
|
+
// sync: { // 生产环境用 safe,开发用 force
|
|
274
|
+
// enabled: true,
|
|
275
|
+
// mode: 'safe', // 生产环境
|
|
276
|
+
// background: true
|
|
277
|
+
// }
|
|
278
|
+
// }
|
|
279
|
+
//
|
|
280
|
+
// ========================================
|
|
281
|
+
// 📝 全局配置已包含:
|
|
282
|
+
// - 缓存系统(MemoryCache)
|
|
283
|
+
// - 日志系统(Logger)
|
|
284
|
+
// - 慢查询日志(SlowQueryLogManager)
|
|
285
|
+
// - 默认值(defaultLimit, maxLimit, validation等)
|
|
286
|
+
//
|
|
287
|
+
// 📝 Model options 只配置模型特异的功能:
|
|
288
|
+
// - timestamps: 某些表不需要(如中间表)
|
|
289
|
+
// - softDelete: 某些表不需要(如会话表)
|
|
290
|
+
// - version: 高并发表才需要
|
|
291
|
+
// - sync: 模型特异的索引定义
|
|
292
|
+
//
|
|
293
|
+
// 📝 数据生命周期处理方式:
|
|
294
|
+
// - 在 schema 中定义 expireAt 字段
|
|
295
|
+
// - 在 indexes 中定义 TTL 索引
|
|
296
|
+
// - 不需要单独的 lifecycle 配置项
|
|
297
|
+
//
|
|
298
|
+
// 📝 自动注入的方法(仅当 softDelete.enabled = true):
|
|
299
|
+
// - model.restore(id) - 恢复软删除数据
|
|
300
|
+
// - model.forceDelete(id) - 强制物理删除
|
|
301
|
+
// - model.findWithDeleted() - 查询包含软删除数据
|
|
302
|
+
// - model.findOnlyDeleted() - 只查询软删除数据
|
|
303
|
+
//
|
|
304
|
+
// 📝 开发建议:
|
|
305
|
+
// 1. 开发阶段:使用简化配置快速迭代(timestamps: true, ...)
|
|
306
|
+
// 2. 优化阶段:根据实际需求调整(可禁用不需要的功能)
|
|
307
|
+
// 3. 生产部署:确认 sync.mode 为 'safe'(避免误删索引)
|
|
308
|
+
// 4. 高并发表:必须启用 version,防止并发冲突
|
|
309
|
+
// 5. 敏感数据:启用 softDelete,避免误删无法恢复
|
|
114
310
|
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft Delete Feature for Model
|
|
3
|
+
*
|
|
4
|
+
* Provides soft delete functionality:
|
|
5
|
+
* - Mark documents as deleted instead of physical deletion
|
|
6
|
+
* - Auto-filter deleted documents in queries
|
|
7
|
+
* - Restore deleted documents
|
|
8
|
+
* - Force physical deletion
|
|
9
|
+
* - TTL index for auto-cleanup
|
|
10
|
+
*
|
|
11
|
+
* @module lib/model/features/soft-delete
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse soft delete configuration
|
|
16
|
+
* @param {Object|boolean} config - Soft delete config
|
|
17
|
+
* @returns {Object|null} Parsed config or null if disabled
|
|
18
|
+
*/
|
|
19
|
+
function parseSoftDeleteConfig(config) {
|
|
20
|
+
if (!config) return null;
|
|
21
|
+
|
|
22
|
+
// shorthand: softDelete: true
|
|
23
|
+
if (config === true) {
|
|
24
|
+
return {
|
|
25
|
+
enabled: true,
|
|
26
|
+
field: 'deletedAt',
|
|
27
|
+
type: 'timestamp',
|
|
28
|
+
ttl: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// full config: softDelete: { ... }
|
|
33
|
+
return {
|
|
34
|
+
enabled: config.enabled !== false,
|
|
35
|
+
field: config.field || 'deletedAt',
|
|
36
|
+
type: config.type || 'timestamp',
|
|
37
|
+
ttl: config.ttl || null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get delete value based on type
|
|
43
|
+
* @param {string} type - 'timestamp' or 'boolean'
|
|
44
|
+
* @returns {Date|boolean} Delete value
|
|
45
|
+
*/
|
|
46
|
+
function getDeleteValue(type) {
|
|
47
|
+
return type === 'boolean' ? true : new Date();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply soft delete filter to query
|
|
52
|
+
* @param {Object} filter - Original filter
|
|
53
|
+
* @param {Object} config - Soft delete config
|
|
54
|
+
* @param {Object} options - Query options
|
|
55
|
+
* @returns {Object} Modified filter
|
|
56
|
+
*/
|
|
57
|
+
function applySoftDeleteFilter(filter, config, options = {}) {
|
|
58
|
+
if (!config?.enabled) return filter;
|
|
59
|
+
|
|
60
|
+
const field = config.field;
|
|
61
|
+
|
|
62
|
+
// Already has explicit deletedAt filter - don't modify
|
|
63
|
+
if (filter[field] !== undefined) {
|
|
64
|
+
return filter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// withDeleted: include all (no filter)
|
|
68
|
+
if (options.withDeleted) {
|
|
69
|
+
return filter;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// onlyDeleted: only deleted documents
|
|
73
|
+
if (options.onlyDeleted) {
|
|
74
|
+
return {
|
|
75
|
+
...filter,
|
|
76
|
+
[field]: { $ne: null }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// default: only non-deleted documents
|
|
81
|
+
return {
|
|
82
|
+
...filter,
|
|
83
|
+
[field]: null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register soft delete hooks
|
|
89
|
+
* @param {Object} modelInstance - Model instance
|
|
90
|
+
* @param {Object} config - Soft delete config
|
|
91
|
+
*/
|
|
92
|
+
function registerSoftDeleteHooks(modelInstance, config) {
|
|
93
|
+
if (!config?.enabled) return;
|
|
94
|
+
|
|
95
|
+
const field = config.field;
|
|
96
|
+
const type = config.type;
|
|
97
|
+
|
|
98
|
+
// Store original deleteOne and deleteMany methods
|
|
99
|
+
const originalDeleteOne = modelInstance.collection.deleteOne.bind(modelInstance.collection);
|
|
100
|
+
const originalDeleteMany = modelInstance.collection.deleteMany.bind(modelInstance.collection);
|
|
101
|
+
|
|
102
|
+
// Override deleteOne - convert to updateOne
|
|
103
|
+
modelInstance.collection.deleteOne = async function(filter, options = {}) {
|
|
104
|
+
// Check if force delete (bypass soft delete)
|
|
105
|
+
if (options._forceDelete) {
|
|
106
|
+
delete options._forceDelete;
|
|
107
|
+
return await originalDeleteOne(filter, options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Soft delete: convert to updateOne
|
|
111
|
+
const updateResult = await modelInstance.collection.updateOne(
|
|
112
|
+
filter,
|
|
113
|
+
{ $set: { [field]: getDeleteValue(type) } },
|
|
114
|
+
options
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Convert updateOne result to deleteOne result format
|
|
118
|
+
return {
|
|
119
|
+
acknowledged: updateResult.acknowledged,
|
|
120
|
+
deletedCount: updateResult.modifiedCount
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Override deleteMany - convert to updateMany
|
|
125
|
+
modelInstance.collection.deleteMany = async function(filter, options = {}) {
|
|
126
|
+
// Check if force delete (bypass soft delete)
|
|
127
|
+
if (options._forceDelete) {
|
|
128
|
+
delete options._forceDelete;
|
|
129
|
+
return await originalDeleteMany(filter, options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Soft delete: convert to updateMany
|
|
133
|
+
const updateResult = await modelInstance.collection.updateMany(
|
|
134
|
+
filter,
|
|
135
|
+
{ $set: { [field]: getDeleteValue(type) } },
|
|
136
|
+
options
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Convert updateMany result to deleteMany result format
|
|
140
|
+
return {
|
|
141
|
+
acknowledged: updateResult.acknowledged,
|
|
142
|
+
deletedCount: updateResult.modifiedCount
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Store original find/findOne/count methods
|
|
147
|
+
const originalFind = modelInstance.collection.find.bind(modelInstance.collection);
|
|
148
|
+
const originalFindOne = modelInstance.collection.findOne.bind(modelInstance.collection);
|
|
149
|
+
const originalCount = modelInstance.collection.count.bind(modelInstance.collection);
|
|
150
|
+
|
|
151
|
+
// Override find - auto-filter deleted
|
|
152
|
+
modelInstance.collection.find = async function(filter = {}, options = {}) {
|
|
153
|
+
const modifiedFilter = applySoftDeleteFilter(filter, config, options);
|
|
154
|
+
return await originalFind(modifiedFilter, options);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Override findOne - auto-filter deleted
|
|
158
|
+
modelInstance.collection.findOne = async function(filter = {}, options = {}) {
|
|
159
|
+
const modifiedFilter = applySoftDeleteFilter(filter, config, options);
|
|
160
|
+
return await originalFindOne(modifiedFilter, options);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Override count - auto-filter deleted
|
|
164
|
+
modelInstance.collection.count = async function(filter = {}, options = {}) {
|
|
165
|
+
const modifiedFilter = applySoftDeleteFilter(filter, config, options);
|
|
166
|
+
return await originalCount(modifiedFilter, options);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add soft delete methods to ModelInstance
|
|
172
|
+
* @param {Object} modelInstance - Model instance
|
|
173
|
+
* @param {Object} config - Soft delete config
|
|
174
|
+
*/
|
|
175
|
+
function addSoftDeleteMethods(modelInstance, config) {
|
|
176
|
+
if (!config?.enabled) return;
|
|
177
|
+
|
|
178
|
+
const field = config.field;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Find documents including deleted ones
|
|
182
|
+
* @param {Object} filter - Query filter
|
|
183
|
+
* @param {Object} options - Query options
|
|
184
|
+
* @returns {Promise<Array>} Documents
|
|
185
|
+
*/
|
|
186
|
+
modelInstance.findWithDeleted = async function(filter = {}, options = {}) {
|
|
187
|
+
return await this.find(filter, { ...options, withDeleted: true });
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find only deleted documents
|
|
192
|
+
* @param {Object} filter - Query filter
|
|
193
|
+
* @param {Object} options - Query options
|
|
194
|
+
* @returns {Promise<Array>} Deleted documents
|
|
195
|
+
*/
|
|
196
|
+
modelInstance.findOnlyDeleted = async function(filter = {}, options = {}) {
|
|
197
|
+
return await this.find(filter, { ...options, onlyDeleted: true });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find one document including deleted ones
|
|
202
|
+
* @param {Object} filter - Query filter
|
|
203
|
+
* @param {Object} options - Query options
|
|
204
|
+
* @returns {Promise<Object|null>} Document
|
|
205
|
+
*/
|
|
206
|
+
modelInstance.findOneWithDeleted = async function(filter = {}, options = {}) {
|
|
207
|
+
return await this.findOne(filter, { ...options, withDeleted: true });
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Find one deleted document
|
|
212
|
+
* @param {Object} filter - Query filter
|
|
213
|
+
* @param {Object} options - Query options
|
|
214
|
+
* @returns {Promise<Object|null>} Deleted document
|
|
215
|
+
*/
|
|
216
|
+
modelInstance.findOneOnlyDeleted = async function(filter = {}, options = {}) {
|
|
217
|
+
return await this.findOne(filter, { ...options, onlyDeleted: true });
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Count documents including deleted ones
|
|
222
|
+
* @param {Object} filter - Query filter
|
|
223
|
+
* @param {Object} options - Query options
|
|
224
|
+
* @returns {Promise<number>} Count
|
|
225
|
+
*/
|
|
226
|
+
modelInstance.countWithDeleted = async function(filter = {}, options = {}) {
|
|
227
|
+
return await this.count(filter, { ...options, withDeleted: true });
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Count only deleted documents
|
|
232
|
+
* @param {Object} filter - Query filter
|
|
233
|
+
* @param {Object} options - Query options
|
|
234
|
+
* @returns {Promise<number>} Count
|
|
235
|
+
*/
|
|
236
|
+
modelInstance.countOnlyDeleted = async function(filter = {}, options = {}) {
|
|
237
|
+
return await this.count(filter, { ...options, onlyDeleted: true });
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Restore a deleted document
|
|
242
|
+
* @param {Object} filter - Query filter
|
|
243
|
+
* @param {Object} options - Update options
|
|
244
|
+
* @returns {Promise<Object>} Update result
|
|
245
|
+
*/
|
|
246
|
+
modelInstance.restore = async function(filter, options) {
|
|
247
|
+
// Add deleted filter to ensure we only restore deleted documents
|
|
248
|
+
const restoreFilter = {
|
|
249
|
+
...filter,
|
|
250
|
+
[field]: { $ne: null }
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return await this.updateOne(
|
|
254
|
+
restoreFilter,
|
|
255
|
+
{ $unset: { [field]: 1 } },
|
|
256
|
+
options
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Restore multiple deleted documents
|
|
262
|
+
* @param {Object} filter - Query filter
|
|
263
|
+
* @param {Object} options - Update options
|
|
264
|
+
* @returns {Promise<Object>} Update result
|
|
265
|
+
*/
|
|
266
|
+
modelInstance.restoreMany = async function(filter, options) {
|
|
267
|
+
// Add deleted filter to ensure we only restore deleted documents
|
|
268
|
+
const restoreFilter = {
|
|
269
|
+
...filter,
|
|
270
|
+
[field]: { $ne: null }
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return await this.updateMany(
|
|
274
|
+
restoreFilter,
|
|
275
|
+
{ $unset: { [field]: 1 } },
|
|
276
|
+
options
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Force physical deletion (bypass soft delete)
|
|
282
|
+
* @param {Object} filter - Query filter
|
|
283
|
+
* @param {Object} options - Delete options
|
|
284
|
+
* @returns {Promise<Object>} Delete result
|
|
285
|
+
*/
|
|
286
|
+
modelInstance.forceDelete = async function(filter, options = {}) {
|
|
287
|
+
// Set flag to bypass soft delete
|
|
288
|
+
return await this.collection.deleteOne(filter, { ...options, _forceDelete: true });
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Force physical deletion of multiple documents
|
|
293
|
+
* @param {Object} filter - Query filter
|
|
294
|
+
* @param {Object} options - Delete options
|
|
295
|
+
* @returns {Promise<Object>} Delete result
|
|
296
|
+
*/
|
|
297
|
+
modelInstance.forceDeleteMany = async function(filter, options = {}) {
|
|
298
|
+
// Set flag to bypass soft delete
|
|
299
|
+
return await this.collection.deleteMany(filter, { ...options, _forceDelete: true });
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Add TTL index for soft deleted documents
|
|
305
|
+
* @param {Object} modelInstance - Model instance
|
|
306
|
+
* @param {Object} config - Soft delete config
|
|
307
|
+
*/
|
|
308
|
+
function addTTLIndex(modelInstance, config) {
|
|
309
|
+
if (!config?.enabled || !config.ttl) return;
|
|
310
|
+
|
|
311
|
+
// Add TTL index to automatically clean up old deleted documents
|
|
312
|
+
modelInstance.indexes.push({
|
|
313
|
+
key: { [config.field]: 1 },
|
|
314
|
+
expireAfterSeconds: config.ttl,
|
|
315
|
+
name: `${config.field}_ttl`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Setup soft delete feature for a model
|
|
321
|
+
* @param {Object} modelInstance - Model instance
|
|
322
|
+
* @param {Object|boolean} config - Soft delete config
|
|
323
|
+
*/
|
|
324
|
+
function setupSoftDelete(modelInstance, config) {
|
|
325
|
+
const parsedConfig = parseSoftDeleteConfig(config);
|
|
326
|
+
|
|
327
|
+
if (!parsedConfig) return;
|
|
328
|
+
|
|
329
|
+
// Store config on model instance
|
|
330
|
+
modelInstance.softDeleteConfig = parsedConfig;
|
|
331
|
+
|
|
332
|
+
// Register hooks
|
|
333
|
+
registerSoftDeleteHooks(modelInstance, parsedConfig);
|
|
334
|
+
|
|
335
|
+
// Add methods
|
|
336
|
+
addSoftDeleteMethods(modelInstance, parsedConfig);
|
|
337
|
+
|
|
338
|
+
// Add TTL index
|
|
339
|
+
addTTLIndex(modelInstance, parsedConfig);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
setupSoftDelete,
|
|
344
|
+
parseSoftDeleteConfig,
|
|
345
|
+
applySoftDeleteFilter,
|
|
346
|
+
getDeleteValue
|
|
347
|
+
};
|
|
348
|
+
|