nx-mongo 3.3.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.
@@ -0,0 +1,1333 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimpleMongoHelper = void 0;
4
+ exports.getByDotPath = getByDotPath;
5
+ exports.computeSignature = computeSignature;
6
+ const mongodb_1 = require("mongodb");
7
+ const crypto_1 = require("crypto");
8
+ /**
9
+ * Extracts values from an object using dot-notation paths with array wildcard support.
10
+ * Supports nested object paths (dot notation) and array wildcards ([]) to collect values from all array elements.
11
+ * @param value - The object to extract values from
12
+ * @param path - Dot-notation path (e.g., "meta.id", "edges[].from", "segments[]")
13
+ * @returns Array of extracted values (flattened and deduplicated for arrays)
14
+ * @example
15
+ * getByDotPath({ segments: [1, 2, 3] }, "segments[]") // [1, 2, 3]
16
+ * getByDotPath({ edges: [{ from: "A" }, { from: "B" }] }, "edges[].from") // ["A", "B"]
17
+ * getByDotPath({ meta: { id: "123" } }, "meta.id") // ["123"]
18
+ */
19
+ function getByDotPath(value, path) {
20
+ if (value == null) {
21
+ return [];
22
+ }
23
+ // Handle array wildcard at the end (e.g., "segments[]", "edges[]")
24
+ if (path.endsWith('[]')) {
25
+ const basePath = path.slice(0, -2);
26
+ const arrayValue = getByDotPath(value, basePath);
27
+ if (!Array.isArray(arrayValue)) {
28
+ return [];
29
+ }
30
+ // Flatten nested arrays recursively
31
+ const flatten = (arr) => {
32
+ const result = [];
33
+ for (const item of arr) {
34
+ if (Array.isArray(item)) {
35
+ result.push(...flatten(item));
36
+ }
37
+ else {
38
+ result.push(item);
39
+ }
40
+ }
41
+ return result;
42
+ };
43
+ return flatten(arrayValue);
44
+ }
45
+ // Handle array wildcard in the middle (e.g., "edges[].from", "items[].meta.id")
46
+ const arrayWildcardMatch = path.match(/^(.+?)\[\]\.(.+)$/);
47
+ if (arrayWildcardMatch) {
48
+ const [, arrayPath, remainingPath] = arrayWildcardMatch;
49
+ const arrayValue = getByDotPath(value, arrayPath);
50
+ if (!Array.isArray(arrayValue)) {
51
+ return [];
52
+ }
53
+ // Collect values from all array elements
54
+ const results = [];
55
+ for (const item of arrayValue) {
56
+ const nestedValues = getByDotPath(item, remainingPath);
57
+ results.push(...nestedValues);
58
+ }
59
+ // Flatten nested arrays recursively
60
+ const flatten = (arr) => {
61
+ const result = [];
62
+ for (const item of arr) {
63
+ if (Array.isArray(item)) {
64
+ result.push(...flatten(item));
65
+ }
66
+ else {
67
+ result.push(item);
68
+ }
69
+ }
70
+ return result;
71
+ };
72
+ return flatten(results);
73
+ }
74
+ // Handle simple dot-notation path (e.g., "meta.id")
75
+ const parts = path.split('.');
76
+ let current = value;
77
+ for (const part of parts) {
78
+ if (current == null) {
79
+ return [];
80
+ }
81
+ if (Array.isArray(current)) {
82
+ // If we hit an array in the middle of the path, collect from all elements
83
+ const results = [];
84
+ for (const item of current) {
85
+ const nestedValue = getByDotPath(item, parts.slice(parts.indexOf(part)).join('.'));
86
+ results.push(...nestedValue);
87
+ }
88
+ return results;
89
+ }
90
+ current = current[part];
91
+ }
92
+ return current != null ? [current] : [];
93
+ }
94
+ /**
95
+ * Normalizes a value to a string representation for signature computation.
96
+ * @param value - The value to normalize
97
+ * @returns Normalized string representation
98
+ */
99
+ function normalizeValue(value) {
100
+ if (value === null || value === undefined) {
101
+ return 'null';
102
+ }
103
+ if (typeof value === 'string') {
104
+ return value;
105
+ }
106
+ if (typeof value === 'number') {
107
+ return String(value);
108
+ }
109
+ if (typeof value === 'boolean') {
110
+ return String(value);
111
+ }
112
+ if (value instanceof Date) {
113
+ return value.toISOString();
114
+ }
115
+ if (Array.isArray(value)) {
116
+ // Flatten, normalize, deduplicate, and sort
117
+ const flatten = (arr) => {
118
+ const result = [];
119
+ for (const item of arr) {
120
+ if (Array.isArray(item)) {
121
+ result.push(...flatten(item));
122
+ }
123
+ else {
124
+ result.push(item);
125
+ }
126
+ }
127
+ return result;
128
+ };
129
+ const flattened = flatten(value);
130
+ const normalized = flattened.map(normalizeValue);
131
+ const unique = Array.from(new Set(normalized));
132
+ const sorted = unique.sort();
133
+ return JSON.stringify(sorted);
134
+ }
135
+ if (typeof value === 'object') {
136
+ // Sort keys for consistent stringification
137
+ const sortedKeys = Object.keys(value).sort();
138
+ const sortedObj = {};
139
+ for (const key of sortedKeys) {
140
+ sortedObj[key] = value[key];
141
+ }
142
+ return JSON.stringify(sortedObj);
143
+ }
144
+ return String(value);
145
+ }
146
+ /**
147
+ * Computes a deterministic signature for a document based on specified keys.
148
+ * The signature is a SHA-256 hash of a canonical representation of the key values.
149
+ * @param doc - The document to compute signature for
150
+ * @param keys - Array of dot-notation paths to extract values from
151
+ * @param options - Optional configuration
152
+ * @param options.algorithm - Hash algorithm to use (default: "sha256")
153
+ * @returns Hex string signature
154
+ * @example
155
+ * computeSignature({ segments: [1, 2], role: "admin" }, ["segments[]", "role"])
156
+ */
157
+ function computeSignature(doc, keys, options) {
158
+ const algorithm = options?.algorithm || 'sha256';
159
+ // Extract values for each key
160
+ const canonicalMap = {};
161
+ for (const key of keys) {
162
+ const values = getByDotPath(doc, key);
163
+ // Normalize and deduplicate values
164
+ const normalized = values.map(normalizeValue);
165
+ const unique = Array.from(new Set(normalized));
166
+ const sorted = unique.sort();
167
+ canonicalMap[key] = sorted;
168
+ }
169
+ // Sort keys alphabetically
170
+ const sortedKeys = Object.keys(canonicalMap).sort();
171
+ const sortedMap = {};
172
+ for (const key of sortedKeys) {
173
+ sortedMap[key] = canonicalMap[key];
174
+ }
175
+ // Stringify and hash
176
+ const jsonString = JSON.stringify(sortedMap);
177
+ const hash = (0, crypto_1.createHash)(algorithm);
178
+ hash.update(jsonString);
179
+ return hash.digest('hex');
180
+ }
181
+ /**
182
+ * Internal class that implements the ProgressAPI interface for stage tracking.
183
+ */
184
+ class ProgressAPIImpl {
185
+ constructor(helper, progressConfig) {
186
+ this.indexEnsured = false;
187
+ this.helper = helper;
188
+ this.config = {
189
+ collection: progressConfig?.collection || 'progress_states',
190
+ uniqueIndexKeys: progressConfig?.uniqueIndexKeys || ['process', 'provider', 'key'],
191
+ defaultProvider: progressConfig?.provider,
192
+ };
193
+ }
194
+ /**
195
+ * Ensures the progress collection and unique index exist.
196
+ * Called lazily on first use.
197
+ */
198
+ async ensureProgressIndex(session) {
199
+ if (this.indexEnsured) {
200
+ return;
201
+ }
202
+ this.helper.ensureInitialized();
203
+ try {
204
+ const collection = this.helper.getDb().collection(this.config.collection);
205
+ const indexes = await collection.indexes();
206
+ // Build index spec from uniqueIndexKeys
207
+ const indexSpec = {};
208
+ for (const key of this.config.uniqueIndexKeys) {
209
+ indexSpec[key] = 1;
210
+ }
211
+ // Check if index already exists
212
+ const existingIndex = indexes.find(idx => {
213
+ const keys = idx.key;
214
+ const indexKeys = Object.keys(keys).sort();
215
+ const expectedKeys = Object.keys(indexSpec).sort();
216
+ return JSON.stringify(indexKeys) === JSON.stringify(expectedKeys);
217
+ });
218
+ if (!existingIndex) {
219
+ const indexOptions = { unique: true };
220
+ if (session) {
221
+ indexOptions.session = session;
222
+ }
223
+ await collection.createIndex(indexSpec, indexOptions);
224
+ }
225
+ this.indexEnsured = true;
226
+ }
227
+ catch (error) {
228
+ const errorMessage = error instanceof Error ? error.message : String(error);
229
+ if (errorMessage.includes('not authorized') || errorMessage.includes('unauthorized')) {
230
+ throw new Error(`Failed to create progress index on collection '${this.config.collection}': ${errorMessage}. ` +
231
+ `This operation requires 'createIndex' privilege.`);
232
+ }
233
+ throw new Error(`Failed to ensure progress index on collection '${this.config.collection}': ${errorMessage}`);
234
+ }
235
+ }
236
+ /**
237
+ * Resolves the provider value, using options provider, then default provider, then undefined.
238
+ */
239
+ resolveProvider(options) {
240
+ return options?.provider ?? this.config.defaultProvider;
241
+ }
242
+ /**
243
+ * Builds a filter for finding a stage record.
244
+ */
245
+ buildFilter(key, process, provider) {
246
+ const filter = { key };
247
+ if (process !== undefined) {
248
+ filter.process = process;
249
+ }
250
+ if (provider !== undefined) {
251
+ filter.provider = provider;
252
+ }
253
+ return filter;
254
+ }
255
+ async isCompleted(key, options) {
256
+ await this.ensureProgressIndex(options?.session);
257
+ this.helper.ensureInitialized();
258
+ const provider = this.resolveProvider(options);
259
+ const process = options?.process;
260
+ const filter = this.buildFilter(key, process, provider);
261
+ const collection = this.helper.getDb().collection(this.config.collection);
262
+ const findOptions = {};
263
+ if (options?.session) {
264
+ findOptions.session = options.session;
265
+ }
266
+ const record = await collection.findOne(filter, findOptions);
267
+ return record?.completed === true;
268
+ }
269
+ async start(identity, options) {
270
+ await this.ensureProgressIndex(options?.session);
271
+ this.helper.ensureInitialized();
272
+ const provider = this.resolveProvider({ provider: identity.provider });
273
+ const process = identity.process;
274
+ const filter = this.buildFilter(identity.key, process, provider);
275
+ const collection = this.helper.getDb().collection(this.config.collection);
276
+ const update = {
277
+ $set: {
278
+ key: identity.key,
279
+ name: identity.name,
280
+ completed: false,
281
+ },
282
+ $setOnInsert: {
283
+ startedAt: new Date(),
284
+ },
285
+ };
286
+ if (process !== undefined) {
287
+ update.$set.process = process;
288
+ }
289
+ if (provider !== undefined) {
290
+ update.$set.provider = provider;
291
+ }
292
+ const updateOptions = { upsert: true };
293
+ if (options?.session) {
294
+ updateOptions.session = options.session;
295
+ }
296
+ await collection.updateOne(filter, update, updateOptions);
297
+ }
298
+ async complete(identity, options) {
299
+ await this.ensureProgressIndex(options?.session);
300
+ this.helper.ensureInitialized();
301
+ const provider = this.resolveProvider({ provider: identity.provider });
302
+ const process = identity.process;
303
+ const filter = this.buildFilter(identity.key, process, provider);
304
+ const collection = this.helper.getDb().collection(this.config.collection);
305
+ const update = {
306
+ $set: {
307
+ key: identity.key,
308
+ completed: true,
309
+ completedAt: new Date(),
310
+ },
311
+ };
312
+ if (identity.name !== undefined) {
313
+ update.$set.name = identity.name;
314
+ }
315
+ if (process !== undefined) {
316
+ update.$set.process = process;
317
+ }
318
+ if (provider !== undefined) {
319
+ update.$set.provider = provider;
320
+ }
321
+ if (identity.metadata) {
322
+ // Merge metadata
323
+ update.$set.metadata = identity.metadata;
324
+ }
325
+ const updateOptions = { upsert: true };
326
+ if (options?.session) {
327
+ updateOptions.session = options.session;
328
+ }
329
+ await collection.updateOne(filter, update, updateOptions);
330
+ }
331
+ async getCompleted(options) {
332
+ await this.ensureProgressIndex(options?.session);
333
+ this.helper.ensureInitialized();
334
+ const provider = this.resolveProvider(options);
335
+ const process = options?.process;
336
+ const filter = { completed: true };
337
+ if (process !== undefined) {
338
+ filter.process = process;
339
+ }
340
+ if (provider !== undefined) {
341
+ filter.provider = provider;
342
+ }
343
+ const collection = this.helper.getDb().collection(this.config.collection);
344
+ const findOptions = { projection: { key: 1, name: 1, completedAt: 1 } };
345
+ if (options?.session) {
346
+ findOptions.session = options.session;
347
+ }
348
+ const records = await collection.find(filter, findOptions).toArray();
349
+ return records.map(r => ({
350
+ key: r.key,
351
+ name: r.name,
352
+ completedAt: r.completedAt,
353
+ }));
354
+ }
355
+ async getProgress(options) {
356
+ await this.ensureProgressIndex(options?.session);
357
+ this.helper.ensureInitialized();
358
+ const provider = this.resolveProvider(options);
359
+ const process = options?.process;
360
+ const filter = {};
361
+ if (process !== undefined) {
362
+ filter.process = process;
363
+ }
364
+ if (provider !== undefined) {
365
+ filter.provider = provider;
366
+ }
367
+ const collection = this.helper.getDb().collection(this.config.collection);
368
+ const findOptions = {};
369
+ if (options?.session) {
370
+ findOptions.session = options.session;
371
+ }
372
+ return await collection.find(filter, findOptions).toArray();
373
+ }
374
+ async reset(key, options) {
375
+ await this.ensureProgressIndex(options?.session);
376
+ this.helper.ensureInitialized();
377
+ const provider = this.resolveProvider(options);
378
+ const process = options?.process;
379
+ const filter = this.buildFilter(key, process, provider);
380
+ const collection = this.helper.getDb().collection(this.config.collection);
381
+ const update = {
382
+ $set: {
383
+ completed: false,
384
+ },
385
+ $unset: {
386
+ completedAt: '',
387
+ metadata: '',
388
+ },
389
+ };
390
+ const updateOptions = {};
391
+ if (options?.session) {
392
+ updateOptions.session = options.session;
393
+ }
394
+ await collection.updateOne(filter, update, updateOptions);
395
+ }
396
+ }
397
+ class SimpleMongoHelper {
398
+ constructor(connectionString, retryOptions, config) {
399
+ this.client = null;
400
+ this.db = null;
401
+ this.isInitialized = false;
402
+ this.config = null;
403
+ this.connectionString = connectionString;
404
+ this.retryOptions = {
405
+ maxRetries: retryOptions?.maxRetries ?? 3,
406
+ retryDelay: retryOptions?.retryDelay ?? 1000,
407
+ exponentialBackoff: retryOptions?.exponentialBackoff ?? true,
408
+ };
409
+ this.config = config || null;
410
+ this.progress = new ProgressAPIImpl(this, config?.progress);
411
+ }
412
+ /**
413
+ * Sets or updates the configuration for ref-based operations.
414
+ * @param config - Configuration object with inputs and outputs
415
+ * @returns This instance for method chaining
416
+ */
417
+ useConfig(config) {
418
+ this.config = config;
419
+ return this;
420
+ }
421
+ /**
422
+ * Tests the MongoDB connection and returns detailed error information if it fails.
423
+ * This method does not establish a persistent connection - use initialize() for that.
424
+ * @returns Object with success status and optional error details
425
+ */
426
+ async testConnection() {
427
+ // Check connection string
428
+ if (!this.connectionString || this.connectionString.trim() === '') {
429
+ return {
430
+ success: false,
431
+ error: {
432
+ type: 'invalid_connection_string',
433
+ message: 'Connection string is missing or empty',
434
+ details: 'Provide a valid MongoDB connection string in the format: mongodb://[username:password@]host[:port][/database][?options]'
435
+ }
436
+ };
437
+ }
438
+ // Validate connection string format
439
+ try {
440
+ const url = new URL(this.connectionString);
441
+ if (url.protocol !== 'mongodb:' && url.protocol !== 'mongodb+srv:') {
442
+ return {
443
+ success: false,
444
+ error: {
445
+ type: 'invalid_connection_string',
446
+ message: 'Invalid connection string protocol',
447
+ details: `Expected protocol 'mongodb://' or 'mongodb+srv://', got '${url.protocol}'`
448
+ }
449
+ };
450
+ }
451
+ }
452
+ catch (urlError) {
453
+ return {
454
+ success: false,
455
+ error: {
456
+ type: 'invalid_connection_string',
457
+ message: 'Connection string is not a valid URL',
458
+ details: `Failed to parse connection string: ${urlError instanceof Error ? urlError.message : String(urlError)}`
459
+ }
460
+ };
461
+ }
462
+ // Check for credentials in connection string
463
+ const hasCredentials = this.connectionString.includes('@') &&
464
+ (this.connectionString.match(/:\/\/[^:]+:[^@]+@/) ||
465
+ this.connectionString.match(/:\/\/[^@]+@/));
466
+ // Try to extract username/password from connection string
467
+ const authMatch = this.connectionString.match(/:\/\/([^:]+):([^@]+)@/);
468
+ if (authMatch) {
469
+ const username = authMatch[1];
470
+ const password = authMatch[2];
471
+ if (!username || username.trim() === '') {
472
+ return {
473
+ success: false,
474
+ error: {
475
+ type: 'missing_credentials',
476
+ message: 'Username is missing in connection string',
477
+ details: 'Connection string contains @ but username is empty. Format: mongodb://username:password@host:port/database'
478
+ }
479
+ };
480
+ }
481
+ if (!password || password.trim() === '') {
482
+ return {
483
+ success: false,
484
+ error: {
485
+ type: 'missing_credentials',
486
+ message: 'Password is missing in connection string',
487
+ details: 'Connection string contains @ but password is empty. Format: mongodb://username:password@host:port/database'
488
+ }
489
+ };
490
+ }
491
+ }
492
+ else if (this.connectionString.includes('@')) {
493
+ // Has @ but no password format - might be using connection string options
494
+ // This is okay, continue to connection test
495
+ }
496
+ // Test actual connection
497
+ let testClient = null;
498
+ try {
499
+ testClient = new mongodb_1.MongoClient(this.connectionString, {
500
+ serverSelectionTimeoutMS: 5000, // 5 second timeout for test
501
+ connectTimeoutMS: 5000
502
+ });
503
+ await testClient.connect();
504
+ // Try to ping the server
505
+ const dbName = this.extractDatabaseName(this.connectionString);
506
+ const testDb = testClient.db(dbName);
507
+ await testDb.admin().ping();
508
+ // Test basic operation (list collections)
509
+ try {
510
+ await testDb.listCollections().toArray();
511
+ }
512
+ catch (listError) {
513
+ // If listCollections fails, might be permission issue but connection works
514
+ const errorMsg = listError instanceof Error ? listError.message : String(listError);
515
+ if (errorMsg.includes('not authorized') || errorMsg.includes('unauthorized')) {
516
+ await testClient.close();
517
+ return {
518
+ success: false,
519
+ error: {
520
+ type: 'authentication_failed',
521
+ message: 'Connection successful but insufficient permissions',
522
+ details: `Connected to MongoDB but cannot list collections. Error: ${errorMsg}. Check user permissions.`
523
+ }
524
+ };
525
+ }
526
+ }
527
+ await testClient.close();
528
+ return { success: true };
529
+ }
530
+ catch (error) {
531
+ if (testClient) {
532
+ try {
533
+ await testClient.close();
534
+ }
535
+ catch {
536
+ // Ignore close errors
537
+ }
538
+ }
539
+ const errorMessage = error instanceof Error ? error.message : String(error);
540
+ const errorString = String(error).toLowerCase();
541
+ // Analyze error type
542
+ if (errorString.includes('authentication failed') ||
543
+ errorString.includes('auth failed') ||
544
+ errorString.includes('invalid credentials') ||
545
+ errorMessage.includes('Authentication failed')) {
546
+ return {
547
+ success: false,
548
+ error: {
549
+ type: 'authentication_failed',
550
+ message: 'Authentication failed',
551
+ details: `Invalid username or password. Error: ${errorMessage}. Verify credentials in connection string.`
552
+ }
553
+ };
554
+ }
555
+ if (errorString.includes('timeout') ||
556
+ errorMessage.includes('timeout') ||
557
+ errorMessage.includes('ETIMEDOUT')) {
558
+ return {
559
+ success: false,
560
+ error: {
561
+ type: 'connection_failed',
562
+ message: 'Connection timeout',
563
+ details: `Cannot reach MongoDB server. Error: ${errorMessage}. Check if server is running, host/port is correct, and firewall allows connections.`
564
+ }
565
+ };
566
+ }
567
+ if (errorString.includes('dns') ||
568
+ errorMessage.includes('ENOTFOUND') ||
569
+ errorMessage.includes('getaddrinfo')) {
570
+ return {
571
+ success: false,
572
+ error: {
573
+ type: 'connection_failed',
574
+ message: 'DNS resolution failed',
575
+ details: `Cannot resolve hostname. Error: ${errorMessage}. Check if hostname is correct and DNS is configured properly.`
576
+ }
577
+ };
578
+ }
579
+ if (errorString.includes('refused') ||
580
+ errorMessage.includes('ECONNREFUSED')) {
581
+ return {
582
+ success: false,
583
+ error: {
584
+ type: 'connection_failed',
585
+ message: 'Connection refused',
586
+ details: `MongoDB server refused connection. Error: ${errorMessage}. Check if MongoDB is running on the specified host and port.`
587
+ }
588
+ };
589
+ }
590
+ if (errorString.includes('network') ||
591
+ errorMessage.includes('network')) {
592
+ return {
593
+ success: false,
594
+ error: {
595
+ type: 'connection_failed',
596
+ message: 'Network error',
597
+ details: `Network connectivity issue. Error: ${errorMessage}. Check network connection and firewall settings.`
598
+ }
599
+ };
600
+ }
601
+ // Generic connection error
602
+ return {
603
+ success: false,
604
+ error: {
605
+ type: 'connection_failed',
606
+ message: 'Connection failed',
607
+ details: `Failed to connect to MongoDB. Error: ${errorMessage}. Verify connection string, server status, and network connectivity.`
608
+ }
609
+ };
610
+ }
611
+ }
612
+ /**
613
+ * Establishes MongoDB connection internally with retry logic.
614
+ * Must be called before using other methods.
615
+ * @throws Error if connection fails or if already initialized
616
+ */
617
+ async initialize() {
618
+ if (this.isInitialized) {
619
+ throw new Error('SimpleMongoHelper is already initialized');
620
+ }
621
+ let lastError = null;
622
+ for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
623
+ try {
624
+ this.client = new mongodb_1.MongoClient(this.connectionString);
625
+ await this.client.connect();
626
+ // Extract database name from connection string or use default
627
+ const dbName = this.extractDatabaseName(this.connectionString);
628
+ this.db = this.client.db(dbName);
629
+ this.isInitialized = true;
630
+ return;
631
+ }
632
+ catch (error) {
633
+ lastError = error instanceof Error ? error : new Error(String(error));
634
+ this.client = null;
635
+ this.db = null;
636
+ if (attempt < this.retryOptions.maxRetries) {
637
+ const delay = this.retryOptions.exponentialBackoff
638
+ ? this.retryOptions.retryDelay * Math.pow(2, attempt)
639
+ : this.retryOptions.retryDelay;
640
+ await new Promise(resolve => setTimeout(resolve, delay));
641
+ }
642
+ }
643
+ }
644
+ throw new Error(`Failed to initialize MongoDB connection after ${this.retryOptions.maxRetries + 1} attempts: ${lastError?.message}`);
645
+ }
646
+ /**
647
+ * Loads data from a collection with optional query filter.
648
+ * @param collectionName - Name of the collection to query
649
+ * @param query - Optional MongoDB query filter
650
+ * @param options - Optional pagination and sorting options
651
+ * @returns Array of documents matching the query or paginated result
652
+ * @throws Error if not initialized or if query fails
653
+ */
654
+ async loadCollection(collectionName, query, options) {
655
+ this.ensureInitialized();
656
+ try {
657
+ const collection = this.db.collection(collectionName);
658
+ const filter = query || {};
659
+ let cursor = collection.find(filter);
660
+ // Apply sorting if provided
661
+ if (options?.sort) {
662
+ cursor = cursor.sort(options.sort);
663
+ }
664
+ // Apply pagination if provided
665
+ if (options?.page && options?.limit) {
666
+ const page = Math.max(1, options.page);
667
+ const limit = Math.max(1, options.limit);
668
+ const skip = (page - 1) * limit;
669
+ cursor = cursor.skip(skip).limit(limit);
670
+ // Get total count for pagination
671
+ const total = await collection.countDocuments(filter);
672
+ const results = await cursor.toArray();
673
+ const totalPages = Math.ceil(total / limit);
674
+ return {
675
+ data: results,
676
+ total,
677
+ page,
678
+ limit,
679
+ totalPages,
680
+ hasNext: page < totalPages,
681
+ hasPrev: page > 1,
682
+ };
683
+ }
684
+ // Return all results if no pagination
685
+ const results = await cursor.toArray();
686
+ return results;
687
+ }
688
+ catch (error) {
689
+ throw new Error(`Failed to load collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
690
+ }
691
+ }
692
+ /**
693
+ * Finds a single document in a collection.
694
+ * @param collectionName - Name of the collection to query
695
+ * @param query - MongoDB query filter
696
+ * @param options - Optional find options (e.g., sort, projection)
697
+ * @returns Single document matching the query or null
698
+ * @throws Error if not initialized or if query fails
699
+ */
700
+ async findOne(collectionName, query, options) {
701
+ this.ensureInitialized();
702
+ try {
703
+ const collection = this.db.collection(collectionName);
704
+ let findOptions = {};
705
+ if (options?.sort) {
706
+ findOptions.sort = options.sort;
707
+ }
708
+ if (options?.projection) {
709
+ findOptions.projection = options.projection;
710
+ }
711
+ const result = await collection.findOne(query, findOptions);
712
+ return result;
713
+ }
714
+ catch (error) {
715
+ throw new Error(`Failed to find document in collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
716
+ }
717
+ }
718
+ /**
719
+ * Inserts data into a collection.
720
+ * @param collectionName - Name of the collection to insert into
721
+ * @param data - Single document or array of documents to insert
722
+ * @param options - Optional insert options (e.g., session for transactions)
723
+ * @returns Insert result(s)
724
+ * @throws Error if not initialized or if insert fails
725
+ */
726
+ async insert(collectionName, data, options) {
727
+ this.ensureInitialized();
728
+ try {
729
+ const collection = this.db.collection(collectionName);
730
+ const insertOptions = options?.session ? { session: options.session } : {};
731
+ if (Array.isArray(data)) {
732
+ const result = await collection.insertMany(data, insertOptions);
733
+ return result;
734
+ }
735
+ else {
736
+ const result = await collection.insertOne(data, insertOptions);
737
+ return result;
738
+ }
739
+ }
740
+ catch (error) {
741
+ throw new Error(`Failed to insert into collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
742
+ }
743
+ }
744
+ /**
745
+ * Updates records in a collection.
746
+ * @param collectionName - Name of the collection to update
747
+ * @param filter - MongoDB query filter to match documents
748
+ * @param updateData - Update operations to apply
749
+ * @param options - Optional update options (e.g., upsert, multi, session for transactions)
750
+ * @returns Update result
751
+ * @throws Error if not initialized or if update fails
752
+ */
753
+ async update(collectionName, filter, updateData, options) {
754
+ this.ensureInitialized();
755
+ try {
756
+ const collection = this.db.collection(collectionName);
757
+ const updateOptions = {};
758
+ if (options?.upsert !== undefined)
759
+ updateOptions.upsert = options.upsert;
760
+ if (options?.session)
761
+ updateOptions.session = options.session;
762
+ if (options?.multi) {
763
+ const result = await collection.updateMany(filter, updateData, updateOptions);
764
+ return result;
765
+ }
766
+ else {
767
+ const result = await collection.updateOne(filter, updateData, updateOptions);
768
+ return result;
769
+ }
770
+ }
771
+ catch (error) {
772
+ throw new Error(`Failed to update collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
773
+ }
774
+ }
775
+ /**
776
+ * Deletes documents from a collection.
777
+ * @param collectionName - Name of the collection to delete from
778
+ * @param filter - MongoDB query filter to match documents
779
+ * @param options - Optional delete options (multi: true for deleteMany, false for deleteOne)
780
+ * @returns Delete result
781
+ * @throws Error if not initialized or if delete fails
782
+ */
783
+ async delete(collectionName, filter, options) {
784
+ this.ensureInitialized();
785
+ try {
786
+ const collection = this.db.collection(collectionName);
787
+ if (options?.multi) {
788
+ const result = await collection.deleteMany(filter);
789
+ return result;
790
+ }
791
+ else {
792
+ const result = await collection.deleteOne(filter);
793
+ return result;
794
+ }
795
+ }
796
+ catch (error) {
797
+ throw new Error(`Failed to delete from collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
798
+ }
799
+ }
800
+ /**
801
+ * Counts documents in a collection matching a query.
802
+ * @param collectionName - Name of the collection to count
803
+ * @param query - Optional MongoDB query filter
804
+ * @returns Number of documents matching the query
805
+ * @throws Error if not initialized or if count fails
806
+ */
807
+ async countDocuments(collectionName, query) {
808
+ this.ensureInitialized();
809
+ try {
810
+ const collection = this.db.collection(collectionName);
811
+ const count = await collection.countDocuments(query || {});
812
+ return count;
813
+ }
814
+ catch (error) {
815
+ throw new Error(`Failed to count documents in collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
816
+ }
817
+ }
818
+ /**
819
+ * Gets an estimated document count for a collection (faster but less accurate).
820
+ * @param collectionName - Name of the collection to count
821
+ * @returns Estimated number of documents
822
+ * @throws Error if not initialized or if count fails
823
+ */
824
+ async estimatedDocumentCount(collectionName) {
825
+ this.ensureInitialized();
826
+ try {
827
+ const collection = this.db.collection(collectionName);
828
+ const count = await collection.estimatedDocumentCount();
829
+ return count;
830
+ }
831
+ catch (error) {
832
+ throw new Error(`Failed to estimate document count in collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
833
+ }
834
+ }
835
+ /**
836
+ * Runs an aggregation pipeline on a collection.
837
+ * @param collectionName - Name of the collection to aggregate
838
+ * @param pipeline - Array of aggregation pipeline stages
839
+ * @returns Array of aggregated results
840
+ * @throws Error if not initialized or if aggregation fails
841
+ */
842
+ async aggregate(collectionName, pipeline) {
843
+ this.ensureInitialized();
844
+ try {
845
+ const collection = this.db.collection(collectionName);
846
+ const results = await collection.aggregate(pipeline).toArray();
847
+ return results;
848
+ }
849
+ catch (error) {
850
+ throw new Error(`Failed to aggregate collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
851
+ }
852
+ }
853
+ /**
854
+ * Creates an index on a collection.
855
+ * @param collectionName - Name of the collection
856
+ * @param indexSpec - Index specification (field name or index spec object)
857
+ * @param options - Optional index creation options
858
+ * @returns Index name
859
+ * @throws Error if not initialized or if index creation fails
860
+ */
861
+ async createIndex(collectionName, indexSpec, options) {
862
+ this.ensureInitialized();
863
+ try {
864
+ const collection = this.db.collection(collectionName);
865
+ const indexName = await collection.createIndex(indexSpec, options);
866
+ return indexName;
867
+ }
868
+ catch (error) {
869
+ throw new Error(`Failed to create index on collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
870
+ }
871
+ }
872
+ /**
873
+ * Drops an index from a collection.
874
+ * @param collectionName - Name of the collection
875
+ * @param indexName - Name of the index to drop
876
+ * @returns Result object
877
+ * @throws Error if not initialized or if index drop fails
878
+ */
879
+ async dropIndex(collectionName, indexName) {
880
+ this.ensureInitialized();
881
+ try {
882
+ const collection = this.db.collection(collectionName);
883
+ const result = await collection.dropIndex(indexName);
884
+ return result;
885
+ }
886
+ catch (error) {
887
+ throw new Error(`Failed to drop index '${indexName}' from collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
888
+ }
889
+ }
890
+ /**
891
+ * Lists all indexes on a collection.
892
+ * @param collectionName - Name of the collection
893
+ * @returns Array of index information
894
+ * @throws Error if not initialized or if listing fails
895
+ */
896
+ async listIndexes(collectionName) {
897
+ this.ensureInitialized();
898
+ try {
899
+ const collection = this.db.collection(collectionName);
900
+ const indexes = await collection.indexes();
901
+ return indexes;
902
+ }
903
+ catch (error) {
904
+ throw new Error(`Failed to list indexes for collection '${collectionName}': ${error instanceof Error ? error.message : String(error)}`);
905
+ }
906
+ }
907
+ /**
908
+ * Starts a new client session for transactions.
909
+ * @returns Client session
910
+ * @throws Error if not initialized
911
+ */
912
+ startSession() {
913
+ this.ensureInitialized();
914
+ if (!this.client) {
915
+ throw new Error('MongoDB client is not available');
916
+ }
917
+ return this.client.startSession();
918
+ }
919
+ /**
920
+ * Executes a function within a transaction.
921
+ * @param callback - Function to execute within the transaction
922
+ * @returns Result of the callback function
923
+ * @throws Error if not initialized or if transaction fails
924
+ */
925
+ async withTransaction(callback) {
926
+ this.ensureInitialized();
927
+ if (!this.client) {
928
+ throw new Error('MongoDB client is not available');
929
+ }
930
+ const session = this.client.startSession();
931
+ try {
932
+ let result;
933
+ await session.withTransaction(async () => {
934
+ result = await callback(session);
935
+ });
936
+ return result;
937
+ }
938
+ catch (error) {
939
+ throw new Error(`Transaction failed: ${error instanceof Error ? error.message : String(error)}`);
940
+ }
941
+ finally {
942
+ await session.endSession();
943
+ }
944
+ }
945
+ /**
946
+ * Loads data from a collection using a ref name from the configuration.
947
+ * @param ref - Application-level reference name (must exist in config.inputs)
948
+ * @param options - Optional pagination and session options
949
+ * @returns Array of documents or paginated result
950
+ * @throws Error if ref not found in config, not initialized, or query fails
951
+ */
952
+ async loadByRef(ref, options) {
953
+ this.ensureInitialized();
954
+ if (!this.config) {
955
+ throw new Error('Configuration is required for loadByRef. Set config using constructor or useConfig().');
956
+ }
957
+ const inputConfig = this.config.inputs.find(input => input.ref === ref);
958
+ if (!inputConfig) {
959
+ throw new Error(`Ref '${ref}' not found in configuration inputs.`);
960
+ }
961
+ // Note: loadCollection doesn't support session yet, but we'll pass it through options
962
+ // For now, we'll use the collection directly with session support
963
+ const collection = this.db.collection(inputConfig.collection);
964
+ const filter = inputConfig.query || {};
965
+ const session = options?.session;
966
+ try {
967
+ const findOptions = {};
968
+ if (session) {
969
+ findOptions.session = session;
970
+ }
971
+ let cursor = collection.find(filter, findOptions);
972
+ // Apply sorting if provided
973
+ if (options?.sort) {
974
+ cursor = cursor.sort(options.sort);
975
+ }
976
+ // Apply pagination if provided
977
+ if (options?.page && options?.limit) {
978
+ const page = Math.max(1, options.page);
979
+ const limit = Math.max(1, options.limit);
980
+ const skip = (page - 1) * limit;
981
+ cursor = cursor.skip(skip).limit(limit);
982
+ // Get total count for pagination
983
+ const countOptions = session ? { session } : {};
984
+ const total = await collection.countDocuments(filter, countOptions);
985
+ const results = await cursor.toArray();
986
+ const totalPages = Math.ceil(total / limit);
987
+ return {
988
+ data: results,
989
+ total,
990
+ page,
991
+ limit,
992
+ totalPages,
993
+ hasNext: page < totalPages,
994
+ hasPrev: page > 1,
995
+ };
996
+ }
997
+ // Return all results if no pagination
998
+ const results = await cursor.toArray();
999
+ return results;
1000
+ }
1001
+ catch (error) {
1002
+ throw new Error(`Failed to load by ref '${ref}': ${error instanceof Error ? error.message : String(error)}`);
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Ensures a unique index exists on the _sig field for signature-based deduplication.
1007
+ * @param collectionName - Name of the collection
1008
+ * @param options - Optional configuration
1009
+ * @param options.fieldName - Field name for signature (default: "_sig")
1010
+ * @param options.unique - Whether index should be unique (default: true)
1011
+ * @returns Result object with creation status and index name
1012
+ * @throws Error if not initialized or if index creation fails (with permission hints)
1013
+ */
1014
+ async ensureSignatureIndex(collectionName, options) {
1015
+ this.ensureInitialized();
1016
+ const fieldName = options?.fieldName || '_sig';
1017
+ const unique = options?.unique !== false; // Default to true
1018
+ try {
1019
+ const collection = this.db.collection(collectionName);
1020
+ const indexes = await collection.indexes();
1021
+ // Find existing index on the signature field
1022
+ const existingIndex = indexes.find(idx => {
1023
+ const keys = idx.key;
1024
+ return keys[fieldName] !== undefined;
1025
+ });
1026
+ // Check if existing index has correct options
1027
+ if (existingIndex && existingIndex.name) {
1028
+ const isUnique = existingIndex.unique === unique;
1029
+ if (isUnique) {
1030
+ // Index exists with correct options
1031
+ return {
1032
+ created: false,
1033
+ indexName: existingIndex.name,
1034
+ };
1035
+ }
1036
+ else {
1037
+ // Index exists but with wrong options - drop and recreate
1038
+ try {
1039
+ await collection.dropIndex(existingIndex.name);
1040
+ }
1041
+ catch (dropError) {
1042
+ // Ignore drop errors (index might not exist)
1043
+ }
1044
+ }
1045
+ }
1046
+ // Create the index
1047
+ const indexSpec = { [fieldName]: 1 };
1048
+ const indexOptions = { unique };
1049
+ const indexName = await collection.createIndex(indexSpec, indexOptions);
1050
+ return {
1051
+ created: true,
1052
+ indexName,
1053
+ };
1054
+ }
1055
+ catch (error) {
1056
+ const errorMessage = error instanceof Error ? error.message : String(error);
1057
+ if (errorMessage.includes('not authorized') || errorMessage.includes('unauthorized')) {
1058
+ throw new Error(`Failed to create signature index on collection '${collectionName}': ${errorMessage}. ` +
1059
+ `This operation requires 'createIndex' privilege.`);
1060
+ }
1061
+ throw new Error(`Failed to ensure signature index on collection '${collectionName}': ${errorMessage}`);
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Writes documents to a collection using a ref name from the configuration.
1066
+ * Supports signature-based deduplication and append/replace modes.
1067
+ * @param ref - Application-level reference name (must exist in config.outputs)
1068
+ * @param documents - Array of documents to write
1069
+ * @param options - Optional write options
1070
+ * @param options.session - Transaction session
1071
+ * @param options.ensureIndex - Whether to ensure signature index exists (default: true)
1072
+ * @returns Result object with counts and errors
1073
+ * @throws Error if ref not found in config or not initialized
1074
+ */
1075
+ async writeByRef(ref, documents, options) {
1076
+ this.ensureInitialized();
1077
+ if (!this.config) {
1078
+ throw new Error('Configuration is required for writeByRef. Set config using constructor or useConfig().');
1079
+ }
1080
+ const outputConfig = this.config.outputs.find(output => output.ref === ref);
1081
+ if (!outputConfig) {
1082
+ throw new Error(`Ref '${ref}' not found in configuration outputs.`);
1083
+ }
1084
+ const collectionName = outputConfig.collection;
1085
+ const keys = outputConfig.keys;
1086
+ const mode = outputConfig.mode || this.config.output?.mode || 'append';
1087
+ const ensureIndex = options?.ensureIndex !== false; // Default to true
1088
+ const session = options?.session;
1089
+ const result = {
1090
+ inserted: 0,
1091
+ updated: 0,
1092
+ errors: [],
1093
+ indexCreated: false,
1094
+ };
1095
+ try {
1096
+ const collection = this.db.collection(collectionName);
1097
+ // Handle replace mode: clear collection first
1098
+ if (mode === 'replace') {
1099
+ const deleteOptions = session ? { session } : {};
1100
+ await collection.deleteMany({}, deleteOptions);
1101
+ }
1102
+ // If no keys specified, use regular insertMany
1103
+ if (!keys || keys.length === 0) {
1104
+ try {
1105
+ const insertOptions = session ? { session, ordered: false } : { ordered: false };
1106
+ const insertResult = await collection.insertMany(documents, insertOptions);
1107
+ result.inserted = insertResult.insertedCount;
1108
+ }
1109
+ catch (error) {
1110
+ // Aggregate errors from insertMany
1111
+ if (error && typeof error === 'object' && 'writeErrors' in error) {
1112
+ const writeErrors = error.writeErrors || [];
1113
+ for (const writeError of writeErrors) {
1114
+ result.errors.push({
1115
+ index: writeError.index,
1116
+ error: new Error(writeError.errmsg || String(writeError.err)),
1117
+ doc: documents[writeError.index],
1118
+ });
1119
+ }
1120
+ result.inserted = error.insertedCount || 0;
1121
+ }
1122
+ else {
1123
+ // Single error
1124
+ result.errors.push({
1125
+ index: 0,
1126
+ error: error instanceof Error ? error : new Error(String(error)),
1127
+ });
1128
+ }
1129
+ }
1130
+ return result;
1131
+ }
1132
+ // Ensure signature index exists
1133
+ if (ensureIndex) {
1134
+ try {
1135
+ const indexResult = await this.ensureSignatureIndex(collectionName, {
1136
+ fieldName: '_sig',
1137
+ unique: true,
1138
+ });
1139
+ result.indexCreated = indexResult.created;
1140
+ }
1141
+ catch (indexError) {
1142
+ result.errors.push({
1143
+ index: -1,
1144
+ error: indexError instanceof Error ? indexError : new Error(String(indexError)),
1145
+ });
1146
+ // Continue even if index creation fails
1147
+ }
1148
+ }
1149
+ // Compute signatures and prepare bulk operations
1150
+ const bulkOps = [];
1151
+ const docWithSigs = [];
1152
+ for (const doc of documents) {
1153
+ try {
1154
+ const sig = computeSignature(doc, keys);
1155
+ docWithSigs.push({ doc, sig });
1156
+ // Add _sig field to document
1157
+ const docWithSig = { ...doc, _sig: sig };
1158
+ bulkOps.push({
1159
+ updateOne: {
1160
+ filter: { _sig: sig },
1161
+ update: { $set: docWithSig },
1162
+ upsert: true,
1163
+ },
1164
+ });
1165
+ }
1166
+ catch (error) {
1167
+ result.errors.push({
1168
+ index: docWithSigs.length,
1169
+ error: error instanceof Error ? error : new Error(String(error)),
1170
+ doc,
1171
+ });
1172
+ }
1173
+ }
1174
+ // Process in batches of 1000
1175
+ const batchSize = 1000;
1176
+ for (let i = 0; i < bulkOps.length; i += batchSize) {
1177
+ const batch = bulkOps.slice(i, i + batchSize);
1178
+ try {
1179
+ const bulkOptions = { ordered: false };
1180
+ if (session) {
1181
+ bulkOptions.session = session;
1182
+ }
1183
+ const bulkResult = await collection.bulkWrite(batch, bulkOptions);
1184
+ result.inserted += bulkResult.upsertedCount || 0;
1185
+ result.updated += bulkResult.modifiedCount || 0;
1186
+ // Handle write errors
1187
+ if (bulkResult.hasWriteErrors()) {
1188
+ const writeErrors = bulkResult.getWriteErrors();
1189
+ for (const writeError of writeErrors) {
1190
+ const originalIndex = i + writeError.index;
1191
+ result.errors.push({
1192
+ index: originalIndex,
1193
+ error: new Error(writeError.errmsg || String(writeError.err)),
1194
+ doc: documents[originalIndex],
1195
+ });
1196
+ }
1197
+ }
1198
+ }
1199
+ catch (error) {
1200
+ // Aggregate batch errors
1201
+ if (error && typeof error === 'object' && 'writeErrors' in error) {
1202
+ const writeErrors = error.writeErrors || [];
1203
+ for (const writeError of writeErrors) {
1204
+ const originalIndex = i + writeError.index;
1205
+ result.errors.push({
1206
+ index: originalIndex,
1207
+ error: new Error(writeError.errmsg || String(writeError.err)),
1208
+ doc: documents[originalIndex],
1209
+ });
1210
+ }
1211
+ result.inserted += error.upsertedCount || 0;
1212
+ result.updated += error.modifiedCount || 0;
1213
+ }
1214
+ else {
1215
+ // Batch-level error - mark all documents in batch as errors
1216
+ for (let j = 0; j < batch.length; j++) {
1217
+ result.errors.push({
1218
+ index: i + j,
1219
+ error: error instanceof Error ? error : new Error(String(error)),
1220
+ doc: documents[i + j],
1221
+ });
1222
+ }
1223
+ }
1224
+ }
1225
+ }
1226
+ return result;
1227
+ }
1228
+ catch (error) {
1229
+ throw new Error(`Failed to write by ref '${ref}': ${error instanceof Error ? error.message : String(error)}`);
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Writes documents to a collection using a ref name and optionally marks a stage as complete.
1234
+ * Combines writeByRef() with progress.complete() for atomic write-and-complete operations.
1235
+ * @param ref - Application-level reference name (must exist in config.outputs)
1236
+ * @param documents - Array of documents to write
1237
+ * @param options - Optional write and completion options
1238
+ * @param options.ensureIndex - Whether to ensure signature index exists (default: true)
1239
+ * @param options.session - Transaction session (if provided, write and complete are atomic)
1240
+ * @param options.complete - Stage completion information (optional)
1241
+ * @returns Result object with write counts, errors, and completion status
1242
+ * @throws Error if ref not found in config or not initialized
1243
+ */
1244
+ async writeStage(ref, documents, options) {
1245
+ this.ensureInitialized();
1246
+ // Write documents using existing writeByRef method
1247
+ const writeResult = await this.writeByRef(ref, documents, {
1248
+ session: options?.session,
1249
+ ensureIndex: options?.ensureIndex,
1250
+ });
1251
+ const result = {
1252
+ ...writeResult,
1253
+ completed: false,
1254
+ };
1255
+ // If complete option is provided, mark stage as complete
1256
+ if (options?.complete) {
1257
+ try {
1258
+ await this.progress.complete({
1259
+ key: options.complete.key,
1260
+ process: options.complete.process,
1261
+ name: options.complete.name,
1262
+ provider: options.complete.provider,
1263
+ metadata: options.complete.metadata,
1264
+ }, { session: options.session });
1265
+ result.completed = true;
1266
+ }
1267
+ catch (error) {
1268
+ // If completion fails, still return write result but indicate completion failed
1269
+ // Don't throw - let caller decide how to handle partial success
1270
+ result.completed = false;
1271
+ // Could add a completionError field if needed, but keeping it simple for now
1272
+ }
1273
+ }
1274
+ return result;
1275
+ }
1276
+ /**
1277
+ * Closes the MongoDB connection and cleans up resources.
1278
+ * @throws Error if disconnect fails
1279
+ */
1280
+ async disconnect() {
1281
+ if (!this.isInitialized || !this.client) {
1282
+ return;
1283
+ }
1284
+ try {
1285
+ await this.client.close();
1286
+ this.client = null;
1287
+ this.db = null;
1288
+ this.isInitialized = false;
1289
+ }
1290
+ catch (error) {
1291
+ throw new Error(`Failed to disconnect: ${error instanceof Error ? error.message : String(error)}`);
1292
+ }
1293
+ }
1294
+ /**
1295
+ * Ensures the helper is initialized before performing operations.
1296
+ * @throws Error if not initialized
1297
+ * @internal
1298
+ */
1299
+ ensureInitialized() {
1300
+ if (!this.isInitialized || !this.client || !this.db) {
1301
+ throw new Error('SimpleMongoHelper must be initialized before use. Call initialize() first.');
1302
+ }
1303
+ }
1304
+ /**
1305
+ * Gets the database instance. Public for use by internal classes.
1306
+ * @internal
1307
+ */
1308
+ getDb() {
1309
+ if (!this.db) {
1310
+ throw new Error('Database is not initialized. Call initialize() first.');
1311
+ }
1312
+ return this.db;
1313
+ }
1314
+ /**
1315
+ * Extracts database name from MongoDB connection string.
1316
+ * @param connectionString - MongoDB connection string
1317
+ * @returns Database name or 'test' as default
1318
+ */
1319
+ extractDatabaseName(connectionString) {
1320
+ try {
1321
+ const url = new URL(connectionString);
1322
+ const dbName = url.pathname.slice(1); // Remove leading '/'
1323
+ return dbName || 'test';
1324
+ }
1325
+ catch {
1326
+ // If URL parsing fails, try to extract from connection string pattern
1327
+ const match = connectionString.match(/\/([^\/\?]+)(\?|$)/);
1328
+ return match ? match[1] : 'test';
1329
+ }
1330
+ }
1331
+ }
1332
+ exports.SimpleMongoHelper = SimpleMongoHelper;
1333
+ //# sourceMappingURL=simpleMongoHelper.js.map