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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { createErrorMessage } = require('../../common/docs-urls');
|
|
7
|
+
const { convertObjectIdStrings, convertAggregationPipeline } = require('../../utils/objectid-converter');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* FindChain 类 - find 查询的链式调用构建器
|
|
@@ -25,7 +26,13 @@ class FindChain {
|
|
|
25
26
|
*/
|
|
26
27
|
constructor(context, query = {}, initialOptions = {}) {
|
|
27
28
|
this._context = context;
|
|
28
|
-
|
|
29
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
30
|
+
this._query = convertObjectIdStrings(query, 'filter', 0, new WeakSet(), {
|
|
31
|
+
logger: context.logger,
|
|
32
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
33
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
34
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
35
|
+
});
|
|
29
36
|
this._options = { ...initialOptions };
|
|
30
37
|
this._executed = false;
|
|
31
38
|
}
|
|
@@ -38,8 +45,8 @@ class FindChain {
|
|
|
38
45
|
limit(value) {
|
|
39
46
|
if (this._executed) {
|
|
40
47
|
throw new Error(createErrorMessage(
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
'Cannot call .limit() after query execution.\n' +
|
|
49
|
+
'Tip: Create a new chain for another query:\n' +
|
|
43
50
|
" const results = await collection('products').find({}).limit(10);",
|
|
44
51
|
'chaining.limit'
|
|
45
52
|
));
|
|
@@ -47,7 +54,7 @@ class FindChain {
|
|
|
47
54
|
if (typeof value !== 'number' || value < 0) {
|
|
48
55
|
throw new Error(createErrorMessage(
|
|
49
56
|
`limit() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
50
|
-
|
|
57
|
+
'Usage: .limit(10)',
|
|
51
58
|
'chaining.limit'
|
|
52
59
|
));
|
|
53
60
|
}
|
|
@@ -63,15 +70,15 @@ class FindChain {
|
|
|
63
70
|
skip(value) {
|
|
64
71
|
if (this._executed) {
|
|
65
72
|
throw new Error(createErrorMessage(
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
'Cannot call .skip() after query execution.\n' +
|
|
74
|
+
'Tip: Create a new chain for another query.',
|
|
68
75
|
'chaining.skip'
|
|
69
76
|
));
|
|
70
77
|
}
|
|
71
78
|
if (typeof value !== 'number' || value < 0) {
|
|
72
79
|
throw new Error(createErrorMessage(
|
|
73
80
|
`skip() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
74
|
-
|
|
81
|
+
'Usage: .skip(10)',
|
|
75
82
|
'chaining.skip'
|
|
76
83
|
));
|
|
77
84
|
}
|
|
@@ -87,15 +94,15 @@ class FindChain {
|
|
|
87
94
|
sort(value) {
|
|
88
95
|
if (this._executed) {
|
|
89
96
|
throw new Error(createErrorMessage(
|
|
90
|
-
|
|
97
|
+
'Cannot call .sort() after query execution.',
|
|
91
98
|
'chaining.sort'
|
|
92
99
|
));
|
|
93
100
|
}
|
|
94
101
|
if (!value || (typeof value !== 'object')) {
|
|
95
102
|
throw new Error(createErrorMessage(
|
|
96
103
|
`sort() requires an object or array, got: ${typeof value}\n` +
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
'Usage: .sort({ price: -1, name: 1 })\n' +
|
|
105
|
+
'Note: Use 1 for ascending, -1 for descending',
|
|
99
106
|
'chaining.sort'
|
|
100
107
|
));
|
|
101
108
|
}
|
|
@@ -111,14 +118,14 @@ class FindChain {
|
|
|
111
118
|
project(value) {
|
|
112
119
|
if (this._executed) {
|
|
113
120
|
throw new Error(createErrorMessage(
|
|
114
|
-
|
|
121
|
+
'Cannot call .project() after query execution.',
|
|
115
122
|
'chaining.project'
|
|
116
123
|
));
|
|
117
124
|
}
|
|
118
125
|
if (!value || typeof value !== 'object') {
|
|
119
126
|
throw new Error(createErrorMessage(
|
|
120
127
|
`project() requires an object or array, got: ${typeof value}\n` +
|
|
121
|
-
|
|
128
|
+
'Usage: .project({ name: 1, price: 1 })',
|
|
122
129
|
'chaining.project'
|
|
123
130
|
));
|
|
124
131
|
}
|
|
@@ -134,14 +141,14 @@ class FindChain {
|
|
|
134
141
|
hint(value) {
|
|
135
142
|
if (this._executed) {
|
|
136
143
|
throw new Error(createErrorMessage(
|
|
137
|
-
|
|
144
|
+
'Cannot call .hint() after query execution.',
|
|
138
145
|
'chaining.hint'
|
|
139
146
|
));
|
|
140
147
|
}
|
|
141
148
|
if (!value) {
|
|
142
149
|
throw new Error(createErrorMessage(
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
'hint() requires an index name or specification\n' +
|
|
151
|
+
'Usage: .hint({ category: 1, price: -1 }) or .hint(\'category_1_price_-1\')',
|
|
145
152
|
'chaining.hint'
|
|
146
153
|
));
|
|
147
154
|
}
|
|
@@ -157,14 +164,14 @@ class FindChain {
|
|
|
157
164
|
collation(value) {
|
|
158
165
|
if (this._executed) {
|
|
159
166
|
throw new Error(createErrorMessage(
|
|
160
|
-
|
|
167
|
+
'Cannot call .collation() after query execution.',
|
|
161
168
|
'chaining.collation'
|
|
162
169
|
));
|
|
163
170
|
}
|
|
164
171
|
if (!value || typeof value !== 'object') {
|
|
165
172
|
throw new Error(createErrorMessage(
|
|
166
173
|
`collation() requires an object, got: ${typeof value}\n` +
|
|
167
|
-
|
|
174
|
+
'Usage: .collation({ locale: \'zh\', strength: 2 })',
|
|
168
175
|
'chaining.collation'
|
|
169
176
|
));
|
|
170
177
|
}
|
|
@@ -180,14 +187,14 @@ class FindChain {
|
|
|
180
187
|
comment(value) {
|
|
181
188
|
if (this._executed) {
|
|
182
189
|
throw new Error(createErrorMessage(
|
|
183
|
-
|
|
190
|
+
'Cannot call .comment() after query execution.',
|
|
184
191
|
'chaining.comment'
|
|
185
192
|
));
|
|
186
193
|
}
|
|
187
194
|
if (typeof value !== 'string') {
|
|
188
195
|
throw new Error(createErrorMessage(
|
|
189
196
|
`comment() requires a string, got: ${typeof value}\n` +
|
|
190
|
-
|
|
197
|
+
'Usage: .comment(\'UserAPI:getProducts:user_123\')',
|
|
191
198
|
'chaining.comment'
|
|
192
199
|
));
|
|
193
200
|
}
|
|
@@ -203,14 +210,14 @@ class FindChain {
|
|
|
203
210
|
maxTimeMS(value) {
|
|
204
211
|
if (this._executed) {
|
|
205
212
|
throw new Error(createErrorMessage(
|
|
206
|
-
|
|
213
|
+
'Cannot call .maxTimeMS() after query execution.',
|
|
207
214
|
'chaining.maxTimeMS'
|
|
208
215
|
));
|
|
209
216
|
}
|
|
210
217
|
if (typeof value !== 'number' || value < 0) {
|
|
211
218
|
throw new Error(createErrorMessage(
|
|
212
219
|
`maxTimeMS() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
213
|
-
|
|
220
|
+
'Usage: .maxTimeMS(5000) // 5 seconds',
|
|
214
221
|
'chaining.maxTimeMS'
|
|
215
222
|
));
|
|
216
223
|
}
|
|
@@ -226,14 +233,14 @@ class FindChain {
|
|
|
226
233
|
batchSize(value) {
|
|
227
234
|
if (this._executed) {
|
|
228
235
|
throw new Error(createErrorMessage(
|
|
229
|
-
|
|
236
|
+
'Cannot call .batchSize() after query execution.',
|
|
230
237
|
'chaining.batchSize'
|
|
231
238
|
));
|
|
232
239
|
}
|
|
233
240
|
if (typeof value !== 'number' || value < 0) {
|
|
234
241
|
throw new Error(createErrorMessage(
|
|
235
242
|
`batchSize() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
236
|
-
|
|
243
|
+
'Usage: .batchSize(1000)',
|
|
237
244
|
'chaining.batchSize'
|
|
238
245
|
));
|
|
239
246
|
}
|
|
@@ -301,8 +308,8 @@ class FindChain {
|
|
|
301
308
|
async toArray() {
|
|
302
309
|
if (this._executed) {
|
|
303
310
|
throw new Error(createErrorMessage(
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
'Query already executed. Create a new chain for another query.\n' +
|
|
312
|
+
'Tip: Each chain can only be executed once:\n' +
|
|
306
313
|
" const results1 = await collection('products').find({}).limit(10);\n" +
|
|
307
314
|
" const results2 = await collection('products').find({}).limit(20); // Create new chain",
|
|
308
315
|
'chaining.toArray'
|
|
@@ -319,11 +326,11 @@ class FindChain {
|
|
|
319
326
|
const skip = this._options.skip;
|
|
320
327
|
const maxTimeMS = this._options.maxTimeMS !== undefined ? this._options.maxTimeMS : defaults.maxTimeMS;
|
|
321
328
|
|
|
322
|
-
const driverOpts = {
|
|
323
|
-
projection: this._options.projection,
|
|
324
|
-
sort,
|
|
325
|
-
skip,
|
|
326
|
-
maxTimeMS
|
|
329
|
+
const driverOpts = {
|
|
330
|
+
projection: this._options.projection,
|
|
331
|
+
sort,
|
|
332
|
+
skip,
|
|
333
|
+
maxTimeMS
|
|
327
334
|
};
|
|
328
335
|
if (this._options.hint) driverOpts.hint = this._options.hint;
|
|
329
336
|
if (this._options.collation) driverOpts.collation = this._options.collation;
|
|
@@ -389,14 +396,14 @@ class AggregateChain {
|
|
|
389
396
|
hint(value) {
|
|
390
397
|
if (this._executed) {
|
|
391
398
|
throw new Error(createErrorMessage(
|
|
392
|
-
|
|
399
|
+
'Cannot call .hint() after query execution.',
|
|
393
400
|
'chaining.hint'
|
|
394
401
|
));
|
|
395
402
|
}
|
|
396
403
|
if (!value) {
|
|
397
404
|
throw new Error(createErrorMessage(
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
'hint() requires an index name or specification\n' +
|
|
406
|
+
'Usage: .hint({ status: 1, createdAt: -1 })',
|
|
400
407
|
'chaining.hint'
|
|
401
408
|
));
|
|
402
409
|
}
|
|
@@ -412,14 +419,14 @@ class AggregateChain {
|
|
|
412
419
|
collation(value) {
|
|
413
420
|
if (this._executed) {
|
|
414
421
|
throw new Error(createErrorMessage(
|
|
415
|
-
|
|
422
|
+
'Cannot call .collation() after query execution.',
|
|
416
423
|
'chaining.collation'
|
|
417
424
|
));
|
|
418
425
|
}
|
|
419
426
|
if (!value || typeof value !== 'object') {
|
|
420
427
|
throw new Error(createErrorMessage(
|
|
421
428
|
`collation() requires an object, got: ${typeof value}\n` +
|
|
422
|
-
|
|
429
|
+
'Usage: .collation({ locale: \'zh\', strength: 2 })',
|
|
423
430
|
'chaining.collation'
|
|
424
431
|
));
|
|
425
432
|
}
|
|
@@ -435,14 +442,14 @@ class AggregateChain {
|
|
|
435
442
|
comment(value) {
|
|
436
443
|
if (this._executed) {
|
|
437
444
|
throw new Error(createErrorMessage(
|
|
438
|
-
|
|
445
|
+
'Cannot call .comment() after query execution.',
|
|
439
446
|
'chaining.comment'
|
|
440
447
|
));
|
|
441
448
|
}
|
|
442
449
|
if (typeof value !== 'string') {
|
|
443
450
|
throw new Error(createErrorMessage(
|
|
444
451
|
`comment() requires a string, got: ${typeof value}\n` +
|
|
445
|
-
|
|
452
|
+
'Usage: .comment(\'OrderAPI:aggregateSales\')',
|
|
446
453
|
'chaining.comment'
|
|
447
454
|
));
|
|
448
455
|
}
|
|
@@ -458,14 +465,14 @@ class AggregateChain {
|
|
|
458
465
|
maxTimeMS(value) {
|
|
459
466
|
if (this._executed) {
|
|
460
467
|
throw new Error(createErrorMessage(
|
|
461
|
-
|
|
468
|
+
'Cannot call .maxTimeMS() after query execution.',
|
|
462
469
|
'chaining.maxTimeMS'
|
|
463
470
|
));
|
|
464
471
|
}
|
|
465
472
|
if (typeof value !== 'number' || value < 0) {
|
|
466
473
|
throw new Error(createErrorMessage(
|
|
467
474
|
`maxTimeMS() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
468
|
-
|
|
475
|
+
'Usage: .maxTimeMS(10000) // 10 seconds',
|
|
469
476
|
'chaining.maxTimeMS'
|
|
470
477
|
));
|
|
471
478
|
}
|
|
@@ -481,14 +488,14 @@ class AggregateChain {
|
|
|
481
488
|
allowDiskUse(value) {
|
|
482
489
|
if (this._executed) {
|
|
483
490
|
throw new Error(createErrorMessage(
|
|
484
|
-
|
|
491
|
+
'Cannot call .allowDiskUse() after query execution.',
|
|
485
492
|
'chaining.allowDiskUse'
|
|
486
493
|
));
|
|
487
494
|
}
|
|
488
495
|
if (typeof value !== 'boolean') {
|
|
489
496
|
throw new Error(createErrorMessage(
|
|
490
497
|
`allowDiskUse() requires a boolean, got: ${typeof value}\n` +
|
|
491
|
-
|
|
498
|
+
'Usage: .allowDiskUse(true)',
|
|
492
499
|
'chaining.allowDiskUse'
|
|
493
500
|
));
|
|
494
501
|
}
|
|
@@ -504,14 +511,14 @@ class AggregateChain {
|
|
|
504
511
|
batchSize(value) {
|
|
505
512
|
if (this._executed) {
|
|
506
513
|
throw new Error(createErrorMessage(
|
|
507
|
-
|
|
514
|
+
'Cannot call .batchSize() after query execution.',
|
|
508
515
|
'chaining.batchSize'
|
|
509
516
|
));
|
|
510
517
|
}
|
|
511
518
|
if (typeof value !== 'number' || value < 0) {
|
|
512
519
|
throw new Error(createErrorMessage(
|
|
513
520
|
`batchSize() requires a non-negative number, got: ${typeof value} (${value})\n` +
|
|
514
|
-
|
|
521
|
+
'Usage: .batchSize(1000)',
|
|
515
522
|
'chaining.batchSize'
|
|
516
523
|
));
|
|
517
524
|
}
|
|
@@ -567,8 +574,8 @@ class AggregateChain {
|
|
|
567
574
|
async toArray() {
|
|
568
575
|
if (this._executed) {
|
|
569
576
|
throw new Error(createErrorMessage(
|
|
570
|
-
|
|
571
|
-
|
|
577
|
+
'Query already executed. Create a new chain for another query.\n' +
|
|
578
|
+
'Tip: Each chain can only be executed once:\n' +
|
|
572
579
|
" const results1 = await collection('orders').aggregate([...]).allowDiskUse(true);\n" +
|
|
573
580
|
" const results2 = await collection('orders').aggregate([...]).maxTimeMS(5000); // Create new chain",
|
|
574
581
|
'chaining.toArray'
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @description 提供文档计数功能,使用 MongoDB 原生推荐的 countDocuments() 和 estimatedDocumentCount() 方法
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* 创建 count 查询操作
|
|
8
10
|
* @param {Object} context - 上下文对象
|
|
@@ -28,11 +30,19 @@ function createCountOps(context) {
|
|
|
28
30
|
* @returns {Promise<number>} 匹配的文档数量;当 explain=true 时返回执行计划对象
|
|
29
31
|
*/
|
|
30
32
|
count: async (query = {}, options = {}) => {
|
|
33
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
34
|
+
const convertedQuery = convertObjectIdStrings(query, 'query', 0, new WeakSet(), {
|
|
35
|
+
logger: context.logger,
|
|
36
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
37
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
38
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
39
|
+
});
|
|
40
|
+
|
|
31
41
|
const { maxTimeMS = defaults.maxTimeMS, explain, comment } = options;
|
|
32
42
|
|
|
33
43
|
// 如果启用 explain,直接返回查询执行计划(不缓存)
|
|
34
44
|
if (explain) {
|
|
35
|
-
const verbosity = typeof explain ===
|
|
45
|
+
const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
|
|
36
46
|
const isEmptyQuery = !query || Object.keys(query).length === 0;
|
|
37
47
|
|
|
38
48
|
if (isEmptyQuery) {
|
|
@@ -44,7 +54,7 @@ function createCountOps(context) {
|
|
|
44
54
|
};
|
|
45
55
|
} else {
|
|
46
56
|
// countDocuments 通过聚合管道实现,使用 aggregate 获取 explain
|
|
47
|
-
const pipeline = [{ $match:
|
|
57
|
+
const pipeline = [{ $match: convertedQuery }, { $count: 'total' }];
|
|
48
58
|
const aggOpts = {
|
|
49
59
|
maxTimeMS,
|
|
50
60
|
...(options.hint && { hint: options.hint }),
|
|
@@ -56,11 +66,11 @@ function createCountOps(context) {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
// 性能优化:判断是否为空查询
|
|
59
|
-
const isEmptyQuery = !
|
|
69
|
+
const isEmptyQuery = !convertedQuery || Object.keys(convertedQuery).length === 0;
|
|
60
70
|
|
|
61
71
|
return run(
|
|
62
|
-
|
|
63
|
-
{ query, ...options },
|
|
72
|
+
'count',
|
|
73
|
+
{ query: convertedQuery, ...options },
|
|
64
74
|
() => {
|
|
65
75
|
if (isEmptyQuery) {
|
|
66
76
|
// 空查询使用 estimatedDocumentCount(快速,基于集合元数据)
|
|
@@ -77,7 +87,7 @@ function createCountOps(context) {
|
|
|
77
87
|
...(options.limit && { limit: options.limit })
|
|
78
88
|
};
|
|
79
89
|
if (comment) countOpts.comment = comment;
|
|
80
|
-
return collection.countDocuments(
|
|
90
|
+
return collection.countDocuments(convertedQuery, countOpts);
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @description 提供字段去重查询功能,使用 MongoDB 原生 distinct() 方法
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* 创建 distinct 查询操作
|
|
8
10
|
* @param {Object} context - 上下文对象
|
|
@@ -26,6 +28,13 @@ function createDistinctOps(context) {
|
|
|
26
28
|
* @returns {Promise<Array>} 返回去重后的值数组;当 explain=true 时返回执行计划对象
|
|
27
29
|
*/
|
|
28
30
|
distinct: async (field, query = {}, options = {}) => {
|
|
31
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
32
|
+
const convertedQuery = convertObjectIdStrings(query, 'query', 0, new WeakSet(), {
|
|
33
|
+
logger: context.logger,
|
|
34
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
35
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
36
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
37
|
+
});
|
|
29
38
|
const {
|
|
30
39
|
maxTimeMS = defaults.maxTimeMS,
|
|
31
40
|
collation,
|
|
@@ -41,11 +50,11 @@ function createDistinctOps(context) {
|
|
|
41
50
|
// 如果启用 explain,通过 aggregate 模拟 distinct 并返回执行计划
|
|
42
51
|
// 注意:MongoDB 原生 distinct 命令不支持 explain,需要通过聚合管道模拟
|
|
43
52
|
if (explain) {
|
|
44
|
-
const verbosity = typeof explain ===
|
|
53
|
+
const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
|
|
45
54
|
// distinct 命令通过聚合管道模拟:$match + $group
|
|
46
55
|
const pipeline = [];
|
|
47
|
-
if (
|
|
48
|
-
pipeline.push({ $match:
|
|
56
|
+
if (convertedQuery && Object.keys(convertedQuery).length > 0) {
|
|
57
|
+
pipeline.push({ $match: convertedQuery });
|
|
49
58
|
}
|
|
50
59
|
pipeline.push({ $group: { _id: `$${field}` } });
|
|
51
60
|
|
|
@@ -57,9 +66,9 @@ function createDistinctOps(context) {
|
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
return run(
|
|
60
|
-
|
|
61
|
-
{ field, query, ...options },
|
|
62
|
-
() => collection.distinct(field,
|
|
69
|
+
'distinct',
|
|
70
|
+
{ field, query: convertedQuery, ...options },
|
|
71
|
+
() => collection.distinct(field, convertedQuery, driverOpts)
|
|
63
72
|
);
|
|
64
73
|
}
|
|
65
74
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { createError, ErrorCodes } = require('../../errors');
|
|
7
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 创建 findAndCount 操作
|
|
@@ -70,6 +71,14 @@ function createFindAndCountOps(context) {
|
|
|
70
71
|
query = {};
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
75
|
+
const convertedQuery = convertObjectIdStrings(query, 'query', 0, new WeakSet(), {
|
|
76
|
+
logger: context.logger,
|
|
77
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
78
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
79
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
80
|
+
});
|
|
81
|
+
|
|
73
82
|
// 2. 提取选项
|
|
74
83
|
const projection = options.projection;
|
|
75
84
|
const sort = options.sort;
|
|
@@ -80,7 +89,7 @@ function createFindAndCountOps(context) {
|
|
|
80
89
|
const comment = options.comment;
|
|
81
90
|
|
|
82
91
|
// 3. 缓存键(包含 query, projection, sort, limit, skip)
|
|
83
|
-
const cacheKey = cache ? `${instanceId}:${type}:${effectiveDbName}:${collection.collectionName}:findAndCount:${JSON.stringify({ query, projection, sort, limit, skip })}` : null;
|
|
92
|
+
const cacheKey = cache ? `${instanceId}:${type}:${effectiveDbName}:${collection.collectionName}:findAndCount:${JSON.stringify({ query: convertedQuery, projection, sort, limit, skip })}` : null;
|
|
84
93
|
|
|
85
94
|
// 4. 检查缓存
|
|
86
95
|
if (cache && cacheTime > 0) {
|
|
@@ -90,7 +99,7 @@ function createFindAndCountOps(context) {
|
|
|
90
99
|
if (cached !== null && cached !== undefined) {
|
|
91
100
|
logger?.debug?.('[findAndCount] 缓存命中', {
|
|
92
101
|
ns: `${effectiveDbName}.${collection.collectionName}`,
|
|
93
|
-
query
|
|
102
|
+
query
|
|
94
103
|
});
|
|
95
104
|
return cached;
|
|
96
105
|
}
|
|
@@ -117,8 +126,8 @@ function createFindAndCountOps(context) {
|
|
|
117
126
|
let data, total;
|
|
118
127
|
try {
|
|
119
128
|
[data, total] = await Promise.all([
|
|
120
|
-
collection.find(
|
|
121
|
-
collection.countDocuments(
|
|
129
|
+
collection.find(convertedQuery, findOptions).toArray(),
|
|
130
|
+
collection.countDocuments(convertedQuery, countOptions)
|
|
122
131
|
]);
|
|
123
132
|
} catch (error) {
|
|
124
133
|
throw error;
|
|
@@ -146,17 +155,17 @@ function createFindAndCountOps(context) {
|
|
|
146
155
|
operation: 'findAndCount',
|
|
147
156
|
durationMs: duration,
|
|
148
157
|
iid: instanceId,
|
|
149
|
-
type
|
|
158
|
+
type,
|
|
150
159
|
db: effectiveDbName,
|
|
151
160
|
collection: collection.collectionName,
|
|
152
161
|
dataCount: data.length,
|
|
153
|
-
total
|
|
162
|
+
total,
|
|
154
163
|
query: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(query) : query,
|
|
155
|
-
projection
|
|
156
|
-
sort
|
|
157
|
-
limit
|
|
158
|
-
skip
|
|
159
|
-
comment
|
|
164
|
+
projection,
|
|
165
|
+
sort,
|
|
166
|
+
limit,
|
|
167
|
+
skip,
|
|
168
|
+
comment
|
|
160
169
|
};
|
|
161
170
|
logger?.warn?.('🐌 Slow query: findAndCount', meta);
|
|
162
171
|
emit?.('slow-query', meta);
|
|
@@ -168,9 +177,9 @@ function createFindAndCountOps(context) {
|
|
|
168
177
|
// 10. 日志记录
|
|
169
178
|
logger?.debug?.('[findAndCount] 查询完成', {
|
|
170
179
|
ns: `${effectiveDbName}.${collection.collectionName}`,
|
|
171
|
-
duration
|
|
180
|
+
duration,
|
|
172
181
|
dataCount: data.length,
|
|
173
|
-
total
|
|
182
|
+
total
|
|
174
183
|
});
|
|
175
184
|
|
|
176
185
|
return result;
|
|
@@ -178,16 +178,16 @@ function createFindByIdsOps(context) {
|
|
|
178
178
|
operation: 'findByIds',
|
|
179
179
|
durationMs: duration,
|
|
180
180
|
iid: instanceId,
|
|
181
|
-
type
|
|
181
|
+
type,
|
|
182
182
|
db: effectiveDbName,
|
|
183
183
|
collection: collection.collectionName,
|
|
184
184
|
idsCount: ids.length,
|
|
185
185
|
uniqueCount: uniqueIds.length,
|
|
186
186
|
resultCount: results.length,
|
|
187
187
|
query: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(query) : query,
|
|
188
|
-
projection
|
|
189
|
-
sort
|
|
190
|
-
comment
|
|
188
|
+
projection,
|
|
189
|
+
sort,
|
|
190
|
+
comment
|
|
191
191
|
};
|
|
192
192
|
logger?.warn?.('🐌 Slow query: findByIds', meta);
|
|
193
193
|
emit?.('slow-query', meta);
|
|
@@ -199,7 +199,7 @@ function createFindByIdsOps(context) {
|
|
|
199
199
|
// 12. 日志记录
|
|
200
200
|
logger?.debug?.('[findByIds] 查询完成', {
|
|
201
201
|
ns: `${effectiveDbName}.${collection.collectionName}`,
|
|
202
|
-
duration
|
|
202
|
+
duration,
|
|
203
203
|
idsCount: ids.length,
|
|
204
204
|
uniqueCount: uniqueIds.length,
|
|
205
205
|
resultCount: results.length
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { normalizeProjection, normalizeSort } = require('../../common/normalize');
|
|
7
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 创建 findOne 查询操作
|
|
@@ -30,6 +31,14 @@ function createFindOneOps(context) {
|
|
|
30
31
|
* @returns {Promise<Object|null>} 返回匹配的第一条记录对象,未找到时返回null;当 explain=true 时返回执行计划
|
|
31
32
|
*/
|
|
32
33
|
findOne: async (query = {}, options = {}) => {
|
|
34
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
35
|
+
const convertedQuery = convertObjectIdStrings(query, 'query', 0, new WeakSet(), {
|
|
36
|
+
logger: context.logger,
|
|
37
|
+
excludeFields: context.autoConvertConfig?.excludeFields,
|
|
38
|
+
customFieldPatterns: context.autoConvertConfig?.customFieldPatterns,
|
|
39
|
+
maxDepth: context.autoConvertConfig?.maxDepth
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
options.projection = normalizeProjection(options.projection);
|
|
34
43
|
const {
|
|
35
44
|
projection,
|
|
@@ -45,14 +54,14 @@ function createFindOneOps(context) {
|
|
|
45
54
|
// 如果启用 explain,直接返回执行计划(不缓存)
|
|
46
55
|
if (explain) {
|
|
47
56
|
const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
|
|
48
|
-
const cursor = collection.find(
|
|
57
|
+
const cursor = collection.find(convertedQuery, driverOpts).limit(1);
|
|
49
58
|
return await cursor.explain(verbosity);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
return run(
|
|
53
62
|
'findOne',
|
|
54
|
-
{ query, ...options },
|
|
55
|
-
() => collection.findOne(
|
|
63
|
+
{ query: convertedQuery, ...options },
|
|
64
|
+
() => collection.findOne(convertedQuery, driverOpts)
|
|
56
65
|
);
|
|
57
66
|
}
|
|
58
67
|
};
|
|
@@ -11,6 +11,7 @@ const { decodeCursor } = require('../../common/cursor');
|
|
|
11
11
|
const { validateLimitAfterBefore, assertCursorSortCompatible } = require('../../common/validation');
|
|
12
12
|
const { makePageResult } = require('../../common/page-result');
|
|
13
13
|
const { normalizeSort } = require('../../common/normalize');
|
|
14
|
+
const { convertObjectIdStrings } = require('../../utils/objectid-converter');
|
|
14
15
|
|
|
15
16
|
// —— Count 队列支持(高并发控制)——
|
|
16
17
|
let countQueue = null;
|
|
@@ -213,6 +214,17 @@ function createFindPage(ctx) {
|
|
|
213
214
|
|
|
214
215
|
return async function findPage(options = {}) {
|
|
215
216
|
const startTime = Date.now(); // 记录开始时间
|
|
217
|
+
|
|
218
|
+
// ✅ v1.3.0: 自动转换 ObjectId 字符串
|
|
219
|
+
if (options.query) {
|
|
220
|
+
options.query = convertObjectIdStrings(options.query, 'filter', 0, new WeakSet(), {
|
|
221
|
+
logger: ctx.logger,
|
|
222
|
+
excludeFields: ctx.autoConvertConfig?.excludeFields,
|
|
223
|
+
customFieldPatterns: ctx.autoConvertConfig?.customFieldPatterns,
|
|
224
|
+
maxDepth: ctx.autoConvertConfig?.maxDepth
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
216
228
|
const MAX_LIMIT = defaults?.findPageMaxLimit ?? 500;
|
|
217
229
|
// 基础校验:limit + after/before 互斥
|
|
218
230
|
validateLimitAfterBefore(options, { maxLimit: MAX_LIMIT });
|