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.
Files changed (77) hide show
  1. package/CHANGELOG.md +2474 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1368 -0
  4. package/index.d.ts +1052 -0
  5. package/lib/cache.js +491 -0
  6. package/lib/common/cursor.js +58 -0
  7. package/lib/common/docs-urls.js +72 -0
  8. package/lib/common/index-options.js +222 -0
  9. package/lib/common/log.js +60 -0
  10. package/lib/common/namespace.js +21 -0
  11. package/lib/common/normalize.js +33 -0
  12. package/lib/common/page-result.js +42 -0
  13. package/lib/common/runner.js +56 -0
  14. package/lib/common/server-features.js +231 -0
  15. package/lib/common/shape-builders.js +26 -0
  16. package/lib/common/validation.js +49 -0
  17. package/lib/connect.js +76 -0
  18. package/lib/constants.js +54 -0
  19. package/lib/count-queue.js +187 -0
  20. package/lib/distributed-cache-invalidator.js +259 -0
  21. package/lib/errors.js +157 -0
  22. package/lib/index.js +352 -0
  23. package/lib/logger.js +224 -0
  24. package/lib/model/examples/test.js +114 -0
  25. package/lib/mongodb/common/accessor-helpers.js +44 -0
  26. package/lib/mongodb/common/agg-pipeline.js +32 -0
  27. package/lib/mongodb/common/iid.js +27 -0
  28. package/lib/mongodb/common/lexicographic-expr.js +52 -0
  29. package/lib/mongodb/common/shape.js +31 -0
  30. package/lib/mongodb/common/sort.js +38 -0
  31. package/lib/mongodb/common/transaction-aware.js +24 -0
  32. package/lib/mongodb/connect.js +178 -0
  33. package/lib/mongodb/index.js +458 -0
  34. package/lib/mongodb/management/admin-ops.js +199 -0
  35. package/lib/mongodb/management/bookmark-ops.js +166 -0
  36. package/lib/mongodb/management/cache-ops.js +49 -0
  37. package/lib/mongodb/management/collection-ops.js +386 -0
  38. package/lib/mongodb/management/database-ops.js +201 -0
  39. package/lib/mongodb/management/index-ops.js +474 -0
  40. package/lib/mongodb/management/index.js +16 -0
  41. package/lib/mongodb/management/namespace.js +30 -0
  42. package/lib/mongodb/management/validation-ops.js +267 -0
  43. package/lib/mongodb/queries/aggregate.js +133 -0
  44. package/lib/mongodb/queries/chain.js +623 -0
  45. package/lib/mongodb/queries/count.js +88 -0
  46. package/lib/mongodb/queries/distinct.js +68 -0
  47. package/lib/mongodb/queries/find-and-count.js +183 -0
  48. package/lib/mongodb/queries/find-by-ids.js +235 -0
  49. package/lib/mongodb/queries/find-one-by-id.js +170 -0
  50. package/lib/mongodb/queries/find-one.js +61 -0
  51. package/lib/mongodb/queries/find-page.js +565 -0
  52. package/lib/mongodb/queries/find.js +161 -0
  53. package/lib/mongodb/queries/index.js +49 -0
  54. package/lib/mongodb/writes/delete-many.js +181 -0
  55. package/lib/mongodb/writes/delete-one.js +173 -0
  56. package/lib/mongodb/writes/find-one-and-delete.js +193 -0
  57. package/lib/mongodb/writes/find-one-and-replace.js +222 -0
  58. package/lib/mongodb/writes/find-one-and-update.js +223 -0
  59. package/lib/mongodb/writes/increment-one.js +243 -0
  60. package/lib/mongodb/writes/index.js +41 -0
  61. package/lib/mongodb/writes/insert-batch.js +498 -0
  62. package/lib/mongodb/writes/insert-many.js +218 -0
  63. package/lib/mongodb/writes/insert-one.js +171 -0
  64. package/lib/mongodb/writes/replace-one.js +199 -0
  65. package/lib/mongodb/writes/result-handler.js +236 -0
  66. package/lib/mongodb/writes/update-many.js +205 -0
  67. package/lib/mongodb/writes/update-one.js +207 -0
  68. package/lib/mongodb/writes/upsert-one.js +190 -0
  69. package/lib/multi-level-cache.js +189 -0
  70. package/lib/operators.js +330 -0
  71. package/lib/redis-cache-adapter.js +237 -0
  72. package/lib/transaction/CacheLockManager.js +161 -0
  73. package/lib/transaction/DistributedCacheLockManager.js +239 -0
  74. package/lib/transaction/Transaction.js +314 -0
  75. package/lib/transaction/TransactionManager.js +266 -0
  76. package/lib/transaction/index.js +10 -0
  77. package/package.json +111 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 索引选项验证和归一化工具
3
+ *
4
+ * @module common/index-options
5
+ * @since 2025-11-17
6
+ */
7
+
8
+ const { createError } = require('../errors');
9
+
10
+ /**
11
+ * 验证索引键定义
12
+ *
13
+ * @param {Object} keys - 索引键定义
14
+ * @throws {Error} 如果索引键无效
15
+ *
16
+ * @example
17
+ * validateIndexKeys({ email: 1 }); // ✓
18
+ * validateIndexKeys({ age: -1 }); // ✓
19
+ * validateIndexKeys({ name: "text" }); // ✓
20
+ * validateIndexKeys({ "$**": 1 }); // ✓ 通配符索引
21
+ * validateIndexKeys({}); // ✗ 抛出错误
22
+ * validateIndexKeys({ email: 2 }); // ✗ 抛出错误
23
+ */
24
+ function validateIndexKeys(keys) {
25
+ if (!keys || typeof keys !== 'object') {
26
+ throw createError(
27
+ 'INVALID_ARGUMENT',
28
+ '索引键必须是对象',
29
+ { keys }
30
+ );
31
+ }
32
+
33
+ const keyNames = Object.keys(keys);
34
+
35
+ if (keyNames.length === 0) {
36
+ throw createError(
37
+ 'INVALID_ARGUMENT',
38
+ '索引键不能为空',
39
+ { keys }
40
+ );
41
+ }
42
+
43
+ // 验证每个键的值
44
+ for (const key of keyNames) {
45
+ const value = keys[key];
46
+
47
+ // 允许的值:1(升序)、-1(降序)、"text"(文本索引)、"2d"(地理索引)、"2dsphere"、"hashed"、"columnstore"
48
+ const validValues = [1, -1, '1', '-1', 'text', '2d', '2dsphere', 'geoHaystack', 'hashed', 'columnstore'];
49
+
50
+ if (!validValues.includes(value)) {
51
+ throw createError(
52
+ 'INVALID_ARGUMENT',
53
+ `索引键 "${key}" 的值无效,允许的值: 1, -1, "text", "2d", "2dsphere", "hashed", "columnstore"`,
54
+ { key, value, validValues }
55
+ );
56
+ }
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * 归一化索引选项
64
+ * 验证并转换为 MongoDB 驱动接受的格式
65
+ *
66
+ * @param {Object} options - 用户提供的索引选项
67
+ * @returns {Object} 归一化后的选项
68
+ *
69
+ * @example
70
+ * normalizeIndexOptions({ unique: true, name: "email_idx" });
71
+ * // 返回: { unique: true, name: "email_idx" }
72
+ */
73
+ function normalizeIndexOptions(options = {}) {
74
+ const normalized = {};
75
+
76
+ // 允许的选项列表
77
+ const allowedOptions = [
78
+ // 通用选项
79
+ 'name', // 索引名称
80
+ 'unique', // 唯一索引
81
+ 'sparse', // 稀疏索引
82
+ 'background', // 后台创建(已废弃但保留兼容)
83
+
84
+ // TTL 索引
85
+ 'expireAfterSeconds', // 文档过期时间
86
+
87
+ // 部分索引
88
+ 'partialFilterExpression', // 部分索引表达式
89
+
90
+ // 排序规则
91
+ 'collation', // 排序规则
92
+
93
+ // 高级选项
94
+ 'hidden', // 隐藏索引(MongoDB 4.4+)
95
+ 'wildcardProjection', // 通配符投影
96
+
97
+ // 存储引擎
98
+ 'storageEngine', // 存储引擎配置
99
+
100
+ // 文本索引
101
+ 'weights', // 文本权重
102
+ 'default_language', // 默认语言
103
+ 'language_override', // 语言覆盖字段
104
+ 'textIndexVersion', // 文本索引版本
105
+
106
+ // 2dsphere 索引
107
+ '2dsphereIndexVersion', // 2dsphere 索引版本
108
+
109
+ // 其他
110
+ 'bits', // 2d 索引精度
111
+ 'min', // 2d 索引最小值
112
+ 'max', // 2d 索引最大值
113
+ 'bucketSize' // geoHaystack 索引桶大小
114
+ ];
115
+
116
+ // 复制允许的选项
117
+ for (const option of allowedOptions) {
118
+ if (options.hasOwnProperty(option)) {
119
+ normalized[option] = options[option];
120
+ }
121
+ }
122
+
123
+ // 特殊处理:background 已废弃,但保留兼容性
124
+ if (normalized.background !== undefined) {
125
+ // MongoDB 4.2+ 会忽略 background 选项,但不会报错
126
+ // 可以添加警告日志(如果有 logger)
127
+ }
128
+
129
+ // 验证特定选项
130
+ if (normalized.unique !== undefined && typeof normalized.unique !== 'boolean') {
131
+ throw createError(
132
+ 'INVALID_ARGUMENT',
133
+ 'unique 选项必须是布尔值',
134
+ { unique: normalized.unique }
135
+ );
136
+ }
137
+
138
+ if (normalized.sparse !== undefined && typeof normalized.sparse !== 'boolean') {
139
+ throw createError(
140
+ 'INVALID_ARGUMENT',
141
+ 'sparse 选项必须是布尔值',
142
+ { sparse: normalized.sparse }
143
+ );
144
+ }
145
+
146
+ if (normalized.hidden !== undefined && typeof normalized.hidden !== 'boolean') {
147
+ throw createError(
148
+ 'INVALID_ARGUMENT',
149
+ 'hidden 选项必须是布尔值',
150
+ { hidden: normalized.hidden }
151
+ );
152
+ }
153
+
154
+ if (normalized.expireAfterSeconds !== undefined) {
155
+ const ttl = normalized.expireAfterSeconds;
156
+ if (typeof ttl !== 'number' || ttl < 0 || !Number.isInteger(ttl)) {
157
+ throw createError(
158
+ 'INVALID_ARGUMENT',
159
+ 'expireAfterSeconds 必须是非负整数',
160
+ { expireAfterSeconds: ttl }
161
+ );
162
+ }
163
+ }
164
+
165
+ if (normalized.name !== undefined && typeof normalized.name !== 'string') {
166
+ throw createError(
167
+ 'INVALID_ARGUMENT',
168
+ 'name 选项必须是字符串',
169
+ { name: normalized.name }
170
+ );
171
+ }
172
+
173
+ if (normalized.partialFilterExpression !== undefined) {
174
+ if (typeof normalized.partialFilterExpression !== 'object' || normalized.partialFilterExpression === null) {
175
+ throw createError(
176
+ 'INVALID_ARGUMENT',
177
+ 'partialFilterExpression 必须是对象',
178
+ { partialFilterExpression: normalized.partialFilterExpression }
179
+ );
180
+ }
181
+ }
182
+
183
+ if (normalized.collation !== undefined) {
184
+ if (typeof normalized.collation !== 'object' || normalized.collation === null) {
185
+ throw createError(
186
+ 'INVALID_ARGUMENT',
187
+ 'collation 必须是对象',
188
+ { collation: normalized.collation }
189
+ );
190
+ }
191
+ }
192
+
193
+ return normalized;
194
+ }
195
+
196
+ /**
197
+ * 生成索引名称(当用户未指定时)
198
+ *
199
+ * @param {Object} keys - 索引键定义
200
+ * @returns {string} 生成的索引名称
201
+ *
202
+ * @example
203
+ * generateIndexName({ email: 1 }); // "email_1"
204
+ * generateIndexName({ userId: 1, status: -1 }); // "userId_1_status_-1"
205
+ * generateIndexName({ name: "text" }); // "name_text"
206
+ */
207
+ function generateIndexName(keys) {
208
+ const parts = [];
209
+
210
+ for (const [field, direction] of Object.entries(keys)) {
211
+ parts.push(`${field}_${direction}`);
212
+ }
213
+
214
+ return parts.join('_');
215
+ }
216
+
217
+ module.exports = {
218
+ validateIndexKeys,
219
+ normalizeIndexOptions,
220
+ generateIndexName
221
+ };
222
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 通用慢查询日志工具
3
+ * - getSlowQueryThreshold: 读取阈值(默认 500ms)
4
+ * - withSlowQueryLog: 统一包装执行并在超阈值时输出去敏慢日志
5
+ */
6
+
7
+ function getSlowQueryThreshold(defaults){
8
+ const d = defaults || {};
9
+ return (d.slowQueryMs && Number(d.slowQueryMs)) || 500;
10
+ }
11
+
12
+ /**
13
+ *
14
+ * @param {import('../..').LoggerLike} logger
15
+ * @param {object} defaults
16
+ * @param {string} op
17
+ * @param {{iid?:string,type?:string,db:string,coll:string}} ns
18
+ * @param {object} options
19
+ * @param {() => Promise<any>} exec
20
+ * @param {(options:any)=>object} [slowLogShaper]
21
+ * @returns {Promise<any>}
22
+ */
23
+ async function withSlowQueryLog(logger, defaults, op, ns, options, exec, slowLogShaper, onEmit){
24
+ const t0 = Date.now();
25
+ const res = await exec();
26
+ const ms = Date.now() - t0;
27
+ const threshold = getSlowQueryThreshold(defaults);
28
+ if (ms > threshold) {
29
+ const scope = defaults?.namespace?.scope;
30
+ const iid = ns?.iid;
31
+ const base = {
32
+ event: (defaults?.log?.slowQueryTag?.event) || 'slow_query',
33
+ code: (defaults?.log?.slowQueryTag?.code) || 'SLOW_QUERY',
34
+ category: 'performance',
35
+ type: ns?.type || 'mongodb',
36
+ iid,
37
+ scope,
38
+ db: ns.db,
39
+ coll: ns.coll,
40
+ op,
41
+ ms,
42
+ threshold,
43
+ ts: new Date().toISOString(),
44
+ ...(typeof slowLogShaper === 'function' ? slowLogShaper(options) : {})
45
+ };
46
+ try {
47
+ if (typeof defaults?.log?.formatSlowQuery === 'function') {
48
+ const formatted = defaults.log.formatSlowQuery(base) || base;
49
+ logger.warn('\u23f1\ufe0f Slow query', formatted);
50
+ if (typeof onEmit === 'function') { try { onEmit(formatted); } catch(_) {} }
51
+ } else {
52
+ logger.warn('\u23f1\ufe0f Slow query', base);
53
+ if (typeof onEmit === 'function') { try { onEmit(base); } catch(_) {} }
54
+ }
55
+ } catch (_) { /* ignore logging errors */ }
56
+ }
57
+ return res;
58
+ }
59
+
60
+ module.exports = { getSlowQueryThreshold, withSlowQueryLog };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 命名空间实例 id(iid)解析流程(通用)
3
+ * 适配器需提供 genInstanceId(databaseName, uri, explicitId?) 实现。
4
+ */
5
+
6
+ /**
7
+ * @param {{ genInstanceId: (db:string, uri?:string, explicitId?:string)=>string }} adapter
8
+ * @param {object} defaults
9
+ * @param {string} currentDb
10
+ * @param {string} initialDb
11
+ * @param {string} uri
12
+ */
13
+ function resolveInstanceId(adapter, defaults, currentDb, initialDb, uri) {
14
+ const explicit = defaults?.namespace?.instanceId;
15
+ if (explicit) return String(explicit);
16
+ const scope = defaults?.namespace?.scope; // 'database'|'connection'
17
+ const dbName = scope === 'connection' ? initialDb : (currentDb || initialDb);
18
+ return adapter.genInstanceId(dbName, uri);
19
+ }
20
+
21
+ module.exports = { resolveInstanceId };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 通用规范化工具
3
+ * - normalizeProjection: 将数组形式的投影转换为对象形式;对象原样返回;其他为 undefined。
4
+ * - normalizeSort: 仅当为对象时返回;否则 undefined。
5
+ */
6
+
7
+ /**
8
+ * 规范化投影参数
9
+ * @param {string[]|Record<string, any>|undefined} p
10
+ * @returns {Record<string, 1>|undefined}
11
+ */
12
+ function normalizeProjection(p) {
13
+ if (!p) return undefined;
14
+ if (Array.isArray(p)) {
15
+ const obj = {};
16
+ for (const k of p) {
17
+ if (typeof k === 'string') obj[k] = 1;
18
+ }
19
+ return Object.keys(obj).length ? obj : undefined;
20
+ }
21
+ return (p && typeof p === 'object') ? p : undefined;
22
+ }
23
+
24
+ /**
25
+ * 规范化排序参数
26
+ * @param {Record<string, 1|-1>|undefined} s
27
+ * @returns {Record<string, 1|-1>|undefined}
28
+ */
29
+ function normalizeSort(s) {
30
+ return (s && typeof s === 'object') ? s : undefined;
31
+ }
32
+
33
+ module.exports = { normalizeProjection, normalizeSort };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 通用分页结果计算
3
+ * - 采用 limit+1 探测是否有更多,然后裁剪为 `items` 与 `pageInfo`。
4
+ * - 游标生成委托给适配器提供的 `pickAnchor`(用于从行/文档提取锚点字段值)。
5
+ */
6
+
7
+ const { encodeCursor } = require('./cursor');
8
+
9
+ /**
10
+ * 生成分页结果
11
+ * @param {any[]} rows - 后端返回的数组(长度可能为 limit 或 limit+1)
12
+ * @param {object} ctx
13
+ * @param {number} ctx.limit
14
+ * @param {Record<string,1|-1>} ctx.stableSort
15
+ * @param {'after'|'before'|null} ctx.direction
16
+ * @param {boolean} ctx.hasCursor
17
+ * @param {(doc:any, sort:Record<string,1|-1>)=>Record<string,any>} ctx.pickAnchor
18
+ * @returns {{ items:any[], pageInfo:{ hasNext:boolean, hasPrev:boolean, startCursor:string|null, endCursor:string|null } }}
19
+ */
20
+ function makePageResult(rows, { limit, stableSort, direction, hasCursor, pickAnchor }) {
21
+ const hasMore = rows.length > limit;
22
+ const items = hasMore ? rows.slice(0, limit) : rows;
23
+
24
+ const first = items[0] || null;
25
+ const last = items[items.length - 1] || null;
26
+
27
+ // 修复:游标不应该包含方向信息,方向由使用游标的参数(after/before)决定
28
+ // 游标只是一个位置标记,不包含使用方向
29
+ const makeCur = (doc) => encodeCursor({ v: 1, s: stableSort, a: pickAnchor(doc, stableSort) });
30
+
31
+ return {
32
+ items,
33
+ pageInfo: {
34
+ hasNext: direction === 'before' ? Boolean(hasCursor) : hasMore,
35
+ hasPrev: direction === 'before' ? hasMore : Boolean(hasCursor),
36
+ startCursor: first ? makeCur(first) : null,
37
+ endCursor: last ? makeCur(last) : null,
38
+ }
39
+ };
40
+ }
41
+
42
+ module.exports = { makePageResult };
@@ -0,0 +1,56 @@
1
+ const CacheFactory = require('../cache');
2
+ const { withSlowQueryLog } = require('./log');
3
+
4
+ /**
5
+ * 统一执行器:包装缓存与慢日志,并可选返回 meta(耗时等)与发出 query 事件
6
+ * @param {import('../..').CacheLike|any} cache
7
+ * @param {{iid:string, type:string, db:string, collection:string}} nsAll
8
+ * @param {import('../..').LoggerLike} logger
9
+ * @param {object} defaults
10
+ * @param {{ keyBuilder?: (op:string, options:any)=>any, slowLogShaper?: (options:any)=>object, onSlowQueryEmit?: (meta:any)=>void, onQueryEmit?: (meta:any)=>void }} hooks
11
+ */
12
+ function createCachedRunner(cache, nsAll, logger, defaults, hooks = {}) {
13
+ const cached = CacheFactory.createCachedReader(cache, nsAll);
14
+ return async (op, options = {}, exec) => {
15
+ const optsForKey = typeof hooks.keyBuilder === 'function' ? hooks.keyBuilder(op, options || {}) : (options || {});
16
+ const runExec = () => cached(op, optsForKey, exec);
17
+ const ns = { db: nsAll.db, coll: nsAll.collection, iid: nsAll.iid, type: nsAll.type };
18
+ const t0 = Date.now();
19
+ let res;
20
+ let err;
21
+ try {
22
+ res = await withSlowQueryLog(logger, defaults, op, ns, options, runExec, hooks.slowLogShaper, hooks.onSlowQueryEmit);
23
+ } catch (e) {
24
+ err = e;
25
+ throw e;
26
+ } finally {
27
+ const endTs = Date.now();
28
+ const durationMs = endTs - t0;
29
+ const wantMeta = !!options.meta;
30
+ const meta = {
31
+ op,
32
+ ns,
33
+ startTs: endTs - durationMs,
34
+ endTs,
35
+ durationMs,
36
+ maxTimeMS: options && options.maxTimeMS,
37
+ };
38
+ if (err) meta.error = { code: err.code, message: String(err && (err.message || err)) };
39
+ // emit query event if enabled
40
+ if (defaults && defaults.metrics && defaults.metrics.emitQueryEvent && typeof hooks.onQueryEmit === 'function') {
41
+ try { hooks.onQueryEmit(meta); } catch (_) {}
42
+ }
43
+ if (wantMeta) {
44
+ // 按方法统一返回:findPage 在对象上挂 meta,其它读 API 返回 { data, meta }
45
+ if (op === 'findPage' && res && typeof res === 'object') {
46
+ res.meta = meta;
47
+ } else {
48
+ res = { data: res, meta };
49
+ }
50
+ }
51
+ }
52
+ return res;
53
+ };
54
+ }
55
+
56
+ module.exports = { createCachedRunner };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * MongoDB Server 特性探测模块
3
+ * 检测 MongoDB Server 版本和支持的特性
4
+ *
5
+ * 使用方式:
6
+ * ```javascript
7
+ * const ServerFeatures = require('./lib/common/server-features');
8
+ *
9
+ * // 方式 1: 传入 monSQLize 实例
10
+ * const msq = new MonSQLize({ ... });
11
+ * await msq.connect();
12
+ * const features = new ServerFeatures(msq);
13
+ * const report = await features.generateFeatureReport();
14
+ *
15
+ * // 方式 2: 传入原生 MongoDB client
16
+ * const { MongoClient } = require('mongodb');
17
+ * const client = await MongoClient.connect(uri);
18
+ * const features = new ServerFeatures(client);
19
+ * ```
20
+ *
21
+ * @module lib/common/server-features
22
+ */
23
+
24
+ /**
25
+ * Server 特性探测类
26
+ */
27
+ class ServerFeatures {
28
+ constructor(clientOrInstance) {
29
+ // 智能识别输入类型
30
+ if (clientOrInstance._adapter && clientOrInstance._adapter.client) {
31
+ // monSQLize 实例(有 _adapter 属性)
32
+ this.client = clientOrInstance._adapter.client;
33
+ } else if (clientOrInstance.client && typeof clientOrInstance.client.db === 'function') {
34
+ // adapter 实例(有 client 属性)
35
+ this.client = clientOrInstance.client;
36
+ } else if (typeof clientOrInstance.db === 'function') {
37
+ // 原生 MongoDB client
38
+ this.client = clientOrInstance;
39
+ } else {
40
+ // 无法识别的类型
41
+ throw new Error('ServerFeatures: Invalid client type. Expected MongoClient, adapter, or monSQLize instance.');
42
+ }
43
+
44
+ this.serverVersion = null;
45
+ this.serverInfo = null;
46
+ }
47
+
48
+ /**
49
+ * 获取 MongoDB Server 版本信息
50
+ * @returns {Promise<Object>} 版本信息
51
+ */
52
+ async getServerInfo() {
53
+ if (this.serverInfo) {
54
+ return this.serverInfo;
55
+ }
56
+
57
+ try {
58
+ const admin = this.client.db().admin();
59
+ const buildInfo = await admin.buildInfo();
60
+
61
+ this.serverInfo = {
62
+ version: buildInfo.version,
63
+ versionArray: buildInfo.versionArray,
64
+ gitVersion: buildInfo.gitVersion,
65
+ bits: buildInfo.bits,
66
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
67
+ };
68
+
69
+ this.serverVersion = this.serverInfo.versionArray;
70
+
71
+ return this.serverInfo;
72
+ } catch (error) {
73
+ console.error('获取 Server 信息失败:', error);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 获取主版本号
80
+ * @returns {Promise<number>} 主版本号
81
+ */
82
+ async getMajorVersion() {
83
+ if (!this.serverVersion) {
84
+ await this.getServerInfo();
85
+ }
86
+ return this.serverVersion ? this.serverVersion[0] : 0;
87
+ }
88
+
89
+ /**
90
+ * 获取次版本号
91
+ * @returns {Promise<number>} 次版本号
92
+ */
93
+ async getMinorVersion() {
94
+ if (!this.serverVersion) {
95
+ await this.getServerInfo();
96
+ }
97
+ return this.serverVersion ? this.serverVersion[1] : 0;
98
+ }
99
+
100
+ /**
101
+ * 检查是否支持事务
102
+ * @returns {Promise<boolean>}
103
+ */
104
+ async supportsTransactions() {
105
+ const major = await this.getMajorVersion();
106
+ // MongoDB 4.0+ 支持事务(副本集)
107
+ return major >= 4;
108
+ }
109
+
110
+ /**
111
+ * 检查是否支持多文档事务
112
+ * @returns {Promise<boolean>}
113
+ */
114
+ async supportsMultiDocumentTransactions() {
115
+ const major = await this.getMajorVersion();
116
+ // MongoDB 4.0+ 副本集支持,4.2+ 分片集群支持
117
+ return major >= 4;
118
+ }
119
+
120
+ /**
121
+ * 检查是否支持通配符索引
122
+ * @returns {Promise<boolean>}
123
+ */
124
+ async supportsWildcardIndexes() {
125
+ const major = await this.getMajorVersion();
126
+ const minor = await this.getMinorVersion();
127
+ // MongoDB 4.2+ 支持通配符索引
128
+ return major > 4 || (major === 4 && minor >= 2);
129
+ }
130
+
131
+ /**
132
+ * 检查是否支持 $function 操作符
133
+ * @returns {Promise<boolean>}
134
+ */
135
+ async supportsFunctionOperator() {
136
+ const major = await this.getMajorVersion();
137
+ const minor = await this.getMinorVersion();
138
+ // MongoDB 4.4+ 支持 $function
139
+ return major > 4 || (major === 4 && minor >= 4);
140
+ }
141
+
142
+ /**
143
+ * 检查是否支持 $setWindowFields 操作符
144
+ * @returns {Promise<boolean>}
145
+ */
146
+ async supportsSetWindowFields() {
147
+ const major = await this.getMajorVersion();
148
+ // MongoDB 5.0+ 支持 $setWindowFields
149
+ return major >= 5;
150
+ }
151
+
152
+ /**
153
+ * 检查是否支持时间序列集合
154
+ * @returns {Promise<boolean>}
155
+ */
156
+ async supportsTimeSeriesCollections() {
157
+ const major = await this.getMajorVersion();
158
+ // MongoDB 5.0+ 支持时间序列集合
159
+ return major >= 5;
160
+ }
161
+
162
+ /**
163
+ * 检查是否支持加密字段
164
+ * @returns {Promise<boolean>}
165
+ */
166
+ async supportsEncryptedFields() {
167
+ const major = await this.getMajorVersion();
168
+ // MongoDB 6.0+ 支持 Queryable Encryption
169
+ return major >= 6;
170
+ }
171
+
172
+ /**
173
+ * 检查是否支持聚合表达式
174
+ * @param {string} expression - 表达式名称(如 '$dateAdd')
175
+ * @returns {Promise<boolean>}
176
+ */
177
+ async supportsAggregationExpression(expression) {
178
+ const major = await this.getMajorVersion();
179
+ const minor = await this.getMinorVersion();
180
+
181
+ // 常见聚合表达式的版本要求
182
+ const expressionVersions = {
183
+ '$function': { major: 4, minor: 4 },
184
+ '$setWindowFields': { major: 5, minor: 0 },
185
+ '$dateAdd': { major: 5, minor: 0 },
186
+ '$dateDiff': { major: 5, minor: 0 },
187
+ '$dateSubtract': { major: 5, minor: 0 },
188
+ '$dateTrunc': { major: 5, minor: 0 },
189
+ '$getField': { major: 5, minor: 0 },
190
+ '$setField': { major: 5, minor: 0 },
191
+ };
192
+
193
+ const required = expressionVersions[expression];
194
+ if (!required) {
195
+ // 未知表达式,假设支持
196
+ return true;
197
+ }
198
+
199
+ return major > required.major ||
200
+ (major === required.major && minor >= required.minor);
201
+ }
202
+
203
+ /**
204
+ * 生成特性支持报告
205
+ * @returns {Promise<Object>} 特性报告
206
+ */
207
+ async generateFeatureReport() {
208
+ const info = await this.getServerInfo();
209
+
210
+ return {
211
+ serverVersion: info ? info.version : 'Unknown',
212
+ features: {
213
+ transactions: await this.supportsTransactions(),
214
+ multiDocumentTransactions: await this.supportsMultiDocumentTransactions(),
215
+ wildcardIndexes: await this.supportsWildcardIndexes(),
216
+ functionOperator: await this.supportsFunctionOperator(),
217
+ setWindowFields: await this.supportsSetWindowFields(),
218
+ timeSeriesCollections: await this.supportsTimeSeriesCollections(),
219
+ encryptedFields: await this.supportsEncryptedFields(),
220
+ },
221
+ aggregationExpressions: {
222
+ '$function': await this.supportsAggregationExpression('$function'),
223
+ '$setWindowFields': await this.supportsAggregationExpression('$setWindowFields'),
224
+ '$dateAdd': await this.supportsAggregationExpression('$dateAdd'),
225
+ },
226
+ };
227
+ }
228
+ }
229
+
230
+ module.exports = ServerFeatures;
231
+
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 通用慢日志形状构造器(去敏)
3
+ * 仅保留安全元信息与标记位;真正的 query/projection/sort 形状由适配器层注入。
4
+ */
5
+
6
+ function buildCommonLogExtra(options) {
7
+ const pick = (obj, fields) => Object.fromEntries((fields || []).filter(k => obj && k in obj).map(k => [k, obj[k]]));
8
+ const meta = pick(options || {}, ['limit', 'skip', 'maxTimeMS', 'cache']);
9
+
10
+ // 聚合阶段名(仅名称,避免输出参数)
11
+ if (options?.pipeline && Array.isArray(options.pipeline)) {
12
+ try {
13
+ meta.pipelineStages = options.pipeline.map(p => Object.keys(p)[0]).slice(0, 30);
14
+ } catch (_) { /* ignore */ }
15
+ }
16
+
17
+ // 游标标记
18
+ if (options?.after || options?.before) {
19
+ meta.hasCursor = true;
20
+ meta.cursorDirection = options.after ? 'after' : 'before';
21
+ }
22
+
23
+ return meta;
24
+ }
25
+
26
+ module.exports = { buildCommonLogExtra };