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.
- package/CHANGELOG.md +204 -2464
- package/README.md +735 -1198
- package/index.d.ts +942 -18
- 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 +173 -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 +225 -29
- package/lib/model/features/soft-delete.js +348 -0
- package/lib/model/features/version.js +156 -0
- package/lib/model/index.js +756 -0
- package/lib/mongodb/common/accessor-helpers.js +17 -3
- package/lib/mongodb/connect.js +68 -13
- package/lib/mongodb/index.js +153 -6
- 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/watch.js +11 -2
- package/lib/mongodb/writes/common/batch-retry.js +64 -0
- package/lib/mongodb/writes/delete-batch.js +322 -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 +22 -7
- package/lib/mongodb/writes/index.js +17 -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-batch.js +358 -0
- 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 +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
|
+
};
|
package/lib/common/validation.js
CHANGED
|
@@ -46,4 +46,67 @@ function assertCursorSortCompatible(currentSort, cursorSort) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
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
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
|
|
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,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
|
-
|
|
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
|
+
|