nx-mongo 3.6.0 → 3.8.1

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: {
@@ -616,7 +652,8 @@ export class SimpleMongoHelper {
616
652
  private isDisconnecting: boolean = false;
617
653
 
618
654
  constructor(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig) {
619
- this.connectionString = connectionString;
655
+ // Strip database name from connection string if present
656
+ this.connectionString = this.stripDatabaseFromConnectionString(connectionString);
620
657
  this.retryOptions = {
621
658
  maxRetries: retryOptions?.maxRetries ?? 3,
622
659
  retryDelay: retryOptions?.retryDelay ?? 1000,
@@ -733,9 +770,8 @@ export class SimpleMongoHelper {
733
770
 
734
771
  await testClient.connect();
735
772
 
736
- // Try to ping the server
737
- const dbName = this.extractDatabaseName(this.connectionString);
738
- const testDb = testClient.db(dbName);
773
+ // Try to ping the server using 'admin' database (default)
774
+ const testDb = testClient.db('admin');
739
775
  await testDb.admin().ping();
740
776
 
741
777
  // Test basic operation (list collections)
@@ -769,81 +805,123 @@ export class SimpleMongoHelper {
769
805
  }
770
806
  }
771
807
 
772
- const errorMessage = error instanceof Error ? error.message : String(error);
773
- const errorString = String(error).toLowerCase();
808
+ // Extract error information more comprehensively
809
+ let errorMessage = 'Unknown error';
810
+ let errorName = 'Unknown';
811
+ let errorCode: string | number | undefined;
812
+
813
+ if (error instanceof Error) {
814
+ errorMessage = error.message || 'Unknown error';
815
+ errorName = error.name || 'Error';
816
+ // Check for MongoDB-specific error properties
817
+ const mongoError = error as any;
818
+ if (mongoError.code) {
819
+ errorCode = mongoError.code;
820
+ }
821
+ if (mongoError.codeName) {
822
+ errorName = mongoError.codeName;
823
+ }
824
+ if (mongoError.errmsg) {
825
+ errorMessage = mongoError.errmsg;
826
+ }
827
+ } else if (error && typeof error === 'object') {
828
+ // Try to extract meaningful information from error object
829
+ const errObj = error as any;
830
+ errorMessage = errObj.message || errObj.errmsg || errObj.error || JSON.stringify(error);
831
+ errorName = errObj.name || errObj.codeName || 'Error';
832
+ errorCode = errObj.code;
833
+ } else {
834
+ errorMessage = String(error);
835
+ }
836
+
837
+ const errorMessageLower = errorMessage.toLowerCase();
838
+
839
+ // Check for Windows-specific localhost resolution issues
840
+ const isLocalhost = this.connectionString.includes('localhost');
841
+ const windowsHint = isLocalhost && process.platform === 'win32'
842
+ ? ' On Windows, try using 127.0.0.1 instead of localhost to avoid IPv6 resolution issues.'
843
+ : '';
774
844
 
775
845
  // Analyze error type
776
- if (errorString.includes('authentication failed') ||
777
- errorString.includes('auth failed') ||
778
- errorString.includes('invalid credentials') ||
779
- errorMessage.includes('Authentication failed')) {
846
+ if (errorMessageLower.includes('authentication failed') ||
847
+ errorMessageLower.includes('auth failed') ||
848
+ errorMessageLower.includes('invalid credentials') ||
849
+ errorMessageLower.includes('authentication failed') ||
850
+ errorCode === 18 || // MongoDB error code for authentication failed
851
+ errorName === 'MongoAuthenticationError') {
780
852
  return {
781
853
  success: false,
782
854
  error: {
783
855
  type: 'authentication_failed',
784
856
  message: 'Authentication failed',
785
- details: `Invalid username or password. Error: ${errorMessage}. Verify credentials in connection string.`
857
+ details: `Invalid username or password. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}. Verify credentials in connection string.`
786
858
  }
787
859
  };
788
860
  }
789
861
 
790
- if (errorString.includes('timeout') ||
791
- errorMessage.includes('timeout') ||
792
- errorMessage.includes('ETIMEDOUT')) {
862
+ if (errorMessageLower.includes('timeout') ||
863
+ errorMessageLower.includes('etimedout') ||
864
+ errorCode === 'ETIMEDOUT' ||
865
+ errorName === 'MongoServerSelectionError') {
793
866
  return {
794
867
  success: false,
795
868
  error: {
796
869
  type: 'connection_failed',
797
870
  message: 'Connection timeout',
798
- details: `Cannot reach MongoDB server. Error: ${errorMessage}. Check if server is running, host/port is correct, and firewall allows connections.`
871
+ details: `Cannot reach MongoDB server. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}. Check if server is running, host/port is correct, and firewall allows connections.${windowsHint}`
799
872
  }
800
873
  };
801
874
  }
802
875
 
803
- if (errorString.includes('dns') ||
804
- errorMessage.includes('ENOTFOUND') ||
805
- errorMessage.includes('getaddrinfo')) {
876
+ if (errorMessageLower.includes('dns') ||
877
+ errorMessageLower.includes('enotfound') ||
878
+ errorMessageLower.includes('getaddrinfo') ||
879
+ errorCode === 'ENOTFOUND') {
806
880
  return {
807
881
  success: false,
808
882
  error: {
809
883
  type: 'connection_failed',
810
884
  message: 'DNS resolution failed',
811
- details: `Cannot resolve hostname. Error: ${errorMessage}. Check if hostname is correct and DNS is configured properly.`
885
+ details: `Cannot resolve hostname. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}. Check if hostname is correct and DNS is configured properly.${windowsHint}`
812
886
  }
813
887
  };
814
888
  }
815
889
 
816
- if (errorString.includes('refused') ||
817
- errorMessage.includes('ECONNREFUSED')) {
890
+ if (errorMessageLower.includes('refused') ||
891
+ errorMessageLower.includes('econnrefused') ||
892
+ errorCode === 'ECONNREFUSED') {
818
893
  return {
819
894
  success: false,
820
895
  error: {
821
896
  type: 'connection_failed',
822
897
  message: 'Connection refused',
823
- details: `MongoDB server refused connection. Error: ${errorMessage}. Check if MongoDB is running on the specified host and port.`
898
+ details: `MongoDB server refused connection. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}. Check if MongoDB is running on the specified host and port.${windowsHint}`
824
899
  }
825
900
  };
826
901
  }
827
902
 
828
- if (errorString.includes('network') ||
829
- errorMessage.includes('network')) {
903
+ if (errorMessageLower.includes('network') ||
904
+ errorMessageLower.includes('network') ||
905
+ errorName === 'MongoNetworkError') {
830
906
  return {
831
907
  success: false,
832
908
  error: {
833
909
  type: 'connection_failed',
834
910
  message: 'Network error',
835
- details: `Network connectivity issue. Error: ${errorMessage}. Check network connection and firewall settings.`
911
+ details: `Network connectivity issue. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}. Check network connection and firewall settings.${windowsHint}`
836
912
  }
837
913
  };
838
914
  }
839
915
 
840
- // Generic connection error
916
+ // Generic connection error with full details
917
+ const details = `Failed to connect to MongoDB. Error: ${errorMessage}${errorCode ? ` (Code: ${errorCode})` : ''}${errorName !== 'Error' ? ` (Type: ${errorName})` : ''}. Verify connection string, server status, and network connectivity.${windowsHint}`;
918
+
841
919
  return {
842
920
  success: false,
843
921
  error: {
844
922
  type: 'connection_failed',
845
923
  message: 'Connection failed',
846
- details: `Failed to connect to MongoDB. Error: ${errorMessage}. Verify connection string, server status, and network connectivity.`
924
+ details
847
925
  }
848
926
  };
849
927
  }
@@ -864,9 +942,8 @@ export class SimpleMongoHelper {
864
942
  try {
865
943
  this.client = new MongoClient(this.connectionString);
866
944
  await this.client.connect();
867
- // Extract database name from connection string or use default
868
- const dbName = this.extractDatabaseName(this.connectionString);
869
- this.db = this.client.db(dbName);
945
+ // Default to 'admin' database for initial connection (not used for operations)
946
+ this.db = this.client.db('admin');
870
947
  this.isInitialized = true;
871
948
  return;
872
949
  } catch (error) {
@@ -897,12 +974,16 @@ export class SimpleMongoHelper {
897
974
  async loadCollection<T extends Document = Document>(
898
975
  collectionName: string,
899
976
  query?: Filter<T>,
900
- options?: PaginationOptions
977
+ options?: PaginationOptions,
978
+ database?: string,
979
+ ref?: string,
980
+ type?: string
901
981
  ): Promise<WithId<T>[] | PaginatedResult<T>> {
902
982
  this.ensureInitialized();
903
983
 
904
984
  try {
905
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
985
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
986
+ const collection: Collection<T> = db.collection<T>(collectionName);
906
987
  const filter = query || {};
907
988
  let cursor = collection.find(filter);
908
989
 
@@ -954,12 +1035,16 @@ export class SimpleMongoHelper {
954
1035
  async findOne<T extends Document = Document>(
955
1036
  collectionName: string,
956
1037
  query: Filter<T>,
957
- options?: { sort?: Sort; projection?: Document }
1038
+ options?: { sort?: Sort; projection?: Document },
1039
+ database?: string,
1040
+ ref?: string,
1041
+ type?: string
958
1042
  ): Promise<WithId<T> | null> {
959
1043
  this.ensureInitialized();
960
1044
 
961
1045
  try {
962
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1046
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1047
+ const collection: Collection<T> = db.collection<T>(collectionName);
963
1048
  let findOptions: any = {};
964
1049
 
965
1050
  if (options?.sort) {
@@ -987,12 +1072,16 @@ export class SimpleMongoHelper {
987
1072
  async insert<T extends Document = Document>(
988
1073
  collectionName: string,
989
1074
  data: OptionalUnlessRequiredId<T> | OptionalUnlessRequiredId<T>[],
990
- options?: { session?: ClientSession }
1075
+ options?: { session?: ClientSession },
1076
+ database?: string,
1077
+ ref?: string,
1078
+ type?: string
991
1079
  ): Promise<any> {
992
1080
  this.ensureInitialized();
993
1081
 
994
1082
  try {
995
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1083
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1084
+ const collection: Collection<T> = db.collection<T>(collectionName);
996
1085
  const insertOptions = options?.session ? { session: options.session } : {};
997
1086
 
998
1087
  if (Array.isArray(data)) {
@@ -1020,12 +1109,16 @@ export class SimpleMongoHelper {
1020
1109
  collectionName: string,
1021
1110
  filter: Filter<T>,
1022
1111
  updateData: UpdateFilter<T>,
1023
- options?: { upsert?: boolean; multi?: boolean; session?: ClientSession }
1112
+ options?: { upsert?: boolean; multi?: boolean; session?: ClientSession },
1113
+ database?: string,
1114
+ ref?: string,
1115
+ type?: string
1024
1116
  ): Promise<any> {
1025
1117
  this.ensureInitialized();
1026
1118
 
1027
1119
  try {
1028
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1120
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1121
+ const collection: Collection<T> = db.collection<T>(collectionName);
1029
1122
  const updateOptions: any = {};
1030
1123
  if (options?.upsert !== undefined) updateOptions.upsert = options.upsert;
1031
1124
  if (options?.session) updateOptions.session = options.session;
@@ -1053,12 +1146,16 @@ export class SimpleMongoHelper {
1053
1146
  async delete<T extends Document = Document>(
1054
1147
  collectionName: string,
1055
1148
  filter: Filter<T>,
1056
- options?: { multi?: boolean }
1149
+ options?: { multi?: boolean },
1150
+ database?: string,
1151
+ ref?: string,
1152
+ type?: string
1057
1153
  ): Promise<any> {
1058
1154
  this.ensureInitialized();
1059
1155
 
1060
1156
  try {
1061
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1157
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1158
+ const collection: Collection<T> = db.collection<T>(collectionName);
1062
1159
 
1063
1160
  if (options?.multi) {
1064
1161
  const result = await collection.deleteMany(filter);
@@ -1081,12 +1178,16 @@ export class SimpleMongoHelper {
1081
1178
  */
1082
1179
  async countDocuments<T extends Document = Document>(
1083
1180
  collectionName: string,
1084
- query?: Filter<T>
1181
+ query?: Filter<T>,
1182
+ database?: string,
1183
+ ref?: string,
1184
+ type?: string
1085
1185
  ): Promise<number> {
1086
1186
  this.ensureInitialized();
1087
1187
 
1088
1188
  try {
1089
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1189
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1190
+ const collection: Collection<T> = db.collection<T>(collectionName);
1090
1191
  const count = await collection.countDocuments(query || {});
1091
1192
  return count;
1092
1193
  } catch (error) {
@@ -1100,11 +1201,12 @@ export class SimpleMongoHelper {
1100
1201
  * @returns Estimated number of documents
1101
1202
  * @throws Error if not initialized or if count fails
1102
1203
  */
1103
- async estimatedDocumentCount(collectionName: string): Promise<number> {
1204
+ async estimatedDocumentCount(collectionName: string, database?: string, ref?: string, type?: string): Promise<number> {
1104
1205
  this.ensureInitialized();
1105
1206
 
1106
1207
  try {
1107
- const collection = this.db!.collection(collectionName);
1208
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1209
+ const collection = db.collection(collectionName);
1108
1210
  const count = await collection.estimatedDocumentCount();
1109
1211
  return count;
1110
1212
  } catch (error) {
@@ -1121,12 +1223,16 @@ export class SimpleMongoHelper {
1121
1223
  */
1122
1224
  async aggregate<T extends Document = Document>(
1123
1225
  collectionName: string,
1124
- pipeline: Document[]
1226
+ pipeline: Document[],
1227
+ database?: string,
1228
+ ref?: string,
1229
+ type?: string
1125
1230
  ): Promise<T[]> {
1126
1231
  this.ensureInitialized();
1127
1232
 
1128
1233
  try {
1129
- const collection: Collection<T> = this.db!.collection<T>(collectionName);
1234
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1235
+ const collection: Collection<T> = db.collection<T>(collectionName);
1130
1236
  const results = await collection.aggregate<T>(pipeline).toArray();
1131
1237
  return results;
1132
1238
  } catch (error) {
@@ -1145,12 +1251,16 @@ export class SimpleMongoHelper {
1145
1251
  async createIndex(
1146
1252
  collectionName: string,
1147
1253
  indexSpec: IndexSpecification,
1148
- options?: CreateIndexesOptions
1254
+ options?: CreateIndexesOptions,
1255
+ database?: string,
1256
+ ref?: string,
1257
+ type?: string
1149
1258
  ): Promise<string> {
1150
1259
  this.ensureInitialized();
1151
1260
 
1152
1261
  try {
1153
- const collection = this.db!.collection(collectionName);
1262
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1263
+ const collection = db.collection(collectionName);
1154
1264
  const indexName = await collection.createIndex(indexSpec, options);
1155
1265
  return indexName;
1156
1266
  } catch (error) {
@@ -1165,11 +1275,12 @@ export class SimpleMongoHelper {
1165
1275
  * @returns Result object
1166
1276
  * @throws Error if not initialized or if index drop fails
1167
1277
  */
1168
- async dropIndex(collectionName: string, indexName: string): Promise<any> {
1278
+ async dropIndex(collectionName: string, indexName: string, database?: string, ref?: string, type?: string): Promise<any> {
1169
1279
  this.ensureInitialized();
1170
1280
 
1171
1281
  try {
1172
- const collection = this.db!.collection(collectionName);
1282
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1283
+ const collection = db.collection(collectionName);
1173
1284
  const result = await collection.dropIndex(indexName);
1174
1285
  return result;
1175
1286
  } catch (error) {
@@ -1183,11 +1294,12 @@ export class SimpleMongoHelper {
1183
1294
  * @returns Array of index information
1184
1295
  * @throws Error if not initialized or if listing fails
1185
1296
  */
1186
- async listIndexes(collectionName: string): Promise<Document[]> {
1297
+ async listIndexes(collectionName: string, database?: string, ref?: string, type?: string): Promise<Document[]> {
1187
1298
  this.ensureInitialized();
1188
1299
 
1189
1300
  try {
1190
- const collection = this.db!.collection(collectionName);
1301
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1302
+ const collection = db.collection(collectionName);
1191
1303
  const indexes = await collection.indexes();
1192
1304
  return indexes;
1193
1305
  } catch (error) {
@@ -1249,7 +1361,7 @@ export class SimpleMongoHelper {
1249
1361
  */
1250
1362
  async loadByRef<T extends Document = Document>(
1251
1363
  ref: string,
1252
- options?: PaginationOptions & { session?: ClientSession }
1364
+ options?: PaginationOptions & { session?: ClientSession; database?: string; ref?: string; type?: string }
1253
1365
  ): Promise<WithId<T>[] | PaginatedResult<T>> {
1254
1366
  this.ensureInitialized();
1255
1367
 
@@ -1262,9 +1374,13 @@ export class SimpleMongoHelper {
1262
1374
  throw new Error(`Ref '${ref}' not found in configuration inputs.`);
1263
1375
  }
1264
1376
 
1377
+ const database = options?.database;
1378
+ const dbRef = options?.ref;
1379
+ const dbType = options?.type;
1380
+ const db = this.getDatabase(this.resolveDatabase({ database, ref: dbRef, type: dbType }));
1265
1381
  // Note: loadCollection doesn't support session yet, but we'll pass it through options
1266
1382
  // For now, we'll use the collection directly with session support
1267
- const collection: Collection<T> = this.db!.collection<T>(inputConfig.collection);
1383
+ const collection: Collection<T> = db.collection<T>(inputConfig.collection);
1268
1384
  const filter = inputConfig.query || {};
1269
1385
  const session = options?.session;
1270
1386
 
@@ -1328,15 +1444,22 @@ export class SimpleMongoHelper {
1328
1444
  options?: {
1329
1445
  fieldName?: string;
1330
1446
  unique?: boolean;
1447
+ database?: string;
1448
+ ref?: string;
1449
+ type?: string;
1331
1450
  }
1332
1451
  ): Promise<EnsureSignatureIndexResult> {
1333
1452
  this.ensureInitialized();
1334
1453
 
1335
1454
  const fieldName = options?.fieldName || '_sig';
1336
1455
  const unique = options?.unique !== false; // Default to true
1456
+ const database = options?.database;
1457
+ const ref = options?.ref;
1458
+ const type = options?.type;
1337
1459
 
1338
1460
  try {
1339
- const collection = this.db!.collection(collectionName);
1461
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1462
+ const collection = db.collection(collectionName);
1340
1463
  const indexes = await collection.indexes();
1341
1464
 
1342
1465
  // Find existing index on the signature field
@@ -1402,6 +1525,9 @@ export class SimpleMongoHelper {
1402
1525
  options?: {
1403
1526
  session?: ClientSession;
1404
1527
  ensureIndex?: boolean;
1528
+ database?: string;
1529
+ ref?: string;
1530
+ type?: string;
1405
1531
  }
1406
1532
  ): Promise<WriteByRefResult> {
1407
1533
  this.ensureInitialized();
@@ -1420,6 +1546,9 @@ export class SimpleMongoHelper {
1420
1546
  const mode = outputConfig.mode || this.config.output?.mode || 'append';
1421
1547
  const ensureIndex = options?.ensureIndex !== false; // Default to true
1422
1548
  const session = options?.session;
1549
+ const database = options?.database;
1550
+ const dbRef = options?.ref;
1551
+ const dbType = options?.type;
1423
1552
 
1424
1553
  const result: WriteByRefResult = {
1425
1554
  inserted: 0,
@@ -1429,7 +1558,8 @@ export class SimpleMongoHelper {
1429
1558
  };
1430
1559
 
1431
1560
  try {
1432
- const collection = this.db!.collection(collectionName);
1561
+ const db = this.getDatabase(this.resolveDatabase({ database, ref: dbRef, type: dbType }));
1562
+ const collection = db.collection(collectionName);
1433
1563
 
1434
1564
  // Handle replace mode: clear collection first
1435
1565
  if (mode === 'replace') {
@@ -1595,6 +1725,9 @@ export class SimpleMongoHelper {
1595
1725
  const writeResult = await this.writeByRef(ref, documents, {
1596
1726
  session: options?.session,
1597
1727
  ensureIndex: options?.ensureIndex,
1728
+ database: options?.database,
1729
+ ref: options?.ref,
1730
+ type: options?.type,
1598
1731
  });
1599
1732
 
1600
1733
  const result: WriteStageResult = {
@@ -1613,7 +1746,7 @@ export class SimpleMongoHelper {
1613
1746
  provider: options.complete.provider,
1614
1747
  metadata: options.complete.metadata,
1615
1748
  },
1616
- { session: options.session }
1749
+ { session: options.session, database: options?.database, ref: options?.ref, type: options?.type }
1617
1750
  );
1618
1751
  result.completed = true;
1619
1752
  } catch (error) {
@@ -1651,6 +1784,9 @@ export class SimpleMongoHelper {
1651
1784
  onUnmatched1 = 'include',
1652
1785
  onUnmatched2 = 'include',
1653
1786
  session,
1787
+ database,
1788
+ ref,
1789
+ type,
1654
1790
  } = options;
1655
1791
 
1656
1792
  // Determine join behavior from joinType or fallback to onUnmatched flags
@@ -1694,9 +1830,10 @@ export class SimpleMongoHelper {
1694
1830
  };
1695
1831
 
1696
1832
  try {
1697
- const coll1 = this.db!.collection(sourceCollection1);
1698
- const coll2 = this.db!.collection(sourceCollection2);
1699
- const targetColl = this.db!.collection(targetCollection);
1833
+ const db = this.getDatabase(this.resolveDatabase({ database, ref, type }));
1834
+ const coll1 = db.collection(sourceCollection1);
1835
+ const coll2 = db.collection(sourceCollection2);
1836
+ const targetColl = db.collection(targetCollection);
1700
1837
 
1701
1838
  const findOptions: any = {};
1702
1839
  if (session) {
@@ -2116,20 +2253,118 @@ export class SimpleMongoHelper {
2116
2253
  }
2117
2254
 
2118
2255
  /**
2119
- * Extracts database name from MongoDB connection string.
2120
- * @param connectionString - MongoDB connection string
2121
- * @returns Database name or 'test' as default
2256
+ * Strips database name from MongoDB connection string, returning base connection string.
2257
+ * @param connectionString - MongoDB connection string (may include database name)
2258
+ * @returns Base connection string without database name
2259
+ * @example
2260
+ * stripDatabaseFromConnectionString('mongodb://localhost:27017/admin')
2261
+ * // Returns: 'mongodb://localhost:27017/'
2122
2262
  */
2123
- private extractDatabaseName(connectionString: string): string {
2263
+ private stripDatabaseFromConnectionString(connectionString: string): string {
2124
2264
  try {
2125
2265
  const url = new URL(connectionString);
2126
- const dbName = url.pathname.slice(1); // Remove leading '/'
2127
- return dbName || 'test';
2266
+ // Remove pathname (database name) but keep trailing slash
2267
+ url.pathname = '/';
2268
+ return url.toString();
2128
2269
  } catch {
2129
- // If URL parsing fails, try to extract from connection string pattern
2130
- const match = connectionString.match(/\/([^\/\?]+)(\?|$)/);
2131
- return match ? match[1] : 'test';
2270
+ // If URL parsing fails, try regex pattern matching
2271
+ // Match: mongodb://host:port/database?options or mongodb://host:port/database
2272
+ const match = connectionString.match(/^([^\/]+\/\/[^\/]+)\/([^\/\?]+)(\?.*)?$/);
2273
+ if (match) {
2274
+ // Return base URL with trailing slash
2275
+ return match[1] + '/';
2276
+ }
2277
+ // If no database found, return as-is (should already be base URL)
2278
+ return connectionString.endsWith('/') ? connectionString : connectionString + '/';
2132
2279
  }
2133
2280
  }
2281
+
2282
+ /**
2283
+ * Resolves the database name from provided options using the databases config map.
2284
+ * Priority: database > ref+type > ref > type
2285
+ * @param options - Options containing database, ref, and/or type
2286
+ * @returns Resolved database name or undefined (will default to 'admin')
2287
+ * @throws Error if no match found or multiple matches found
2288
+ * @internal
2289
+ */
2290
+ private resolveDatabase(options?: { database?: string; ref?: string; type?: string }): string | undefined {
2291
+ // Priority 1: If database is provided directly, use it
2292
+ if (options?.database) {
2293
+ return options.database;
2294
+ }
2295
+
2296
+ // If no config or no databases map, return undefined (will default to 'admin')
2297
+ if (!this.config || !this.config.databases || this.config.databases.length === 0) {
2298
+ return undefined;
2299
+ }
2300
+
2301
+ const databases = this.config.databases;
2302
+
2303
+ // Priority 2: If both ref and type are provided, find exact match
2304
+ if (options?.ref && options?.type) {
2305
+ const matches = databases.filter(db => db.ref === options.ref && db.type === options.type);
2306
+ if (matches.length === 0) {
2307
+ throw new Error(`No database found for ref: ${options.ref} and type: ${options.type}`);
2308
+ }
2309
+ if (matches.length > 1) {
2310
+ throw new Error(`Multiple databases found for ref: ${options.ref} and type: ${options.type}`);
2311
+ }
2312
+ return matches[0].database;
2313
+ }
2314
+
2315
+ // Priority 3: If ref is provided, find by ref
2316
+ if (options?.ref) {
2317
+ const matches = databases.filter(db => db.ref === options.ref);
2318
+ if (matches.length === 0) {
2319
+ throw new Error(`No database found for ref: ${options.ref}`);
2320
+ }
2321
+ if (matches.length > 1) {
2322
+ throw new Error(`Multiple databases found for ref: ${options.ref}. Use 'type' parameter to narrow down.`);
2323
+ }
2324
+ return matches[0].database;
2325
+ }
2326
+
2327
+ // Priority 4: If type is provided, find by type
2328
+ if (options?.type) {
2329
+ const matches = databases.filter(db => db.type === options.type);
2330
+ if (matches.length === 0) {
2331
+ throw new Error(`No database found for type: ${options.type}`);
2332
+ }
2333
+ if (matches.length > 1) {
2334
+ throw new Error(`Multiple databases found for type: ${options.type}. Use 'ref' parameter to narrow down.`);
2335
+ }
2336
+ return matches[0].database;
2337
+ }
2338
+
2339
+ // No options provided, return undefined (will default to 'admin')
2340
+ return undefined;
2341
+ }
2342
+
2343
+ /**
2344
+ * Gets a database instance by name. Defaults to 'admin' if no name provided.
2345
+ * @param databaseName - Optional database name (defaults to 'admin')
2346
+ * @returns Database instance
2347
+ * @throws Error if client is not initialized
2348
+ * @internal
2349
+ */
2350
+ private getDatabase(databaseName?: string): Db {
2351
+ if (!this.client) {
2352
+ throw new Error('MongoDB client not initialized. Call initialize() first.');
2353
+ }
2354
+ return this.client.db(databaseName || 'admin');
2355
+ }
2356
+
2357
+ /**
2358
+ * Gets a database instance by name. Defaults to 'admin' if no name provided.
2359
+ * Public method for use by ProgressAPIImpl.
2360
+ * @param databaseName - Optional database name (defaults to 'admin')
2361
+ * @param ref - Optional ref for database resolution
2362
+ * @param type - Optional type for database resolution
2363
+ * @returns Database instance
2364
+ * @throws Error if client is not initialized
2365
+ */
2366
+ public getDatabaseByName(databaseName?: string, ref?: string, type?: string): Db {
2367
+ return this.getDatabase(this.resolveDatabase({ database: databaseName, ref, type }));
2368
+ }
2134
2369
  }
2135
2370