@lark-apaas/nestjs-datapaas 1.0.16 → 1.0.18
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/dist/index.cjs +277 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -6
- package/dist/index.d.ts +60 -6
- package/dist/index.js +277 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,8 @@ var import_common2 = require("@nestjs/common");
|
|
|
47
47
|
var import_postgres_js = require("drizzle-orm/postgres-js");
|
|
48
48
|
var import_drizzle_orm = require("drizzle-orm");
|
|
49
49
|
var import_postgres = __toESM(require("postgres"), 1);
|
|
50
|
+
var import_promises = require("fs/promises");
|
|
51
|
+
var import_node_fs = require("fs");
|
|
50
52
|
|
|
51
53
|
// src/database/const.ts
|
|
52
54
|
var DRIZZLE_DATABASE = "DRIZZLE_DATABASE";
|
|
@@ -222,19 +224,38 @@ var DrizzleDatabaseManager = class {
|
|
|
222
224
|
observable;
|
|
223
225
|
requestContext;
|
|
224
226
|
db = null;
|
|
227
|
+
sqlClient = null;
|
|
228
|
+
connectionOpPromise = null;
|
|
225
229
|
currentConnectionInfo = null;
|
|
226
230
|
isConnected = false;
|
|
227
231
|
skipInitDbConnection = false;
|
|
232
|
+
currentToken = null;
|
|
233
|
+
currentResolvedConnectionString = null;
|
|
234
|
+
tokenWatchFilePath = null;
|
|
235
|
+
tokenWatchDebounceTimer = null;
|
|
236
|
+
refreshingByTokenChange = false;
|
|
237
|
+
sqlClientShutdownTimeoutSeconds = 5;
|
|
238
|
+
/**
|
|
239
|
+
* 构造函数:注入配置、日志能力与请求上下文。
|
|
240
|
+
*/
|
|
228
241
|
constructor(config, observable, requestContext) {
|
|
229
242
|
this.config = config;
|
|
230
243
|
this.observable = observable;
|
|
231
244
|
this.requestContext = requestContext;
|
|
232
245
|
this.skipInitDbConnection = !!process.env.DEPRECATED_SKIP_INIT_DB_CONNECTION;
|
|
233
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* 模块初始化:建立首个数据库连接并启动 token 监听。
|
|
249
|
+
*/
|
|
234
250
|
async onModuleInit() {
|
|
235
251
|
await this.initialize();
|
|
252
|
+
this.startTokenWatcher();
|
|
236
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* 模块销毁:先停止 token 监听,再断开数据库连接。
|
|
256
|
+
*/
|
|
237
257
|
async onModuleDestroy() {
|
|
258
|
+
this.stopTokenWatcher();
|
|
238
259
|
await this.disconnect();
|
|
239
260
|
}
|
|
240
261
|
/**
|
|
@@ -249,29 +270,30 @@ var DrizzleDatabaseManager = class {
|
|
|
249
270
|
};
|
|
250
271
|
return this.db;
|
|
251
272
|
}
|
|
252
|
-
|
|
253
|
-
const { connectionString, connectionConfig } = parseConnectionInfo(this.config.connectionString, this.config);
|
|
254
|
-
const baseSql = (0, import_postgres.default)(connectionString, connectionConfig);
|
|
255
|
-
const sqlProxy = genPostgresProxy(baseSql, this.observable, this.requestContext);
|
|
256
|
-
this.db = (0, import_postgres_js.drizzle)(sqlProxy, {
|
|
257
|
-
schema: this.config.schema,
|
|
258
|
-
logger: this.config.logger
|
|
259
|
-
});
|
|
260
|
-
this.currentConnectionInfo = {
|
|
261
|
-
connectionString,
|
|
262
|
-
connectionConfig: {
|
|
263
|
-
ssl: connectionConfig.ssl,
|
|
264
|
-
connectionTimeout: connectionConfig.connect_timeout,
|
|
265
|
-
idleTimeout: connectionConfig.idle_timeout,
|
|
266
|
-
maxConnections: connectionConfig.max
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
this.isConnected = true;
|
|
273
|
+
if (this.db && this.isConnected) {
|
|
270
274
|
return this.db;
|
|
271
|
-
} catch (error) {
|
|
272
|
-
this.isConnected = false;
|
|
273
|
-
throw new Error(`Failed to initialize database connection: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
274
275
|
}
|
|
276
|
+
if (this.connectionOpPromise) {
|
|
277
|
+
return this.connectionOpPromise;
|
|
278
|
+
}
|
|
279
|
+
return this.runWithConnectionLock(async () => {
|
|
280
|
+
if (this.db && this.isConnected) {
|
|
281
|
+
return this.db;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const token = await this.readConnectionTokenFromFile();
|
|
285
|
+
const resolvedConnectionString = this.appendTokenToConnectionString(this.config.connectionString, token);
|
|
286
|
+
const nextState = await this.createConnectedState(resolvedConnectionString, token);
|
|
287
|
+
this.applyConnectedState(nextState);
|
|
288
|
+
return nextState.db;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (!this.db || !this.isConnected) {
|
|
291
|
+
this.isConnected = false;
|
|
292
|
+
this.db = null;
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Failed to initialize database connection: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
275
297
|
}
|
|
276
298
|
/**
|
|
277
299
|
* 获取数据库实例
|
|
@@ -301,27 +323,34 @@ var DrizzleDatabaseManager = class {
|
|
|
301
323
|
}
|
|
302
324
|
}
|
|
303
325
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
326
|
+
* 重新连接数据库。
|
|
327
|
+
* 注意:重连时不关闭 token watcher,避免重连后丢失后续文件变更监听。
|
|
328
|
+
* 同时通过 promise 锁避免并发重连导致状态抖动。
|
|
306
329
|
* @returns 新的数据库实例
|
|
307
330
|
*/
|
|
308
|
-
async reconnect(
|
|
309
|
-
|
|
310
|
-
return this.initialize();
|
|
331
|
+
async reconnect() {
|
|
332
|
+
return this.reconnectInternal();
|
|
311
333
|
}
|
|
312
334
|
/**
|
|
313
335
|
* 断开数据库连接
|
|
314
336
|
*/
|
|
315
|
-
async disconnect() {
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
async disconnect(shouldStopWatcher = true) {
|
|
338
|
+
if (shouldStopWatcher) {
|
|
339
|
+
this.stopTokenWatcher();
|
|
340
|
+
}
|
|
341
|
+
const pendingOp = this.connectionOpPromise;
|
|
342
|
+
if (pendingOp) {
|
|
343
|
+
await Promise.allSettled([
|
|
344
|
+
pendingOp
|
|
345
|
+
]);
|
|
324
346
|
}
|
|
347
|
+
await this.shutdownSqlClientSafely(this.sqlClient, "Error disconnecting from database:");
|
|
348
|
+
this.sqlClient = null;
|
|
349
|
+
this.db = null;
|
|
350
|
+
this.isConnected = false;
|
|
351
|
+
this.currentConnectionInfo = null;
|
|
352
|
+
this.currentResolvedConnectionString = null;
|
|
353
|
+
this.currentToken = null;
|
|
325
354
|
}
|
|
326
355
|
/**
|
|
327
356
|
* 获取当前连接信息
|
|
@@ -356,6 +385,193 @@ var DrizzleDatabaseManager = class {
|
|
|
356
385
|
connectionInfo: this.currentConnectionInfo
|
|
357
386
|
};
|
|
358
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* 读取 token 文件,空文件等价于无 token
|
|
390
|
+
*/
|
|
391
|
+
async readConnectionTokenFromFile() {
|
|
392
|
+
const tokenFilePath = this.config.connectionTokenFilePath;
|
|
393
|
+
if (!tokenFilePath) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const raw = await (0, import_promises.readFile)(tokenFilePath, "utf-8");
|
|
398
|
+
const trimmed = raw.trim();
|
|
399
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 基于基础 connectionString 和 token 生成最终连接串
|
|
406
|
+
* 只修改 options 参数值,在末尾追加 -c miaoda.zti_token=<token>
|
|
407
|
+
*/
|
|
408
|
+
appendTokenToConnectionString(connectionString, token) {
|
|
409
|
+
if (!token) {
|
|
410
|
+
return connectionString;
|
|
411
|
+
}
|
|
412
|
+
const url = new URL(connectionString);
|
|
413
|
+
const currentOptions = url.searchParams.get("options") || "";
|
|
414
|
+
const optionsWithoutToken = currentOptions.replace(/(?:^|\s)-c\s+miaoda.zti_token=[^\s]*/g, " ").replace(/\s+/g, " ").trim();
|
|
415
|
+
const tokenOption = `-c miaoda.zti_token=${token}`;
|
|
416
|
+
const nextOptions = [
|
|
417
|
+
optionsWithoutToken,
|
|
418
|
+
tokenOption
|
|
419
|
+
].filter(Boolean).join(" ").trim();
|
|
420
|
+
url.searchParams.set("options", nextOptions);
|
|
421
|
+
return url.toString();
|
|
422
|
+
}
|
|
423
|
+
async createConnectedState(resolvedConnectionString, token) {
|
|
424
|
+
const { connectionString, connectionConfig } = parseConnectionInfo(resolvedConnectionString, this.config);
|
|
425
|
+
const baseSql = (0, import_postgres.default)(connectionString, connectionConfig);
|
|
426
|
+
try {
|
|
427
|
+
const sqlProxy = genPostgresProxy(baseSql, this.observable, this.requestContext);
|
|
428
|
+
const db = (0, import_postgres_js.drizzle)(sqlProxy, {
|
|
429
|
+
schema: this.config.schema,
|
|
430
|
+
logger: this.config.logger
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
db,
|
|
434
|
+
sqlClient: baseSql,
|
|
435
|
+
resolvedConnectionString: connectionString,
|
|
436
|
+
token,
|
|
437
|
+
connectionInfo: {
|
|
438
|
+
connectionString,
|
|
439
|
+
connectionConfig: {
|
|
440
|
+
ssl: connectionConfig.ssl,
|
|
441
|
+
connectionTimeout: connectionConfig.connect_timeout,
|
|
442
|
+
idleTimeout: connectionConfig.idle_timeout,
|
|
443
|
+
maxConnections: connectionConfig.max
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
} catch (error) {
|
|
448
|
+
await this.shutdownSqlClientSafely(baseSql, "Error closing failed new database client:");
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
applyConnectedState(nextState) {
|
|
453
|
+
this.db = nextState.db;
|
|
454
|
+
this.sqlClient = nextState.sqlClient;
|
|
455
|
+
this.currentConnectionInfo = nextState.connectionInfo;
|
|
456
|
+
this.currentResolvedConnectionString = nextState.resolvedConnectionString;
|
|
457
|
+
this.currentToken = nextState.token;
|
|
458
|
+
this.isConnected = true;
|
|
459
|
+
}
|
|
460
|
+
reconnectInternal(nextResolvedConnectionString, nextToken) {
|
|
461
|
+
return this.runWithConnectionLock(() => this.reconnectWithResolvedConnectionString(nextResolvedConnectionString, nextToken));
|
|
462
|
+
}
|
|
463
|
+
runWithConnectionLock(operation) {
|
|
464
|
+
if (this.connectionOpPromise) {
|
|
465
|
+
return this.connectionOpPromise;
|
|
466
|
+
}
|
|
467
|
+
this.connectionOpPromise = (async () => {
|
|
468
|
+
try {
|
|
469
|
+
return await operation();
|
|
470
|
+
} finally {
|
|
471
|
+
this.connectionOpPromise = null;
|
|
472
|
+
}
|
|
473
|
+
})();
|
|
474
|
+
return this.connectionOpPromise;
|
|
475
|
+
}
|
|
476
|
+
async reconnectWithResolvedConnectionString(nextResolvedConnectionString, nextToken) {
|
|
477
|
+
const resolvedToken = nextToken ?? await this.readConnectionTokenFromFile();
|
|
478
|
+
const resolvedConnectionString = nextResolvedConnectionString ?? this.appendTokenToConnectionString(this.config.connectionString, resolvedToken);
|
|
479
|
+
const nextState = await this.createConnectedState(resolvedConnectionString, resolvedToken);
|
|
480
|
+
const previousSqlClient = this.sqlClient;
|
|
481
|
+
this.applyConnectedState(nextState);
|
|
482
|
+
if (previousSqlClient && previousSqlClient !== nextState.sqlClient) {
|
|
483
|
+
await this.shutdownSqlClientSafely(previousSqlClient, "Error shutting down previous database client after reconnect:");
|
|
484
|
+
}
|
|
485
|
+
return nextState.db;
|
|
486
|
+
}
|
|
487
|
+
async shutdownSqlClientSafely(client, errorPrefix) {
|
|
488
|
+
try {
|
|
489
|
+
if (client && typeof client.end === "function") {
|
|
490
|
+
await client.end({
|
|
491
|
+
timeout: this.sqlClientShutdownTimeoutSeconds
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
console.error(errorPrefix, error);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 启动 token 文件监听;若已存在旧监听则先清理后重建。
|
|
500
|
+
* 使用 watchFile 轮询,规避虚拟化/网络文件系统下 fs.watch 不可靠的问题。
|
|
501
|
+
*/
|
|
502
|
+
startTokenWatcher() {
|
|
503
|
+
const tokenFilePath = this.config.connectionTokenFilePath;
|
|
504
|
+
if (!tokenFilePath || this.skipInitDbConnection) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.stopTokenWatcher();
|
|
508
|
+
this.startTokenWatchFileFallback(tokenFilePath);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* watchFile 兜底监听:即使 fs.watch 丢事件,也能通过轮询感知 token 文件变化。
|
|
512
|
+
*/
|
|
513
|
+
startTokenWatchFileFallback(tokenFilePath) {
|
|
514
|
+
if (this.tokenWatchFilePath === tokenFilePath) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (this.tokenWatchFilePath) {
|
|
518
|
+
(0, import_node_fs.unwatchFile)(this.tokenWatchFilePath);
|
|
519
|
+
this.tokenWatchFilePath = null;
|
|
520
|
+
}
|
|
521
|
+
(0, import_node_fs.watchFile)(tokenFilePath, {
|
|
522
|
+
interval: 6e5
|
|
523
|
+
}, () => {
|
|
524
|
+
this.scheduleTokenRefresh();
|
|
525
|
+
});
|
|
526
|
+
this.tokenWatchFilePath = tokenFilePath;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* 停止 watcher 与防抖定时器,释放监听资源。
|
|
530
|
+
*/
|
|
531
|
+
stopTokenWatcher() {
|
|
532
|
+
if (this.tokenWatchDebounceTimer) {
|
|
533
|
+
clearTimeout(this.tokenWatchDebounceTimer);
|
|
534
|
+
this.tokenWatchDebounceTimer = null;
|
|
535
|
+
}
|
|
536
|
+
if (this.tokenWatchFilePath) {
|
|
537
|
+
(0, import_node_fs.unwatchFile)(this.tokenWatchFilePath);
|
|
538
|
+
this.tokenWatchFilePath = null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* 对频繁文件事件做防抖,避免短时间重复重连。
|
|
543
|
+
*/
|
|
544
|
+
scheduleTokenRefresh() {
|
|
545
|
+
if (this.tokenWatchDebounceTimer) {
|
|
546
|
+
clearTimeout(this.tokenWatchDebounceTimer);
|
|
547
|
+
}
|
|
548
|
+
this.tokenWatchDebounceTimer = setTimeout(() => {
|
|
549
|
+
void this.refreshConnectionIfTokenChanged();
|
|
550
|
+
}, 300);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* token 变化时刷新连接:仅在 token/连接串发生变化,或当前连接不可用时触发重连。
|
|
554
|
+
*/
|
|
555
|
+
async refreshConnectionIfTokenChanged() {
|
|
556
|
+
if (this.refreshingByTokenChange) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.refreshingByTokenChange = true;
|
|
560
|
+
try {
|
|
561
|
+
const nextToken = await this.readConnectionTokenFromFile();
|
|
562
|
+
const nextResolvedConnectionString = this.appendTokenToConnectionString(this.config.connectionString, nextToken);
|
|
563
|
+
const sameToken = nextToken === this.currentToken;
|
|
564
|
+
const sameConnectionString = nextResolvedConnectionString === this.currentResolvedConnectionString;
|
|
565
|
+
if (sameToken && sameConnectionString && this.isConnected) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
await this.reconnectInternal(nextResolvedConnectionString, nextToken);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error("Failed to refresh database connection from token file:", error);
|
|
571
|
+
} finally {
|
|
572
|
+
this.refreshingByTokenChange = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
359
575
|
};
|
|
360
576
|
DrizzleDatabaseManager = _ts_decorate([
|
|
361
577
|
(0, import_common2.Injectable)(),
|
|
@@ -416,11 +632,10 @@ var DataPaasDatabaseService = class {
|
|
|
416
632
|
}
|
|
417
633
|
/**
|
|
418
634
|
* 重新连接数据库
|
|
419
|
-
* @param forceRefresh 是否强制刷新连接信息
|
|
420
635
|
* @returns 新的数据库实例
|
|
421
636
|
*/
|
|
422
|
-
async reconnect(
|
|
423
|
-
return this.databaseManager.reconnect(
|
|
637
|
+
async reconnect() {
|
|
638
|
+
return this.databaseManager.reconnect();
|
|
424
639
|
}
|
|
425
640
|
/**
|
|
426
641
|
* 获取当前连接信息
|
|
@@ -584,7 +799,8 @@ var DataPaasModule = class _DataPaasModule {
|
|
|
584
799
|
connectionTimeout: options.connectionTimeout || 10,
|
|
585
800
|
ssl: options.ssl || (sslModeRequired ? "require" : false),
|
|
586
801
|
autoContext: options.autoContext == null ? true : !!options.autoContext,
|
|
587
|
-
roleSchema
|
|
802
|
+
roleSchema,
|
|
803
|
+
connectionTokenFilePath: options.connectionTokenFilePath
|
|
588
804
|
};
|
|
589
805
|
const providers = [
|
|
590
806
|
// 配置提供者 - 使用 useValue
|
|
@@ -611,7 +827,16 @@ var DataPaasModule = class _DataPaasModule {
|
|
|
611
827
|
provide: DRIZZLE_DATABASE,
|
|
612
828
|
useFactory: /* @__PURE__ */ __name(async (manager) => {
|
|
613
829
|
await manager.initialize();
|
|
614
|
-
return
|
|
830
|
+
return new Proxy({}, {
|
|
831
|
+
get: /* @__PURE__ */ __name((_target, prop, receiver) => {
|
|
832
|
+
const db = manager.getDatabase();
|
|
833
|
+
const value = Reflect.get(db, prop, receiver);
|
|
834
|
+
if (typeof value === "function") {
|
|
835
|
+
return value.bind(db);
|
|
836
|
+
}
|
|
837
|
+
return value;
|
|
838
|
+
}, "get")
|
|
839
|
+
});
|
|
615
840
|
}, "useFactory"),
|
|
616
841
|
inject: [
|
|
617
842
|
DrizzleDatabaseManager
|
|
@@ -670,7 +895,16 @@ var DataPaasModule = class _DataPaasModule {
|
|
|
670
895
|
provide: DRIZZLE_DATABASE,
|
|
671
896
|
useFactory: /* @__PURE__ */ __name(async (manager) => {
|
|
672
897
|
await manager.initialize();
|
|
673
|
-
return
|
|
898
|
+
return new Proxy({}, {
|
|
899
|
+
get: /* @__PURE__ */ __name((_target, prop, receiver) => {
|
|
900
|
+
const db = manager.getDatabase();
|
|
901
|
+
const value = Reflect.get(db, prop, receiver);
|
|
902
|
+
if (typeof value === "function") {
|
|
903
|
+
return value.bind(db);
|
|
904
|
+
}
|
|
905
|
+
return value;
|
|
906
|
+
}, "get")
|
|
907
|
+
});
|
|
674
908
|
}, "useFactory"),
|
|
675
909
|
inject: [
|
|
676
910
|
DrizzleDatabaseManager
|
|
@@ -743,7 +977,8 @@ var DataPaasModule = class _DataPaasModule {
|
|
|
743
977
|
connectionTimeout: config.connectionTimeout || 10,
|
|
744
978
|
ssl: config.ssl || (sslModeRequired ? "require" : false),
|
|
745
979
|
autoContext: config.autoContext == null ? true : !!config.autoContext,
|
|
746
|
-
roleSchema
|
|
980
|
+
roleSchema,
|
|
981
|
+
connectionTokenFilePath: config.connectionTokenFilePath
|
|
747
982
|
};
|
|
748
983
|
}
|
|
749
984
|
};
|