monsqlize 1.0.1 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +204 -2464
  2. package/README.md +735 -1198
  3. package/index.d.ts +942 -18
  4. package/lib/cache.js +8 -8
  5. package/lib/common/validation.js +64 -1
  6. package/lib/connect.js +3 -3
  7. package/lib/errors.js +10 -0
  8. package/lib/index.js +173 -9
  9. package/lib/infrastructure/ssh-tunnel-ssh2.js +211 -0
  10. package/lib/infrastructure/ssh-tunnel.js +40 -0
  11. package/lib/infrastructure/uri-parser.js +35 -0
  12. package/lib/lock/Lock.js +66 -0
  13. package/lib/lock/errors.js +27 -0
  14. package/lib/lock/index.js +12 -0
  15. package/lib/logger.js +1 -1
  16. package/lib/model/examples/test.js +225 -29
  17. package/lib/model/features/soft-delete.js +348 -0
  18. package/lib/model/features/version.js +156 -0
  19. package/lib/model/index.js +756 -0
  20. package/lib/mongodb/common/accessor-helpers.js +17 -3
  21. package/lib/mongodb/connect.js +68 -13
  22. package/lib/mongodb/index.js +153 -6
  23. package/lib/mongodb/management/collection-ops.js +4 -4
  24. package/lib/mongodb/management/index-ops.js +18 -18
  25. package/lib/mongodb/management/validation-ops.js +3 -3
  26. package/lib/mongodb/queries/aggregate.js +14 -5
  27. package/lib/mongodb/queries/chain.js +52 -45
  28. package/lib/mongodb/queries/count.js +16 -6
  29. package/lib/mongodb/queries/distinct.js +15 -6
  30. package/lib/mongodb/queries/find-and-count.js +22 -13
  31. package/lib/mongodb/queries/find-by-ids.js +5 -5
  32. package/lib/mongodb/queries/find-one-by-id.js +1 -1
  33. package/lib/mongodb/queries/find-one.js +12 -3
  34. package/lib/mongodb/queries/find-page.js +12 -0
  35. package/lib/mongodb/queries/find.js +15 -6
  36. package/lib/mongodb/queries/watch.js +11 -2
  37. package/lib/mongodb/writes/common/batch-retry.js +64 -0
  38. package/lib/mongodb/writes/delete-batch.js +322 -0
  39. package/lib/mongodb/writes/delete-many.js +20 -11
  40. package/lib/mongodb/writes/delete-one.js +18 -9
  41. package/lib/mongodb/writes/find-one-and-delete.js +19 -10
  42. package/lib/mongodb/writes/find-one-and-replace.js +36 -20
  43. package/lib/mongodb/writes/find-one-and-update.js +36 -20
  44. package/lib/mongodb/writes/increment-one.js +22 -7
  45. package/lib/mongodb/writes/index.js +17 -13
  46. package/lib/mongodb/writes/insert-batch.js +46 -37
  47. package/lib/mongodb/writes/insert-many.js +22 -13
  48. package/lib/mongodb/writes/insert-one.js +18 -9
  49. package/lib/mongodb/writes/replace-one.js +33 -17
  50. package/lib/mongodb/writes/result-handler.js +14 -14
  51. package/lib/mongodb/writes/update-batch.js +358 -0
  52. package/lib/mongodb/writes/update-many.js +34 -18
  53. package/lib/mongodb/writes/update-one.js +33 -17
  54. package/lib/mongodb/writes/upsert-one.js +25 -9
  55. package/lib/operators.js +1 -1
  56. package/lib/redis-cache-adapter.js +3 -3
  57. package/lib/slow-query-log/base-storage.js +69 -0
  58. package/lib/slow-query-log/batch-queue.js +96 -0
  59. package/lib/slow-query-log/config-manager.js +195 -0
  60. package/lib/slow-query-log/index.js +237 -0
  61. package/lib/slow-query-log/mongodb-storage.js +323 -0
  62. package/lib/slow-query-log/query-hash.js +38 -0
  63. package/lib/transaction/DistributedCacheLockManager.js +240 -5
  64. package/lib/transaction/Transaction.js +1 -1
  65. package/lib/utils/objectid-converter.js +566 -0
  66. package/package.json +18 -6
@@ -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
+
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Model 乐观锁版本控制功能
3
+ *
4
+ * 功能:
5
+ * 1. 插入时自动初始化 version: 0
6
+ * 2. 更新时自动递增版本号
7
+ * 3. 支持并发冲突检测
8
+ * 4. 支持自定义字段名
9
+ *
10
+ * 使用示例:
11
+ * ```javascript
12
+ * Model.define('users', {
13
+ * options: {
14
+ * version: true // 或 { enabled: true, field: '__v' }
15
+ * }
16
+ * });
17
+ *
18
+ * // 插入时自动初始化
19
+ * await User.insertOne({ username: 'john' });
20
+ * // { _id, username: 'john', version: 0 }
21
+ *
22
+ * // 更新时自动递增
23
+ * await User.updateOne({ _id, version: 0 }, { $set: { status: 'active' } });
24
+ * // 实际执行:{ $set: { status: 'active' }, $inc: { version: 1 } }
25
+ *
26
+ * // 并发冲突检测
27
+ * const result = await User.updateOne({ _id, version: 0 }, { $set: { status: 'inactive' } });
28
+ * // result.modifiedCount === 0(版本号不匹配,更新失败)
29
+ * ```
30
+ *
31
+ * @module lib/model/features/version
32
+ */
33
+
34
+ /**
35
+ * 解析版本控制配置
36
+ *
37
+ * @param {boolean|object} versionConfig - 版本控制配置
38
+ * @returns {object|null} 解析后的配置对象
39
+ */
40
+ function parseVersionConfig(versionConfig) {
41
+ if (!versionConfig) {
42
+ return null;
43
+ }
44
+
45
+ // 简单模式:version: true
46
+ if (versionConfig === true) {
47
+ return {
48
+ enabled: true,
49
+ field: 'version'
50
+ };
51
+ }
52
+
53
+ // 完整配置模式
54
+ if (typeof versionConfig === 'object') {
55
+ return {
56
+ enabled: versionConfig.enabled !== false,
57
+ field: versionConfig.field || 'version'
58
+ };
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * 为 Model 实例设置版本控制功能
66
+ *
67
+ * @param {object} modelInstance - Model 实例
68
+ * @param {boolean|object} versionConfig - 版本控制配置
69
+ */
70
+ function setupVersion(modelInstance, versionConfig) {
71
+ // 解析配置
72
+ const config = parseVersionConfig(versionConfig);
73
+
74
+ if (!config || !config.enabled) {
75
+ return; // 未启用,直接返回
76
+ }
77
+
78
+ const { field } = config;
79
+
80
+ // 保存配置到 Model 实例(供测试验证)
81
+ modelInstance._versionConfig = config;
82
+
83
+ // ========== 功能1: 插入时初始化版本号 ==========
84
+
85
+ // 保存原始方法
86
+ const originalInsertOne = modelInstance.collection.insertOne.bind(modelInstance.collection);
87
+ const originalInsertMany = modelInstance.collection.insertMany.bind(modelInstance.collection);
88
+
89
+ // 覆盖 insertOne
90
+ modelInstance.collection.insertOne = async function(doc, options) {
91
+ // 只在用户未手动设置时添加版本号
92
+ if (doc && typeof doc === 'object' && doc[field] === undefined) {
93
+ doc[field] = 0;
94
+ }
95
+ return await originalInsertOne(doc, options);
96
+ };
97
+
98
+ // 覆盖 insertMany
99
+ modelInstance.collection.insertMany = async function(docs, options) {
100
+ if (Array.isArray(docs)) {
101
+ docs.forEach(doc => {
102
+ if (doc && typeof doc === 'object' && doc[field] === undefined) {
103
+ doc[field] = 0;
104
+ }
105
+ });
106
+ }
107
+ return await originalInsertMany(docs, options);
108
+ };
109
+
110
+ // ========== 功能2: 更新时自动递增版本号 ==========
111
+
112
+ // 保存原始方法
113
+ const originalUpdateOne = modelInstance.collection.updateOne.bind(modelInstance.collection);
114
+ const originalUpdateMany = modelInstance.collection.updateMany.bind(modelInstance.collection);
115
+
116
+ // 覆盖 updateOne
117
+ modelInstance.collection.updateOne = async function(filter, update, options) {
118
+ // 检查 update 是否为空
119
+ if (update && typeof update === 'object') {
120
+ // 检查用户是否已手动设置 $inc.version
121
+ if (!update.$inc || update.$inc[field] === undefined) {
122
+ // 自动添加 $inc
123
+ if (!update.$inc) {
124
+ update.$inc = {};
125
+ }
126
+ update.$inc[field] = 1;
127
+ }
128
+ }
129
+ return await originalUpdateOne(filter, update, options);
130
+ };
131
+
132
+ // 覆盖 updateMany
133
+ modelInstance.collection.updateMany = async function(filter, update, options) {
134
+ // 检查 update 是否为空
135
+ if (update && typeof update === 'object') {
136
+ // 检查用户是否已手动设置 $inc.version
137
+ if (!update.$inc || update.$inc[field] === undefined) {
138
+ // 自动添加 $inc
139
+ if (!update.$inc) {
140
+ update.$inc = {};
141
+ }
142
+ update.$inc[field] = 1;
143
+ }
144
+ }
145
+ return await originalUpdateMany(filter, update, options);
146
+ };
147
+
148
+ // 注意:并发冲突检测通过 filter 中的版本号自然实现
149
+ // 用户提供 { _id, version: 0 } 时,如果版本号不匹配,matchedCount 为 0
150
+ }
151
+
152
+ module.exports = {
153
+ setupVersion,
154
+ parseVersionConfig // 导出供测试使用
155
+ };
156
+