monsqlize 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -2419
- package/README.md +630 -1070
- package/index.d.ts +252 -15
- package/lib/cache.js +8 -8
- package/lib/common/validation.js +64 -1
- package/lib/connect.js +3 -3
- package/lib/errors.js +10 -0
- package/lib/index.js +118 -9
- package/lib/infrastructure/ssh-tunnel-ssh2.js +211 -0
- package/lib/infrastructure/ssh-tunnel.js +40 -0
- package/lib/infrastructure/uri-parser.js +35 -0
- package/lib/lock/Lock.js +66 -0
- package/lib/lock/errors.js +27 -0
- package/lib/lock/index.js +12 -0
- package/lib/logger.js +1 -1
- package/lib/model/examples/test.js +4 -4
- package/lib/mongodb/common/accessor-helpers.js +17 -3
- package/lib/mongodb/connect.js +68 -13
- package/lib/mongodb/index.js +140 -7
- package/lib/mongodb/management/collection-ops.js +4 -4
- package/lib/mongodb/management/index-ops.js +18 -18
- package/lib/mongodb/management/validation-ops.js +3 -3
- package/lib/mongodb/queries/aggregate.js +14 -5
- package/lib/mongodb/queries/chain.js +52 -45
- package/lib/mongodb/queries/count.js +16 -6
- package/lib/mongodb/queries/distinct.js +15 -6
- package/lib/mongodb/queries/find-and-count.js +22 -13
- package/lib/mongodb/queries/find-by-ids.js +5 -5
- package/lib/mongodb/queries/find-one-by-id.js +1 -1
- package/lib/mongodb/queries/find-one.js +12 -3
- package/lib/mongodb/queries/find-page.js +12 -0
- package/lib/mongodb/queries/find.js +15 -6
- package/lib/mongodb/queries/index.js +1 -0
- package/lib/mongodb/queries/watch.js +537 -0
- package/lib/mongodb/writes/delete-many.js +20 -11
- package/lib/mongodb/writes/delete-one.js +18 -9
- package/lib/mongodb/writes/find-one-and-delete.js +19 -10
- package/lib/mongodb/writes/find-one-and-replace.js +36 -20
- package/lib/mongodb/writes/find-one-and-update.js +36 -20
- package/lib/mongodb/writes/increment-one.js +16 -7
- package/lib/mongodb/writes/index.js +13 -13
- package/lib/mongodb/writes/insert-batch.js +46 -37
- package/lib/mongodb/writes/insert-many.js +22 -13
- package/lib/mongodb/writes/insert-one.js +18 -9
- package/lib/mongodb/writes/replace-one.js +33 -17
- package/lib/mongodb/writes/result-handler.js +14 -14
- package/lib/mongodb/writes/update-many.js +34 -18
- package/lib/mongodb/writes/update-one.js +33 -17
- package/lib/mongodb/writes/upsert-one.js +25 -9
- package/lib/operators.js +1 -1
- package/lib/redis-cache-adapter.js +3 -3
- package/lib/slow-query-log/base-storage.js +69 -0
- package/lib/slow-query-log/batch-queue.js +96 -0
- package/lib/slow-query-log/config-manager.js +195 -0
- package/lib/slow-query-log/index.js +237 -0
- package/lib/slow-query-log/mongodb-storage.js +323 -0
- package/lib/slow-query-log/query-hash.js +38 -0
- package/lib/transaction/DistributedCacheLockManager.js +240 -5
- package/lib/transaction/Transaction.js +1 -1
- package/lib/utils/objectid-converter.js +566 -0
- package/package.json +11 -5
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
|
|
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.
|
|
175
|
-
typeof this.
|
|
176
|
-
this.
|
|
177
|
-
this.
|
|
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.
|
|
184
|
-
lockKeyPrefix: this.
|
|
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
|
|
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
|
+
|
package/lib/lock/Lock.js
ADDED
|
@@ -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
|
+
|
package/lib/logger.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
52
|
+
.update(CacheFactory.stableStringify(normalizedOpts.pipeline || []))
|
|
39
53
|
.digest('hex');
|
|
40
|
-
const { pipeline, ...rest } =
|
|
54
|
+
const { pipeline, ...rest } = normalizedOpts;
|
|
41
55
|
return { ...rest, pipelineHash };
|
|
42
56
|
}
|
|
43
57
|
|