monsqlize 1.3.1 → 2.0.0

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 (113) hide show
  1. package/CHANGELOG.md +415 -146
  2. package/LICENSE +201 -21
  3. package/README.md +537 -928
  4. package/changelogs/README.md +160 -0
  5. package/changelogs/v2.0.0.md +222 -0
  6. package/dist/cjs/index.cjs +10600 -0
  7. package/dist/cjs/mongodb/common/transaction-aware.cjs +10 -0
  8. package/dist/cjs/transaction/CacheLockManager.cjs +100 -0
  9. package/dist/cjs/transaction/Transaction.cjs +158 -0
  10. package/dist/cjs/transaction/TransactionManager.cjs +298 -0
  11. package/dist/esm/index.mjs +10650 -0
  12. package/dist/types/base.d.ts +81 -0
  13. package/dist/types/collection.d.ts +1031 -0
  14. package/dist/types/expression.d.ts +115 -0
  15. package/dist/types/index.d.ts +23 -0
  16. package/dist/types/lock.d.ts +74 -0
  17. package/dist/types/model.d.ts +526 -0
  18. package/dist/types/mongodb.d.ts +49 -0
  19. package/dist/types/monsqlize.d.ts +491 -0
  20. package/dist/types/pool.d.ts +84 -0
  21. package/dist/types/runtime.d.ts +362 -0
  22. package/dist/types/saga.d.ts +143 -0
  23. package/dist/types/slow-query-log.d.ts +126 -0
  24. package/dist/types/sync.d.ts +103 -0
  25. package/dist/types/transaction.d.ts +132 -0
  26. package/package.json +69 -67
  27. package/index.d.ts +0 -1289
  28. package/lib/cache.js +0 -491
  29. package/lib/common/cursor.js +0 -58
  30. package/lib/common/docs-urls.js +0 -72
  31. package/lib/common/index-options.js +0 -222
  32. package/lib/common/log.js +0 -60
  33. package/lib/common/namespace.js +0 -21
  34. package/lib/common/normalize.js +0 -33
  35. package/lib/common/page-result.js +0 -42
  36. package/lib/common/runner.js +0 -56
  37. package/lib/common/server-features.js +0 -231
  38. package/lib/common/shape-builders.js +0 -26
  39. package/lib/common/validation.js +0 -112
  40. package/lib/connect.js +0 -76
  41. package/lib/constants.js +0 -54
  42. package/lib/count-queue.js +0 -187
  43. package/lib/distributed-cache-invalidator.js +0 -259
  44. package/lib/errors.js +0 -167
  45. package/lib/index.js +0 -461
  46. package/lib/infrastructure/ssh-tunnel-ssh2.js +0 -211
  47. package/lib/infrastructure/ssh-tunnel.js +0 -40
  48. package/lib/infrastructure/uri-parser.js +0 -35
  49. package/lib/lock/Lock.js +0 -66
  50. package/lib/lock/errors.js +0 -27
  51. package/lib/lock/index.js +0 -12
  52. package/lib/logger.js +0 -224
  53. package/lib/model/examples/test.js +0 -114
  54. package/lib/mongodb/common/accessor-helpers.js +0 -58
  55. package/lib/mongodb/common/agg-pipeline.js +0 -32
  56. package/lib/mongodb/common/iid.js +0 -27
  57. package/lib/mongodb/common/lexicographic-expr.js +0 -52
  58. package/lib/mongodb/common/shape.js +0 -31
  59. package/lib/mongodb/common/sort.js +0 -38
  60. package/lib/mongodb/common/transaction-aware.js +0 -24
  61. package/lib/mongodb/connect.js +0 -233
  62. package/lib/mongodb/index.js +0 -591
  63. package/lib/mongodb/management/admin-ops.js +0 -199
  64. package/lib/mongodb/management/bookmark-ops.js +0 -166
  65. package/lib/mongodb/management/cache-ops.js +0 -49
  66. package/lib/mongodb/management/collection-ops.js +0 -386
  67. package/lib/mongodb/management/database-ops.js +0 -201
  68. package/lib/mongodb/management/index-ops.js +0 -474
  69. package/lib/mongodb/management/index.js +0 -16
  70. package/lib/mongodb/management/namespace.js +0 -30
  71. package/lib/mongodb/management/validation-ops.js +0 -267
  72. package/lib/mongodb/queries/aggregate.js +0 -142
  73. package/lib/mongodb/queries/chain.js +0 -630
  74. package/lib/mongodb/queries/count.js +0 -98
  75. package/lib/mongodb/queries/distinct.js +0 -77
  76. package/lib/mongodb/queries/find-and-count.js +0 -192
  77. package/lib/mongodb/queries/find-by-ids.js +0 -235
  78. package/lib/mongodb/queries/find-one-by-id.js +0 -170
  79. package/lib/mongodb/queries/find-one.js +0 -70
  80. package/lib/mongodb/queries/find-page.js +0 -577
  81. package/lib/mongodb/queries/find.js +0 -170
  82. package/lib/mongodb/queries/index.js +0 -50
  83. package/lib/mongodb/queries/watch.js +0 -537
  84. package/lib/mongodb/writes/delete-many.js +0 -190
  85. package/lib/mongodb/writes/delete-one.js +0 -182
  86. package/lib/mongodb/writes/find-one-and-delete.js +0 -202
  87. package/lib/mongodb/writes/find-one-and-replace.js +0 -238
  88. package/lib/mongodb/writes/find-one-and-update.js +0 -239
  89. package/lib/mongodb/writes/increment-one.js +0 -252
  90. package/lib/mongodb/writes/index.js +0 -41
  91. package/lib/mongodb/writes/insert-batch.js +0 -507
  92. package/lib/mongodb/writes/insert-many.js +0 -227
  93. package/lib/mongodb/writes/insert-one.js +0 -180
  94. package/lib/mongodb/writes/replace-one.js +0 -215
  95. package/lib/mongodb/writes/result-handler.js +0 -236
  96. package/lib/mongodb/writes/update-many.js +0 -221
  97. package/lib/mongodb/writes/update-one.js +0 -223
  98. package/lib/mongodb/writes/upsert-one.js +0 -206
  99. package/lib/multi-level-cache.js +0 -189
  100. package/lib/operators.js +0 -330
  101. package/lib/redis-cache-adapter.js +0 -237
  102. package/lib/slow-query-log/base-storage.js +0 -69
  103. package/lib/slow-query-log/batch-queue.js +0 -96
  104. package/lib/slow-query-log/config-manager.js +0 -195
  105. package/lib/slow-query-log/index.js +0 -237
  106. package/lib/slow-query-log/mongodb-storage.js +0 -323
  107. package/lib/slow-query-log/query-hash.js +0 -38
  108. package/lib/transaction/CacheLockManager.js +0 -161
  109. package/lib/transaction/DistributedCacheLockManager.js +0 -474
  110. package/lib/transaction/Transaction.js +0 -314
  111. package/lib/transaction/TransactionManager.js +0 -266
  112. package/lib/transaction/index.js +0 -10
  113. package/lib/utils/objectid-converter.js +0 -566
@@ -1,98 +0,0 @@
1
- /**
2
- * count 查询模块
3
- * @description 提供文档计数功能,使用 MongoDB 原生推荐的 countDocuments() 和 estimatedDocumentCount() 方法
4
- */
5
-
6
- const { convertObjectIdStrings } = require('../../utils/objectid-converter');
7
-
8
- /**
9
- * 创建 count 查询操作
10
- * @param {Object} context - 上下文对象
11
- * @returns {Object} 包含 count 方法的对象
12
- */
13
- function createCountOps(context) {
14
- const { collection, defaults, run, effectiveDbName } = context;
15
-
16
- return {
17
- /**
18
- * 统计文档数量
19
- * @description 根据查询条件统计匹配的文档数量。空查询时使用 estimatedDocumentCount(基于元数据,快速),有查询条件时使用 countDocuments(精确统计)
20
- * @param {Object} [query={}] - 查询条件,使用 MongoDB 查询语法,空对象表示统计所有文档
21
- * @param {Object} [options={}] - 查询选项配置对象
22
- * @param {number} [options.cache=0] - 缓存时间(毫秒),0表示不缓存,>0时结果将被缓存指定时间
23
- * @param {number} [options.maxTimeMS] - 查询超时时间(毫秒),防止长时间查询阻塞
24
- * @param {boolean|string} [options.explain] - 是否返回查询执行计划,可选值:true/'queryPlanner'/'executionStats'/'allPlansExecution'
25
- * @param {string} [options.hint] - 索引提示,指定使用的索引名称或索引规范(仅 countDocuments)
26
- * @param {Object} [options.collation] - 排序规则配置(仅 countDocuments)
27
- * @param {number} [options.skip] - 跳过的文档数量(仅 countDocuments)
28
- * @param {number} [options.limit] - 限制统计的文档数量(仅 countDocuments)
29
- * @param {string} [options.comment] - 查询注释,用于日志和性能分析
30
- * @returns {Promise<number>} 匹配的文档数量;当 explain=true 时返回执行计划对象
31
- */
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
-
41
- const { maxTimeMS = defaults.maxTimeMS, explain, comment } = options;
42
-
43
- // 如果启用 explain,直接返回查询执行计划(不缓存)
44
- if (explain) {
45
- const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
46
- const isEmptyQuery = !query || Object.keys(query).length === 0;
47
-
48
- if (isEmptyQuery) {
49
- // estimatedDocumentCount 没有 explain,返回集合统计信息
50
- return {
51
- queryPlanner: { plannerVersion: 1, namespace: `${effectiveDbName}.${collection.collectionName}` },
52
- executionStats: { executionSuccess: true, estimatedCount: true },
53
- command: { estimatedDocumentCount: collection.collectionName }
54
- };
55
- } else {
56
- // countDocuments 通过聚合管道实现,使用 aggregate 获取 explain
57
- const pipeline = [{ $match: convertedQuery }, { $count: 'total' }];
58
- const aggOpts = {
59
- maxTimeMS,
60
- ...(options.hint && { hint: options.hint }),
61
- ...(options.collation && { collation: options.collation })
62
- };
63
- if (comment) aggOpts.comment = comment;
64
- return await collection.aggregate(pipeline, aggOpts).explain(verbosity);
65
- }
66
- }
67
-
68
- // 性能优化:判断是否为空查询
69
- const isEmptyQuery = !convertedQuery || Object.keys(convertedQuery).length === 0;
70
-
71
- return run(
72
- 'count',
73
- { query: convertedQuery, ...options },
74
- () => {
75
- if (isEmptyQuery) {
76
- // 空查询使用 estimatedDocumentCount(快速,基于集合元数据)
77
- const estOpts = { maxTimeMS };
78
- if (comment) estOpts.comment = comment;
79
- return collection.estimatedDocumentCount(estOpts);
80
- } else {
81
- // 有查询条件使用 countDocuments(精确统计)
82
- const countOpts = {
83
- maxTimeMS,
84
- ...(options.hint && { hint: options.hint }),
85
- ...(options.collation && { collation: options.collation }),
86
- ...(options.skip && { skip: options.skip }),
87
- ...(options.limit && { limit: options.limit })
88
- };
89
- if (comment) countOpts.comment = comment;
90
- return collection.countDocuments(convertedQuery, countOpts);
91
- }
92
- }
93
- );
94
- }
95
- };
96
- }
97
-
98
- module.exports = createCountOps;
@@ -1,77 +0,0 @@
1
- /**
2
- * distinct 查询模块
3
- * @description 提供字段去重查询功能,使用 MongoDB 原生 distinct() 方法
4
- */
5
-
6
- const { convertObjectIdStrings } = require('../../utils/objectid-converter');
7
-
8
- /**
9
- * 创建 distinct 查询操作
10
- * @param {Object} context - 上下文对象
11
- * @returns {Object} 包含 distinct 方法的对象
12
- */
13
- function createDistinctOps(context) {
14
- const { collection, defaults, run } = context;
15
-
16
- return {
17
- /**
18
- * 字段去重查询
19
- * @description 对指定字段进行去重查询,返回该字段的所有唯一值数组。支持嵌套字段和数组字段(自动展开去重)
20
- * @param {string} field - 要去重的字段名,支持嵌套字段(如 'user.name'、'address.city')
21
- * @param {Object} [query={}] - 查询条件,只对匹配的文档进行去重,使用 MongoDB 查询语法
22
- * @param {Object} [options={}] - 查询选项配置对象
23
- * @param {number} [options.maxTimeMS] - 查询超时时间(毫秒),防止长时间查询阻塞
24
- * @param {Object} [options.collation] - 排序规则配置,用于字符串比较和去重(如不区分大小写)
25
- * @param {string} [options.comment] - 查询注释,用于日志和性能分析
26
- * @param {number} [options.cache=0] - 缓存时间(毫秒),0表示不缓存,>0时结果将被缓存指定时间
27
- * @param {boolean|string} [options.explain] - 是否返回查询执行计划,可选值:true/'queryPlanner'/'executionStats'/'allPlansExecution'
28
- * @returns {Promise<Array>} 返回去重后的值数组;当 explain=true 时返回执行计划对象
29
- */
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
- });
38
- const {
39
- maxTimeMS = defaults.maxTimeMS,
40
- collation,
41
- comment,
42
- explain
43
- } = options;
44
-
45
- // 构建驱动选项
46
- const driverOpts = { maxTimeMS };
47
- if (collation) driverOpts.collation = collation;
48
- if (comment) driverOpts.comment = comment;
49
-
50
- // 如果启用 explain,通过 aggregate 模拟 distinct 并返回执行计划
51
- // 注意:MongoDB 原生 distinct 命令不支持 explain,需要通过聚合管道模拟
52
- if (explain) {
53
- const verbosity = typeof explain === 'string' ? explain : 'queryPlanner';
54
- // distinct 命令通过聚合管道模拟:$match + $group
55
- const pipeline = [];
56
- if (convertedQuery && Object.keys(convertedQuery).length > 0) {
57
- pipeline.push({ $match: convertedQuery });
58
- }
59
- pipeline.push({ $group: { _id: `$${field}` } });
60
-
61
- const aggOpts = { maxTimeMS };
62
- if (collation) aggOpts.collation = collation;
63
- if (comment) aggOpts.comment = comment;
64
-
65
- return await collection.aggregate(pipeline, aggOpts).explain(verbosity);
66
- }
67
-
68
- return run(
69
- 'distinct',
70
- { field, query: convertedQuery, ...options },
71
- () => collection.distinct(field, convertedQuery, driverOpts)
72
- );
73
- }
74
- };
75
- }
76
-
77
- module.exports = createDistinctOps;
@@ -1,192 +0,0 @@
1
- /**
2
- * findAndCount 查询操作模块
3
- * @description 便利方法:同时返回数据和总数
4
- */
5
-
6
- const { createError, ErrorCodes } = require('../../errors');
7
- const { convertObjectIdStrings } = require('../../utils/objectid-converter');
8
-
9
- /**
10
- * 创建 findAndCount 操作
11
- * @param {Object} context - 上下文对象
12
- * @returns {Function} findAndCount 方法
13
- */
14
- function createFindAndCountOps(context) {
15
- const {
16
- collection,
17
- defaults,
18
- instanceId,
19
- effectiveDbName,
20
- logger,
21
- emit,
22
- mongoSlowLogShaper,
23
- cache,
24
- type
25
- } = context;
26
-
27
- /**
28
- * 查询数据并返回总数(同时执行)
29
- * @param {Object} [query={}] - 查询条件
30
- * @param {Object} [options={}] - 查询选项
31
- * @param {Object} [options.projection] - 字段投影
32
- * @param {Object} [options.sort] - 排序方式
33
- * @param {number} [options.limit] - 限制返回数量
34
- * @param {number} [options.skip] - 跳过数量
35
- * @param {number} [options.cache] - 缓存时间(毫秒)
36
- * @param {number} [options.maxTimeMS] - 查询超时(毫秒)
37
- * @param {string} [options.comment] - 查询注释
38
- * @returns {Promise<Object>} { data, total }
39
- *
40
- * @example
41
- * // 基础用法
42
- * const { data, total } = await collection('users').findAndCount(
43
- * { status: 'active' },
44
- * { limit: 10, skip: 0 }
45
- * );
46
- *
47
- * @example
48
- * // 分页查询
49
- * const page = 1;
50
- * const pageSize = 20;
51
- * const { data, total } = await collection('users').findAndCount(
52
- * { role: 'user' },
53
- * { limit: pageSize, skip: (page - 1) * pageSize }
54
- * );
55
- * const totalPages = Math.ceil(total / pageSize);
56
- */
57
- const findAndCount = async function findAndCount(query = {}, options = {}) {
58
- const startTime = Date.now();
59
-
60
- // 1. 参数验证和归一化
61
- if (query !== null && typeof query !== 'object' || Array.isArray(query)) {
62
- throw createError(
63
- ErrorCodes.INVALID_ARGUMENT,
64
- 'query 必须是对象',
65
- [{ field: 'query', type: 'type', message: 'query 必须是对象', received: typeof query }]
66
- );
67
- }
68
-
69
- // 将 null 转为空对象
70
- if (query === null) {
71
- query = {};
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
-
82
- // 2. 提取选项
83
- const projection = options.projection;
84
- const sort = options.sort;
85
- const limit = options.limit; // 不使用默认值,未指定时查询所有
86
- const skip = options.skip || 0;
87
- const cacheTime = options.cache !== undefined ? options.cache : defaults.cache;
88
- const maxTimeMS = options.maxTimeMS !== undefined ? options.maxTimeMS : defaults.maxTimeMS;
89
- const comment = options.comment;
90
-
91
- // 3. 缓存键(包含 query, projection, sort, limit, skip)
92
- const cacheKey = cache ? `${instanceId}:${type}:${effectiveDbName}:${collection.collectionName}:findAndCount:${JSON.stringify({ query: convertedQuery, projection, sort, limit, skip })}` : null;
93
-
94
- // 4. 检查缓存
95
- if (cache && cacheTime > 0) {
96
- try {
97
- const cached = await cache.get(cacheKey);
98
- // 必须检查 !== null 和 !== undefined,因为 undefined 也会被缓存
99
- if (cached !== null && cached !== undefined) {
100
- logger?.debug?.('[findAndCount] 缓存命中', {
101
- ns: `${effectiveDbName}.${collection.collectionName}`,
102
- query
103
- });
104
- return cached;
105
- }
106
- } catch (cacheError) {
107
- logger?.warn?.('[findAndCount] 缓存读取失败', { error: cacheError.message });
108
- }
109
- }
110
-
111
- // 5. 构建查询选项
112
- const findOptions = { maxTimeMS };
113
- if (projection) findOptions.projection = projection;
114
- if (sort) findOptions.sort = sort;
115
- // limit: undefined/null 表示不限制,0 表示返回0条,其他数字表示限制数量
116
- if (limit !== undefined && limit !== null) {
117
- findOptions.limit = limit;
118
- }
119
- if (skip) findOptions.skip = skip;
120
- if (comment) findOptions.comment = comment;
121
-
122
- const countOptions = { maxTimeMS };
123
- if (comment) countOptions.comment = comment;
124
-
125
- // 6. 并行执行查询和计数
126
- let data, total;
127
- try {
128
- [data, total] = await Promise.all([
129
- collection.find(convertedQuery, findOptions).toArray(),
130
- collection.countDocuments(convertedQuery, countOptions)
131
- ]);
132
- } catch (error) {
133
- throw error;
134
- }
135
-
136
- // 7. 构建结果
137
- const result = { data, total };
138
-
139
- // 8. 写入缓存
140
- if (cache && cacheTime > 0) {
141
- try {
142
- await cache.set(cacheKey, result, cacheTime);
143
- } catch (cacheError) {
144
- logger?.warn?.('[findAndCount] 缓存写入失败', { error: cacheError.message });
145
- }
146
- }
147
-
148
- // 9. 慢查询日志
149
- const duration = Date.now() - startTime;
150
- const slowQueryMs = defaults?.slowQueryMs || 1000;
151
-
152
- if (duration >= slowQueryMs) {
153
- try {
154
- const meta = {
155
- operation: 'findAndCount',
156
- durationMs: duration,
157
- iid: instanceId,
158
- type,
159
- db: effectiveDbName,
160
- collection: collection.collectionName,
161
- dataCount: data.length,
162
- total,
163
- query: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(query) : query,
164
- projection,
165
- sort,
166
- limit,
167
- skip,
168
- comment
169
- };
170
- logger?.warn?.('🐌 Slow query: findAndCount', meta);
171
- emit?.('slow-query', meta);
172
- } catch (_) {
173
- // 忽略日志错误
174
- }
175
- }
176
-
177
- // 10. 日志记录
178
- logger?.debug?.('[findAndCount] 查询完成', {
179
- ns: `${effectiveDbName}.${collection.collectionName}`,
180
- duration,
181
- dataCount: data.length,
182
- total
183
- });
184
-
185
- return result;
186
- };
187
-
188
- return { findAndCount };
189
- }
190
-
191
- module.exports = { createFindAndCountOps };
192
-
@@ -1,235 +0,0 @@
1
- /**
2
- * findByIds 查询操作模块
3
- * @description 便利方法:批量通过 _id 数组查询多个文档
4
- */
5
-
6
- const { ObjectId } = require('mongodb');
7
- const { createError, ErrorCodes } = require('../../errors');
8
-
9
- /**
10
- * 创建 findByIds 操作
11
- * @param {Object} context - 上下文对象
12
- * @returns {Function} findByIds 方法
13
- */
14
- function createFindByIdsOps(context) {
15
- const {
16
- collection,
17
- defaults,
18
- instanceId,
19
- effectiveDbName,
20
- logger,
21
- emit,
22
- mongoSlowLogShaper,
23
- cache,
24
- type
25
- } = context;
26
-
27
- /**
28
- * 批量通过 _id 查询多个文档
29
- * @param {Array<string|ObjectId>} ids - _id 数组(支持字符串和 ObjectId)
30
- * @param {Object} [options={}] - 查询选项
31
- * @param {Object} [options.projection] - 字段投影
32
- * @param {Object} [options.sort] - 排序方式
33
- * @param {number} [options.cache] - 缓存时间(毫秒)
34
- * @param {number} [options.maxTimeMS] - 查询超时(毫秒)
35
- * @param {string} [options.comment] - 查询注释
36
- * @param {boolean} [options.preserveOrder=false] - 是否保持 ids 数组的顺序
37
- * @returns {Promise<Array>} 文档数组
38
- *
39
- * @example
40
- * // 基础用法
41
- * const users = await collection('users').findByIds([
42
- * '507f1f77bcf86cd799439011',
43
- * '507f1f77bcf86cd799439012'
44
- * ]);
45
- *
46
- * @example
47
- * // 带选项
48
- * const users = await collection('users').findByIds(
49
- * ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'],
50
- * {
51
- * projection: { name: 1, email: 1 },
52
- * preserveOrder: true
53
- * }
54
- * );
55
- */
56
- const findByIds = async function findByIds(ids, options = {}) {
57
- const startTime = Date.now();
58
-
59
- // 1. 参数验证
60
- if (!Array.isArray(ids)) {
61
- throw createError(
62
- ErrorCodes.INVALID_ARGUMENT,
63
- 'ids 必须是数组',
64
- [{ field: 'ids', type: 'type', message: 'ids 必须是数组', received: typeof ids }]
65
- );
66
- }
67
-
68
- if (ids.length === 0) {
69
- // 空数组直接返回空结果
70
- return [];
71
- }
72
-
73
- // 2. 转换所有 ID 为 ObjectId
74
- const objectIds = [];
75
- const invalidIds = [];
76
-
77
- for (let i = 0; i < ids.length; i++) {
78
- const id = ids[i];
79
-
80
- if (id instanceof ObjectId) {
81
- objectIds.push(id);
82
- } else if (typeof id === 'string') {
83
- // 验证字符串是否是有效的 ObjectId 格式(24 个十六进制字符)
84
- if (!/^[0-9a-fA-F]{24}$/.test(id)) {
85
- invalidIds.push({ index: i, value: id });
86
- } else {
87
- objectIds.push(new ObjectId(id));
88
- }
89
- } else {
90
- // 拒绝其他类型(数字、对象等)
91
- invalidIds.push({ index: i, value: id, type: typeof id });
92
- }
93
- }
94
-
95
- // 如果有无效 ID,抛出错误
96
- if (invalidIds.length > 0) {
97
- throw createError(
98
- ErrorCodes.INVALID_ARGUMENT,
99
- `ids 数组包含 ${invalidIds.length} 个无效 ID`,
100
- invalidIds.map(item => ({
101
- field: `ids[${item.index}]`,
102
- type: 'format',
103
- message: '无效的 ObjectId 格式',
104
- received: item.value
105
- }))
106
- );
107
- }
108
-
109
- // 3. 去重(避免重复查询)
110
- const uniqueIds = [...new Set(objectIds.map(id => id.toString()))].map(id => new ObjectId(id));
111
-
112
- // 4. 提取选项
113
- const projection = options.projection;
114
- const sort = options.sort;
115
- const cacheTime = options.cache !== undefined ? options.cache : defaults.cache;
116
- const maxTimeMS = options.maxTimeMS !== undefined ? options.maxTimeMS : defaults.maxTimeMS;
117
- const comment = options.comment;
118
- const preserveOrder = options.preserveOrder === true;
119
-
120
- // 5. 构建查询
121
- const query = { _id: { $in: uniqueIds } };
122
-
123
- // 6. 缓存键
124
- const cacheKey = cache ? `${instanceId}:${type}:${effectiveDbName}:${collection.collectionName}:findByIds:${JSON.stringify({ ids: uniqueIds.map(id => id.toString()), projection, sort })}` : null;
125
-
126
- // 7. 检查缓存
127
- if (cache && cacheTime > 0) {
128
- try {
129
- const cached = await cache.get(cacheKey);
130
- if (cached != null) {
131
- logger?.debug?.('[findByIds] 缓存命中', {
132
- ns: `${effectiveDbName}.${collection.collectionName}`,
133
- idsCount: ids.length,
134
- uniqueCount: uniqueIds.length
135
- });
136
-
137
- // 如果需要保持顺序,重新排序结果
138
- if (preserveOrder) {
139
- return reorderResults(cached, objectIds);
140
- }
141
- return cached;
142
- }
143
- } catch (cacheError) {
144
- logger?.warn?.('[findByIds] 缓存读取失败', { error: cacheError.message });
145
- }
146
- }
147
-
148
- // 8. 构建查询选项
149
- const findOptions = { maxTimeMS };
150
- if (projection) findOptions.projection = projection;
151
- if (sort) findOptions.sort = sort;
152
- if (comment) findOptions.comment = comment;
153
-
154
- // 9. 执行查询
155
- let results;
156
- try {
157
- results = await collection.find(query, findOptions).toArray();
158
- } catch (error) {
159
- throw error;
160
- }
161
-
162
- // 10. 写入缓存
163
- if (cache && cacheTime > 0 && results) {
164
- try {
165
- await cache.set(cacheKey, results, cacheTime);
166
- } catch (cacheError) {
167
- logger?.warn?.('[findByIds] 缓存写入失败', { error: cacheError.message });
168
- }
169
- }
170
-
171
- // 11. 慢查询日志
172
- const duration = Date.now() - startTime;
173
- const slowQueryMs = defaults?.slowQueryMs || 1000;
174
-
175
- if (duration >= slowQueryMs) {
176
- try {
177
- const meta = {
178
- operation: 'findByIds',
179
- durationMs: duration,
180
- iid: instanceId,
181
- type,
182
- db: effectiveDbName,
183
- collection: collection.collectionName,
184
- idsCount: ids.length,
185
- uniqueCount: uniqueIds.length,
186
- resultCount: results.length,
187
- query: mongoSlowLogShaper?.sanitize ? mongoSlowLogShaper.sanitize(query) : query,
188
- projection,
189
- sort,
190
- comment
191
- };
192
- logger?.warn?.('🐌 Slow query: findByIds', meta);
193
- emit?.('slow-query', meta);
194
- } catch (_) {
195
- // 忽略日志错误
196
- }
197
- }
198
-
199
- // 12. 日志记录
200
- logger?.debug?.('[findByIds] 查询完成', {
201
- ns: `${effectiveDbName}.${collection.collectionName}`,
202
- duration,
203
- idsCount: ids.length,
204
- uniqueCount: uniqueIds.length,
205
- resultCount: results.length
206
- });
207
-
208
- // 13. 如果需要保持顺序,重新排序结果
209
- if (preserveOrder) {
210
- return reorderResults(results, objectIds);
211
- }
212
-
213
- return results;
214
- };
215
-
216
- /**
217
- * 根据原始 ID 顺序重新排序结果
218
- * @param {Array} results - 查询结果
219
- * @param {Array<ObjectId>} orderedIds - 原始 ID 顺序
220
- * @returns {Array} 排序后的结果
221
- */
222
- function reorderResults(results, orderedIds) {
223
- const resultMap = new Map();
224
- results.forEach(doc => {
225
- resultMap.set(doc._id.toString(), doc);
226
- });
227
-
228
- return orderedIds.map(id => resultMap.get(id.toString())).filter(doc => doc !== undefined);
229
- }
230
-
231
- return { findByIds };
232
- }
233
-
234
- module.exports = { createFindByIdsOps };
235
-