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.
Files changed (61) hide show
  1. package/CHANGELOG.md +92 -2419
  2. package/README.md +630 -1070
  3. package/index.d.ts +252 -15
  4. package/lib/cache.js +8 -8
  5. package/lib/common/validation.js +64 -1
  6. package/lib/connect.js +3 -3
  7. package/lib/errors.js +10 -0
  8. package/lib/index.js +118 -9
  9. package/lib/infrastructure/ssh-tunnel-ssh2.js +211 -0
  10. package/lib/infrastructure/ssh-tunnel.js +40 -0
  11. package/lib/infrastructure/uri-parser.js +35 -0
  12. package/lib/lock/Lock.js +66 -0
  13. package/lib/lock/errors.js +27 -0
  14. package/lib/lock/index.js +12 -0
  15. package/lib/logger.js +1 -1
  16. package/lib/model/examples/test.js +4 -4
  17. package/lib/mongodb/common/accessor-helpers.js +17 -3
  18. package/lib/mongodb/connect.js +68 -13
  19. package/lib/mongodb/index.js +140 -7
  20. package/lib/mongodb/management/collection-ops.js +4 -4
  21. package/lib/mongodb/management/index-ops.js +18 -18
  22. package/lib/mongodb/management/validation-ops.js +3 -3
  23. package/lib/mongodb/queries/aggregate.js +14 -5
  24. package/lib/mongodb/queries/chain.js +52 -45
  25. package/lib/mongodb/queries/count.js +16 -6
  26. package/lib/mongodb/queries/distinct.js +15 -6
  27. package/lib/mongodb/queries/find-and-count.js +22 -13
  28. package/lib/mongodb/queries/find-by-ids.js +5 -5
  29. package/lib/mongodb/queries/find-one-by-id.js +1 -1
  30. package/lib/mongodb/queries/find-one.js +12 -3
  31. package/lib/mongodb/queries/find-page.js +12 -0
  32. package/lib/mongodb/queries/find.js +15 -6
  33. package/lib/mongodb/queries/index.js +1 -0
  34. package/lib/mongodb/queries/watch.js +537 -0
  35. package/lib/mongodb/writes/delete-many.js +20 -11
  36. package/lib/mongodb/writes/delete-one.js +18 -9
  37. package/lib/mongodb/writes/find-one-and-delete.js +19 -10
  38. package/lib/mongodb/writes/find-one-and-replace.js +36 -20
  39. package/lib/mongodb/writes/find-one-and-update.js +36 -20
  40. package/lib/mongodb/writes/increment-one.js +16 -7
  41. package/lib/mongodb/writes/index.js +13 -13
  42. package/lib/mongodb/writes/insert-batch.js +46 -37
  43. package/lib/mongodb/writes/insert-many.js +22 -13
  44. package/lib/mongodb/writes/insert-one.js +18 -9
  45. package/lib/mongodb/writes/replace-one.js +33 -17
  46. package/lib/mongodb/writes/result-handler.js +14 -14
  47. package/lib/mongodb/writes/update-many.js +34 -18
  48. package/lib/mongodb/writes/update-one.js +33 -17
  49. package/lib/mongodb/writes/upsert-one.js +25 -9
  50. package/lib/operators.js +1 -1
  51. package/lib/redis-cache-adapter.js +3 -3
  52. package/lib/slow-query-log/base-storage.js +69 -0
  53. package/lib/slow-query-log/batch-queue.js +96 -0
  54. package/lib/slow-query-log/config-manager.js +195 -0
  55. package/lib/slow-query-log/index.js +237 -0
  56. package/lib/slow-query-log/mongodb-storage.js +323 -0
  57. package/lib/slow-query-log/query-hash.js +38 -0
  58. package/lib/transaction/DistributedCacheLockManager.js +240 -5
  59. package/lib/transaction/Transaction.js +1 -1
  60. package/lib/utils/objectid-converter.js +566 -0
  61. package/package.json +11 -5
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ const { createRedisCacheAdapter } = require('./redis-cache-adapter');
5
5
  const TransactionManager = require('./transaction/TransactionManager');
6
6
  const CacheLockManager = require('./transaction/CacheLockManager');
7
7
  const DistributedCacheInvalidator = require('./distributed-cache-invalidator');
8
+ const { validateRange } = require('./common/validation');
8
9
 
9
10
  module.exports = class {
10
11
 
@@ -28,6 +29,12 @@ module.exports = class {
28
29
  this.databaseName = databaseName;
29
30
  this.config = config;
30
31
 
32
+ // ✅ v1.3.0: 自动 ObjectId 转换配置
33
+ this.autoConvertConfig = this._initAutoConvertConfig(
34
+ options.autoConvertObjectId,
35
+ options.type
36
+ );
37
+
31
38
  // 🔧 修复:保存 distributed 配置到单独的变量
32
39
  this._cacheConfig = cache;
33
40
 
@@ -50,6 +57,20 @@ module.exports = class {
50
57
  // 使用 Logger 工具类创建日志记录器
51
58
  this.logger = Logger.create(logger);
52
59
 
60
+ // 🔒 参数验证:防止 DoS 攻击(允许null值用于显式禁用)
61
+ if (options.maxTimeMS !== undefined && options.maxTimeMS !== null) {
62
+ validateRange(options.maxTimeMS, 1, 300000, 'maxTimeMS');
63
+ }
64
+ if (options.findLimit !== undefined && options.findLimit !== null) {
65
+ validateRange(options.findLimit, 1, 10000, 'findLimit');
66
+ }
67
+ if (options.findPageMaxLimit !== undefined && options.findPageMaxLimit !== null) {
68
+ validateRange(options.findPageMaxLimit, 1, 10000, 'findPageMaxLimit');
69
+ }
70
+ if (options.slowQueryMs !== undefined && options.slowQueryMs !== null && options.slowQueryMs !== -1) {
71
+ validateRange(options.slowQueryMs, 0, 60000, 'slowQueryMs');
72
+ }
73
+
53
74
  // 集中默认配置(库内默认 + 用户覆盖)
54
75
  const DEFAULTS = {
55
76
  maxTimeMS: 2000,
@@ -83,6 +104,8 @@ module.exports = class {
83
104
  findPageMaxLimit: options.findPageMaxLimit,
84
105
  cursorSecret: options.cursorSecret,
85
106
  log: options.log,
107
+ // 🔴 v1.3.1: 慢查询日志持久化存储配置
108
+ slowQueryLog: options.slowQueryLog,
86
109
  });
87
110
  // 冻结默认配置,避免运行期被意外修改
88
111
  this.defaults = Object.freeze(this.defaults);
@@ -140,7 +163,7 @@ module.exports = class {
140
163
 
141
164
  this._cacheInvalidator = new DistributedCacheInvalidator({
142
165
  redisUrl: this._cacheConfig.distributed.redisUrl,
143
- redis: redis,
166
+ redis,
144
167
  channel: this._cacheConfig.distributed.channel,
145
168
  instanceId: this._cacheConfig.distributed.instanceId,
146
169
  cache: this.cache,
@@ -171,17 +194,17 @@ module.exports = class {
171
194
  // 初始化事务管理器和缓存锁管理器
172
195
  if (this.type === 'mongodb' && instance.client) {
173
196
  // 检查是否配置了分布式事务锁
174
- const useDistributedLock = this.cache &&
175
- typeof this.cache.transaction === 'object' &&
176
- this.cache.transaction.distributedLock &&
177
- this.cache.transaction.distributedLock.redis;
197
+ const useDistributedLock = this._cacheConfig &&
198
+ typeof this._cacheConfig.transaction === 'object' &&
199
+ this._cacheConfig.transaction.distributedLock &&
200
+ this._cacheConfig.transaction.distributedLock.redis;
178
201
 
179
202
  if (useDistributedLock) {
180
203
  // 使用分布式缓存锁管理器
181
204
  const DistributedCacheLockManager = require('./transaction/DistributedCacheLockManager');
182
205
  this._lockManager = new DistributedCacheLockManager({
183
- redis: this.cache.transaction.distributedLock.redis,
184
- lockKeyPrefix: this.cache.transaction.distributedLock.keyPrefix || 'monsqlize:cache:lock:',
206
+ redis: this._cacheConfig.transaction.distributedLock.redis,
207
+ lockKeyPrefix: this._cacheConfig.transaction.distributedLock.keyPrefix || 'monsqlize:cache:lock:',
185
208
  maxDuration: 300000,
186
209
  logger: this.logger
187
210
  });
@@ -215,6 +238,27 @@ module.exports = class {
215
238
  hasLockManager: !!this._lockManager,
216
239
  isDistributed: useDistributedLock
217
240
  });
241
+
242
+ // 🆕 v1.4.0: 挂载业务锁 API(仅在使用分布式锁时可用)
243
+ if (this._lockManager && typeof this._lockManager.withLock === 'function') {
244
+ this.dbInstance.withLock = (key, callback, opts) =>
245
+ this._lockManager.withLock(key, callback, opts);
246
+ this.dbInstance.acquireLock = (key, opts) =>
247
+ this._lockManager.acquireLock(key, opts);
248
+ this.dbInstance.tryAcquireLock = (key, opts) =>
249
+ this._lockManager.tryAcquireLock(key, opts);
250
+ this.dbInstance.getLockStats = () =>
251
+ this._lockManager.getStats();
252
+
253
+ this.logger.info('✅ Business lock API initialized', {
254
+ isDistributed: useDistributedLock
255
+ });
256
+ } else {
257
+ this.logger.warn('⚠️ Business lock API not available (Redis required)', {
258
+ hasLockManager: !!this._lockManager,
259
+ isDistributed: useDistributedLock
260
+ });
261
+ }
218
262
  } else {
219
263
  this.logger.warn('⚠️ Transaction manager not initialized', {
220
264
  type: this.type,
@@ -293,6 +337,19 @@ module.exports = class {
293
337
  return { status: 'down', connected: false };
294
338
  }
295
339
 
340
+ /**
341
+ * 查询慢查询日志(v1.3.1+)
342
+ * @param {Object} filter - 查询条件 { db, collection, operation }
343
+ * @param {Object} options - 查询选项 { sort, limit, skip }
344
+ * @returns {Promise<Array>} 慢查询日志列表
345
+ */
346
+ async getSlowQueryLogs(filter, options) {
347
+ if (this._adapter && typeof this._adapter.getSlowQueryLogs === 'function') {
348
+ return this._adapter.getSlowQueryLogs(filter, options);
349
+ }
350
+ throw new Error('Slow query log feature is not enabled or not supported');
351
+ }
352
+
296
353
  /**
297
354
  * 事件订阅(适配器透传)
298
355
  * @param {'connected'|'closed'|'error'|'slow-query'} event
@@ -347,6 +404,58 @@ module.exports = class {
347
404
  /**
348
405
  * 导出工具函数:创建 Redis 缓存适配器
349
406
  * @static
407
+ * @param {import('ioredis').Redis | import('ioredis').Cluster} client - Redis客户端
408
+ * @param {Object} [options] - 配置选项
409
+ * @returns {import('./cache').CacheLike} Redis缓存适配器
350
410
  */
351
- static createRedisCacheAdapter = createRedisCacheAdapter;
352
- }
411
+ static createRedisCacheAdapter(client, options) {
412
+ return createRedisCacheAdapter(client, options);
413
+ }
414
+
415
+ /**
416
+ * 初始化 ObjectId 自动转换配置
417
+ * @private
418
+ * @param {boolean|Object} config - 用户配置
419
+ * @param {string} dbType - 数据库类型
420
+ * @returns {Object} 配置对象
421
+ */
422
+ _initAutoConvertConfig(config, dbType) {
423
+ // 只在 MongoDB 类型下启用
424
+ if (dbType !== 'mongodb') {
425
+ return { enabled: false };
426
+ }
427
+
428
+ // 默认配置
429
+ const defaults = {
430
+ enabled: true,
431
+ excludeFields: [],
432
+ customFieldPatterns: [],
433
+ maxDepth: 10,
434
+ logLevel: 'warn'
435
+ };
436
+
437
+ // 用户禁用
438
+ if (config === false) {
439
+ return { enabled: false };
440
+ }
441
+
442
+ // 用户自定义配置
443
+ if (typeof config === 'object' && config !== null) {
444
+ return {
445
+ enabled: config.enabled !== false,
446
+ excludeFields: Array.isArray(config.excludeFields)
447
+ ? config.excludeFields
448
+ : defaults.excludeFields,
449
+ customFieldPatterns: Array.isArray(config.customFieldPatterns)
450
+ ? config.customFieldPatterns
451
+ : defaults.customFieldPatterns,
452
+ maxDepth: typeof config.maxDepth === 'number'
453
+ ? config.maxDepth
454
+ : defaults.maxDepth,
455
+ logLevel: config.logLevel || defaults.logLevel
456
+ };
457
+ }
458
+
459
+ return defaults;
460
+ }
461
+ };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * SSH隧道管理器 - ssh2实现
3
+ * 支持密码认证和私钥认证
4
+ */
5
+
6
+ const { Client } = require('ssh2');
7
+ const net = require('net');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ /**
13
+ * 基于ssh2的SSH隧道管理器
14
+ * 支持密码认证和私钥认证
15
+ */
16
+ class SSHTunnelSSH2 {
17
+ /**
18
+ * @param {Object} sshConfig - SSH配置
19
+ * @param {string} sshConfig.host - SSH服务器地址
20
+ * @param {number} [sshConfig.port=22] - SSH端口
21
+ * @param {string} sshConfig.username - SSH用户名
22
+ * @param {string} [sshConfig.password] - SSH密码
23
+ * @param {string} [sshConfig.privateKey] - 私钥内容
24
+ * @param {string} [sshConfig.privateKeyPath] - 私钥路径
25
+ * @param {string} [sshConfig.passphrase] - 私钥密码
26
+ * @param {number} [sshConfig.localPort] - 本地监听端口(可选,默认随机)
27
+ * @param {number} [sshConfig.readyTimeout=20000] - 连接超时(毫秒)
28
+ * @param {number} [sshConfig.keepaliveInterval=30000] - 心跳间隔(毫秒)
29
+ * @param {string} remoteHost - 远程主机(数据库服务器地址)
30
+ * @param {number} remotePort - 远程端口(数据库端口)
31
+ * @param {Object} options - 可选配置
32
+ * @param {Object} options.logger - 日志记录器
33
+ * @param {string} options.name - 隧道名称(用于日志)
34
+ */
35
+ constructor(sshConfig, remoteHost, remotePort, options = {}) {
36
+ this.sshConfig = sshConfig;
37
+ this.remoteHost = remoteHost;
38
+ this.remotePort = remotePort;
39
+ this.logger = options.logger;
40
+ this.name = options.name || `${remoteHost}:${remotePort}`;
41
+
42
+ this.sshClient = null;
43
+ this.server = null;
44
+ this.localPort = null;
45
+ this.isConnected = false;
46
+ }
47
+
48
+ /**
49
+ * 建立SSH隧道
50
+ * @returns {Promise<number>} 本地监听端口
51
+ */
52
+ async connect() {
53
+ if (this.isConnected) {
54
+ this.logger?.warn?.(`SSH tunnel [${this.name}] already connected`);
55
+ return this.localPort;
56
+ }
57
+
58
+ return new Promise((resolve, reject) => {
59
+ this.sshClient = new Client();
60
+
61
+ this.sshClient.on('ready', () => {
62
+ this.logger?.info?.(`✅ SSH connection established [${this.name}]`);
63
+
64
+ // 创建本地TCP服务器(端口转发)
65
+ this.server = net.createServer((sock) => {
66
+ this.sshClient.forwardOut(
67
+ sock.remoteAddress,
68
+ sock.remotePort,
69
+ this.remoteHost,
70
+ this.remotePort,
71
+ (err, stream) => {
72
+ if (err) {
73
+ this.logger?.error?.(`SSH forward error [${this.name}]`, err);
74
+ return sock.end();
75
+ }
76
+ sock.pipe(stream).pipe(sock);
77
+ }
78
+ );
79
+ });
80
+
81
+ // 监听本地端口(0 = 随机端口)
82
+ const port = this.sshConfig.localPort || 0;
83
+ this.server.listen(port, 'localhost', () => {
84
+ this.localPort = this.server.address().port;
85
+ this.isConnected = true;
86
+
87
+ this.logger?.info?.(`✅ SSH tunnel ready [${this.name}]`, {
88
+ localPort: this.localPort,
89
+ remote: `${this.remoteHost}:${this.remotePort}`
90
+ });
91
+
92
+ resolve(this.localPort);
93
+ });
94
+
95
+ this.server.on('error', (err) => {
96
+ this.logger?.error?.(`Local server error [${this.name}]`, err);
97
+ reject(err);
98
+ });
99
+ });
100
+
101
+ this.sshClient.on('error', (err) => {
102
+ this.logger?.error?.(`SSH connection error [${this.name}]`, err);
103
+ reject(err);
104
+ });
105
+
106
+ this.sshClient.on('end', () => {
107
+ this.logger?.info?.(`SSH connection ended [${this.name}]`);
108
+ this.isConnected = false;
109
+ });
110
+
111
+ // 构建认证配置并连接
112
+ try {
113
+ const authConfig = this._buildAuthConfig();
114
+ this.sshClient.connect(authConfig);
115
+ } catch (err) {
116
+ reject(err);
117
+ }
118
+ });
119
+ }
120
+
121
+ /**
122
+ * 关闭SSH隧道
123
+ */
124
+ async close() {
125
+ if (this.server) {
126
+ this.server.close();
127
+ this.server = null;
128
+ }
129
+
130
+ if (this.sshClient) {
131
+ this.sshClient.end();
132
+ this.sshClient = null;
133
+ }
134
+
135
+ this.isConnected = false;
136
+ this.localPort = null;
137
+
138
+ this.logger?.info?.(`✅ SSH tunnel closed [${this.name}]`);
139
+ }
140
+
141
+ /**
142
+ * 获取隧道连接URI
143
+ * @param {string} protocol - 数据库协议(mongodb/postgresql/mysql/redis)
144
+ * @param {string} originalUri - 原始URI
145
+ * @returns {string} 替换后的本地URI
146
+ */
147
+ getTunnelUri(protocol, originalUri) {
148
+ if (!this.isConnected) {
149
+ throw new Error(`SSH tunnel [${this.name}] not connected`);
150
+ }
151
+
152
+ // 替换主机:端口为 localhost:本地端口
153
+ const pattern = new RegExp(`${protocol}://([^@]*@)?([^:/]+):(\\d+)`);
154
+ const replacement = `${protocol}://$1localhost:${this.localPort}`;
155
+
156
+ return originalUri.replace(pattern, replacement);
157
+ }
158
+
159
+ /**
160
+ * 获取本地连接地址
161
+ * @returns {string} localhost:端口
162
+ */
163
+ getLocalAddress() {
164
+ if (!this.isConnected) {
165
+ throw new Error(`SSH tunnel [${this.name}] not connected`);
166
+ }
167
+ return `localhost:${this.localPort}`;
168
+ }
169
+
170
+ /**
171
+ * 构建SSH认证配置
172
+ * @private
173
+ */
174
+ _buildAuthConfig() {
175
+ const { host, port, username, password, privateKey, privateKeyPath, passphrase } = this.sshConfig;
176
+
177
+ if (!host || !username) {
178
+ throw new Error('SSH config requires: host, username');
179
+ }
180
+
181
+ const config = {
182
+ host,
183
+ port: port || 22,
184
+ username,
185
+ readyTimeout: this.sshConfig.readyTimeout || 20000,
186
+ keepaliveInterval: this.sshConfig.keepaliveInterval || 30000,
187
+ };
188
+
189
+ // 优先使用私钥认证
190
+ if (privateKey) {
191
+ config.privateKey = privateKey;
192
+ if (passphrase) config.passphrase = passphrase;
193
+ } else if (privateKeyPath) {
194
+ const resolvedPath = privateKeyPath.startsWith('~')
195
+ ? path.join(os.homedir(), privateKeyPath.slice(1))
196
+ : privateKeyPath;
197
+ config.privateKey = fs.readFileSync(resolvedPath);
198
+ if (passphrase) config.passphrase = passphrase;
199
+ } else if (password) {
200
+ // 密码认证
201
+ config.password = password;
202
+ } else {
203
+ throw new Error('SSH authentication required: privateKey, privateKeyPath, or password');
204
+ }
205
+
206
+ return config;
207
+ }
208
+ }
209
+
210
+ module.exports = { SSHTunnelSSH2 };
211
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SSH隧道管理器 - 统一入口
3
+ * 使用ssh2库实现,支持密码认证和私钥认证
4
+ */
5
+
6
+ const { SSHTunnelSSH2 } = require('./ssh-tunnel-ssh2');
7
+
8
+ /**
9
+ * SSH隧道管理器
10
+ * 直接使用ssh2实现
11
+ */
12
+ class SSHTunnelManager {
13
+ /**
14
+ * 创建SSH隧道实例
15
+ * @param {Object} sshConfig - SSH配置
16
+ * @param {string} remoteHost - 远程主机
17
+ * @param {number} remotePort - 远程端口
18
+ * @param {Object} options - 可选配置
19
+ * @returns {Object} SSH隧道实例
20
+ */
21
+ static create(sshConfig, remoteHost, remotePort, options = {}) {
22
+ return new SSHTunnelSSH2(sshConfig, remoteHost, remotePort, options);
23
+ }
24
+
25
+ /**
26
+ * 获取当前实现信息
27
+ * @returns {Object}
28
+ */
29
+ static getInfo() {
30
+ return {
31
+ implementation: 'ssh2',
32
+ supportsPassword: true,
33
+ supportsPrivateKey: true,
34
+ dependencies: ['ssh2']
35
+ };
36
+ }
37
+ }
38
+
39
+ module.exports = { SSHTunnelManager };
40
+
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 通用URI解析器
3
+ * 支持:mongodb://、postgresql://、mysql://、redis://
4
+ */
5
+
6
+ /**
7
+ * 解析数据库连接URI
8
+ * @param {string} uri - 数据库连接URI
9
+ * @returns {{protocol: string, auth: string|null, host: string, port: number}} 解析结果
10
+ */
11
+ function parseUri(uri) {
12
+ const patterns = {
13
+ mongodb: /mongodb:\/\/([^@]*@)?([^:/]+):(\d+)/,
14
+ postgresql: /postgresql:\/\/([^@]*@)?([^:/]+):(\d+)/,
15
+ mysql: /mysql:\/\/([^@]*@)?([^:/]+):(\d+)/,
16
+ redis: /redis:\/\/([^@]*@)?([^:/]+):(\d+)/
17
+ };
18
+
19
+ for (const [protocol, pattern] of Object.entries(patterns)) {
20
+ const match = uri.match(pattern);
21
+ if (match) {
22
+ return {
23
+ protocol,
24
+ auth: match[1] ? match[1].slice(0, -1) : null, // 去掉末尾的@
25
+ host: match[2],
26
+ port: parseInt(match[3], 10)
27
+ };
28
+ }
29
+ }
30
+
31
+ throw new Error(`Unsupported URI format: ${uri}`);
32
+ }
33
+
34
+ module.exports = { parseUri };
35
+
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 业务锁对象
3
+ * 表示一个已获取的锁,提供释放和续期方法
4
+ */
5
+ class Lock {
6
+ /**
7
+ * @param {string} key - 锁的标识
8
+ * @param {string} lockId - 锁的唯一ID
9
+ * @param {Object} manager - 锁管理器实例
10
+ * @param {number} ttl - 锁的过期时间(毫秒)
11
+ */
12
+ constructor(key, lockId, manager, ttl) {
13
+ this.key = key;
14
+ this.lockId = lockId;
15
+ this.manager = manager;
16
+ this.ttl = ttl;
17
+ this.released = false;
18
+ this.acquiredAt = Date.now();
19
+ }
20
+
21
+ /**
22
+ * 释放锁
23
+ * @returns {Promise<boolean>}
24
+ */
25
+ async release() {
26
+ if (this.released) {
27
+ return false;
28
+ }
29
+
30
+ const result = await this.manager.releaseLock(this.key, this.lockId);
31
+ this.released = true;
32
+ return result;
33
+ }
34
+
35
+ /**
36
+ * 续期(延长锁的过期时间)
37
+ * @param {number} [ttl] - 新的过期时间,默认使用原TTL
38
+ * @returns {Promise<boolean>}
39
+ */
40
+ async renew(ttl) {
41
+ if (this.released) {
42
+ return false;
43
+ }
44
+
45
+ return this.manager.renewLock(this.key, this.lockId, ttl || this.ttl);
46
+ }
47
+
48
+ /**
49
+ * 检查锁是否仍被持有
50
+ * @returns {boolean}
51
+ */
52
+ isHeld() {
53
+ return !this.released;
54
+ }
55
+
56
+ /**
57
+ * 获取锁持有时间
58
+ * @returns {number} 毫秒
59
+ */
60
+ getHoldTime() {
61
+ return Date.now() - this.acquiredAt;
62
+ }
63
+ }
64
+
65
+ module.exports = Lock;
66
+
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 锁获取失败错误
3
+ */
4
+ class LockAcquireError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'LockAcquireError';
8
+ this.code = 'LOCK_ACQUIRE_FAILED';
9
+ }
10
+ }
11
+
12
+ /**
13
+ * 锁超时错误
14
+ */
15
+ class LockTimeoutError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'LockTimeoutError';
19
+ this.code = 'LOCK_TIMEOUT';
20
+ }
21
+ }
22
+
23
+ module.exports = {
24
+ LockAcquireError,
25
+ LockTimeoutError
26
+ };
27
+
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 业务锁模块导出
3
+ */
4
+ const Lock = require('./Lock');
5
+ const { LockAcquireError, LockTimeoutError } = require('./errors');
6
+
7
+ module.exports = {
8
+ Lock,
9
+ LockAcquireError,
10
+ LockTimeoutError
11
+ };
12
+
package/lib/logger.js CHANGED
@@ -220,5 +220,5 @@ module.exports = class Logger {
220
220
  static generateTraceId() {
221
221
  return generateTraceId();
222
222
  }
223
- }
223
+ };
224
224
 
@@ -15,7 +15,7 @@ module.exports = {
15
15
  password: sc.string().pattern(/^[a-zA-Z0-9]{6,30}$/).required(),
16
16
  age: sc.number().integer().min(0).default(18), // 默认值
17
17
  role: sc.string().valid('admin', 'user').default('user'),
18
- }
18
+ };
19
19
  },
20
20
 
21
21
  // 自定义方法
@@ -36,7 +36,7 @@ module.exports = {
36
36
  return this.find({ username: name });
37
37
  }
38
38
  }
39
- }
39
+ };
40
40
  },
41
41
 
42
42
  // 支持操作前、后处理
@@ -63,7 +63,7 @@ module.exports = {
63
63
  before:(ctx,options)=>{},
64
64
  after:(ctx,result)=>{},
65
65
  }
66
- }
66
+ };
67
67
  },
68
68
 
69
69
  // 创建索引
@@ -111,4 +111,4 @@ module.exports = {
111
111
 
112
112
  // 自动索引 / 慢查询统计 → 全局 ORM 层统一管理
113
113
  // 恢复软删除 → 全局方法注入到每个启用 softDelete 的模型
114
- }
114
+ };
@@ -8,6 +8,7 @@ const crypto = require('crypto');
8
8
  const CacheFactory = require('../../cache');
9
9
  const { buildCommonLogExtra } = require('../../common/shape-builders');
10
10
  const { shapeQuery, shapeProjection, shapeSort } = require('./shape');
11
+ const { normalizeForCache } = require('../../utils/objectid-converter');
11
12
 
12
13
  /**
13
14
  * Mongo 专属:慢日志去敏形状构造器
@@ -26,18 +27,31 @@ function mongoSlowLogShaper(options) {
26
27
  * Mongo 专属:缓存键构造器
27
28
  * 仅在 findPage 时:对 pipeline 做稳定串行化并 sha256 → pipelineHash;
28
29
  * 同时从参与键的 options 中去除原始 pipeline,避免键过长与不稳定。
30
+ *
31
+ * ✅ v1.3.0 新增:标准化 ObjectId
32
+ * - 将所有 ObjectId 实例转换为字符串
33
+ * - 确保 ObjectId('xxx') 和 'xxx' 生成相同的缓存键
34
+ * - 避免缓存失效和重复查询
35
+ *
29
36
  * @param {string} op
30
37
  * @param {object} options
31
38
  * @returns {object} 用于参与缓存键构造的 options 视图
32
39
  */
33
40
  function mongoKeyBuilder(op, options) {
34
41
  const opts = options || {};
35
- if (op !== 'findPage') return opts;
42
+
43
+ // ✅ 标准化 options 中的 ObjectId(转为字符串)
44
+ // 确保查询缓存键一致性
45
+ const normalizedOpts = normalizeForCache(opts);
46
+
47
+ if (op !== 'findPage') return normalizedOpts;
48
+
49
+ // findPage 特殊处理:pipeline 转 hash
36
50
  const pipelineHash = crypto
37
51
  .createHash('sha256')
38
- .update(CacheFactory.stableStringify(opts.pipeline || []))
52
+ .update(CacheFactory.stableStringify(normalizedOpts.pipeline || []))
39
53
  .digest('hex');
40
- const { pipeline, ...rest } = opts;
54
+ const { pipeline, ...rest } = normalizedOpts;
41
55
  return { ...rest, pipelineHash };
42
56
  }
43
57