nx-mongo 3.8.1 → 3.8.3

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.
@@ -1,5 +1,182 @@
1
1
  import { MongoClient, Db, Collection, Filter, UpdateFilter, OptionalUnlessRequiredId, Document, WithId, ClientSession, Sort, IndexSpecification, CreateIndexesOptions } from 'mongodb';
2
2
  import { createHash } from 'crypto';
3
+ import { getEnvVariables, autoLoadConfig } from 'nx-config2';
4
+
5
+ /**
6
+ * Log levels ordered from lowest to highest severity
7
+ */
8
+ type LogLevel = 'verbose' | 'debug' | 'info' | 'warn' | 'error';
9
+
10
+ /**
11
+ * Log level numeric values for comparison (lower = more verbose)
12
+ */
13
+ const LOG_LEVEL_VALUES: Record<LogLevel, number> = {
14
+ verbose: 0,
15
+ debug: 1,
16
+ info: 2,
17
+ warn: 3,
18
+ error: 4,
19
+ };
20
+
21
+ /**
22
+ * Console logger with environment-based level control
23
+ */
24
+ class ConsoleLogger {
25
+ private globalMinLevel: LogLevel;
26
+ private debugNamespaces: Set<string> = new Set();
27
+ private debugPatterns: string[] = [];
28
+
29
+ constructor() {
30
+ // Auto-load config from .env files
31
+ autoLoadConfig();
32
+
33
+ // Get ENV_PREFIX from environment or use default
34
+ const envPrefix = process.env.ENV_PREFIX || '';
35
+
36
+ // Read LOGS_LEVEL from environment
37
+ // Priority: {ENV_PREFIX}_LOG_LEVEL > LOGS_LEVEL > default 'info'
38
+ const logLevelEnv = (envPrefix ? process.env[`${envPrefix}_LOG_LEVEL`] : null) ||
39
+ process.env.LOGS_LEVEL ||
40
+ 'info';
41
+
42
+ // Validate and set global minimum level
43
+ if (this.isValidLogLevel(logLevelEnv)) {
44
+ this.globalMinLevel = logLevelEnv as LogLevel;
45
+ } else {
46
+ this.globalMinLevel = 'info';
47
+ }
48
+
49
+ // Parse DEBUG environment variable for namespace matching
50
+ const debugEnv = process.env.DEBUG || '';
51
+ if (debugEnv) {
52
+ const patterns = debugEnv.split(',').map(p => p.trim());
53
+ for (const pattern of patterns) {
54
+ if (pattern === '*') {
55
+ // Wildcard matches everything
56
+ this.debugPatterns.push('*');
57
+ } else {
58
+ // Store pattern for matching
59
+ this.debugPatterns.push(pattern);
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Checks if a string is a valid log level
67
+ */
68
+ private isValidLogLevel(level: string): level is LogLevel {
69
+ return ['verbose', 'debug', 'info', 'warn', 'error'].includes(level);
70
+ }
71
+
72
+ /**
73
+ * Checks if a namespace matches any DEBUG pattern
74
+ */
75
+ private matchesDebugPattern(namespace: string): boolean {
76
+ if (this.debugPatterns.length === 0) {
77
+ return false;
78
+ }
79
+
80
+ for (const pattern of this.debugPatterns) {
81
+ if (pattern === '*') {
82
+ return true;
83
+ }
84
+
85
+ // Simple wildcard matching: "my-pkg*" matches "my-pkg", "my-pkg:sub", etc.
86
+ if (pattern.endsWith('*')) {
87
+ const prefix = pattern.slice(0, -1);
88
+ if (namespace.startsWith(prefix)) {
89
+ return true;
90
+ }
91
+ } else if (pattern === namespace) {
92
+ return true;
93
+ }
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ /**
100
+ * Determines if a log entry should be printed to console
101
+ */
102
+ private shouldPrint(level: LogLevel, namespace?: string): boolean {
103
+ const levelValue = LOG_LEVEL_VALUES[level];
104
+ const minLevelValue = LOG_LEVEL_VALUES[this.globalMinLevel];
105
+
106
+ // Rule 1: Print if level >= global minimum level
107
+ if (levelValue >= minLevelValue) {
108
+ return true;
109
+ }
110
+
111
+ // Rule 2: Print verbose/debug if namespace matches DEBUG pattern
112
+ if ((level === 'verbose' || level === 'debug') && namespace) {
113
+ if (this.matchesDebugPattern(namespace)) {
114
+ return true;
115
+ }
116
+ }
117
+
118
+ return false;
119
+ }
120
+
121
+ /**
122
+ * Logs a message to console
123
+ */
124
+ log(level: LogLevel, message: string, namespace?: string, throwError: boolean = false): void {
125
+ if (!this.shouldPrint(level, namespace)) {
126
+ return;
127
+ }
128
+
129
+ // Format message with namespace if provided
130
+ const formattedMessage = namespace ? `[${namespace}] ${message}` : message;
131
+
132
+ // Use appropriate console method
133
+ switch (level) {
134
+ case 'verbose':
135
+ case 'debug':
136
+ console.debug(formattedMessage);
137
+ break;
138
+ case 'info':
139
+ console.info(formattedMessage);
140
+ break;
141
+ case 'warn':
142
+ console.warn(formattedMessage);
143
+ break;
144
+ case 'error':
145
+ console.error(formattedMessage);
146
+ // Errors throw by default unless explicitly disabled
147
+ if (throwError !== false) {
148
+ throw new Error(formattedMessage);
149
+ }
150
+ break;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Convenience methods for each log level
156
+ */
157
+ verbose(message: string, namespace?: string): void {
158
+ this.log('verbose', message, namespace, false);
159
+ }
160
+
161
+ debug(message: string, namespace?: string): void {
162
+ this.log('debug', message, namespace, false);
163
+ }
164
+
165
+ info(message: string, namespace?: string): void {
166
+ this.log('info', message, namespace, false);
167
+ }
168
+
169
+ warn(message: string, namespace?: string): void {
170
+ this.log('warn', message, namespace, false);
171
+ }
172
+
173
+ error(message: string, namespace?: string, throwError: boolean = true): void {
174
+ this.log('error', message, namespace, throwError);
175
+ }
176
+ }
177
+
178
+ // Singleton logger instance
179
+ const logger = new ConsoleLogger();
3
180
 
4
181
  export interface PaginationOptions {
5
182
  page?: number;
@@ -351,6 +528,94 @@ export function computeSignature(
351
528
  return hash.digest('hex');
352
529
  }
353
530
 
531
+ /**
532
+ * Extracts database name from MongoDB connection string URI.
533
+ * Only extracts from pathname, NOT from query parameters.
534
+ * @param uri - MongoDB connection string
535
+ * @returns Database name or null if not found
536
+ */
537
+ function extractDatabaseNameFromUri(uri: string): string | null {
538
+ try {
539
+ const url = new URL(uri);
540
+ // Only extract from pathname, NOT from query parameters
541
+ const dbName = url.pathname ? url.pathname.slice(1) : null; // Remove leading /
542
+
543
+ // Validate: database name should not contain query parameter values
544
+ if (dbName && (dbName.includes('?') || dbName.includes('&') || dbName.includes('='))) {
545
+ // This means we're incorrectly parsing query params
546
+ return null; // Don't use invalid database name
547
+ }
548
+
549
+ return dbName || null;
550
+ } catch (error) {
551
+ // Fallback: regex approach
552
+ // Match: mongodb://.../databaseName?query
553
+ // Don't match: mongodb://...?authSource=admin (no path)
554
+ const pathMatch = uri.match(/mongodb:\/\/[^\/]+\/([^\/\?]+)(\?|$)/);
555
+ if (pathMatch && pathMatch[1]) {
556
+ return pathMatch[1];
557
+ }
558
+ return null;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Validates a MongoDB database name according to MongoDB rules.
564
+ * @param dbName - Database name to validate
565
+ * @returns true if valid, false otherwise
566
+ */
567
+ function isValidDatabaseName(dbName: string): boolean {
568
+ if (!dbName || typeof dbName !== 'string') {
569
+ return false;
570
+ }
571
+
572
+ // Database names cannot contain: / ? & =
573
+ // These are URL/query parameter characters
574
+ if (dbName.includes('/') || dbName.includes('?') || dbName.includes('&') || dbName.includes('=')) {
575
+ return false;
576
+ }
577
+
578
+ // Database names cannot be empty or just whitespace
579
+ if (dbName.trim().length === 0) {
580
+ return false;
581
+ }
582
+
583
+ // MongoDB database name rules
584
+ // Cannot contain: / \ . " $ * < > : | ?
585
+ const invalidChars = /[\/\\\.\"\$*<>:|?]/;
586
+ if (invalidChars.test(dbName)) {
587
+ return false;
588
+ }
589
+
590
+ // Cannot start or end with space or dot
591
+ if (dbName.trim() !== dbName || dbName.startsWith('.') || dbName.endsWith('.')) {
592
+ return false;
593
+ }
594
+
595
+ return true;
596
+ }
597
+
598
+ /**
599
+ * Cleans a connection string by removing database name from URI path.
600
+ * Preserves query parameters (authSource, etc.).
601
+ * @param uri - MongoDB connection string
602
+ * @returns Cleaned connection string without database name in path
603
+ */
604
+ function cleanConnectionString(uri: string): string {
605
+ // Remove database name from URI path
606
+ // Preserve query parameters (authSource, etc.)
607
+ try {
608
+ const url = new URL(uri);
609
+ url.pathname = ''; // Remove database name from path
610
+ return url.toString().replace(/\/\?/g, '?').replace(/\/$/, '');
611
+ } catch (error) {
612
+ // Fallback: regex approach
613
+ return uri.replace(/\/[^\/\?]+(\?|$)/, (match, query) => {
614
+ return query === '?' ? '?' : '';
615
+ }).replace(/\/$/, '');
616
+ }
617
+ }
618
+
354
619
  /**
355
620
  * Internal class that implements the ProgressAPI interface for stage tracking.
356
621
  */
@@ -642,6 +907,7 @@ class ProgressAPIImpl implements ProgressAPI {
642
907
 
643
908
  export class SimpleMongoHelper {
644
909
  private connectionString: string;
910
+ private originalConnectionString: string; // Store original for database name extraction
645
911
  private client: MongoClient | null = null;
646
912
  protected db: Db | null = null;
647
913
  private isInitialized: boolean = false;
@@ -650,9 +916,12 @@ export class SimpleMongoHelper {
650
916
  public readonly progress: ProgressAPI;
651
917
  private cleanupRegistered: boolean = false;
652
918
  private isDisconnecting: boolean = false;
919
+ private namespace: string = 'nx-mongo';
653
920
 
654
921
  constructor(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig) {
655
- // Strip database name from connection string if present
922
+ // Store original connection string for database name extraction
923
+ this.originalConnectionString = connectionString;
924
+ // Strip database name from connection string if present (for backward compatibility)
656
925
  this.connectionString = this.stripDatabaseFromConnectionString(connectionString);
657
926
  this.retryOptions = {
658
927
  maxRetries: retryOptions?.maxRetries ?? 3,
@@ -673,9 +942,43 @@ export class SimpleMongoHelper {
673
942
  */
674
943
  useConfig(config: HelperConfig): this {
675
944
  this.config = config;
945
+ logger.debug('Configuration updated', this.namespace);
946
+ return this;
947
+ }
948
+
949
+ /**
950
+ * Sets the logging namespace for this instance
951
+ * @param namespace - Namespace string for logging
952
+ * @returns This instance for method chaining
953
+ */
954
+ setNamespace(namespace: string): this {
955
+ this.namespace = namespace;
676
956
  return this;
677
957
  }
678
958
 
959
+ /**
960
+ * Logging methods - delegates to console logger
961
+ */
962
+ logVerbose(message: string): void {
963
+ logger.verbose(message, this.namespace);
964
+ }
965
+
966
+ logDebug(message: string): void {
967
+ logger.debug(message, this.namespace);
968
+ }
969
+
970
+ logInfo(message: string): void {
971
+ logger.info(message, this.namespace);
972
+ }
973
+
974
+ logWarn(message: string): void {
975
+ logger.warn(message, this.namespace);
976
+ }
977
+
978
+ logError(message: string, throwError: boolean = true): void {
979
+ logger.error(message, this.namespace, throwError);
980
+ }
981
+
679
982
  /**
680
983
  * Tests the MongoDB connection and returns detailed error information if it fails.
681
984
  * This method does not establish a persistent connection - use initialize() for that.
@@ -930,37 +1233,77 @@ export class SimpleMongoHelper {
930
1233
  /**
931
1234
  * Establishes MongoDB connection internally with retry logic.
932
1235
  * Must be called before using other methods.
1236
+ * @param options - Optional initialization options
1237
+ * @param options.connectionString - Connection string (overrides constructor value if provided)
1238
+ * @param options.databaseName - Database name (takes priority over URI extraction)
933
1239
  * @throws Error if connection fails or if already initialized
934
1240
  */
935
- async initialize(): Promise<void> {
1241
+ async initialize(options?: { connectionString?: string; databaseName?: string }): Promise<void> {
936
1242
  if (this.isInitialized) {
937
- throw new Error('SimpleMongoHelper is already initialized');
1243
+ this.logError('SimpleMongoHelper is already initialized');
938
1244
  }
939
1245
 
1246
+ logger.debug('Initializing MongoDB connection', this.namespace);
1247
+
1248
+ // Use provided connection string or original (not stripped) connection string
1249
+ const connectionString = options?.connectionString || this.originalConnectionString;
1250
+
1251
+ // ✅ Priority 1: Use provided databaseName parameter
1252
+ let dbName: string | undefined = options?.databaseName;
1253
+ const dbNameProvided = !!dbName;
1254
+
1255
+ // ✅ Priority 2: Only extract from URI if databaseName not provided
1256
+ if (!dbName) {
1257
+ const extractedDbName = extractDatabaseNameFromUri(connectionString);
1258
+ dbName = extractedDbName || undefined;
1259
+ }
1260
+
1261
+ // ✅ Validate database name
1262
+ if (dbName && !isValidDatabaseName(dbName)) {
1263
+ this.logError(`Invalid database name: '${dbName}'. Database names cannot contain: / ? & =`);
1264
+ }
1265
+
1266
+ if (!dbName) {
1267
+ this.logError('Database name is required. Provide databaseName parameter or include it in connection string path.');
1268
+ }
1269
+
1270
+ logger.debug(`Using database: ${dbName}`, this.namespace);
1271
+
1272
+ // ✅ Clean connection string if databaseName was provided separately
1273
+ // (to avoid using database name from URI path when we have explicit databaseName)
1274
+ const cleanUri = dbNameProvided ?
1275
+ cleanConnectionString(connectionString) :
1276
+ connectionString;
1277
+
940
1278
  let lastError: Error | null = null;
941
1279
  for (let attempt = 0; attempt <= this.retryOptions.maxRetries!; attempt++) {
942
1280
  try {
943
- this.client = new MongoClient(this.connectionString);
1281
+ logger.verbose(`Connection attempt ${attempt + 1}/${this.retryOptions.maxRetries! + 1}`, this.namespace);
1282
+ this.client = new MongoClient(cleanUri);
944
1283
  await this.client.connect();
945
- // Default to 'admin' database for initial connection (not used for operations)
946
- this.db = this.client.db('admin');
1284
+ // Use the resolved database name
1285
+ this.db = this.client.db(dbName);
947
1286
  this.isInitialized = true;
1287
+ logger.info(`Successfully connected to MongoDB database: ${dbName}`, this.namespace);
948
1288
  return;
949
1289
  } catch (error) {
950
1290
  lastError = error instanceof Error ? error : new Error(String(error));
951
1291
  this.client = null;
952
1292
  this.db = null;
953
1293
 
1294
+ logger.warn(`Connection attempt ${attempt + 1} failed: ${lastError.message}`, this.namespace);
1295
+
954
1296
  if (attempt < this.retryOptions.maxRetries!) {
955
1297
  const delay = this.retryOptions.exponentialBackoff
956
1298
  ? this.retryOptions.retryDelay! * Math.pow(2, attempt)
957
1299
  : this.retryOptions.retryDelay!;
1300
+ logger.debug(`Retrying in ${delay}ms...`, this.namespace);
958
1301
  await new Promise(resolve => setTimeout(resolve, delay));
959
1302
  }
960
1303
  }
961
1304
  }
962
1305
 
963
- throw new Error(`Failed to initialize MongoDB connection after ${this.retryOptions.maxRetries! + 1} attempts: ${lastError?.message}`);
1306
+ this.logError(`Failed to initialize MongoDB connection after ${this.retryOptions.maxRetries! + 1} attempts: ${lastError?.message}`);
964
1307
  }
965
1308
 
966
1309
  /**
@@ -1020,7 +1363,9 @@ export class SimpleMongoHelper {
1020
1363
  const results = await cursor.toArray();
1021
1364
  return results;
1022
1365
  } catch (error) {
1023
- throw new Error(`Failed to load collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
1366
+ const errorMsg = `Failed to load collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`;
1367
+ this.logError(errorMsg);
1368
+ throw new Error(errorMsg); // TypeScript control flow
1024
1369
  }
1025
1370
  }
1026
1371
 
@@ -1057,7 +1402,9 @@ export class SimpleMongoHelper {
1057
1402
  const result = await collection.findOne(query, findOptions);
1058
1403
  return result;
1059
1404
  } catch (error) {
1060
- throw new Error(`Failed to find document in collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
1405
+ const errorMsg = `Failed to find document in collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`;
1406
+ this.logError(errorMsg);
1407
+ throw new Error(errorMsg); // TypeScript control flow
1061
1408
  }
1062
1409
  }
1063
1410
 
@@ -1092,7 +1439,9 @@ export class SimpleMongoHelper {
1092
1439
  return result;
1093
1440
  }
1094
1441
  } catch (error) {
1095
- throw new Error(`Failed to insert into collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
1442
+ const errorMsg = `Failed to insert into collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`;
1443
+ this.logError(errorMsg);
1444
+ throw new Error(errorMsg); // TypeScript control flow
1096
1445
  }
1097
1446
  }
1098
1447
 
@@ -1131,7 +1480,9 @@ export class SimpleMongoHelper {
1131
1480
  return result;
1132
1481
  }
1133
1482
  } catch (error) {
1134
- throw new Error(`Failed to update collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
1483
+ const errorMsg = `Failed to update collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`;
1484
+ this.logError(errorMsg);
1485
+ throw new Error(errorMsg); // TypeScript control flow
1135
1486
  }
1136
1487
  }
1137
1488
 
@@ -1165,7 +1516,9 @@ export class SimpleMongoHelper {
1165
1516
  return result;
1166
1517
  }
1167
1518
  } catch (error) {
1168
- throw new Error(`Failed to delete from collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
1519
+ const errorMsg = `Failed to delete from collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`;
1520
+ this.logError(errorMsg);
1521
+ throw new Error(errorMsg); // TypeScript control flow
1169
1522
  }
1170
1523
  }
1171
1524
 
@@ -1,47 +0,0 @@
1
- import { SimpleMongoHelper } from './src/simpleMongoHelper';
2
-
3
- async function testConnection() {
4
- // Test with the connection string from the blocker report
5
- const connectionString = 'mongodb://localhost:27017/';
6
- const helper = new SimpleMongoHelper(connectionString);
7
-
8
- console.log('Testing MongoDB connection...');
9
- console.log(`Connection String: ${connectionString}`);
10
- console.log(`Database: server`);
11
- console.log('');
12
-
13
- const result = await helper.testConnection();
14
-
15
- if (result.success) {
16
- console.log('✅ Connection test passed!');
17
- console.log('You can now call helper.initialize() to establish a persistent connection.');
18
- } else {
19
- console.error('❌ Connection test failed!');
20
- console.error('');
21
- console.error('Error Type:', result.error?.type);
22
- console.error('Error Message:', result.error?.message);
23
- console.error('Error Details:', result.error?.details);
24
- console.error('');
25
-
26
- // Show full error object for debugging
27
- console.error('Full error object:');
28
- console.error(JSON.stringify(result.error, null, 2));
29
-
30
- // Provide troubleshooting tips
31
- console.error('');
32
- console.error('Troubleshooting tips:');
33
- if (result.error?.type === 'connection_failed') {
34
- console.error(' - Try using 127.0.0.1 instead of localhost');
35
- console.error(' - Verify MongoDB is running: mongosh --eval "db.version()"');
36
- console.error(' - Check if MongoDB is listening on port 27017');
37
- console.error(' - Check Windows Firewall settings');
38
- }
39
- }
40
- }
41
-
42
- // Run the test
43
- testConnection().catch((error) => {
44
- console.error('Unexpected error:', error);
45
- process.exit(1);
46
- });
47
-