nx-mongo 3.5.0 → 3.8.0

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.
@@ -47,6 +47,11 @@ export interface HelperConfig {
47
47
  uniqueIndexKeys?: string[];
48
48
  provider?: string;
49
49
  };
50
+ databases?: Array<{
51
+ ref: string;
52
+ type: string;
53
+ database: string;
54
+ }>;
50
55
  }
51
56
 
52
57
  export interface WriteByRefResult {
@@ -82,7 +87,7 @@ export interface StageRecord extends StageIdentity {
82
87
  metadata?: StageMetadata;
83
88
  }
84
89
 
85
- export type ProgressSessionOptions = { session?: ClientSession };
90
+ export type ProgressSessionOptions = { session?: ClientSession; database?: string; ref?: string; type?: string };
86
91
 
87
92
  export interface ProgressAPI {
88
93
  isCompleted(key: string, options?: ProgressSessionOptions & { process?: string; provider?: string }): Promise<boolean>;
@@ -99,6 +104,9 @@ export interface ProgressAPI {
99
104
  export interface WriteStageOptions {
100
105
  ensureIndex?: boolean;
101
106
  session?: ClientSession;
107
+ database?: string;
108
+ ref?: string;
109
+ type?: string;
102
110
  complete?: {
103
111
  key: string;
104
112
  process?: string;
@@ -126,6 +134,9 @@ export interface MergeCollectionsOptions {
126
134
  onUnmatched1?: 'include' | 'skip'; // What to do with unmatched records from collection 1 (default: 'include', deprecated: use joinType instead)
127
135
  onUnmatched2?: 'include' | 'skip'; // What to do with unmatched records from collection 2 (default: 'include', deprecated: use joinType instead)
128
136
  session?: ClientSession;
137
+ database?: string; // Database name (defaults to 'admin')
138
+ ref?: string; // Optional ref for database resolution
139
+ type?: string; // Optional type for database resolution
129
140
  }
130
141
 
131
142
  export interface MergeCollectionsResult {
@@ -361,7 +372,7 @@ class ProgressAPIImpl implements ProgressAPI {
361
372
  * Ensures the progress collection and unique index exist.
362
373
  * Called lazily on first use.
363
374
  */
364
- private async ensureProgressIndex(session?: ClientSession): Promise<void> {
375
+ private async ensureProgressIndex(session?: ClientSession, database?: string, ref?: string, type?: string): Promise<void> {
365
376
  if (this.indexEnsured) {
366
377
  return;
367
378
  }
@@ -369,7 +380,8 @@ class ProgressAPIImpl implements ProgressAPI {
369
380
  this.helper.ensureInitialized();
370
381
 
371
382
  try {
372
- const collection = this.helper.getDb().collection(this.config.collection);
383
+ const db = this.helper.getDatabaseByName(database, ref, type);
384
+ const collection = db.collection(this.config.collection);
373
385
  const indexes = await collection.indexes();
374
386
 
375
387
  // Build index spec from uniqueIndexKeys
@@ -429,13 +441,17 @@ class ProgressAPIImpl implements ProgressAPI {
429
441
  }
430
442
 
431
443
  async isCompleted(key: string, options?: ProgressSessionOptions & { process?: string; provider?: string }): Promise<boolean> {
432
- await this.ensureProgressIndex(options?.session);
444
+ const database = options?.database;
445
+ const ref = options?.ref;
446
+ const type = options?.type;
447
+ await this.ensureProgressIndex(options?.session, database, ref, type);
433
448
  this.helper.ensureInitialized();
434
449
 
435
450
  const provider = this.resolveProvider(options);
436
451
  const process = options?.process;
437
452
  const filter = this.buildFilter(key, process, provider);
438
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
453
+ const db = this.helper.getDatabaseByName(database, ref, type);
454
+ const collection = db.collection<StageRecord>(this.config.collection);
439
455
 
440
456
  const findOptions: any = {};
441
457
  if (options?.session) {
@@ -447,13 +463,17 @@ class ProgressAPIImpl implements ProgressAPI {
447
463
  }
448
464
 
449
465
  async start(identity: StageIdentity, options?: ProgressSessionOptions): Promise<void> {
450
- await this.ensureProgressIndex(options?.session);
466
+ const database = options?.database;
467
+ const ref = options?.ref;
468
+ const type = options?.type;
469
+ await this.ensureProgressIndex(options?.session, database, ref, type);
451
470
  this.helper.ensureInitialized();
452
471
 
453
472
  const provider = this.resolveProvider({ provider: identity.provider });
454
473
  const process = identity.process;
455
474
  const filter = this.buildFilter(identity.key, process, provider);
456
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
475
+ const db = this.helper.getDatabaseByName(database, ref, type);
476
+ const collection = db.collection<StageRecord>(this.config.collection);
457
477
 
458
478
  const update: UpdateFilter<StageRecord> = {
459
479
  $set: {
@@ -485,13 +505,17 @@ class ProgressAPIImpl implements ProgressAPI {
485
505
  identity: StageIdentity & { metadata?: StageMetadata },
486
506
  options?: ProgressSessionOptions
487
507
  ): Promise<void> {
488
- await this.ensureProgressIndex(options?.session);
508
+ const database = options?.database;
509
+ const ref = options?.ref;
510
+ const type = options?.type;
511
+ await this.ensureProgressIndex(options?.session, database, ref, type);
489
512
  this.helper.ensureInitialized();
490
513
 
491
514
  const provider = this.resolveProvider({ provider: identity.provider });
492
515
  const process = identity.process;
493
516
  const filter = this.buildFilter(identity.key, process, provider);
494
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
517
+ const db = this.helper.getDatabaseByName(database, ref, type);
518
+ const collection = db.collection<StageRecord>(this.config.collection);
495
519
 
496
520
  const update: UpdateFilter<StageRecord> = {
497
521
  $set: {
@@ -526,7 +550,10 @@ class ProgressAPIImpl implements ProgressAPI {
526
550
  }
527
551
 
528
552
  async getCompleted(options?: ProgressSessionOptions & { process?: string; provider?: string }): Promise<Array<Pick<StageRecord, 'key' | 'name' | 'completedAt'>>> {
529
- await this.ensureProgressIndex(options?.session);
553
+ const database = options?.database;
554
+ const ref = options?.ref;
555
+ const type = options?.type;
556
+ await this.ensureProgressIndex(options?.session, database, ref, type);
530
557
  this.helper.ensureInitialized();
531
558
 
532
559
  const provider = this.resolveProvider(options);
@@ -539,7 +566,8 @@ class ProgressAPIImpl implements ProgressAPI {
539
566
  filter.provider = provider;
540
567
  }
541
568
 
542
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
569
+ const db = this.helper.getDatabaseByName(database, ref, type);
570
+ const collection = db.collection<StageRecord>(this.config.collection);
543
571
  const findOptions: any = { projection: { key: 1, name: 1, completedAt: 1 } };
544
572
  if (options?.session) {
545
573
  findOptions.session = options.session;
@@ -554,7 +582,10 @@ class ProgressAPIImpl implements ProgressAPI {
554
582
  }
555
583
 
556
584
  async getProgress(options?: ProgressSessionOptions & { process?: string; provider?: string }): Promise<StageRecord[]> {
557
- await this.ensureProgressIndex(options?.session);
585
+ const database = options?.database;
586
+ const ref = options?.ref;
587
+ const type = options?.type;
588
+ await this.ensureProgressIndex(options?.session, database, ref, type);
558
589
  this.helper.ensureInitialized();
559
590
 
560
591
  const provider = this.resolveProvider(options);
@@ -567,7 +598,8 @@ class ProgressAPIImpl implements ProgressAPI {
567
598
  filter.provider = provider;
568
599
  }
569
600
 
570
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
601
+ const db = this.helper.getDatabaseByName(database, ref, type);
602
+ const collection = db.collection<StageRecord>(this.config.collection);
571
603
  const findOptions: any = {};
572
604
  if (options?.session) {
573
605
  findOptions.session = options.session;
@@ -577,13 +609,17 @@ class ProgressAPIImpl implements ProgressAPI {
577
609
  }
578
610
 
579
611
  async reset(key: string, options?: ProgressSessionOptions & { process?: string; provider?: string }): Promise<void> {
580
- await this.ensureProgressIndex(options?.session);
612
+ const database = options?.database;
613
+ const ref = options?.ref;
614
+ const type = options?.type;
615
+ await this.ensureProgressIndex(options?.session, database, ref, type);
581
616
  this.helper.ensureInitialized();
582
617
 
583
618
  const provider = this.resolveProvider(options);
584
619
  const process = options?.process;
585
620
  const filter = this.buildFilter(key, process, provider);
586
- const collection = this.helper.getDb().collection<StageRecord>(this.config.collection);
621
+ const db = this.helper.getDatabaseByName(database, ref, type);
622
+ const collection = db.collection<StageRecord>(this.config.collection);
587
623
 
588
624
  const update: UpdateFilter<StageRecord> = {
589
625
  $set: {
@@ -612,9 +648,12 @@ export class SimpleMongoHelper {
612
648
  private retryOptions: RetryOptions;
613
649
  private config: HelperConfig | null = null;
614
650
  public readonly progress: ProgressAPI;
651
+ private cleanupRegistered: boolean = false;
652
+ private isDisconnecting: boolean = false;
615
653
 
616
654
  constructor(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig) {
617
- this.connectionString = connectionString;
655
+ // Strip database name from connection string if present
656
+ this.connectionString = this.stripDatabaseFromConnectionString(connectionString);
618
657
  this.retryOptions = {
619
658
  maxRetries: retryOptions?.maxRetries ?? 3,
620
659
  retryDelay: retryOptions?.retryDelay ?? 1000,
@@ -622,6 +661,9 @@ export class SimpleMongoHelper {
622
661
  };
623
662
  this.config = config || null;
624
663
  this.progress = new ProgressAPIImpl(this, config?.progress);
664
+
665
+ // Register automatic cleanup on process exit
666
+ this.registerCleanup();
625
667
  }
626
668
 
627
669
  /**
@@ -728,9 +770,8 @@ export class SimpleMongoHelper {
728
770
 
729
771
  await testClient.connect();
730
772
 
731
- // Try to ping the server
732
- const dbName = this.extractDatabaseName(this.connectionString);
733
- const testDb = testClient.db(dbName);
773
+ // Try to ping the server using 'admin' database (default)
774
+ const testDb = testClient.db('admin');
734
775
  await testDb.admin().ping();
735
776
 
736
777
  // Test basic operation (list collections)
@@ -859,9 +900,8 @@ export class SimpleMongoHelper {
859
900
  try {
860
901
  this.client = new MongoClient(this.connectionString);
861
902
  await this.client.connect();
862
- // Extract database name from connection string or use default
863
- const dbName = this.extractDatabaseName(this.connectionString);
864
- this.db = this.client.db(dbName);
903
+ // Default to 'admin' database for initial connection (not used for operations)
904
+ this.db = this.client.db('admin');
865
905
  this.isInitialized = true;
866
906
  return;
867
907
  } catch (error) {
@@ -892,12 +932,16 @@ export class SimpleMongoHelper {
892
932
  async loadCollection<T extends Document = Document>(
893
933
  collectionName: string,
894
934
  query?: Filter<T>,
895
- options?: PaginationOptions
935
+ options?: PaginationOptions,
936
+ database?: string,
937
+ ref?: string,
938
+ type?: string
896
939
  ): Promise<WithId<T>[] | PaginatedResult<T>> {
897
940
  this.ensureInitialized();
898
941
 
899
942
  try {
900
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
943
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
944
+ const collection: Collection<T> = db.collection<T>(collectionName);
901
945
  const filter = query || {};
902
946
  let cursor = collection.find(filter);
903
947
 
@@ -949,12 +993,16 @@ export class SimpleMongoHelper {
949
993
  async findOne<T extends Document = Document>(
950
994
  collectionName: string,
951
995
  query: Filter<T>,
952
- options?: { sort?: Sort; projection?: Document }
996
+ options?: { sort?: Sort; projection?: Document },
997
+ database?: string,
998
+ ref?: string,
999
+ type?: string
953
1000
  ): Promise<WithId<T> | null> {
954
1001
  this.ensureInitialized();
955
1002
 
956
1003
  try {
957
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1004
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1005
+ const collection: Collection<T> = db.collection<T>(collectionName);
958
1006
  let findOptions: any = {};
959
1007
 
960
1008
  if (options?.sort) {
@@ -982,12 +1030,16 @@ export class SimpleMongoHelper {
982
1030
  async insert<T extends Document = Document>(
983
1031
  collectionName: string,
984
1032
  data: OptionalUnlessRequiredId<T> | OptionalUnlessRequiredId<T>[],
985
- options?: { session?: ClientSession }
1033
+ options?: { session?: ClientSession },
1034
+ database?: string,
1035
+ ref?: string,
1036
+ type?: string
986
1037
  ): Promise<any> {
987
1038
  this.ensureInitialized();
988
1039
 
989
1040
  try {
990
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1041
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1042
+ const collection: Collection<T> = db.collection<T>(collectionName);
991
1043
  const insertOptions = options?.session ? { session: options.session } : {};
992
1044
 
993
1045
  if (Array.isArray(data)) {
@@ -1015,12 +1067,16 @@ export class SimpleMongoHelper {
1015
1067
  collectionName: string,
1016
1068
  filter: Filter<T>,
1017
1069
  updateData: UpdateFilter<T>,
1018
- options?: { upsert?: boolean; multi?: boolean; session?: ClientSession }
1070
+ options?: { upsert?: boolean; multi?: boolean; session?: ClientSession },
1071
+ database?: string,
1072
+ ref?: string,
1073
+ type?: string
1019
1074
  ): Promise<any> {
1020
1075
  this.ensureInitialized();
1021
1076
 
1022
1077
  try {
1023
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1078
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1079
+ const collection: Collection<T> = db.collection<T>(collectionName);
1024
1080
  const updateOptions: any = {};
1025
1081
  if (options?.upsert !== undefined) updateOptions.upsert = options.upsert;
1026
1082
  if (options?.session) updateOptions.session = options.session;
@@ -1048,12 +1104,16 @@ export class SimpleMongoHelper {
1048
1104
  async delete<T extends Document = Document>(
1049
1105
  collectionName: string,
1050
1106
  filter: Filter<T>,
1051
- options?: { multi?: boolean }
1107
+ options?: { multi?: boolean },
1108
+ database?: string,
1109
+ ref?: string,
1110
+ type?: string
1052
1111
  ): Promise<any> {
1053
1112
  this.ensureInitialized();
1054
1113
 
1055
1114
  try {
1056
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1115
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1116
+ const collection: Collection<T> = db.collection<T>(collectionName);
1057
1117
 
1058
1118
  if (options?.multi) {
1059
1119
  const result = await collection.deleteMany(filter);
@@ -1076,12 +1136,16 @@ export class SimpleMongoHelper {
1076
1136
  */
1077
1137
  async countDocuments<T extends Document = Document>(
1078
1138
  collectionName: string,
1079
- query?: Filter<T>
1139
+ query?: Filter<T>,
1140
+ database?: string,
1141
+ ref?: string,
1142
+ type?: string
1080
1143
  ): Promise<number> {
1081
1144
  this.ensureInitialized();
1082
1145
 
1083
1146
  try {
1084
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1147
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1148
+ const collection: Collection<T> = db.collection<T>(collectionName);
1085
1149
  const count = await collection.countDocuments(query || {});
1086
1150
  return count;
1087
1151
  } catch (error) {
@@ -1095,11 +1159,12 @@ export class SimpleMongoHelper {
1095
1159
  * @returns Estimated number of documents
1096
1160
  * @throws Error if not initialized or if count fails
1097
1161
  */
1098
- async estimatedDocumentCount(collectionName: string): Promise<number> {
1162
+ async estimatedDocumentCount(collectionName: string, database?: string, ref?: string, type?: string): Promise<number> {
1099
1163
  this.ensureInitialized();
1100
1164
 
1101
1165
  try {
1102
- const collection = this.db!.collection(collectionName);
1166
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1167
+ const collection = db.collection(collectionName);
1103
1168
  const count = await collection.estimatedDocumentCount();
1104
1169
  return count;
1105
1170
  } catch (error) {
@@ -1116,12 +1181,16 @@ export class SimpleMongoHelper {
1116
1181
  */
1117
1182
  async aggregate<T extends Document = Document>(
1118
1183
  collectionName: string,
1119
- pipeline: Document[]
1184
+ pipeline: Document[],
1185
+ database?: string,
1186
+ ref?: string,
1187
+ type?: string
1120
1188
  ): Promise<T[]> {
1121
1189
  this.ensureInitialized();
1122
1190
 
1123
1191
  try {
1124
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1192
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1193
+ const collection: Collection<T> = db.collection<T>(collectionName);
1125
1194
  const results = await collection.aggregate<T>(pipeline).toArray();
1126
1195
  return results;
1127
1196
  } catch (error) {
@@ -1140,12 +1209,16 @@ export class SimpleMongoHelper {
1140
1209
  async createIndex(
1141
1210
  collectionName: string,
1142
1211
  indexSpec: IndexSpecification,
1143
- options?: CreateIndexesOptions
1212
+ options?: CreateIndexesOptions,
1213
+ database?: string,
1214
+ ref?: string,
1215
+ type?: string
1144
1216
  ): Promise<string> {
1145
1217
  this.ensureInitialized();
1146
1218
 
1147
1219
  try {
1148
- const collection = this.db!.collection(collectionName);
1220
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1221
+ const collection = db.collection(collectionName);
1149
1222
  const indexName = await collection.createIndex(indexSpec, options);
1150
1223
  return indexName;
1151
1224
  } catch (error) {
@@ -1160,11 +1233,12 @@ export class SimpleMongoHelper {
1160
1233
  * @returns Result object
1161
1234
  * @throws Error if not initialized or if index drop fails
1162
1235
  */
1163
- async dropIndex(collectionName: string, indexName: string): Promise<any> {
1236
+ async dropIndex(collectionName: string, indexName: string, database?: string, ref?: string, type?: string): Promise<any> {
1164
1237
  this.ensureInitialized();
1165
1238
 
1166
1239
  try {
1167
- const collection = this.db!.collection(collectionName);
1240
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1241
+ const collection = db.collection(collectionName);
1168
1242
  const result = await collection.dropIndex(indexName);
1169
1243
  return result;
1170
1244
  } catch (error) {
@@ -1178,11 +1252,12 @@ export class SimpleMongoHelper {
1178
1252
  * @returns Array of index information
1179
1253
  * @throws Error if not initialized or if listing fails
1180
1254
  */
1181
- async listIndexes(collectionName: string): Promise<Document[]> {
1255
+ async listIndexes(collectionName: string, database?: string, ref?: string, type?: string): Promise<Document[]> {
1182
1256
  this.ensureInitialized();
1183
1257
 
1184
1258
  try {
1185
- const collection = this.db!.collection(collectionName);
1259
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1260
+ const collection = db.collection(collectionName);
1186
1261
  const indexes = await collection.indexes();
1187
1262
  return indexes;
1188
1263
  } catch (error) {
@@ -1244,7 +1319,7 @@ export class SimpleMongoHelper {
1244
1319
  */
1245
1320
  async loadByRef<T extends Document = Document>(
1246
1321
  ref: string,
1247
- options?: PaginationOptions & { session?: ClientSession }
1322
+ options?: PaginationOptions & { session?: ClientSession; database?: string; ref?: string; type?: string }
1248
1323
  ): Promise<WithId<T>[] | PaginatedResult<T>> {
1249
1324
  this.ensureInitialized();
1250
1325
 
@@ -1257,9 +1332,13 @@ export class SimpleMongoHelper {
1257
1332
  throw new Error(`Ref '${ref}' not found in configuration inputs.`);
1258
1333
  }
1259
1334
 
1335
+ const database = options?.database;
1336
+ const dbRef = options?.ref;
1337
+ const dbType = options?.type;
1338
+ const db = this.getDatabase(this.resolveDatabase({ database, ref: dbRef, type: dbType }));
1260
1339
  // Note: loadCollection doesn't support session yet, but we'll pass it through options
1261
1340
  // For now, we'll use the collection directly with session support
1262
- const collection: Collection<T> = this.db!.collection<T>(inputConfig.collection);
1341
+ const collection: Collection<T> = db.collection<T>(inputConfig.collection);
1263
1342
  const filter = inputConfig.query || {};
1264
1343
  const session = options?.session;
1265
1344
 
@@ -1323,15 +1402,22 @@ export class SimpleMongoHelper {
1323
1402
  options?: {
1324
1403
  fieldName?: string;
1325
1404
  unique?: boolean;
1405
+ database?: string;
1406
+ ref?: string;
1407
+ type?: string;
1326
1408
  }
1327
1409
  ): Promise<EnsureSignatureIndexResult> {
1328
1410
  this.ensureInitialized();
1329
1411
 
1330
1412
  const fieldName = options?.fieldName || '_sig';
1331
1413
  const unique = options?.unique !== false; // Default to true
1414
+ const database = options?.database;
1415
+ const ref = options?.ref;
1416
+ const type = options?.type;
1332
1417
 
1333
1418
  try {
1334
- const collection = this.db!.collection(collectionName);
1419
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1420
+ const collection = db.collection(collectionName);
1335
1421
  const indexes = await collection.indexes();
1336
1422
 
1337
1423
  // Find existing index on the signature field
@@ -1397,6 +1483,9 @@ export class SimpleMongoHelper {
1397
1483
  options?: {
1398
1484
  session?: ClientSession;
1399
1485
  ensureIndex?: boolean;
1486
+ database?: string;
1487
+ ref?: string;
1488
+ type?: string;
1400
1489
  }
1401
1490
  ): Promise<WriteByRefResult> {
1402
1491
  this.ensureInitialized();
@@ -1415,6 +1504,9 @@ export class SimpleMongoHelper {
1415
1504
  const mode = outputConfig.mode || this.config.output?.mode || 'append';
1416
1505
  const ensureIndex = options?.ensureIndex !== false; // Default to true
1417
1506
  const session = options?.session;
1507
+ const database = options?.database;
1508
+ const dbRef = options?.ref;
1509
+ const dbType = options?.type;
1418
1510
 
1419
1511
  const result: WriteByRefResult = {
1420
1512
  inserted: 0,
@@ -1424,7 +1516,8 @@ export class SimpleMongoHelper {
1424
1516
  };
1425
1517
 
1426
1518
  try {
1427
- const collection = this.db!.collection(collectionName);
1519
+ const db = this.getDatabase(this.resolveDatabase({ database, ref: dbRef, type: dbType }));
1520
+ const collection = db.collection(collectionName);
1428
1521
 
1429
1522
  // Handle replace mode: clear collection first
1430
1523
  if (mode === 'replace') {
@@ -1590,6 +1683,9 @@ export class SimpleMongoHelper {
1590
1683
  const writeResult = await this.writeByRef(ref, documents, {
1591
1684
  session: options?.session,
1592
1685
  ensureIndex: options?.ensureIndex,
1686
+ database: options?.database,
1687
+ ref: options?.ref,
1688
+ type: options?.type,
1593
1689
  });
1594
1690
 
1595
1691
  const result: WriteStageResult = {
@@ -1608,7 +1704,7 @@ export class SimpleMongoHelper {
1608
1704
  provider: options.complete.provider,
1609
1705
  metadata: options.complete.metadata,
1610
1706
  },
1611
- { session: options.session }
1707
+ { session: options.session, database: options?.database, ref: options?.ref, type: options?.type }
1612
1708
  );
1613
1709
  result.completed = true;
1614
1710
  } catch (error) {
@@ -1646,6 +1742,9 @@ export class SimpleMongoHelper {
1646
1742
  onUnmatched1 = 'include',
1647
1743
  onUnmatched2 = 'include',
1648
1744
  session,
1745
+ database,
1746
+ ref,
1747
+ type,
1649
1748
  } = options;
1650
1749
 
1651
1750
  // Determine join behavior from joinType or fallback to onUnmatched flags
@@ -1689,9 +1788,10 @@ export class SimpleMongoHelper {
1689
1788
  };
1690
1789
 
1691
1790
  try {
1692
- const coll1 = this.db!.collection(sourceCollection1);
1693
- const coll2 = this.db!.collection(sourceCollection2);
1694
- const targetColl = this.db!.collection(targetCollection);
1791
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1792
+ const coll1 = db.collection(sourceCollection1);
1793
+ const coll2 = db.collection(sourceCollection2);
1794
+ const targetColl = db.collection(targetCollection);
1695
1795
 
1696
1796
  const findOptions: any = {};
1697
1797
  if (session) {
@@ -1982,22 +2082,109 @@ export class SimpleMongoHelper {
1982
2082
  }
1983
2083
  }
1984
2084
 
2085
+ /**
2086
+ * Registers automatic cleanup handlers for process exit events.
2087
+ * This ensures connections are closed gracefully when the application terminates.
2088
+ * Uses a global registry to handle multiple instances without conflicts.
2089
+ * @internal
2090
+ */
2091
+ private registerCleanup(): void {
2092
+ if (this.cleanupRegistered) {
2093
+ return;
2094
+ }
2095
+ this.cleanupRegistered = true;
2096
+
2097
+ // Global registry for all SimpleMongoHelper instances
2098
+ const globalRegistry = (global as any).__nxMongoHelperInstances || new Set<SimpleMongoHelper>();
2099
+ (global as any).__nxMongoHelperInstances = globalRegistry;
2100
+ globalRegistry.add(this);
2101
+
2102
+ // Register global cleanup handlers only once
2103
+ if (!(global as any).__nxMongoCleanupRegistered) {
2104
+ (global as any).__nxMongoCleanupRegistered = true;
2105
+
2106
+ if (typeof process !== 'undefined') {
2107
+ const cleanupAll = async (signal?: string) => {
2108
+ const instances = (global as any).__nxMongoHelperInstances as Set<SimpleMongoHelper>;
2109
+ if (!instances || instances.size === 0) {
2110
+ return;
2111
+ }
2112
+
2113
+ // Cleanup all instances in parallel with timeout
2114
+ const cleanupPromises = Array.from(instances).map(async (instance: SimpleMongoHelper) => {
2115
+ try {
2116
+ // Use the public disconnect method, but with timeout protection
2117
+ const timeout = new Promise((_, reject) =>
2118
+ setTimeout(() => reject(new Error('Disconnect timeout')), 5000)
2119
+ );
2120
+
2121
+ await Promise.race([
2122
+ instance.disconnect(),
2123
+ timeout
2124
+ ]).catch(() => {
2125
+ // Ignore timeout errors during automatic cleanup
2126
+ });
2127
+ } catch (error) {
2128
+ // Silently ignore errors during automatic cleanup
2129
+ }
2130
+ });
2131
+
2132
+ await Promise.all(cleanupPromises).catch(() => {
2133
+ // Ignore errors during cleanup
2134
+ });
2135
+ };
2136
+
2137
+ // Graceful shutdown signals
2138
+ process.once('SIGINT', () => {
2139
+ cleanupAll('SIGINT').finally(() => {
2140
+ process.exit(0);
2141
+ });
2142
+ });
2143
+
2144
+ process.once('SIGTERM', () => {
2145
+ cleanupAll('SIGTERM').finally(() => {
2146
+ process.exit(0);
2147
+ });
2148
+ });
2149
+
2150
+ // Before exit (allows async cleanup)
2151
+ process.once('beforeExit', () => {
2152
+ cleanupAll('beforeExit');
2153
+ });
2154
+
2155
+ // Note: We don't handle uncaughtException/unhandledRejection here
2156
+ // as those should be handled by the application, not the library
2157
+ // The beforeExit handler will still clean up connections
2158
+ }
2159
+ }
2160
+ }
2161
+
1985
2162
  /**
1986
2163
  * Closes the MongoDB connection and cleans up resources.
1987
- * @throws Error if disconnect fails
2164
+ * This method is called automatically on process exit, but can also be called manually.
2165
+ * @throws Error if disconnect fails (only when called manually, not during automatic cleanup)
1988
2166
  */
1989
2167
  async disconnect(): Promise<void> {
2168
+ if (this.isDisconnecting) {
2169
+ return;
2170
+ }
2171
+
1990
2172
  if (!this.isInitialized || !this.client) {
1991
2173
  return;
1992
2174
  }
1993
2175
 
2176
+ this.isDisconnecting = true;
2177
+
1994
2178
  try {
1995
2179
  await this.client.close();
1996
2180
  this.client = null;
1997
2181
  this.db = null;
1998
2182
  this.isInitialized = false;
1999
2183
  } catch (error) {
2184
+ this.isDisconnecting = false;
2000
2185
  throw new Error(`Failed to disconnect: ${error instanceof Error ? error.message : String(error)}`);
2186
+ } finally {
2187
+ this.isDisconnecting = false;
2001
2188
  }
2002
2189
  }
2003
2190
 
@@ -2024,20 +2211,118 @@ export class SimpleMongoHelper {
2024
2211
  }
2025
2212
 
2026
2213
  /**
2027
- * Extracts database name from MongoDB connection string.
2028
- * @param connectionString - MongoDB connection string
2029
- * @returns Database name or 'test' as default
2214
+ * Strips database name from MongoDB connection string, returning base connection string.
2215
+ * @param connectionString - MongoDB connection string (may include database name)
2216
+ * @returns Base connection string without database name
2217
+ * @example
2218
+ * stripDatabaseFromConnectionString('mongodb://localhost:27017/admin')
2219
+ * // Returns: 'mongodb://localhost:27017/'
2030
2220
  */
2031
- private extractDatabaseName(connectionString: string): string {
2221
+ private stripDatabaseFromConnectionString(connectionString: string): string {
2032
2222
  try {
2033
2223
  const url = new URL(connectionString);
2034
- const dbName = url.pathname.slice(1); // Remove leading '/'
2035
- return dbName || 'test';
2224
+ // Remove pathname (database name) but keep trailing slash
2225
+ url.pathname = '/';
2226
+ return url.toString();
2036
2227
  } catch {
2037
- // If URL parsing fails, try to extract from connection string pattern
2038
- const match = connectionString.match(/\/([^\/\?]+)(\?|$)/);
2039
- return match ? match[1] : 'test';
2228
+ // If URL parsing fails, try regex pattern matching
2229
+ // Match: mongodb://host:port/database?options or mongodb://host:port/database
2230
+ const match = connectionString.match(/^([^\/]+\/\/[^\/]+)\/([^\/\?]+)(\?.*)?$/);
2231
+ if (match) {
2232
+ // Return base URL with trailing slash
2233
+ return match[1] + '/';
2234
+ }
2235
+ // If no database found, return as-is (should already be base URL)
2236
+ return connectionString.endsWith('/') ? connectionString : connectionString + '/';
2237
+ }
2238
+ }
2239
+
2240
+ /**
2241
+ * Resolves the database name from provided options using the databases config map.
2242
+ * Priority: database > ref+type > ref > type
2243
+ * @param options - Options containing database, ref, and/or type
2244
+ * @returns Resolved database name or undefined (will default to 'admin')
2245
+ * @throws Error if no match found or multiple matches found
2246
+ * @internal
2247
+ */
2248
+ private resolveDatabase(options?: { database?: string; ref?: string; type?: string }): string | undefined {
2249
+ // Priority 1: If database is provided directly, use it
2250
+ if (options?.database) {
2251
+ return options.database;
2252
+ }
2253
+
2254
+ // If no config or no databases map, return undefined (will default to 'admin')
2255
+ if (!this.config || !this.config.databases || this.config.databases.length === 0) {
2256
+ return undefined;
2257
+ }
2258
+
2259
+ const databases = this.config.databases;
2260
+
2261
+ // Priority 2: If both ref and type are provided, find exact match
2262
+ if (options?.ref && options?.type) {
2263
+ const matches = databases.filter(db => db.ref === options.ref && db.type === options.type);
2264
+ if (matches.length === 0) {
2265
+ throw new Error(`No database found for ref: ${options.ref} and type: ${options.type}`);
2266
+ }
2267
+ if (matches.length > 1) {
2268
+ throw new Error(`Multiple databases found for ref: ${options.ref} and type: ${options.type}`);
2269
+ }
2270
+ return matches[0].database;
2271
+ }
2272
+
2273
+ // Priority 3: If ref is provided, find by ref
2274
+ if (options?.ref) {
2275
+ const matches = databases.filter(db => db.ref === options.ref);
2276
+ if (matches.length === 0) {
2277
+ throw new Error(`No database found for ref: ${options.ref}`);
2278
+ }
2279
+ if (matches.length > 1) {
2280
+ throw new Error(`Multiple databases found for ref: ${options.ref}. Use 'type' parameter to narrow down.`);
2281
+ }
2282
+ return matches[0].database;
2040
2283
  }
2284
+
2285
+ // Priority 4: If type is provided, find by type
2286
+ if (options?.type) {
2287
+ const matches = databases.filter(db => db.type === options.type);
2288
+ if (matches.length === 0) {
2289
+ throw new Error(`No database found for type: ${options.type}`);
2290
+ }
2291
+ if (matches.length > 1) {
2292
+ throw new Error(`Multiple databases found for type: ${options.type}. Use 'ref' parameter to narrow down.`);
2293
+ }
2294
+ return matches[0].database;
2295
+ }
2296
+
2297
+ // No options provided, return undefined (will default to 'admin')
2298
+ return undefined;
2299
+ }
2300
+
2301
+ /**
2302
+ * Gets a database instance by name. Defaults to 'admin' if no name provided.
2303
+ * @param databaseName - Optional database name (defaults to 'admin')
2304
+ * @returns Database instance
2305
+ * @throws Error if client is not initialized
2306
+ * @internal
2307
+ */
2308
+ private getDatabase(databaseName?: string): Db {
2309
+ if (!this.client) {
2310
+ throw new Error('MongoDB client not initialized. Call initialize() first.');
2311
+ }
2312
+ return this.client.db(databaseName || 'admin');
2313
+ }
2314
+
2315
+ /**
2316
+ * Gets a database instance by name. Defaults to 'admin' if no name provided.
2317
+ * Public method for use by ProgressAPIImpl.
2318
+ * @param databaseName - Optional database name (defaults to 'admin')
2319
+ * @param ref - Optional ref for database resolution
2320
+ * @param type - Optional type for database resolution
2321
+ * @returns Database instance
2322
+ * @throws Error if client is not initialized
2323
+ */
2324
+ public getDatabaseByName(databaseName?: string, ref?: string, type?: string): Db {
2325
+ return this.getDatabase(this.resolveDatabase({ database: databaseName, ref, type }));
2041
2326
  }
2042
2327
  }
2043
2328