monsqlize 1.0.0 → 1.0.2
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 +92 -2419
- package/README.md +630 -1070
- package/index.d.ts +252 -15
- package/lib/cache.js +8 -8
- package/lib/common/validation.js +64 -1
- package/lib/connect.js +3 -3
- package/lib/errors.js +10 -0
- package/lib/index.js +118 -9
- package/lib/infrastructure/ssh-tunnel-ssh2.js +211 -0
- package/lib/infrastructure/ssh-tunnel.js +40 -0
- package/lib/infrastructure/uri-parser.js +35 -0
- package/lib/lock/Lock.js +66 -0
- package/lib/lock/errors.js +27 -0
- package/lib/lock/index.js +12 -0
- package/lib/logger.js +1 -1
- package/lib/model/examples/test.js +4 -4
- package/lib/mongodb/common/accessor-helpers.js +17 -3
- package/lib/mongodb/connect.js +68 -13
- package/lib/mongodb/index.js +140 -7
- package/lib/mongodb/management/collection-ops.js +4 -4
- package/lib/mongodb/management/index-ops.js +18 -18
- package/lib/mongodb/management/validation-ops.js +3 -3
- package/lib/mongodb/queries/aggregate.js +14 -5
- package/lib/mongodb/queries/chain.js +52 -45
- package/lib/mongodb/queries/count.js +16 -6
- package/lib/mongodb/queries/distinct.js +15 -6
- package/lib/mongodb/queries/find-and-count.js +22 -13
- package/lib/mongodb/queries/find-by-ids.js +5 -5
- package/lib/mongodb/queries/find-one-by-id.js +1 -1
- package/lib/mongodb/queries/find-one.js +12 -3
- package/lib/mongodb/queries/find-page.js +12 -0
- package/lib/mongodb/queries/find.js +15 -6
- package/lib/mongodb/queries/index.js +1 -0
- package/lib/mongodb/queries/watch.js +537 -0
- package/lib/mongodb/writes/delete-many.js +20 -11
- package/lib/mongodb/writes/delete-one.js +18 -9
- package/lib/mongodb/writes/find-one-and-delete.js +19 -10
- package/lib/mongodb/writes/find-one-and-replace.js +36 -20
- package/lib/mongodb/writes/find-one-and-update.js +36 -20
- package/lib/mongodb/writes/increment-one.js +16 -7
- package/lib/mongodb/writes/index.js +13 -13
- package/lib/mongodb/writes/insert-batch.js +46 -37
- package/lib/mongodb/writes/insert-many.js +22 -13
- package/lib/mongodb/writes/insert-one.js +18 -9
- package/lib/mongodb/writes/replace-one.js +33 -17
- package/lib/mongodb/writes/result-handler.js +14 -14
- package/lib/mongodb/writes/update-many.js +34 -18
- package/lib/mongodb/writes/update-one.js +33 -17
- package/lib/mongodb/writes/upsert-one.js +25 -9
- package/lib/operators.js +1 -1
- package/lib/redis-cache-adapter.js +3 -3
- package/lib/slow-query-log/base-storage.js +69 -0
- package/lib/slow-query-log/batch-queue.js +96 -0
- package/lib/slow-query-log/config-manager.js +195 -0
- package/lib/slow-query-log/index.js +237 -0
- package/lib/slow-query-log/mongodb-storage.js +323 -0
- package/lib/slow-query-log/query-hash.js +38 -0
- package/lib/transaction/DistributedCacheLockManager.js +240 -5
- package/lib/transaction/Transaction.js +1 -1
- package/lib/utils/objectid-converter.js +566 -0
- package/package.json +11 -5
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { normalizeProjection, normalizeSort } = require('../../common/normalize');
|
|
7
7
|
const { FindChain } = require('./chain');
|
|
8
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 创建 find 查询操作
|
|
@@ -51,12 +52,20 @@ function createFindOps(context) {
|
|
|
51
52
|
* @returns {Promise<Array>|ReadableStream|FindChain} 记录数组或可读流(当 stream: true 时);当 explain=true 时返回执行计划;默认返回 FindChain 实例支持链式调用
|
|
52
53
|
*/
|
|
53
54
|
find: (query = {}, options = {}) => {
|
|
55
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
56
|
+
const convertedQuery = convertObjectIdStrings(query, 'query', 0, new WeakSet(), {
|
|
57
|
+
logger: context.logger,
|
|
58
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
59
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
60
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
61
|
+
});
|
|
62
|
+
|
|
54
63
|
// 如果没有提供 options 或 options 为空对象,返回 FindChain 以支持完整的链式调用
|
|
55
64
|
const hasOptions = options && Object.keys(options).length > 0;
|
|
56
65
|
|
|
57
66
|
if (!hasOptions) {
|
|
58
67
|
// 返回 FindChain 实例,支持 .limit().skip().sort() 等链式调用
|
|
59
|
-
return new FindChain(context,
|
|
68
|
+
return new FindChain(context, convertedQuery, {});
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
// 如果提供了 options,执行原有逻辑(向后兼容)
|
|
@@ -81,13 +90,13 @@ function createFindOps(context) {
|
|
|
81
90
|
// 如果启用 explain,直接返回执行计划(不缓存)
|
|
82
91
|
if (explain) {
|
|
83
92
|
const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
|
|
84
|
-
const cursor = collection.find(
|
|
93
|
+
const cursor = collection.find(convertedQuery, driverOpts);
|
|
85
94
|
return cursor.explain(verbosity);
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
// 如果启用流式返回,直接返回 MongoDB 游标流
|
|
89
98
|
if (stream) {
|
|
90
|
-
const cursor = collection.find(
|
|
99
|
+
const cursor = collection.find(convertedQuery, driverOpts);
|
|
91
100
|
const readableStream = cursor.stream();
|
|
92
101
|
|
|
93
102
|
// 添加慢查询日志支持
|
|
@@ -127,13 +136,13 @@ function createFindOps(context) {
|
|
|
127
136
|
// 执行查询的 Promise
|
|
128
137
|
const resultPromise = run(
|
|
129
138
|
'find',
|
|
130
|
-
{ query, ...options },
|
|
131
|
-
async () => collection.find(
|
|
139
|
+
{ query: convertedQuery, ...options },
|
|
140
|
+
async () => collection.find(convertedQuery, driverOpts).toArray()
|
|
132
141
|
);
|
|
133
142
|
|
|
134
143
|
// 添加 explain 方法支持链式调用(与原生 MongoDB 一致)
|
|
135
144
|
resultPromise.explain = async (verbosity = 'queryPlanner') => {
|
|
136
|
-
const cursor = collection.find(
|
|
145
|
+
const cursor = collection.find(convertedQuery, driverOpts);
|
|
137
146
|
return cursor.explain(verbosity);
|
|
138
147
|
};
|
|
139
148
|
|
|
@@ -42,6 +42,7 @@ module.exports = {
|
|
|
42
42
|
createAggregateOps: require('./aggregate'),
|
|
43
43
|
createDistinctOps: require('./distinct'),
|
|
44
44
|
createFindPageOps, // 新增工厂函数
|
|
45
|
+
createWatchOps: require('./watch').createWatchOps, // 🆕 watch 方法
|
|
45
46
|
// 导出原始函数和辅助函数供 bookmark 模块使用
|
|
46
47
|
createFindPage,
|
|
47
48
|
bookmarkKey,
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watch 查询模块 - MongoDB Change Streams 封装
|
|
3
|
+
* @description 提供实时数据监听功能,支持自动重连、断点续传、智能缓存失效
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('events');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ChangeStream 包装类
|
|
10
|
+
* 提供自动重连、resumeToken 管理、智能缓存失效等功能
|
|
11
|
+
*/
|
|
12
|
+
class ChangeStreamWrapper {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} changeStream - MongoDB ChangeStream 实例
|
|
15
|
+
* @param {Object} collection - MongoDB Collection 实例
|
|
16
|
+
* @param {Array} pipeline - 聚合管道
|
|
17
|
+
* @param {Object} options - 配置选项
|
|
18
|
+
* @param {Object} context - 上下文对象
|
|
19
|
+
*/
|
|
20
|
+
constructor(changeStream, collection, pipeline, options, context) {
|
|
21
|
+
this._stream = changeStream;
|
|
22
|
+
this._collection = collection;
|
|
23
|
+
this._pipeline = pipeline;
|
|
24
|
+
this._options = options;
|
|
25
|
+
this._context = context;
|
|
26
|
+
|
|
27
|
+
// 状态管理
|
|
28
|
+
this._closed = false;
|
|
29
|
+
this._reconnecting = false;
|
|
30
|
+
this._reconnectAttempts = 0;
|
|
31
|
+
this._lastResumeToken = null;
|
|
32
|
+
|
|
33
|
+
// 统计信息
|
|
34
|
+
this._stats = {
|
|
35
|
+
totalChanges: 0,
|
|
36
|
+
reconnectAttempts: 0,
|
|
37
|
+
lastReconnectTime: null,
|
|
38
|
+
startTime: Date.now(),
|
|
39
|
+
cacheInvalidations: 0,
|
|
40
|
+
errors: 0
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 事件发射器
|
|
44
|
+
this._emitter = new EventEmitter();
|
|
45
|
+
|
|
46
|
+
// 设置事件监听
|
|
47
|
+
this._setupListeners();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 设置 MongoDB ChangeStream 事件监听
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_setupListeners() {
|
|
55
|
+
if (!this._stream) return;
|
|
56
|
+
|
|
57
|
+
// 监听变更事件
|
|
58
|
+
this._stream.on('change', (change) => {
|
|
59
|
+
this._lastResumeToken = change._id;
|
|
60
|
+
this._stats.totalChanges++;
|
|
61
|
+
this._handleChange(change);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 监听错误事件
|
|
65
|
+
this._stream.on('error', (error) => {
|
|
66
|
+
this._stats.errors++;
|
|
67
|
+
this._handleError(error);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 监听关闭事件
|
|
71
|
+
this._stream.on('close', () => {
|
|
72
|
+
if (!this._closed) {
|
|
73
|
+
this._handleClose();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 监听结束事件
|
|
78
|
+
this._stream.on('end', () => {
|
|
79
|
+
if (!this._closed) {
|
|
80
|
+
this._handleClose();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 处理变更事件
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
async _handleChange(change) {
|
|
90
|
+
try {
|
|
91
|
+
// 触发用户事件
|
|
92
|
+
this._emitter.emit('change', change);
|
|
93
|
+
|
|
94
|
+
// 自动缓存失效
|
|
95
|
+
if (this._options.autoInvalidateCache !== false) {
|
|
96
|
+
await this._invalidateCache(change);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (this._context.logger) {
|
|
100
|
+
this._context.logger.error('[Watch] Error handling change:', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 处理错误事件
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_handleError(error) {
|
|
110
|
+
const errorType = this._classifyError(error);
|
|
111
|
+
|
|
112
|
+
if (this._context.logger) {
|
|
113
|
+
this._context.logger.warn(`[Watch] Error (${errorType}):`, error.message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (errorType === 'transient') {
|
|
117
|
+
// 瞬态错误:自动重连,不触发用户事件
|
|
118
|
+
this._reconnect();
|
|
119
|
+
} else if (errorType === 'resumable') {
|
|
120
|
+
// 持久性错误:清除 token,重新开始,触发用户事件
|
|
121
|
+
this._lastResumeToken = null;
|
|
122
|
+
this._reconnect();
|
|
123
|
+
this._emitter.emit('error', error);
|
|
124
|
+
} else {
|
|
125
|
+
// 致命错误:触发 fatal 事件,然后停止监听
|
|
126
|
+
this._emitter.emit('fatal', error);
|
|
127
|
+
// 使用 setImmediate 确保 fatal 事件处理器先执行完成
|
|
128
|
+
// 避免在事件处理器中访问已关闭的资源
|
|
129
|
+
setImmediate(() => this.close());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 处理关闭事件
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_handleClose() {
|
|
138
|
+
if (this._options.autoReconnect !== false && !this._closed) {
|
|
139
|
+
// 自动重连
|
|
140
|
+
this._reconnect();
|
|
141
|
+
} else {
|
|
142
|
+
this._emitter.emit('close');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 分类错误类型
|
|
148
|
+
* @private
|
|
149
|
+
* @param {Error} error - 错误对象
|
|
150
|
+
* @returns {string} 'transient' | 'resumable' | 'fatal'
|
|
151
|
+
*/
|
|
152
|
+
_classifyError(error) {
|
|
153
|
+
const code = error.code;
|
|
154
|
+
const message = error.message || '';
|
|
155
|
+
|
|
156
|
+
// 瞬态错误(自动重试,不通知用户)
|
|
157
|
+
if (code === 'ECONNRESET' ||
|
|
158
|
+
code === 'ETIMEDOUT' ||
|
|
159
|
+
code === 'EPIPE' ||
|
|
160
|
+
message.includes('interrupted') ||
|
|
161
|
+
message.includes('connection') ||
|
|
162
|
+
message.includes('network') ||
|
|
163
|
+
message.includes('timeout')) {
|
|
164
|
+
return 'transient';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 持久性错误(清除 token,通知用户)
|
|
168
|
+
if (message.includes('resume token') ||
|
|
169
|
+
message.includes('change stream history lost') ||
|
|
170
|
+
message.includes('ChangeStreamHistoryLost')) {
|
|
171
|
+
return 'resumable';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 致命错误(停止监听)
|
|
175
|
+
if (message.includes('collection dropped') ||
|
|
176
|
+
message.includes('database dropped') ||
|
|
177
|
+
message.includes('ns not found') ||
|
|
178
|
+
message.includes('Unauthorized')) {
|
|
179
|
+
return 'fatal';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 默认为瞬态错误(尝试重连)
|
|
183
|
+
return 'transient';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 自动重连
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
async _reconnect() {
|
|
191
|
+
if (this._closed) return;
|
|
192
|
+
|
|
193
|
+
// 如果已经在重连,记录并跳过
|
|
194
|
+
if (this._reconnecting) {
|
|
195
|
+
if (this._context.logger) {
|
|
196
|
+
this._context.logger.debug('[Watch] Reconnect already in progress, skipping');
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this._reconnecting = true;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
this._reconnectAttempts++;
|
|
205
|
+
this._stats.reconnectAttempts++;
|
|
206
|
+
|
|
207
|
+
// 指数退避
|
|
208
|
+
const baseInterval = this._options.reconnectInterval || 1000;
|
|
209
|
+
const maxDelay = this._options.maxReconnectDelay || 60000;
|
|
210
|
+
const delay = Math.min(
|
|
211
|
+
baseInterval * Math.pow(2, this._reconnectAttempts - 1),
|
|
212
|
+
maxDelay
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
this._stats.lastReconnectTime = new Date().toISOString();
|
|
216
|
+
|
|
217
|
+
this._emitter.emit('reconnect', {
|
|
218
|
+
attempt: this._reconnectAttempts,
|
|
219
|
+
delay,
|
|
220
|
+
lastError: this._stats.lastError
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (this._context.logger) {
|
|
224
|
+
this._context.logger.info(
|
|
225
|
+
`[Watch] Reconnecting... attempt ${this._reconnectAttempts}, delay ${delay}ms`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
230
|
+
|
|
231
|
+
// 关闭旧的 stream
|
|
232
|
+
if (this._stream) {
|
|
233
|
+
try {
|
|
234
|
+
await this._stream.close();
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 构建新的选项
|
|
239
|
+
const watchOptions = { ...this._options };
|
|
240
|
+
|
|
241
|
+
// 移除 monSQLize 扩展选项
|
|
242
|
+
delete watchOptions.autoReconnect;
|
|
243
|
+
delete watchOptions.reconnectInterval;
|
|
244
|
+
delete watchOptions.maxReconnectDelay;
|
|
245
|
+
delete watchOptions.autoInvalidateCache;
|
|
246
|
+
|
|
247
|
+
// 添加 resumeToken
|
|
248
|
+
if (this._lastResumeToken) {
|
|
249
|
+
watchOptions.resumeAfter = this._lastResumeToken;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 重建 changeStream
|
|
253
|
+
this._stream = this._collection.watch(this._pipeline, watchOptions);
|
|
254
|
+
|
|
255
|
+
// 重新设置监听
|
|
256
|
+
this._setupListeners();
|
|
257
|
+
|
|
258
|
+
// 重置状态
|
|
259
|
+
this._reconnectAttempts = 0;
|
|
260
|
+
|
|
261
|
+
this._emitter.emit('resume', this._lastResumeToken);
|
|
262
|
+
|
|
263
|
+
if (this._context.logger) {
|
|
264
|
+
this._context.logger.info('[Watch] Reconnected successfully');
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
this._stats.lastError = error.message;
|
|
268
|
+
|
|
269
|
+
if (this._context.logger) {
|
|
270
|
+
this._context.logger.error('[Watch] Reconnect failed:', error.message);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 使用 setTimeout 避免同步递归调用 _handleError
|
|
274
|
+
// 确保当前调用栈完成后再触发下一次重连
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
if (!this._closed) {
|
|
277
|
+
this._handleError(error);
|
|
278
|
+
}
|
|
279
|
+
}, 0);
|
|
280
|
+
} finally {
|
|
281
|
+
// 确保无论成功失败都重置标志
|
|
282
|
+
this._reconnecting = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 智能缓存失效
|
|
288
|
+
* @private
|
|
289
|
+
* @param {Object} change - Change event
|
|
290
|
+
*/
|
|
291
|
+
async _invalidateCache(change) {
|
|
292
|
+
const { operationType, documentKey, ns } = change;
|
|
293
|
+
const collectionName = ns.coll;
|
|
294
|
+
const patterns = [];
|
|
295
|
+
|
|
296
|
+
// 根据操作类型构建失效模式
|
|
297
|
+
switch (operationType) {
|
|
298
|
+
case 'insert':
|
|
299
|
+
// 失效列表查询和计数
|
|
300
|
+
patterns.push(`*:${collectionName}:find:*`);
|
|
301
|
+
patterns.push(`*:${collectionName}:findPage:*`);
|
|
302
|
+
patterns.push(`*:${collectionName}:count:*`);
|
|
303
|
+
patterns.push(`*:${collectionName}:findAndCount:*`);
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 'update':
|
|
307
|
+
case 'replace':
|
|
308
|
+
// 失效单个文档和列表查询
|
|
309
|
+
if (documentKey?._id) {
|
|
310
|
+
patterns.push(`*:${collectionName}:findOne:*${documentKey._id}*`);
|
|
311
|
+
patterns.push(`*:${collectionName}:findOneById:${documentKey._id}*`);
|
|
312
|
+
}
|
|
313
|
+
patterns.push(`*:${collectionName}:find:*`);
|
|
314
|
+
patterns.push(`*:${collectionName}:findPage:*`);
|
|
315
|
+
patterns.push(`*:${collectionName}:findAndCount:*`);
|
|
316
|
+
break;
|
|
317
|
+
|
|
318
|
+
case 'delete':
|
|
319
|
+
// 失效单个文档和列表查询
|
|
320
|
+
if (documentKey?._id) {
|
|
321
|
+
patterns.push(`*:${collectionName}:findOne:*${documentKey._id}*`);
|
|
322
|
+
patterns.push(`*:${collectionName}:findOneById:${documentKey._id}*`);
|
|
323
|
+
}
|
|
324
|
+
patterns.push(`*:${collectionName}:find:*`);
|
|
325
|
+
patterns.push(`*:${collectionName}:findPage:*`);
|
|
326
|
+
patterns.push(`*:${collectionName}:count:*`);
|
|
327
|
+
patterns.push(`*:${collectionName}:findAndCount:*`);
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case 'drop':
|
|
331
|
+
case 'rename':
|
|
332
|
+
case 'dropDatabase':
|
|
333
|
+
// 失效整个集合
|
|
334
|
+
patterns.push(`*:${collectionName}:*`);
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
default:
|
|
338
|
+
// 其他操作类型,不失效缓存
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 执行失效
|
|
343
|
+
for (const pattern of patterns) {
|
|
344
|
+
try {
|
|
345
|
+
// 直接调用 cache.delPattern()
|
|
346
|
+
// 自动触发 DistributedCacheInvalidator.invalidate()
|
|
347
|
+
// 自动广播到其他实例
|
|
348
|
+
const cache = this._context.cache;
|
|
349
|
+
if (cache && typeof cache.delPattern === 'function') {
|
|
350
|
+
await cache.delPattern(pattern);
|
|
351
|
+
this._stats.cacheInvalidations++;
|
|
352
|
+
|
|
353
|
+
if (this._context.logger) {
|
|
354
|
+
this._context.logger.debug(
|
|
355
|
+
`[Watch] Invalidated cache: ${pattern}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (this._context.logger) {
|
|
361
|
+
this._context.logger.error(
|
|
362
|
+
`[Watch] Cache invalidation failed: ${error.message}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============================================
|
|
370
|
+
// 公共 API
|
|
371
|
+
// ============================================
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 监听事件
|
|
375
|
+
* @param {string} event - 事件名称
|
|
376
|
+
* @param {Function} handler - 事件处理函数
|
|
377
|
+
* @returns {ChangeStreamWrapper} 返回自身,支持链式调用
|
|
378
|
+
*/
|
|
379
|
+
on(event, handler) {
|
|
380
|
+
this._emitter.on(event, handler);
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 监听事件(一次性)
|
|
386
|
+
* @param {string} event - 事件名称
|
|
387
|
+
* @param {Function} handler - 事件处理函数
|
|
388
|
+
* @returns {ChangeStreamWrapper} 返回自身,支持链式调用
|
|
389
|
+
*/
|
|
390
|
+
once(event, handler) {
|
|
391
|
+
this._emitter.once(event, handler);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 移除事件监听
|
|
397
|
+
* @param {string} event - 事件名称
|
|
398
|
+
* @param {Function} handler - 事件处理函数
|
|
399
|
+
* @returns {ChangeStreamWrapper} 返回自身,支持链式调用
|
|
400
|
+
*/
|
|
401
|
+
off(event, handler) {
|
|
402
|
+
if (this._emitter.off) {
|
|
403
|
+
this._emitter.off(event, handler);
|
|
404
|
+
} else {
|
|
405
|
+
this._emitter.removeListener(event, handler);
|
|
406
|
+
}
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 关闭监听
|
|
412
|
+
*/
|
|
413
|
+
async close() {
|
|
414
|
+
if (this._closed) return;
|
|
415
|
+
|
|
416
|
+
this._closed = true;
|
|
417
|
+
|
|
418
|
+
if (this._stream) {
|
|
419
|
+
try {
|
|
420
|
+
await this._stream.close();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (this._context.logger) {
|
|
423
|
+
this._context.logger.error('[Watch] Error closing stream:', error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 先触发 close 事件,再移除监听器
|
|
429
|
+
this._emitter.emit('close');
|
|
430
|
+
|
|
431
|
+
// 移除所有事件监听器
|
|
432
|
+
this._emitter.removeAllListeners();
|
|
433
|
+
|
|
434
|
+
if (this._context.logger) {
|
|
435
|
+
this._context.logger.info('[Watch] Closed');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 检查是否已关闭
|
|
441
|
+
* @returns {boolean}
|
|
442
|
+
*/
|
|
443
|
+
isClosed() {
|
|
444
|
+
return this._closed;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* 获取当前 resumeToken
|
|
449
|
+
* @returns {Object|null}
|
|
450
|
+
*/
|
|
451
|
+
getResumeToken() {
|
|
452
|
+
return this._lastResumeToken;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 获取统计信息
|
|
457
|
+
* @returns {Object}
|
|
458
|
+
*/
|
|
459
|
+
getStats() {
|
|
460
|
+
return {
|
|
461
|
+
totalChanges: this._stats.totalChanges,
|
|
462
|
+
reconnectAttempts: this._stats.reconnectAttempts,
|
|
463
|
+
lastReconnectTime: this._stats.lastReconnectTime,
|
|
464
|
+
uptime: Date.now() - this._stats.startTime,
|
|
465
|
+
isActive: !this._closed && !this._reconnecting,
|
|
466
|
+
cacheInvalidations: this._stats.cacheInvalidations,
|
|
467
|
+
errors: this._stats.errors
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 创建 watch 操作
|
|
474
|
+
* @param {Object} context - 上下文对象
|
|
475
|
+
* @returns {Object} 包含 watch 方法的对象
|
|
476
|
+
*/
|
|
477
|
+
function createWatchOps(context) {
|
|
478
|
+
return {
|
|
479
|
+
/**
|
|
480
|
+
* 监听集合变更
|
|
481
|
+
* @param {Array} [pipeline=[]] - 聚合管道(过滤事件)
|
|
482
|
+
* @param {Object} [options={}] - 配置选项
|
|
483
|
+
* @returns {ChangeStreamWrapper}
|
|
484
|
+
*/
|
|
485
|
+
watch: (pipeline = [], options = {}) => {
|
|
486
|
+
// 参数验证
|
|
487
|
+
if (!Array.isArray(pipeline)) {
|
|
488
|
+
throw new Error('pipeline must be an array');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
492
|
+
const { convertAggregationPipeline } = require('../../utils/objectid-converter');
|
|
493
|
+
const convertedPipeline = convertAggregationPipeline(pipeline, 0, {
|
|
494
|
+
logger: context.logger,
|
|
495
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
496
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
497
|
+
maxDepth: context.autoConvertConfig?.maxDepth || 5
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// 构建 MongoDB watch 选项
|
|
501
|
+
const watchOptions = {
|
|
502
|
+
fullDocument: options.fullDocument || 'updateLookup',
|
|
503
|
+
...options
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// 移除 monSQLize 扩展选项(避免传递给 MongoDB)
|
|
507
|
+
delete watchOptions.autoReconnect;
|
|
508
|
+
delete watchOptions.reconnectInterval;
|
|
509
|
+
delete watchOptions.maxReconnectDelay;
|
|
510
|
+
delete watchOptions.autoInvalidateCache;
|
|
511
|
+
|
|
512
|
+
// 创建 MongoDB ChangeStream
|
|
513
|
+
const changeStream = context.collection.watch(convertedPipeline, watchOptions);
|
|
514
|
+
|
|
515
|
+
// 包装为 ChangeStreamWrapper
|
|
516
|
+
const wrapper = new ChangeStreamWrapper(
|
|
517
|
+
changeStream,
|
|
518
|
+
context.collection,
|
|
519
|
+
convertedPipeline,
|
|
520
|
+
options,
|
|
521
|
+
context
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
if (context.logger) {
|
|
525
|
+
context.logger.info('[Watch] Started watching collection:', context.collection.collectionName);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return wrapper;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
module.exports = {
|
|
534
|
+
createWatchOps,
|
|
535
|
+
ChangeStreamWrapper
|
|
536
|
+
};
|
|
537
|
+
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 删除所有匹配的文档
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { createError, ErrorCodes } = require(
|
|
7
|
-
const CacheFactory = require(
|
|
8
|
-
const { isInTransaction, getTransactionFromSession } = require(
|
|
6
|
+
const { createError, ErrorCodes } = require('../../errors');
|
|
7
|
+
const CacheFactory = require('../../cache');
|
|
8
|
+
const { isInTransaction, getTransactionFromSession } = require('../common/transaction-aware');
|
|
9
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 创建 deleteMany 操作
|
|
@@ -61,36 +62,44 @@ function createDeleteManyOps(context) {
|
|
|
61
62
|
const startTime = Date.now();
|
|
62
63
|
|
|
63
64
|
// 1. 参数验证
|
|
64
|
-
if (!filter || typeof filter !==
|
|
65
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
65
66
|
throw createError(
|
|
66
67
|
ErrorCodes.INVALID_ARGUMENT,
|
|
67
|
-
|
|
68
|
-
[{ field:
|
|
68
|
+
'filter 必须是对象类型',
|
|
69
|
+
[{ field: 'filter', type: 'object.required', message: 'filter 是必需参数且必须是对象' }]
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
74
|
+
const convertedFilter = convertObjectIdStrings(filter, 'filter', 0, new WeakSet(), {
|
|
75
|
+
logger: context.logger,
|
|
76
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
77
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
78
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
79
|
+
});
|
|
80
|
+
|
|
72
81
|
// 2. 警告:空 filter 会删除所有文档
|
|
73
|
-
if (Object.keys(
|
|
74
|
-
logger.warn(
|
|
82
|
+
if (Object.keys(convertedFilter).length === 0) {
|
|
83
|
+
logger.warn('[deleteMany] 警告: 空 filter 将删除集合中的所有文档', {
|
|
75
84
|
ns: `${databaseName}.${collectionName}`,
|
|
76
85
|
comment: options.comment
|
|
77
86
|
});
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
// 3. 构建操作上下文
|
|
81
|
-
const operation =
|
|
90
|
+
const operation = 'deleteMany';
|
|
82
91
|
const ns = `${databaseName}.${collectionName}`;
|
|
83
92
|
|
|
84
93
|
try {
|
|
85
94
|
// 4. 执行删除操作
|
|
86
|
-
const result = await nativeCollection.deleteMany(
|
|
95
|
+
const result = await nativeCollection.deleteMany(convertedFilter, options);
|
|
87
96
|
|
|
88
97
|
// 5. 自动失效缓存(如果有文档被删除)
|
|
89
98
|
if (cache && result.deletedCount > 0) {
|
|
90
99
|
try {
|
|
91
100
|
const ns = {
|
|
92
101
|
iid: instanceId,
|
|
93
|
-
type:
|
|
102
|
+
type: 'mongodb',
|
|
94
103
|
db: databaseName,
|
|
95
104
|
collection: collectionName
|
|
96
105
|
};
|