monsqlize 1.0.1 → 1.0.5

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 (66) hide show
  1. package/CHANGELOG.md +204 -2464
  2. package/README.md +735 -1198
  3. package/index.d.ts +942 -18
  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 +173 -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 +225 -29
  17. package/lib/model/features/soft-delete.js +348 -0
  18. package/lib/model/features/version.js +156 -0
  19. package/lib/model/index.js +756 -0
  20. package/lib/mongodb/common/accessor-helpers.js +17 -3
  21. package/lib/mongodb/connect.js +68 -13
  22. package/lib/mongodb/index.js +153 -6
  23. package/lib/mongodb/management/collection-ops.js +4 -4
  24. package/lib/mongodb/management/index-ops.js +18 -18
  25. package/lib/mongodb/management/validation-ops.js +3 -3
  26. package/lib/mongodb/queries/aggregate.js +14 -5
  27. package/lib/mongodb/queries/chain.js +52 -45
  28. package/lib/mongodb/queries/count.js +16 -6
  29. package/lib/mongodb/queries/distinct.js +15 -6
  30. package/lib/mongodb/queries/find-and-count.js +22 -13
  31. package/lib/mongodb/queries/find-by-ids.js +5 -5
  32. package/lib/mongodb/queries/find-one-by-id.js +1 -1
  33. package/lib/mongodb/queries/find-one.js +12 -3
  34. package/lib/mongodb/queries/find-page.js +12 -0
  35. package/lib/mongodb/queries/find.js +15 -6
  36. package/lib/mongodb/queries/watch.js +11 -2
  37. package/lib/mongodb/writes/common/batch-retry.js +64 -0
  38. package/lib/mongodb/writes/delete-batch.js +322 -0
  39. package/lib/mongodb/writes/delete-many.js +20 -11
  40. package/lib/mongodb/writes/delete-one.js +18 -9
  41. package/lib/mongodb/writes/find-one-and-delete.js +19 -10
  42. package/lib/mongodb/writes/find-one-and-replace.js +36 -20
  43. package/lib/mongodb/writes/find-one-and-update.js +36 -20
  44. package/lib/mongodb/writes/increment-one.js +22 -7
  45. package/lib/mongodb/writes/index.js +17 -13
  46. package/lib/mongodb/writes/insert-batch.js +46 -37
  47. package/lib/mongodb/writes/insert-many.js +22 -13
  48. package/lib/mongodb/writes/insert-one.js +18 -9
  49. package/lib/mongodb/writes/replace-one.js +33 -17
  50. package/lib/mongodb/writes/result-handler.js +14 -14
  51. package/lib/mongodb/writes/update-batch.js +358 -0
  52. package/lib/mongodb/writes/update-many.js +34 -18
  53. package/lib/mongodb/writes/update-one.js +33 -17
  54. package/lib/mongodb/writes/upsert-one.js +25 -9
  55. package/lib/operators.js +1 -1
  56. package/lib/redis-cache-adapter.js +3 -3
  57. package/lib/slow-query-log/base-storage.js +69 -0
  58. package/lib/slow-query-log/batch-queue.js +96 -0
  59. package/lib/slow-query-log/config-manager.js +195 -0
  60. package/lib/slow-query-log/index.js +237 -0
  61. package/lib/slow-query-log/mongodb-storage.js +323 -0
  62. package/lib/slow-query-log/query-hash.js +38 -0
  63. package/lib/transaction/DistributedCacheLockManager.js +240 -5
  64. package/lib/transaction/Transaction.js +1 -1
  65. package/lib/utils/objectid-converter.js +566 -0
  66. package/package.json +18 -6
package/lib/cache.js CHANGED
@@ -13,7 +13,7 @@ class Cache {
13
13
  enableStats: options.enableStats !== false, // 启用统计信息
14
14
  ...options
15
15
  };
16
-
16
+
17
17
  // 统计信息
18
18
  this.stats = {
19
19
  hits: 0,
@@ -141,13 +141,13 @@ class Cache {
141
141
  async delPattern(pattern) {
142
142
  const regex = this._patternToRegex(pattern);
143
143
  const keysToDelete = [];
144
-
144
+
145
145
  for (const key of this.cache.keys()) {
146
146
  if (regex.test(key)) {
147
147
  keysToDelete.push(key);
148
148
  }
149
149
  }
150
-
150
+
151
151
 
152
152
  return await this.delMany(keysToDelete);
153
153
  }
@@ -231,7 +231,7 @@ class Cache {
231
231
  _estimateSize(key, value) {
232
232
  const keySize = typeof key === 'string' ? key.length * 2 : 8;
233
233
  let valueSize = 8; // 默认值
234
-
234
+
235
235
  if (typeof value === 'string') {
236
236
  valueSize = value.length * 2;
237
237
  } else if (typeof value === 'object' && value !== null) {
@@ -241,7 +241,7 @@ class Cache {
241
241
  valueSize = 100; // 估算值
242
242
  }
243
243
  }
244
-
244
+
245
245
  return keySize + valueSize;
246
246
  }
247
247
 
@@ -471,7 +471,7 @@ module.exports = class CacheFactory {
471
471
  return (op, base = {}, fetcher) => {
472
472
  // 检查是否在事务中
473
473
  const inTransaction = base.session && base.session.__monSQLizeTransaction;
474
-
474
+
475
475
  // 事务内默认不缓存(除非显式指定 cache)
476
476
  let ttl = 0;
477
477
  if (inTransaction) {
@@ -481,11 +481,11 @@ module.exports = class CacheFactory {
481
481
  // 非事务:正常处理 cache 参数
482
482
  ttl = base.cache ? Number(base.cache) : 0;
483
483
  }
484
-
484
+
485
485
  // 使用浅拷贝构建用于键的对象,避免修改调用方入参
486
486
  const { cache: _cacheTTL, maxTimeMS: _maxTimeMS, session: _session, ...keyBase } = base || {};
487
487
  const key = this.buildCacheKey({ ...ctx, op, base: keyBase });
488
488
  return this.readThrough(cache, ttl, key, fetcher);
489
489
  };
490
490
  }
491
- }
491
+ };
@@ -46,4 +46,67 @@ function assertCursorSortCompatible(currentSort, cursorSort) {
46
46
  }
47
47
  }
48
48
 
49
- module.exports = { validateLimitAfterBefore, assertCursorSortCompatible };
49
+ /**
50
+ * 验证数值范围
51
+ * @param {number} value - 要验证的数值
52
+ * @param {number} min - 最小值(包含)
53
+ * @param {number} max - 最大值(包含)
54
+ * @param {string} name - 参数名称(用于错误消息)
55
+ * @returns {number} 验证通过后的值
56
+ * @throws {Error} 当值不在有效范围内时抛出 INVALID_ARGUMENT 错误
57
+ */
58
+ function validateRange(value, min, max, name) {
59
+ const { ErrorCodes, createError } = require('../errors');
60
+
61
+ // 检查是否为有效数字
62
+ if (typeof value !== 'number' || isNaN(value)) {
63
+ throw createError(
64
+ ErrorCodes.INVALID_ARGUMENT,
65
+ `${name} 必须是一个有效的数字`
66
+ );
67
+ }
68
+
69
+ // 检查是否为有限数(排除 Infinity)
70
+ if (!isFinite(value)) {
71
+ throw createError(
72
+ ErrorCodes.INVALID_ARGUMENT,
73
+ `${name} 必须是有限数字`
74
+ );
75
+ }
76
+
77
+ // 检查范围
78
+ if (value < min || value > max) {
79
+ throw createError(
80
+ ErrorCodes.INVALID_ARGUMENT,
81
+ `${name} 必须在 ${min} 到 ${max} 之间,当前值: ${value}`
82
+ );
83
+ }
84
+
85
+ return value;
86
+ }
87
+
88
+ /**
89
+ * 验证正整数
90
+ * @param {number} value - 要验证的数值
91
+ * @param {string} name - 参数名称(用于错误消息)
92
+ * @returns {number} 验证通过后的值
93
+ * @throws {Error} 当值不是正整数时抛出错误
94
+ */
95
+ function validatePositiveInteger(value, name) {
96
+ const { ErrorCodes, createError } = require('../errors');
97
+
98
+ if (!Number.isInteger(value) || value <= 0) {
99
+ throw createError(
100
+ ErrorCodes.INVALID_ARGUMENT,
101
+ `${name} 必须是正整数,当前值: ${value}`
102
+ );
103
+ }
104
+ return value;
105
+ }
106
+
107
+ module.exports = {
108
+ validateLimitAfterBefore,
109
+ assertCursorSortCompatible,
110
+ validateRange,
111
+ validatePositiveInteger
112
+ };
package/lib/connect.js CHANGED
@@ -68,9 +68,9 @@ module.exports = class ConnectionManager {
68
68
  const db = (databaseName)=>{
69
69
  return {
70
70
  collection:(collectionName)=>instance.collection(databaseName, collectionName)
71
- }
72
- }
71
+ };
72
+ };
73
73
 
74
74
  return { collection, db, instance };
75
75
  }
76
- }
76
+ };
package/lib/errors.js CHANGED
@@ -43,6 +43,10 @@ const ErrorCodes = {
43
43
  DOCUMENTS_REQUIRED: 'DOCUMENTS_REQUIRED',
44
44
  DUPLICATE_KEY: 'DUPLICATE_KEY',
45
45
  WRITE_CONFLICT: 'WRITE_CONFLICT',
46
+
47
+ // 🆕 v1.4.0: 锁相关错误
48
+ LOCK_ACQUIRE_FAILED: 'LOCK_ACQUIRE_FAILED',
49
+ LOCK_TIMEOUT: 'LOCK_TIMEOUT',
46
50
  };
47
51
 
48
52
  /**
@@ -145,6 +149,9 @@ function createWriteError(operation, message, cause = null) {
145
149
  );
146
150
  }
147
151
 
152
+ // 🆕 v1.4.0: 导入锁错误类
153
+ const { LockAcquireError, LockTimeoutError } = require('./lock/errors');
154
+
148
155
  module.exports = {
149
156
  ErrorCodes,
150
157
  createError,
@@ -153,5 +160,8 @@ module.exports = {
153
160
  createConnectionError,
154
161
  createQueryTimeoutError,
155
162
  createWriteError,
163
+ // 🆕 v1.4.0: 锁错误类
164
+ LockAcquireError,
165
+ LockTimeoutError,
156
166
  };
157
167
 
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,113 @@ 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缓存适配器
410
+ */
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
+
462
+ /**
463
+ * 获取 Model 实例
464
+ *
465
+ * @param {string} collectionName - 集合名称
466
+ * @returns {ModelInstance} Model 实例
467
+ * @throws {Error} Model 未定义
468
+ * @throws {Error} 数据库未连接
469
+ *
470
+ * @example
471
+ * // 1. 定义 Model
472
+ * const { Model } = require('monsqlize');
473
+ * Model.define('users', {
474
+ * schema: (dsl) => dsl({ username: 'string!' })
475
+ * });
476
+ *
477
+ * // 2. 连接数据库并获取 Model 实例
478
+ * const msq = new MonSQLize({ ... });
479
+ * await msq.connect();
480
+ * const User = msq.model('users');
481
+ *
482
+ * // 3. 使用 Model
483
+ * const result = await User.find({ status: 'active' });
350
484
  */
351
- static createRedisCacheAdapter = createRedisCacheAdapter;
352
- }
485
+ model(collectionName) {
486
+ // 检查数据库是否已连接
487
+ if (!this.dbInstance) {
488
+ const err = new Error('Database is not connected. Call connect() before accessing models.');
489
+ err.code = 'NOT_CONNECTED';
490
+ throw err;
491
+ }
492
+
493
+ // 检查 Model 是否已定义
494
+ const Model = require('./model');
495
+ if (!Model.has(collectionName)) {
496
+ const err = new Error(`Model '${collectionName}' is not defined. Call Model.define() first.`);
497
+ err.code = 'MODEL_NOT_DEFINED';
498
+ throw err;
499
+ }
500
+
501
+ // 获取 Model 定义
502
+ const modelDef = Model.get(collectionName);
503
+
504
+ // 获取 collection 实例
505
+ const collection = this.dbInstance.collection(collectionName);
506
+
507
+ // 创建 ModelInstance
508
+ const ModelInstanceClass = require('./model').ModelInstance;
509
+ return new ModelInstanceClass(collection, modelDef.definition, this);
510
+ }
511
+ };
512
+
513
+ // ========== 导出 Model 类 ==========
514
+ module.exports.Model = require('./model');
515
+
516
+
@@ -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
+