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