nx-mongo 4.0.0 → 4.0.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.
package/README.md CHANGED
@@ -1,1726 +1,1743 @@
1
- # nx-mongo
2
-
3
- **Version:** 4.0.0
4
-
5
- ## 🚀 Env-Ready Component (ERC 2.0)
6
-
7
- This component supports **zero-config initialization** via environment variables using [nx-config2](https://github.com/xeonox/nx-config2).
8
-
9
- A lightweight, feature-rich MongoDB helper library for Node.js and TypeScript. Provides a simple, intuitive API for common MongoDB operations with built-in retry logic, pagination, transactions, config-driven ref mapping, and signature-based deduplication.
10
-
11
- ## Features
12
-
13
- - ✅ **Simple API** - Easy-to-use methods for common MongoDB operations
14
- - ✅ **TypeScript Support** - Full TypeScript support with type safety
15
- - ✅ **Connection Retry** - Automatic retry with exponential backoff
16
- - ✅ **Automatic Cleanup** - Connections automatically close on app exit (SIGINT, SIGTERM, etc.)
17
- - ✅ **Pagination** - Built-in pagination support with metadata
18
- - ✅ **Transactions** - Full transaction support for multi-operation consistency
19
- - ✅ **Aggregation** - Complete aggregation pipeline support
20
- - ✅ **Index Management** - Create, drop, and list indexes
21
- - ✅ **Count Operations** - Accurate and estimated document counting
22
- - ✅ **Session Support** - Transaction sessions for complex operations
23
- - ✅ **Config-driven Ref Mapping** - Map application-level refs to MongoDB collections
24
- - ✅ **Signature-based Deduplication** - Automatic duplicate prevention using document signatures
25
- - ✅ **Append/Replace Modes** - Flexible write modes for data pipelines
26
-
27
- ## Installation
28
-
29
- ```bash
30
- npm install nx-mongo
31
- ```
32
-
33
- ### ERC 2.0 Setup
34
-
35
- 1. Copy `.env.example` to `.env`:
36
- ```bash
37
- cp node_modules/nx-mongo/.env.example .env
38
- ```
39
-
40
- 2. Fill in required values in `.env`:
41
- ```env
42
- MONGO_CONNECTION_STRING=mongodb://localhost:27017/
43
- ```
44
-
45
- 3. Use with zero config:
46
- ```typescript
47
- const helper = new SimpleMongoHelper();
48
- await helper.initialize();
49
- ```
50
-
51
- ## Quick Start
52
-
53
- ### Zero-Config Mode (ERC 2.0)
54
-
55
- ```typescript
56
- import { SimpleMongoHelper } from 'nx-mongo';
57
-
58
- // Auto-discovers configuration from environment variables
59
- // Set MONGO_CONNECTION_STRING or MONGODB_URI in your .env file
60
- const helper = new SimpleMongoHelper();
61
-
62
- // Initialize connection
63
- await helper.initialize();
64
- ```
65
-
66
- **Environment Variables:**
67
- - `MONGO_CONNECTION_STRING` or `MONGODB_URI` (required) - MongoDB connection string
68
- - `MONGO_MAX_RETRIES` (optional, default: 3) - Maximum retry attempts
69
- - `MONGO_RETRY_DELAY` (optional, default: 1000) - Initial retry delay in milliseconds
70
- - `MONGO_EXPONENTIAL_BACKOFF` (optional, default: true) - Use exponential backoff
71
-
72
- See `.env.example` for the complete list of required and optional variables with descriptions.
73
-
74
- ### Advanced Mode (Programmatic Configuration)
75
-
76
- ```typescript
77
- import { SimpleMongoHelper } from 'nx-mongo';
78
-
79
- // Explicit configuration (bypasses auto-discovery)
80
- const helper = new SimpleMongoHelper('mongodb://localhost:27017/', {
81
- maxRetries: 5,
82
- retryDelay: 2000
83
- });
84
-
85
- // Or use config object
86
- const helper = new SimpleMongoHelper({
87
- connectionString: 'mongodb://localhost:27017/',
88
- retryOptions: {
89
- maxRetries: 5,
90
- retryDelay: 2000
91
- }
92
- });
93
-
94
- // Initialize connection
95
- await helper.initialize();
96
- ```
97
-
98
- ### Legacy Mode (Backward Compatible)
99
-
100
- ```typescript
101
- import { SimpleMongoHelper } from 'nx-mongo';
102
-
103
- // Connection string: database name is ignored/stripped automatically
104
- // Use base connection string: mongodb://localhost:27017/
105
- const helper = new SimpleMongoHelper('mongodb://localhost:27017/');
106
-
107
- // Initialize connection
108
- await helper.initialize();
109
-
110
- // Insert a document (defaults to 'admin' database)
111
- await helper.insert('users', {
112
- name: 'John Doe',
113
- email: 'john@example.com',
114
- age: 30
115
- });
116
-
117
- // Insert into a specific database
118
- await helper.insert('users', {
119
- name: 'Jane Doe',
120
- email: 'jane@example.com',
121
- age: 28
122
- }, {}, 'mydb'); // Specify database name
123
-
124
- // Find documents from 'admin' database (default)
125
- const users = await helper.loadCollection('users');
126
-
127
- // Find documents from specific database
128
- const mydbUsers = await helper.loadCollection('users', {}, undefined, 'mydb');
129
-
130
- // Find one document
131
- const user = await helper.findOne('users', { email: 'john@example.com' });
132
-
133
- // Find from specific database
134
- const mydbUser = await helper.findOne('users', { email: 'jane@example.com' }, undefined, 'mydb');
135
-
136
- // Update document
137
- await helper.update(
138
- 'users',
139
- { email: 'john@example.com' },
140
- { $set: { age: 31 } }
141
- );
142
-
143
- // Update in specific database
144
- await helper.update(
145
- 'users',
146
- { email: 'jane@example.com' },
147
- { $set: { age: 29 } },
148
- undefined,
149
- 'mydb'
150
- );
151
-
152
- // Delete document
153
- await helper.delete('users', { email: 'john@example.com' });
154
-
155
- // Disconnect
156
- await helper.disconnect();
157
- ```
158
-
159
- **Note:** The connection string database name (if present) is automatically stripped. All operations default to the `'admin'` database unless you specify a different database name as the last parameter.
160
-
161
- ## API Reference
162
-
163
- ### Constructor
164
-
165
- ```typescript
166
- // Zero-Config Mode (ERC 2.0)
167
- new SimpleMongoHelper()
168
-
169
- // Advanced Mode (Config Object)
170
- new SimpleMongoHelper(config: SimpleMongoHelperConfig)
171
-
172
- // Legacy Mode (Backward Compatible)
173
- new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig)
174
- ```
175
-
176
- **Parameters:**
177
-
178
- **Zero-Config Mode:**
179
- - No parameters - auto-discovers from environment variables
180
-
181
- **Advanced Mode (Config Object):**
182
- - `config.connectionString` (optional) - MongoDB connection string (defaults to `MONGO_CONNECTION_STRING` or `MONGODB_URI` env var)
183
- - `config.retryOptions` (optional) - Retry configuration
184
- - `maxRetries?: number` - Maximum retry attempts (default: 3, or `MONGO_MAX_RETRIES` env var)
185
- - `retryDelay?: number` - Initial retry delay in ms (default: 1000, or `MONGO_RETRY_DELAY` env var)
186
- - `exponentialBackoff?: boolean` - Use exponential backoff (default: true, or `MONGO_EXPONENTIAL_BACKOFF` env var)
187
- - `config.config` (optional) - HelperConfig for ref-based operations
188
-
189
- **Legacy Mode:**
190
- - `connectionString` - MongoDB base connection string (database name is automatically stripped if present)
191
- - Example: `'mongodb://localhost:27017/'` or `'mongodb://localhost:27017/admin'` (both work the same)
192
- - `retryOptions` (optional) - Retry configuration
193
- - `config` (optional) - HelperConfig for ref-based operations
194
-
195
- **Examples:**
196
- ```typescript
197
- // Zero-Config Mode (ERC 2.0)
198
- const helper = new SimpleMongoHelper(); // Uses MONGO_CONNECTION_STRING from env
199
-
200
- // Advanced Mode
201
- const helper = new SimpleMongoHelper({
202
- connectionString: 'mongodb://localhost:27017/',
203
- retryOptions: { maxRetries: 5, retryDelay: 2000 }
204
- });
205
-
206
- // Legacy Mode (still supported)
207
- const helper = new SimpleMongoHelper(
208
- 'mongodb://localhost:27017/',
209
- { maxRetries: 5, retryDelay: 2000 }
210
- );
211
- ```
212
-
213
- **Important:** The database name in the connection string is automatically stripped. All operations default to the `'admin'` database unless you specify a different database name per operation.
214
-
215
- ### Connection Methods
216
-
217
- #### `testConnection(): Promise<{ success: boolean; error?: { type: string; message: string; details?: string } }>`
218
-
219
- Tests the MongoDB connection and returns detailed error information if it fails. This method does not establish a persistent connection - use `initialize()` for that.
220
-
221
- **Returns:**
222
- - `success: boolean` - Whether the connection test succeeded
223
- - `error?: object` - Error details if connection failed
224
- - `type` - Error type: `'missing_credentials' | 'invalid_connection_string' | 'connection_failed' | 'authentication_failed' | 'config_error' | 'unknown'`
225
- - `message` - Human-readable error message
226
- - `details` - Detailed error information and troubleshooting tips
227
-
228
- **Example:**
229
- ```typescript
230
- const result = await helper.testConnection();
231
- if (!result.success) {
232
- console.error('Connection test failed!');
233
- console.error('Error Type:', result.error?.type);
234
- console.error('Error Message:', result.error?.message);
235
- console.error('Error Details:', result.error?.details);
236
-
237
- // Handle error based on type
238
- switch (result.error?.type) {
239
- case 'connection_failed':
240
- console.error('Cannot connect to MongoDB server. Check if server is running.');
241
- // On Windows, try using 127.0.0.1 instead of localhost
242
- break;
243
- case 'authentication_failed':
244
- console.error('Invalid credentials. Check username and password.');
245
- break;
246
- case 'invalid_connection_string':
247
- console.error('Connection string format is invalid.');
248
- break;
249
- default:
250
- console.error('Unknown error occurred.');
251
- }
252
- } else {
253
- console.log('Connection test passed!');
254
- await helper.initialize();
255
- }
256
- ```
257
-
258
- **Error Types:**
259
- - `missing_credentials` - Username or password missing in connection string
260
- - `invalid_connection_string` - Connection string format is invalid
261
- - `connection_failed` - Cannot reach MongoDB server (timeout, DNS, network, etc.)
262
- - `authentication_failed` - Invalid credentials or insufficient permissions
263
- - `config_error` - Configuration issues
264
- - `unknown` - Unexpected error
265
-
266
- **Troubleshooting Tips:**
267
- - **Windows users**: If using `localhost` fails, try `127.0.0.1` instead (e.g., `mongodb://127.0.0.1:27017/`) to avoid IPv6 resolution issues
268
- - **Connection timeout**: Verify MongoDB is running and accessible on the specified host and port
269
- - **Connection refused**: Check if MongoDB is listening on the correct port (default: 27017)
270
- - **Authentication failed**: Verify username and password in the connection string
271
-
272
- #### `initialize(): Promise<void>`
273
-
274
- Establishes MongoDB connection with automatic retry logic. Must be called before using other methods.
275
-
276
- ```typescript
277
- await helper.initialize();
278
- ```
279
-
280
- #### `disconnect(): Promise<void>`
281
-
282
- Closes the MongoDB connection and cleans up resources. **Note:** Connections are automatically closed when your application exits (handles SIGINT, SIGTERM, and beforeExit events), so manual disconnection is optional but recommended for explicit cleanup.
283
-
284
- ```typescript
285
- await helper.disconnect();
286
- ```
287
-
288
- **Automatic Cleanup:**
289
- - Connections are automatically closed when the Node.js process receives `SIGINT` (Ctrl+C) or `SIGTERM` signals
290
- - All `SimpleMongoHelper` instances are cleaned up in parallel with a 5-second timeout
291
- - Multiple instances are handled gracefully through a global registry
292
- - Manual `disconnect()` is still recommended for explicit cleanup in your code
293
-
294
- ### Query Methods
295
-
296
- #### `loadCollection<T>(collectionName: string, query?: Filter<T>, options?: PaginationOptions, database?: string): Promise<WithId<T>[] | PaginatedResult<T>>`
297
-
298
- Loads documents from a collection with optional query filter and pagination.
299
-
300
- **Parameters:**
301
- - `collectionName` - Name of the collection
302
- - `query` (optional) - MongoDB query filter
303
- - `options` (optional) - Pagination and sorting options
304
- - `page?: number` - Page number (1-indexed)
305
- - `limit?: number` - Documents per page
306
- - `sort?: Sort` - Sort specification
307
- - `database` (optional) - Database name (defaults to `'admin'`)
308
-
309
- **Returns:**
310
- - Without pagination: `WithId<T>[]`
311
- - With pagination: `PaginatedResult<T>` with metadata
312
-
313
- **Examples:**
314
- ```typescript
315
- // Load all documents from 'admin' database (default)
316
- const allUsers = await helper.loadCollection('users');
317
-
318
- // Load from specific database
319
- const mydbUsers = await helper.loadCollection('users', {}, undefined, 'mydb');
320
-
321
- // Load with query
322
- const activeUsers = await helper.loadCollection('users', { active: true });
323
-
324
- // Load with pagination
325
- const result = await helper.loadCollection('users', {}, {
326
- page: 1,
327
- limit: 10,
328
- sort: { createdAt: -1 }
329
- });
330
- // result.data - array of documents
331
- // result.total - total count
332
- // result.page - current page
333
- // result.totalPages - total pages
334
- // result.hasNext - has next page
335
- // result.hasPrev - has previous page
336
-
337
- // Load with pagination from specific database
338
- const mydbResult = await helper.loadCollection('users', {}, {
339
- page: 1,
340
- limit: 10,
341
- sort: { createdAt: -1 }
342
- }, 'mydb');
343
- ```
344
-
345
- #### `findOne<T>(collectionName: string, query: Filter<T>, options?: { sort?: Sort; projection?: Document }, database?: string): Promise<WithId<T> | null>`
346
-
347
- Finds a single document in a collection.
348
-
349
- **Parameters:**
350
- - `collectionName` - Name of the collection
351
- - `query` - MongoDB query filter
352
- - `options` (optional) - Find options
353
- - `sort?: Sort` - Sort specification
354
- - `projection?: Document` - Field projection
355
- - `database` (optional) - Database name (defaults to `'admin'`)
356
-
357
- **Example:**
358
- ```typescript
359
- const user = await helper.findOne('users', { email: 'john@example.com' });
360
- const latestUser = await helper.findOne('users', {}, { sort: { createdAt: -1 } });
361
- ```
362
-
363
- ### Insert Methods
364
-
365
- #### `insert<T>(collectionName: string, data: T | T[], options?: { session?: ClientSession }, database?: string): Promise<any>`
366
-
367
- Inserts one or more documents into a collection.
368
-
369
- **Parameters:**
370
- - `collectionName` - Name of the collection
371
- - `data` - Single document or array of documents
372
- - `options` (optional) - Insert options
373
- - `session?: ClientSession` - Transaction session
374
-
375
- **Examples:**
376
- ```typescript
377
- // Insert single document
378
- await helper.insert('users', {
379
- name: 'John Doe',
380
- email: 'john@example.com'
381
- });
382
-
383
- // Insert multiple documents
384
- await helper.insert('users', [
385
- { name: 'John', email: 'john@example.com' },
386
- { name: 'Jane', email: 'jane@example.com' }
387
- ]);
388
-
389
- // Insert within transaction
390
- const session = helper.startSession();
391
- await session.withTransaction(async () => {
392
- await helper.insert('users', { name: 'John' }, { session });
393
- });
394
- ```
395
-
396
- ### Update Methods
397
-
398
- #### `update<T>(collectionName: string, filter: Filter<T>, updateData: UpdateFilter<T>, options?: { upsert?: boolean; multi?: boolean; session?: ClientSession }, database?: string): Promise<any>`
399
-
400
- Updates documents in a collection.
401
-
402
- **Parameters:**
403
- - `collectionName` - Name of the collection
404
- - `filter` - MongoDB query filter
405
- - `updateData` - Update operations
406
- - `options` (optional) - Update options
407
- - `upsert?: boolean` - Create if not exists
408
- - `multi?: boolean` - Update multiple documents (default: false)
409
- - `session?: ClientSession` - Transaction session
410
-
411
- **Examples:**
412
- ```typescript
413
- // Update single document
414
- await helper.update(
415
- 'users',
416
- { email: 'john@example.com' },
417
- { $set: { age: 31 } }
418
- );
419
-
420
- // Update multiple documents
421
- await helper.update(
422
- 'users',
423
- { role: 'user' },
424
- { $set: { lastLogin: new Date() } },
425
- { multi: true }
426
- );
427
-
428
- // Upsert (create if not exists)
429
- await helper.update(
430
- 'users',
431
- { email: 'john@example.com' },
432
- { $set: { name: 'John Doe', email: 'john@example.com' } },
433
- { upsert: true }
434
- );
435
- ```
436
-
437
- ### Delete Methods
438
-
439
- #### `delete<T>(collectionName: string, filter: Filter<T>, options?: { multi?: boolean }, database?: string): Promise<any>`
440
-
441
- Deletes documents from a collection.
442
-
443
- **Parameters:**
444
- - `collectionName` - Name of the collection
445
- - `filter` - MongoDB query filter
446
- - `options` (optional) - Delete options
447
- - `multi?: boolean` - Delete multiple documents (default: false)
448
-
449
- **Examples:**
450
- ```typescript
451
- // Delete single document
452
- await helper.delete('users', { email: 'john@example.com' });
453
-
454
- // Delete multiple documents
455
- await helper.delete('users', { role: 'guest' }, { multi: true });
456
- ```
457
-
458
- ### Collection Merge Methods
459
-
460
- #### `mergeCollections(options: MergeCollectionsOptions): Promise<MergeCollectionsResult>`
461
-
462
- Merges two collections into a new target collection using various strategies (index-based, key-based, or composite-key). Useful for combining original records with assessment results or joining related data.
463
-
464
- **Parameters:**
465
- - `sourceCollection1` - Name of first source collection (e.g., original records)
466
- - `sourceCollection2` - Name of second source collection (e.g., assessment results)
467
- - `targetCollection` - Name of target collection for merged results
468
- - `strategy` - Merge strategy: `'index' | 'key' | 'composite'`
469
- - `key` - (For 'key' strategy) Field name to match on (supports dot notation)
470
- - `compositeKeys` - (For 'composite' strategy) Array of field names for composite key matching
471
- - `joinType` - (For 'key' and 'composite' strategies) SQL-style join type: `'inner' | 'left' | 'right' | 'outer'` (optional, overrides onUnmatched flags)
472
- - `fieldPrefix1` - Prefix for fields from collection 1 (default: 'record')
473
- - `fieldPrefix2` - Prefix for fields from collection 2 (default: 'assessment')
474
- - `includeIndex` - Include original index in merged document (default: true for index strategy)
475
- - `onUnmatched1` - (Deprecated: use `joinType` instead) What to do with unmatched records from collection 1: 'include' | 'skip' (default: 'include')
476
- - `onUnmatched2` - (Deprecated: use `joinType` instead) What to do with unmatched records from collection 2: 'include' | 'skip' (default: 'include')
477
- - `session` - Optional transaction session
478
- - `database` - Optional database name (defaults to `'admin'`)
479
-
480
- **Returns:**
481
- ```typescript
482
- interface MergeCollectionsResult {
483
- merged: number; // Total merged documents
484
- unmatched1: number; // Unmatched documents from collection 1
485
- unmatched2: number; // Unmatched documents from collection 2
486
- errors: Array<{ index: number; error: Error; doc?: any }>;
487
- }
488
- ```
489
-
490
- **Strategies:**
491
-
492
- 1. **Index-based** (`strategy: 'index'`): Merges by array position. Assumes both collections are in the same order.
493
- ```typescript
494
- const result = await helper.mergeCollections({
495
- sourceCollection1: 'original_records',
496
- sourceCollection2: 'assessments',
497
- targetCollection: 'merged_results',
498
- strategy: 'index',
499
- fieldPrefix1: 'record',
500
- fieldPrefix2: 'assessment',
501
- includeIndex: true
502
- });
503
- // Result: { recordIndex: 0, record: {...}, assessment: {...} }
504
- ```
505
-
506
- 2. **Key-based** (`strategy: 'key'`): Merges by matching a single unique field. Supports SQL-style join types.
507
- ```typescript
508
- // INNER JOIN - Only matched records
509
- const result = await helper.mergeCollections({
510
- sourceCollection1: 'applications',
511
- sourceCollection2: 'assessments',
512
- targetCollection: 'merged',
513
- strategy: 'key',
514
- key: 'id',
515
- joinType: 'inner' // Only records with matching assessments
516
- });
517
-
518
- // LEFT JOIN - All records, with assessments where available
519
- const result = await helper.mergeCollections({
520
- sourceCollection1: 'applications',
521
- sourceCollection2: 'assessments',
522
- targetCollection: 'merged',
523
- strategy: 'key',
524
- key: 'id',
525
- joinType: 'left' // All apps, null assessment if no match
526
- });
527
-
528
- // RIGHT JOIN - All assessments, with records where available
529
- const result = await helper.mergeCollections({
530
- sourceCollection1: 'applications',
531
- sourceCollection2: 'assessments',
532
- targetCollection: 'merged',
533
- strategy: 'key',
534
- key: 'id',
535
- joinType: 'right' // All assessments, null record if no match
536
- });
537
-
538
- // FULL OUTER JOIN - Everything from both sides
539
- const result = await helper.mergeCollections({
540
- sourceCollection1: 'applications',
541
- sourceCollection2: 'assessments',
542
- targetCollection: 'merged',
543
- strategy: 'key',
544
- key: 'id',
545
- joinType: 'outer' // All apps and all assessments
546
- });
547
- ```
548
-
549
- 3. **Composite-key** (`strategy: 'composite'`): Merges by matching multiple fields (e.g., name + ports + zones). Also supports join types.
550
- ```typescript
551
- const result = await helper.mergeCollections({
552
- sourceCollection1: 'original_records',
553
- sourceCollection2: 'assessments',
554
- targetCollection: 'merged',
555
- strategy: 'composite',
556
- compositeKeys: ['name', 'ports[]', 'zones[]'], // Arrays are sorted for matching
557
- joinType: 'left', // All records, assessments where match
558
- fieldPrefix1: 'record',
559
- fieldPrefix2: 'assessment'
560
- });
561
- ```
562
-
563
- **SQL-Style Join Types:**
564
-
565
- - **`'inner'`** - INNER JOIN: Returns only records that have matches in both collections
566
- - **`'left'`** - LEFT JOIN: Returns all records from collection 1, with matching records from collection 2 (null if no match)
567
- - **`'right'`** - RIGHT JOIN: Returns all records from collection 2, with matching records from collection 1 (null if no match)
568
- - **`'outer'`** - FULL OUTER JOIN: Returns all records from both collections, matching where possible
569
-
570
- **Multiple Matches:** When a key appears multiple times in collection 2, the merge creates multiple rows (one per match), just like SQL joins. For example, if "app1" has 2 assessments, you'll get 2 merged rows.
571
-
572
- **Examples:**
573
-
574
- ```typescript
575
- // Index-based merge (fast but requires same order)
576
- const result1 = await helper.mergeCollections({
577
- sourceCollection1: 'records',
578
- sourceCollection2: 'assessments',
579
- targetCollection: 'merged',
580
- strategy: 'index'
581
- });
582
- console.log(`Merged ${result1.merged} documents, ${result1.unmatched1} unmatched from collection 1`);
583
-
584
- // INNER JOIN - Only complete records (both sides matched)
585
- const result2 = await helper.mergeCollections({
586
- sourceCollection1: 'apps',
587
- sourceCollection2: 'assessments',
588
- targetCollection: 'merged',
589
- strategy: 'key',
590
- key: 'appId',
591
- joinType: 'inner' // Only apps that have assessments
592
- });
593
-
594
- // LEFT JOIN - All apps, assessments where available
595
- const result3 = await helper.mergeCollections({
596
- sourceCollection1: 'apps',
597
- sourceCollection2: 'assessments',
598
- targetCollection: 'merged',
599
- strategy: 'key',
600
- key: 'appId',
601
- joinType: 'left' // All apps, null assessment if no match
602
- });
603
-
604
- // RIGHT JOIN - All assessments, apps where available
605
- const result4 = await helper.mergeCollections({
606
- sourceCollection1: 'apps',
607
- sourceCollection2: 'assessments',
608
- targetCollection: 'merged',
609
- strategy: 'key',
610
- key: 'appId',
611
- joinType: 'right' // All assessments, null app if no match
612
- });
613
-
614
- // FULL OUTER JOIN - Everything from both sides
615
- const result5 = await helper.mergeCollections({
616
- sourceCollection1: 'apps',
617
- sourceCollection2: 'assessments',
618
- targetCollection: 'merged',
619
- strategy: 'key',
620
- key: 'appId',
621
- joinType: 'outer' // All apps and all assessments
622
- });
623
-
624
- // Composite-key merge with LEFT JOIN
625
- const result6 = await helper.mergeCollections({
626
- sourceCollection1: 'original',
627
- sourceCollection2: 'assessments',
628
- targetCollection: 'merged',
629
- strategy: 'composite',
630
- compositeKeys: ['name', 'ports[]', 'zones[]'],
631
- joinType: 'left',
632
- fieldPrefix1: 'record',
633
- fieldPrefix2: 'assessment',
634
- includeIndex: true
635
- });
636
-
637
- // Handling multiple matches (one app, multiple assessments)
638
- // If "app1" has 2 assessments, you'll get 2 merged rows:
639
- // - { record: {id: 1, name: "app1"}, assessment: {appId: 1, risk: "high"} }
640
- // - { record: {id: 1, name: "app1"}, assessment: {appId: 1, risk: "medium"} }
641
- ```
642
-
643
- **Notes:**
644
- - **Index-based merging** is fast but fragile if collections are reordered
645
- - **Key-based merging** is safer and recommended when you have unique identifiers
646
- - **Composite-key merging** handles cases where no single unique field exists
647
- - **SQL-style join types** (`inner`, `left`, `right`, `outer`) provide explicit control over unmatched records
648
- - **Multiple matches** create multiple rows (SQL-style) - if a key has duplicates, you get one row per match
649
- - Array fields in composite keys are automatically sorted for consistent matching
650
- - Supports dot notation for nested fields (e.g., `'meta.id'`, `'ports[]'`)
651
- - Transaction support available via `session` option
652
- - Legacy `onUnmatched1`/`onUnmatched2` flags still work but are deprecated in favor of `joinType`
653
-
654
- ### Count Methods
655
-
656
- #### `countDocuments<T>(collectionName: string, query?: Filter<T>, database?: string): Promise<number>`
657
-
658
- Counts documents matching a query (accurate count).
659
-
660
- **Parameters:**
661
- - `collectionName` - Name of the collection
662
- - `query` (optional) - MongoDB query filter
663
- - `database` (optional) - Database name (defaults to `'admin'`)
664
-
665
- **Example:**
666
- ```typescript
667
- const userCount = await helper.countDocuments('users');
668
- const activeUserCount = await helper.countDocuments('users', { active: true });
669
- ```
670
-
671
- #### `estimatedDocumentCount(collectionName: string): Promise<number>`
672
-
673
- Gets estimated document count (faster but less accurate).
674
-
675
- **Example:**
676
- ```typescript
677
- const estimatedCount = await helper.estimatedDocumentCount('users');
678
- ```
679
-
680
- ### Aggregation Methods
681
-
682
- #### `aggregate<T>(collectionName: string, pipeline: Document[], database?: string): Promise<T[]>`
683
-
684
- Runs an aggregation pipeline on a collection.
685
-
686
- **Parameters:**
687
- - `collectionName` - Name of the collection
688
- - `pipeline` - Array of aggregation pipeline stages
689
- - `database` (optional) - Database name (defaults to `'admin'`)
690
-
691
- **Example:**
692
- ```typescript
693
- const result = await helper.aggregate('orders', [
694
- { $match: { status: 'completed' } },
695
- { $group: {
696
- _id: '$customerId',
697
- total: { $sum: '$amount' },
698
- count: { $sum: 1 }
699
- }},
700
- { $sort: { total: -1 } }
701
- ]);
702
- ```
703
-
704
- ### Index Methods
705
-
706
- #### `createIndex(collectionName: string, indexSpec: IndexSpecification, options?: CreateIndexesOptions, database?: string): Promise<string>`
707
-
708
- Creates an index on a collection.
709
-
710
- **Parameters:**
711
- - `collectionName` - Name of the collection
712
- - `indexSpec` - Index specification
713
- - `options` (optional) - Index creation options
714
- - `database` (optional) - Database name (defaults to `'admin'`)
715
-
716
- **Example:**
717
- ```typescript
718
- // Simple index
719
- await helper.createIndex('users', { email: 1 });
720
-
721
- // Unique index
722
- await helper.createIndex('users', { email: 1 }, { unique: true });
723
-
724
- // Compound index
725
- await helper.createIndex('users', { email: 1, createdAt: -1 });
726
- ```
727
-
728
- #### `dropIndex(collectionName: string, indexName: string, database?: string): Promise<any>`
729
-
730
- Drops an index from a collection.
731
-
732
- **Parameters:**
733
- - `collectionName` - Name of the collection
734
- - `indexName` - Name of the index to drop
735
- - `database` (optional) - Database name (defaults to `'admin'`)
736
-
737
- **Example:**
738
- ```typescript
739
- await helper.dropIndex('users', 'email_1');
740
- ```
741
-
742
- #### `listIndexes(collectionName: string, database?: string): Promise<Document[]>`
743
-
744
- Lists all indexes on a collection.
745
-
746
- **Parameters:**
747
- - `collectionName` - Name of the collection
748
- - `database` (optional) - Database name (defaults to `'admin'`)
749
-
750
- **Example:**
751
- ```typescript
752
- const indexes = await helper.listIndexes('users');
753
- indexes.forEach(idx => console.log(idx.name));
754
- ```
755
-
756
- ### Transaction Methods
757
-
758
- #### `startSession(): ClientSession`
759
-
760
- Starts a new client session for transactions.
761
-
762
- **Example:**
763
- ```typescript
764
- const session = helper.startSession();
765
- ```
766
-
767
- #### `withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>`
768
-
769
- Executes a function within a transaction.
770
-
771
- **Example:**
772
- ```typescript
773
- await helper.withTransaction(async (session) => {
774
- await helper.insert('users', { name: 'John' }, { session });
775
- await helper.update('accounts', { userId: '123' }, { $inc: { balance: 100 } }, { session });
776
- return 'Transaction completed';
777
- });
778
- ```
779
-
780
- **Note:** Transactions require a MongoDB replica set or sharded cluster.
781
-
782
- ## Config-driven Ref Mapping and Signature-based Deduplication
783
-
784
- ### Overview
785
-
786
- The helper supports config-driven collection mapping and signature-based deduplication. All logic (queries, keys, hashing, append/replace) is generic and built into the helper - applications only pass refs and documents.
787
-
788
- ### Configuration Schema
789
-
790
- ```typescript
791
- interface HelperConfig {
792
- inputs: Array<{
793
- ref: string; // Application-level reference name
794
- collection: string; // MongoDB collection name
795
- query?: Filter<any>; // Optional MongoDB query filter
796
- }>;
797
- outputs: Array<{
798
- ref: string; // Application-level reference name
799
- collection: string; // MongoDB collection name
800
- keys?: string[]; // Optional: dot-paths for signature generation
801
- mode?: "append" | "replace"; // Optional: write mode (default from global)
802
- }>;
803
- output?: {
804
- mode?: "append" | "replace"; // Global default mode (default: "append")
805
- };
806
- progress?: {
807
- collection?: string; // Progress collection name (default: "progress_states")
808
- uniqueIndexKeys?: string[]; // Unique index keys (default: ["provider","key"])
809
- provider?: string; // Default provider namespace for this helper instance
810
- };
811
- databases?: Array<{
812
- ref: string; // Reference identifier
813
- type: string; // Type identifier
814
- database: string; // Database name to use
815
- }>;
816
- }
817
- ```
818
-
819
- **Example Configuration:**
820
-
821
- ```typescript
822
- const config = {
823
- inputs: [
824
- { ref: "topology", collection: "topology-definition-neo-data", query: {} },
825
- { ref: "vulnerabilities", collection: "vulnerabilities-data", query: { severity: { "$in": ["high","critical"] } } },
826
- { ref: "entities", collection: "entities-data" },
827
- { ref: "crownJewels", collection: "entities-data", query: { type: "crown_jewel" } }
828
- ],
829
- outputs: [
830
- { ref: "paths", collection: "paths-neo-data", keys: ["segments[]","edges[].from","edges[].to","target_role"], mode: "append" },
831
- { ref: "prioritizedPaths", collection: "prioritized_paths-neo-data", keys: ["segments[]","outside","contains_crown_jewel"], mode: "replace" },
832
- { ref: "assetPaths", collection: "asset_paths-neo-data", keys: ["asset_ip","segments[]"], mode: "append" }
833
- ],
834
- output: { mode: "append" }
835
- };
836
- ```
837
-
838
- ### Constructor with Config
839
-
840
- ```typescript
841
- new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig)
842
- ```
843
-
844
- **Example:**
845
-
846
- ```typescript
847
- const helper = new SimpleMongoHelper(
848
- 'mongodb://localhost:27017/my-db',
849
- { maxRetries: 5 },
850
- config
851
- );
852
- ```
853
-
854
- ### Config Methods
855
-
856
- #### `useConfig(config: HelperConfig): this`
857
-
858
- Sets or updates the configuration for ref-based operations.
859
-
860
- ```typescript
861
- helper.useConfig(config);
862
- ```
863
-
864
- ### Database Selection via Ref/Type Map
865
-
866
- The helper supports config-driven database selection using `ref` and `type` parameters. This allows you to map logical identifiers to database names without hardcoding them in your application code.
867
-
868
- **Configuration:**
869
-
870
- ```typescript
871
- const config = {
872
- // ... inputs, outputs, etc.
873
- databases: [
874
- { ref: "app1", type: "production", database: "app1_prod" },
875
- { ref: "app1", type: "staging", database: "app1_staging" },
876
- { ref: "app2", type: "production", database: "app2_prod" },
877
- { ref: "app2", type: "staging", database: "app2_staging" },
878
- ]
879
- };
880
- ```
881
-
882
- **Usage in CRUD Operations:**
883
-
884
- All CRUD operations now support optional `ref` and `type` parameters for automatic database resolution:
885
-
886
- ```typescript
887
- // Priority 1: Direct database parameter (highest priority)
888
- await helper.insert('users', { name: 'John' }, {}, 'mydb');
889
-
890
- // Priority 2: Using ref + type (exact match)
891
- await helper.insert('users', { name: 'John' }, {}, undefined, 'app1', 'production');
892
- // Resolves to 'app1_prod' database
893
-
894
- // Priority 3: Using ref alone (must have exactly one match)
895
- await helper.insert('users', { name: 'John' }, {}, undefined, 'app1');
896
- // Throws error if multiple matches found
897
-
898
- // Priority 4: Using type alone (must have exactly one match)
899
- await helper.insert('users', { name: 'John' }, {}, undefined, undefined, 'production');
900
- // Throws error if multiple matches found
901
- ```
902
-
903
- **Database Resolution Priority:**
904
-
905
- 1. **Direct `database` parameter** - If provided, it's used immediately (highest priority)
906
- 2. **`ref` + `type`** - If both provided, finds exact match in databases map
907
- 3. **`ref` alone** - If only ref provided, finds entries matching ref (must be exactly one)
908
- 4. **`type` alone** - If only type provided, finds entries matching type (must be exactly one)
909
- 5. **Default** - If none provided, defaults to `'admin'` database
910
-
911
- **Error Handling:**
912
-
913
- - If no match found: throws error with descriptive message
914
- - If multiple matches found: throws error suggesting to use additional parameter to narrow down
915
-
916
- **Example:**
917
-
918
- ```typescript
919
- const config = {
920
- databases: [
921
- { ref: "tenant1", type: "prod", database: "tenant1_prod" },
922
- { ref: "tenant1", type: "dev", database: "tenant1_dev" },
923
- { ref: "tenant2", type: "prod", database: "tenant2_prod" },
924
- ]
925
- };
926
-
927
- const helper = new SimpleMongoHelper('mongodb://localhost:27017/', undefined, config);
928
- await helper.initialize();
929
-
930
- // Use ref + type for exact match
931
- await helper.insert('users', { name: 'John' }, {}, undefined, 'tenant1', 'prod');
932
- // Uses 'tenant1_prod' database
933
-
934
- // Use ref alone (only works if exactly one match)
935
- // This would throw error because tenant1 has 2 matches (prod and dev)
936
- // await helper.insert('users', { name: 'John' }, {}, undefined, 'tenant1');
937
-
938
- // Use type alone (only works if exactly one match)
939
- // This would throw error because 'prod' has 2 matches (tenant1 and tenant2)
940
- // await helper.insert('users', { name: 'John' }, {}, undefined, undefined, 'prod');
941
- ```
942
-
943
- ### Ref-based Operations
944
-
945
- #### `loadByRef<T>(ref: string, options?: PaginationOptions & { session?: ClientSession; database?: string; ref?: string; type?: string }): Promise<WithId<T>[] | PaginatedResult<T>>`
946
-
947
- Loads data from a collection using a ref name from the configuration.
948
-
949
- **Parameters:**
950
- - `ref` - Application-level reference name (must exist in config.inputs)
951
- - `options` (optional) - Pagination and session options
952
- - `page?: number` - Page number (1-indexed)
953
- - `limit?: number` - Documents per page
954
- - `sort?: Sort` - Sort specification
955
- - `session?: ClientSession` - Transaction session
956
- - `database?: string` - Database name (defaults to `'admin'`)
957
- - `ref?: string` - Optional ref for database resolution
958
- - `type?: string` - Optional type for database resolution
959
-
960
- **Example:**
961
-
962
- ```typescript
963
- // Load using ref (applies query automatically)
964
- const topology = await helper.loadByRef('topology');
965
- const vulns = await helper.loadByRef('vulnerabilities');
966
-
967
- // With pagination
968
- const result = await helper.loadByRef('topology', {
969
- page: 1,
970
- limit: 10,
971
- sort: { createdAt: -1 }
972
- });
973
- ```
974
-
975
- #### `writeByRef(ref: string, documents: any[], options?: { session?: ClientSession; ensureIndex?: boolean; database?: string; ref?: string; type?: string }): Promise<WriteByRefResult>`
976
-
977
- Writes documents to a collection using a ref name from the configuration. Supports signature-based deduplication and append/replace modes.
978
-
979
- **Parameters:**
980
- - `ref` - Application-level reference name (must exist in config.outputs)
981
- - `documents` - Array of documents to write
982
- - `options` (optional) - Write options
983
- - `session?: ClientSession` - Transaction session
984
- - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
985
- - `database?: string` - Database name (defaults to `'admin'`)
986
- - `ref?: string` - Optional ref for database resolution
987
- - `type?: string` - Optional type for database resolution
988
-
989
- **Returns:**
990
-
991
- ```typescript
992
- interface WriteByRefResult {
993
- inserted: number;
994
- updated: number;
995
- errors: Array<{ index: number; error: Error; doc?: any }>;
996
- indexCreated: boolean;
997
- }
998
- ```
999
-
1000
- **Example:**
1001
-
1002
- ```typescript
1003
- // Write using ref (automatic deduplication, uses keys from config)
1004
- const result = await helper.writeByRef('paths', pathDocuments);
1005
- console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
1006
- console.log(`Index created: ${result.indexCreated}`);
1007
-
1008
- // Replace mode (clears collection first)
1009
- await helper.writeByRef('prioritizedPaths', prioritizedDocs);
1010
- ```
1011
-
1012
- #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
1013
-
1014
- Writes documents to a collection and optionally marks a stage as complete atomically. See the [Progress Tracking](#progress-tracking) section for details and examples.
1015
-
1016
- **Parameters:**
1017
- - `ref` - Application-level reference name (must exist in config.outputs)
1018
- - `documents` - Array of documents to write
1019
- - `options` (optional) - Write and completion options
1020
- - `session?: ClientSession` - Transaction session
1021
- - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
1022
- - `database?: string` - Database name (defaults to `'admin'`)
1023
- - `complete?: object` - Stage completion information (optional)
1024
-
1025
- **Example:**
1026
-
1027
- ```typescript
1028
- // Write and mark stage complete in one call
1029
- await helper.writeStage('tier1', documents, {
1030
- complete: {
1031
- key: 'tier1',
1032
- process: 'processA',
1033
- name: 'System Inventory',
1034
- provider: 'nessus',
1035
- metadata: { itemCount: documents.length }
1036
- }
1037
- });
1038
- ```
1039
-
1040
- ### Signature Index Management
1041
-
1042
- #### `ensureSignatureIndex(collectionName: string, options?: { fieldName?: string; unique?: boolean }): Promise<EnsureSignatureIndexResult>`
1043
-
1044
- Ensures a unique index exists on the signature field for signature-based deduplication.
1045
-
1046
- **Parameters:**
1047
- - `collectionName` - Name of the collection
1048
- - `options` (optional) - Index configuration
1049
- - `fieldName?: string` - Field name for signature (default: "_sig")
1050
- - `unique?: boolean` - Whether index should be unique (default: true)
1051
-
1052
- **Returns:**
1053
-
1054
- ```typescript
1055
- interface EnsureSignatureIndexResult {
1056
- created: boolean;
1057
- indexName: string;
1058
- }
1059
- ```
1060
-
1061
- **Example:**
1062
-
1063
- ```typescript
1064
- const result = await helper.ensureSignatureIndex('paths-neo-data');
1065
- console.log(`Index created: ${result.created}, Name: ${result.indexName}`);
1066
- ```
1067
-
1068
- ## Progress Tracking
1069
-
1070
- ### Overview
1071
-
1072
- The helper provides built-in support for tracking provider-defined pipeline stages. This enables applications to:
1073
- - Track completion status of different stages (e.g., "tier1", "tier2", "enrichment")
1074
- - Skip already-completed stages on resumption
1075
- - Atomically write documents and mark stages complete
1076
- - Support multi-provider databases with provider namespaces
1077
-
1078
- ### Configuration
1079
-
1080
- Progress tracking is configured via the `progress` option in `HelperConfig`:
1081
-
1082
- ```typescript
1083
- const config = {
1084
- // ... inputs and outputs
1085
- progress: {
1086
- collection: "progress_states", // Optional: default "progress_states"
1087
- uniqueIndexKeys: ["process", "provider", "key"], // Optional: default ["process","provider","key"]
1088
- provider: "nessus" // Optional: default provider for this instance
1089
- }
1090
- };
1091
- ```
1092
-
1093
- ### Progress API
1094
-
1095
- The progress API is available via `helper.progress`:
1096
-
1097
- #### `isCompleted(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<boolean>`
1098
-
1099
- Checks if a stage is completed. Stages are scoped by process, so the same key can exist in different processes.
1100
-
1101
- **Example:**
1102
- ```typescript
1103
- // Check stage in a specific process
1104
- if (await helper.progress.isCompleted('tier1', { process: 'processA', provider: 'nessus' })) {
1105
- console.log('Stage "tier1" in processA already completed, skipping...');
1106
- }
1107
-
1108
- // Same key, different process
1109
- if (await helper.progress.isCompleted('tier1', { process: 'processB', provider: 'nessus' })) {
1110
- console.log('Stage "tier1" in processB already completed, skipping...');
1111
- }
1112
- ```
1113
-
1114
- #### `start(identity: StageIdentity, options?: { session?: ClientSession }): Promise<void>`
1115
-
1116
- Marks a stage as started. Idempotent - safe to call multiple times. Stages are scoped by process.
1117
-
1118
- **Example:**
1119
- ```typescript
1120
- await helper.progress.start({
1121
- key: 'tier1',
1122
- process: 'processA',
1123
- name: 'System Inventory',
1124
- provider: 'nessus'
1125
- });
1126
- ```
1127
-
1128
- #### `complete(identity: StageIdentity & { metadata?: StageMetadata }, options?: { session?: ClientSession }): Promise<void>`
1129
-
1130
- Marks a stage as completed with optional metadata. Idempotent - safe to call multiple times. Stages are scoped by process.
1131
-
1132
- **Example:**
1133
- ```typescript
1134
- await helper.progress.complete({
1135
- key: 'tier1',
1136
- process: 'processA',
1137
- name: 'System Inventory',
1138
- provider: 'nessus',
1139
- metadata: {
1140
- itemCount: 150,
1141
- durationMs: 5000
1142
- }
1143
- });
1144
- ```
1145
-
1146
- #### `getCompleted(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<Array<{ key: string; name?: string; completedAt?: Date }>>`
1147
-
1148
- Gets a list of all completed stages, optionally filtered by process and/or provider.
1149
-
1150
- **Example:**
1151
- ```typescript
1152
- // Get all completed stages for a specific process
1153
- const completed = await helper.progress.getCompleted({ process: 'processA', provider: 'nessus' });
1154
- // → [{ key: 'tier1', name: 'System Inventory', completedAt: Date }, ...]
1155
-
1156
- // Get all completed stages across all processes for a provider
1157
- const allCompleted = await helper.progress.getCompleted({ provider: 'nessus' });
1158
- ```
1159
-
1160
- #### `getProgress(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<StageRecord[]>`
1161
-
1162
- Gets all stage records (both completed and in-progress), optionally filtered by process and/or provider.
1163
-
1164
- **Example:**
1165
- ```typescript
1166
- // Get all stages for a specific process
1167
- const allStages = await helper.progress.getProgress({ process: 'processA', provider: 'nessus' });
1168
-
1169
- // Get all stages for a provider across all processes
1170
- const allProviderStages = await helper.progress.getProgress({ provider: 'nessus' });
1171
- ```
1172
-
1173
- #### `reset(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<void>`
1174
-
1175
- Resets a stage to not-started state (clears completion status). Stages are scoped by process.
1176
-
1177
- **Example:**
1178
- ```typescript
1179
- await helper.progress.reset('tier1', { process: 'processA', provider: 'nessus' });
1180
- ```
1181
-
1182
- ### Stage-Aware Writes
1183
-
1184
- #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
1185
-
1186
- Writes documents to a collection and optionally marks a stage as complete in a single call. If a session is provided, both operations are atomic within the transaction.
1187
-
1188
- **Parameters:**
1189
- - `ref` - Application-level reference name (must exist in config.outputs)
1190
- - `documents` - Array of documents to write
1191
- - `options` (optional) - Write and completion options
1192
- - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
1193
- - `session?: ClientSession` - Transaction session (makes write and complete atomic)
1194
- - `complete?: { key: string; name?: string; provider?: string; metadata?: StageMetadata }` - Stage completion info
1195
-
1196
- **Returns:**
1197
- ```typescript
1198
- interface WriteStageResult extends WriteByRefResult {
1199
- completed?: boolean; // true if stage was marked complete
1200
- }
1201
- ```
1202
-
1203
- **Examples:**
1204
-
1205
- ```typescript
1206
- // Skip completed stages, then save-and-complete in one call
1207
- const processName = 'processA';
1208
- if (!force && (await helper.progress.isCompleted('tier1', { process: processName, provider: 'nessus' }))) {
1209
- console.log('Skipping stage "tier1" in processA');
1210
- } else {
1211
- const docs = [
1212
- { type: 'server_status', ...status },
1213
- ...scanners.map(s => ({ type: 'scanner', ...s }))
1214
- ];
1215
- await helper.writeStage('tier1', docs, {
1216
- complete: {
1217
- key: 'tier1',
1218
- process: processName,
1219
- name: 'System Inventory',
1220
- provider: 'nessus',
1221
- metadata: { itemCount: docs.length }
1222
- }
1223
- });
1224
- }
1225
-
1226
- // Transactional multi-write with explicit completion
1227
- const session = helper.startSession();
1228
- try {
1229
- await session.withTransaction(async () => {
1230
- await helper.writeByRef('tier2_scans', scans, { session });
1231
- await helper.writeByRef('tier2_hosts', hosts, { session });
1232
- await helper.progress.complete({
1233
- key: 'tier2',
1234
- process: 'processA',
1235
- name: 'Scan Inventory',
1236
- provider: 'nessus',
1237
- metadata: { itemCount: hosts.length }
1238
- }, { session });
1239
- });
1240
- } finally {
1241
- await session.endSession();
1242
- }
1243
- ```
1244
-
1245
- ### Usage Patterns
1246
-
1247
- #### Resumption Pattern
1248
-
1249
- ```typescript
1250
- const processName = 'processA';
1251
- const stages = ['tier1', 'tier2', 'tier3'];
1252
-
1253
- for (const stageKey of stages) {
1254
- if (await helper.progress.isCompleted(stageKey, { process: processName, provider: 'nessus' })) {
1255
- console.log(`Skipping completed stage: ${stageKey} in ${processName}`);
1256
- continue;
1257
- }
1258
-
1259
- await helper.progress.start({ key: stageKey, process: processName, provider: 'nessus' });
1260
-
1261
- try {
1262
- const docs = await processStage(stageKey);
1263
- await helper.writeStage(`ref_${stageKey}`, docs, {
1264
- complete: { key: stageKey, process: processName, provider: 'nessus' }
1265
- });
1266
- } catch (error) {
1267
- console.error(`Stage ${stageKey} in ${processName} failed:`, error);
1268
- // Stage remains incomplete, can be retried
1269
- }
1270
- }
1271
-
1272
- // Different process can have same stage keys independently
1273
- const processB = 'processB';
1274
- if (!await helper.progress.isCompleted('tier1', { process: processB, provider: 'nessus' })) {
1275
- // Process B's tier1 is independent from Process A's tier1
1276
- await helper.progress.start({ key: 'tier1', process: processB, provider: 'nessus' });
1277
- }
1278
- ```
1279
-
1280
- ### Utility Functions
1281
-
1282
- #### `getByDotPath(value: any, path: string): any[]`
1283
-
1284
- Extracts values from an object using dot-notation paths with array wildcard support.
1285
-
1286
- **Parameters:**
1287
- - `value` - The object to extract values from
1288
- - `path` - Dot-notation path (e.g., "meta.id", "edges[].from", "segments[]")
1289
-
1290
- **Returns:** Array of extracted values (flattened and deduplicated for arrays)
1291
-
1292
- **Examples:**
1293
-
1294
- ```typescript
1295
- import { getByDotPath } from 'nx-mongo';
1296
-
1297
- // Simple path
1298
- getByDotPath({ meta: { id: "123" } }, "meta.id"); // ["123"]
1299
-
1300
- // Array wildcard
1301
- getByDotPath({ segments: [1, 2, 3] }, "segments[]"); // [1, 2, 3]
1302
-
1303
- // Nested array access
1304
- getByDotPath({ edges: [{ from: "A" }, { from: "B" }] }, "edges[].from"); // ["A", "B"]
1305
- ```
1306
-
1307
- #### `computeSignature(doc: any, keys: string[], options?: { algorithm?: "sha256" | "sha1" | "md5" }): string`
1308
-
1309
- Computes a deterministic signature for a document based on specified keys.
1310
-
1311
- **Parameters:**
1312
- - `doc` - The document to compute signature for
1313
- - `keys` - Array of dot-notation paths to extract values from
1314
- - `options` (optional) - Configuration
1315
- - `algorithm?: "sha256" | "sha1" | "md5"` - Hash algorithm (default: "sha256")
1316
-
1317
- **Returns:** Hex string signature
1318
-
1319
- **Example:**
1320
-
1321
- ```typescript
1322
- import { computeSignature } from 'nx-mongo';
1323
-
1324
- const sig = computeSignature(
1325
- { segments: [1, 2], role: "admin" },
1326
- ["segments[]", "role"]
1327
- );
1328
- ```
1329
-
1330
- ### Signature Algorithm
1331
-
1332
- The signature generation follows these steps:
1333
-
1334
- 1. **Extract values** for each key using `getByDotPath`
1335
- 2. **Normalize values**:
1336
- - **Strings**: As-is
1337
- - **Numbers**: `String(value)`
1338
- - **Booleans**: `"true"` or `"false"`
1339
- - **Dates**: `value.toISOString()` (UTC)
1340
- - **Null/Undefined**: `"null"`
1341
- - **Objects**: `JSON.stringify(value, Object.keys(value).sort())` (sorted keys)
1342
- - **Arrays**: Flatten recursively, normalize each element, deduplicate, sort lexicographically
1343
- 3. **Create canonical map**: `{ key1: [normalized values], key2: [normalized values], ... }`
1344
- 4. **Sort keys** alphabetically
1345
- 5. **Stringify**: `JSON.stringify(canonicalMap)`
1346
- 6. **Hash**: SHA-256 (or configurable algorithm)
1347
- 7. **Return**: Hex string
1348
-
1349
- ### Usage Examples
1350
-
1351
- #### Basic Usage with Config
1352
-
1353
- ```typescript
1354
- import { SimpleMongoHelper } from 'nx-mongo';
1355
-
1356
- const config = {
1357
- inputs: [
1358
- { ref: "topology", collection: "topology-definition", query: {} },
1359
- { ref: "vulnerabilities", collection: "vulnerabilities", query: { severity: "high" } }
1360
- ],
1361
- outputs: [
1362
- { ref: "paths", collection: "paths", keys: ["segments[]", "target_role"], mode: "append" },
1363
- { ref: "prioritizedPaths", collection: "prioritized_paths", keys: ["segments[]"], mode: "replace" }
1364
- ],
1365
- output: { mode: "append" }
1366
- };
1367
-
1368
- const helper = new SimpleMongoHelper('mongodb://localhost:27017/mydb', undefined, config);
1369
- await helper.initialize();
1370
-
1371
- // Load using ref (applies query automatically)
1372
- const topology = await helper.loadByRef('topology');
1373
- const vulns = await helper.loadByRef('vulnerabilities');
1374
-
1375
- // Write using ref (automatic deduplication, uses keys from config)
1376
- const result = await helper.writeByRef('paths', pathDocuments);
1377
- console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
1378
-
1379
- // Replace mode (clears collection first)
1380
- await helper.writeByRef('prioritizedPaths', prioritizedDocs);
1381
- ```
1382
-
1383
- #### With Transactions
1384
-
1385
- ```typescript
1386
- const session = helper.startSession();
1387
- try {
1388
- await session.withTransaction(async () => {
1389
- await helper.writeByRef('paths', docs, { session });
1390
- await helper.writeByRef('prioritizedPaths', prioDocs, { session });
1391
- });
1392
- } finally {
1393
- await session.endSession();
1394
- }
1395
- ```
1396
-
1397
- #### Standalone Utilities
1398
-
1399
- ```typescript
1400
- import { getByDotPath, computeSignature } from 'nx-mongo';
1401
-
1402
- // Extract values
1403
- const values = getByDotPath(doc, "edges[].from"); // ["A", "B", "C"]
1404
-
1405
- // Compute signature
1406
- const sig = computeSignature(doc, ["segments[]", "target_role"]);
1407
- ```
1408
-
1409
- ## TypeScript Interfaces
1410
-
1411
- ### PaginationOptions
1412
-
1413
- ```typescript
1414
- interface PaginationOptions {
1415
- page?: number;
1416
- limit?: number;
1417
- sort?: Sort;
1418
- }
1419
- ```
1420
-
1421
- ### PaginatedResult
1422
-
1423
- ```typescript
1424
- interface PaginatedResult<T> {
1425
- data: WithId<T>[];
1426
- total: number;
1427
- page: number;
1428
- limit: number;
1429
- totalPages: number;
1430
- hasNext: boolean;
1431
- hasPrev: boolean;
1432
- }
1433
- ```
1434
-
1435
- ### RetryOptions
1436
-
1437
- ```typescript
1438
- interface RetryOptions {
1439
- maxRetries?: number;
1440
- retryDelay?: number;
1441
- exponentialBackoff?: boolean;
1442
- }
1443
- ```
1444
-
1445
- ### HelperConfig
1446
-
1447
- ```typescript
1448
- interface HelperConfig {
1449
- inputs: InputConfig[];
1450
- outputs: OutputConfig[];
1451
- output?: {
1452
- mode?: 'append' | 'replace';
1453
- };
1454
- progress?: {
1455
- collection?: string;
1456
- uniqueIndexKeys?: string[];
1457
- provider?: string;
1458
- };
1459
- }
1460
-
1461
- interface InputConfig {
1462
- ref: string;
1463
- collection: string;
1464
- query?: Filter<any>;
1465
- }
1466
-
1467
- interface OutputConfig {
1468
- ref: string;
1469
- collection: string;
1470
- keys?: string[];
1471
- mode?: 'append' | 'replace';
1472
- }
1473
- ```
1474
-
1475
- ### WriteByRefResult
1476
-
1477
- ```typescript
1478
- interface WriteByRefResult {
1479
- inserted: number;
1480
- updated: number;
1481
- errors: Array<{ index: number; error: Error; doc?: any }>;
1482
- indexCreated: boolean;
1483
- }
1484
- ```
1485
-
1486
- ### EnsureSignatureIndexResult
1487
-
1488
- ```typescript
1489
- interface EnsureSignatureIndexResult {
1490
- created: boolean;
1491
- indexName: string;
1492
- }
1493
- ```
1494
-
1495
- ### Progress Tracking Interfaces
1496
-
1497
- ```typescript
1498
- interface StageIdentity {
1499
- key: string;
1500
- process?: string;
1501
- provider?: string;
1502
- name?: string;
1503
- }
1504
-
1505
- interface StageMetadata {
1506
- itemCount?: number;
1507
- errorCount?: number;
1508
- durationMs?: number;
1509
- [key: string]: any;
1510
- }
1511
-
1512
- interface StageRecord extends StageIdentity {
1513
- completed: boolean;
1514
- startedAt?: Date;
1515
- completedAt?: Date;
1516
- metadata?: StageMetadata;
1517
- }
1518
-
1519
- interface WriteStageOptions {
1520
- ensureIndex?: boolean;
1521
- session?: ClientSession;
1522
- complete?: {
1523
- key: string;
1524
- process?: string;
1525
- name?: string;
1526
- provider?: string;
1527
- metadata?: StageMetadata;
1528
- };
1529
- }
1530
-
1531
- interface WriteStageResult extends WriteByRefResult {
1532
- completed?: boolean;
1533
- }
1534
- ```
1535
-
1536
- ### Merge Collections Interfaces
1537
-
1538
- ```typescript
1539
- interface MergeCollectionsOptions {
1540
- sourceCollection1: string;
1541
- sourceCollection2: string;
1542
- targetCollection: string;
1543
- strategy: 'index' | 'key' | 'composite';
1544
- key?: string;
1545
- compositeKeys?: string[];
1546
- joinType?: 'inner' | 'left' | 'right' | 'outer'; // SQL-style join type
1547
- fieldPrefix1?: string;
1548
- fieldPrefix2?: string;
1549
- includeIndex?: boolean;
1550
- onUnmatched1?: 'include' | 'skip'; // Deprecated: use joinType instead
1551
- onUnmatched2?: 'include' | 'skip'; // Deprecated: use joinType instead
1552
- session?: ClientSession;
1553
- }
1554
-
1555
- interface MergeCollectionsResult {
1556
- merged: number;
1557
- unmatched1: number;
1558
- unmatched2: number;
1559
- errors: Array<{ index: number; error: Error; doc?: any }>;
1560
- }
1561
- ```
1562
-
1563
- ## Error Handling
1564
-
1565
- All methods throw errors with descriptive messages. Always wrap operations in try-catch blocks:
1566
-
1567
- ```typescript
1568
- try {
1569
- await helper.initialize();
1570
- const users = await helper.loadCollection('users');
1571
- } catch (error) {
1572
- console.error('Operation failed:', error.message);
1573
- }
1574
- ```
1575
-
1576
- ## Best Practices
1577
-
1578
- 1. **Always initialize before use:**
1579
- ```typescript
1580
- await helper.initialize();
1581
- ```
1582
-
1583
- 2. **Use transactions for multi-operation consistency:**
1584
- ```typescript
1585
- await helper.withTransaction(async (session) => {
1586
- // Multiple operations
1587
- });
1588
- ```
1589
-
1590
- 3. **Use pagination for large datasets:**
1591
- ```typescript
1592
- const result = await helper.loadCollection('users', {}, { page: 1, limit: 50 });
1593
- ```
1594
-
1595
- 4. **Create indexes for frequently queried fields:**
1596
- ```typescript
1597
- await helper.createIndex('users', { email: 1 }, { unique: true });
1598
- ```
1599
-
1600
- 5. **Disconnect when done (optional but recommended):**
1601
- ```typescript
1602
- await helper.disconnect();
1603
- ```
1604
- **Note:** Connections automatically close on app exit (SIGINT/SIGTERM), but explicit disconnection is recommended for better control and immediate cleanup.
1605
-
1606
- ## ERC 2.0 Compliance
1607
-
1608
- ✅ **Auto-discovers configuration from environment variables**
1609
- ✅ **Type-safe with automatic coercion and validation**
1610
- ✅ **All dependency requirements documented**
1611
- ✅ **Transitive requirements automatically merged**
1612
-
1613
- **Dependencies:**
1614
- - ✅ `nx-config2` (Configuration engine)
1615
- - â„šī¸ `mongodb` (non-ERC) - requirements manually documented
1616
- - â„šī¸ `micro-logs` (non-ERC) - no environment variables required
1617
-
1618
- **Verification:**
1619
- ```bash
1620
- npx nx-config2 erc-verify
1621
- ```
1622
-
1623
- ## License
1624
-
1625
- ISC
1626
-
1627
- ## Contributing
1628
-
1629
- Contributions are welcome! Please feel free to submit a Pull Request.
1630
-
1631
- ## Changelog
1632
-
1633
- ### 3.8.1
1634
- - **Fixed `testConnection()` error reporting bug**: Improved error extraction from MongoDB error objects to prevent `[object Object]` display
1635
- - Enhanced error handling to extract MongoDB-specific error properties (`code`, `codeName`, `errmsg`)
1636
- - Added Windows-specific troubleshooting hints for localhost connection issues (suggests using `127.0.0.1` instead of `localhost`)
1637
- - Improved error messages with error codes and types for better debugging
1638
- - Updated documentation with better error handling examples
1639
-
1640
- ### 3.8.0
1641
- - **Database selection via ref/type map**: Added config-driven database selection using `ref` and `type` parameters
1642
- - Added `databases` array to `HelperConfig` for mapping ref/type combinations to database names
1643
- - All CRUD operations now support optional `ref` and `type` parameters for automatic database resolution
1644
- - Database resolution priority: direct `database` parameter > `ref` + `type` > `ref` alone > `type` alone
1645
- - Throws descriptive errors when no match or multiple matches found in database map
1646
- - Updated Progress API to support database resolution via ref/type
1647
- - Updated `loadByRef`, `writeByRef`, `writeStage`, and `mergeCollections` to support database map resolution
1648
-
1649
- ### 3.7.0
1650
- - **Separated database from connection string**: Database name is now specified per operation, not in connection string
1651
- - **Multi-database support**: All operations accept optional `database` parameter (defaults to `'admin'`)
1652
- - Connection string database name is automatically stripped if present (e.g., `mongodb://localhost:27017/admin` becomes `mongodb://localhost:27017/`)
1653
- - Updated all methods (`insert`, `update`, `delete`, `loadCollection`, `findOne`, `countDocuments`, `aggregate`, `createIndex`, `dropIndex`, `listIndexes`, `writeByRef`, `loadByRef`, `mergeCollections`, `writeStage`, and progress API) to support per-operation database selection
1654
- - **Breaking change**: No backward compatibility - all code must be updated to use new database parameter
1655
-
1656
- ### 3.6.0
1657
- - **Automatic connection cleanup**: Connections now automatically close on app exit (SIGINT, SIGTERM, beforeExit)
1658
- - **Multi-instance support**: Global registry handles multiple `SimpleMongoHelper` instances gracefully
1659
- - **Timeout protection**: 5-second timeout prevents hanging during automatic cleanup
1660
- - Connections are properly managed and cleaned up even if users forget to call `disconnect()`
1661
-
1662
- ### 3.5.0
1663
- - Enhanced `mergeCollections()` with SQL-style join types (`inner`, `left`, `right`, `outer`)
1664
- - **Multiple match handling**: Now creates multiple rows when keys have duplicates (SQL-style behavior)
1665
- - Improved key-based and composite-key merging to handle one-to-many and many-to-many relationships
1666
- - Added explicit join type control for better clarity and SQL compatibility
1667
- - Legacy `onUnmatched1`/`onUnmatched2` flags deprecated in favor of `joinType` parameter
1668
-
1669
- ### 3.4.0
1670
- - Added `mergeCollections()` method for merging two collections into a new target collection
1671
- - Supports three merge strategies: index-based, key-based, and composite-key merging
1672
- - Index-based merging for same-order collections
1673
- - Key-based merging using unique identifiers (supports dot notation)
1674
- - Composite-key merging using multiple fields (e.g., name + ports + zones)
1675
- - Configurable field prefixes and unmatched record handling
1676
- - Transaction support for atomic merge operations
1677
-
1678
- ### 3.3.0
1679
- - Added `testConnection()` method for detailed connection testing and error diagnostics
1680
- - Package renamed from `nx-mongodb-helper` to `nx-mongo` (shorter, cleaner name)
1681
- - Connection test provides detailed error messages for missing credentials, invalid connection strings, authentication failures, and network issues
1682
-
1683
- ### 3.2.0
1684
- - Added process-scoped stages support - stages can now be scoped by process identifier
1685
- - Updated default unique index to include `process` field: `['process', 'provider', 'key']`
1686
- - All ProgressAPI methods now accept `process` parameter for process-scoped stage tracking
1687
- - Updated `writeStage()` to support process-scoped completion
1688
- - Stages with the same key can exist independently in different processes
1689
-
1690
- ### 3.1.0
1691
- - Added built-in progress tracking API (`helper.progress`) for provider-defined pipeline stages
1692
- - Added `writeStage()` method that combines document writing with stage completion
1693
- - Added progress tracking configuration to `HelperConfig` (collection, uniqueIndexKeys, provider)
1694
- - Progress API supports idempotent operations, transactions, and provider namespaces
1695
- - All progress operations support optional transaction sessions for atomicity
1696
-
1697
- ### 3.0.0
1698
- - Added config-driven ref mapping (HelperConfig, InputConfig, OutputConfig)
1699
- - Added signature-based deduplication with automatic index management
1700
- - Added `loadByRef()` method for loading data by ref name
1701
- - Added `writeByRef()` method with signature computation, bulk upsert, and append/replace modes
1702
- - Added `ensureSignatureIndex()` method for signature index management
1703
- - Added `useConfig()` method for runtime config updates
1704
- - Added `getByDotPath()` utility function for dot-notation path extraction with array wildcards
1705
- - Added `computeSignature()` utility function for deterministic document signatures
1706
- - Enhanced constructor to accept optional config parameter
1707
- - All new methods support transaction sessions
1708
-
1709
- ### 2.0.1
1710
- - Package renamed from `nx-mongodb-helper` to `nx-mongo`
1711
- - Add version number to README header
1712
-
1713
- ### 2.0.0
1714
- - Added delete operations
1715
- - Added findOne operation
1716
- - Added count operations (countDocuments, estimatedDocumentCount)
1717
- - Added pagination support
1718
- - Added aggregation pipeline support
1719
- - Added transaction support
1720
- - Added connection retry logic with exponential backoff
1721
- - Added index management (createIndex, dropIndex, listIndexes)
1722
- - Enhanced insert and update methods with session support
1723
-
1724
- ### 1.0.0
1725
- - Initial release with basic CRUD operations
1726
-
1
+ # nx-mongo
2
+
3
+ **Version:** 4.0.1
4
+
5
+ ## đŸ“Ļ ES Module Package
6
+
7
+ This package is now an **ES module** package (`"type": "module"`). It uses ES module syntax (`import`/`export`) and is compatible with ES module environments.
8
+
9
+ ## 🚀 Env-Ready Component (ERC 2.0)
10
+
11
+ This component supports **zero-config initialization** via environment variables using [nx-config2](https://github.com/xeonox/nx-config2).
12
+
13
+ A lightweight, feature-rich MongoDB helper library for Node.js and TypeScript. Provides a simple, intuitive API for common MongoDB operations with built-in retry logic, pagination, transactions, config-driven ref mapping, and signature-based deduplication.
14
+
15
+ ## Features
16
+
17
+ - ✅ **Simple API** - Easy-to-use methods for common MongoDB operations
18
+ - ✅ **TypeScript Support** - Full TypeScript support with type safety
19
+ - ✅ **Connection Retry** - Automatic retry with exponential backoff
20
+ - ✅ **Automatic Cleanup** - Connections automatically close on app exit (SIGINT, SIGTERM, etc.)
21
+ - ✅ **Pagination** - Built-in pagination support with metadata
22
+ - ✅ **Transactions** - Full transaction support for multi-operation consistency
23
+ - ✅ **Aggregation** - Complete aggregation pipeline support
24
+ - ✅ **Index Management** - Create, drop, and list indexes
25
+ - ✅ **Count Operations** - Accurate and estimated document counting
26
+ - ✅ **Session Support** - Transaction sessions for complex operations
27
+ - ✅ **Config-driven Ref Mapping** - Map application-level refs to MongoDB collections
28
+ - ✅ **Signature-based Deduplication** - Automatic duplicate prevention using document signatures
29
+ - ✅ **Append/Replace Modes** - Flexible write modes for data pipelines
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install nx-mongo
35
+ ```
36
+
37
+ ### ERC 2.0 Setup
38
+
39
+ 1. Copy `.env.example` to `.env`:
40
+ ```bash
41
+ cp node_modules/nx-mongo/.env.example .env
42
+ ```
43
+
44
+ 2. Fill in required values in `.env`:
45
+ ```env
46
+ MONGO_CONNECTION_STRING=mongodb://localhost:27017/
47
+ ```
48
+
49
+ 3. Use with zero config:
50
+ ```typescript
51
+ const helper = new SimpleMongoHelper();
52
+ await helper.initialize();
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ### Zero-Config Mode (ERC 2.0)
58
+
59
+ ```typescript
60
+ import { SimpleMongoHelper } from 'nx-mongo';
61
+
62
+ // Auto-discovers configuration from environment variables
63
+ // Set MONGO_CONNECTION_STRING or MONGODB_URI in your .env file
64
+ const helper = new SimpleMongoHelper();
65
+
66
+ // Initialize connection
67
+ await helper.initialize();
68
+ ```
69
+
70
+ **Environment Variables:**
71
+ - `MONGO_CONNECTION_STRING` or `MONGODB_URI` (required) - MongoDB connection string
72
+ - `MONGO_MAX_RETRIES` (optional, default: 3) - Maximum retry attempts
73
+ - `MONGO_RETRY_DELAY` (optional, default: 1000) - Initial retry delay in milliseconds
74
+ - `MONGO_EXPONENTIAL_BACKOFF` (optional, default: true) - Use exponential backoff
75
+
76
+ See `.env.example` for the complete list of required and optional variables with descriptions.
77
+
78
+ ### Advanced Mode (Programmatic Configuration)
79
+
80
+ ```typescript
81
+ import { SimpleMongoHelper } from 'nx-mongo';
82
+
83
+ // Explicit configuration (bypasses auto-discovery)
84
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/', {
85
+ maxRetries: 5,
86
+ retryDelay: 2000
87
+ });
88
+
89
+ // Or use config object
90
+ const helper = new SimpleMongoHelper({
91
+ connectionString: 'mongodb://localhost:27017/',
92
+ retryOptions: {
93
+ maxRetries: 5,
94
+ retryDelay: 2000
95
+ }
96
+ });
97
+
98
+ // Initialize connection
99
+ await helper.initialize();
100
+ ```
101
+
102
+ ### Legacy Mode (Backward Compatible)
103
+
104
+ ```typescript
105
+ import { SimpleMongoHelper } from 'nx-mongo';
106
+
107
+ // Connection string: database name is ignored/stripped automatically
108
+ // Use base connection string: mongodb://localhost:27017/
109
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/');
110
+
111
+ // Initialize connection
112
+ await helper.initialize();
113
+
114
+ // Insert a document (defaults to 'admin' database)
115
+ await helper.insert('users', {
116
+ name: 'John Doe',
117
+ email: 'john@example.com',
118
+ age: 30
119
+ });
120
+
121
+ // Insert into a specific database
122
+ await helper.insert('users', {
123
+ name: 'Jane Doe',
124
+ email: 'jane@example.com',
125
+ age: 28
126
+ }, {}, 'mydb'); // Specify database name
127
+
128
+ // Find documents from 'admin' database (default)
129
+ const users = await helper.loadCollection('users');
130
+
131
+ // Find documents from specific database
132
+ const mydbUsers = await helper.loadCollection('users', {}, undefined, 'mydb');
133
+
134
+ // Find one document
135
+ const user = await helper.findOne('users', { email: 'john@example.com' });
136
+
137
+ // Find from specific database
138
+ const mydbUser = await helper.findOne('users', { email: 'jane@example.com' }, undefined, 'mydb');
139
+
140
+ // Update document
141
+ await helper.update(
142
+ 'users',
143
+ { email: 'john@example.com' },
144
+ { $set: { age: 31 } }
145
+ );
146
+
147
+ // Update in specific database
148
+ await helper.update(
149
+ 'users',
150
+ { email: 'jane@example.com' },
151
+ { $set: { age: 29 } },
152
+ undefined,
153
+ 'mydb'
154
+ );
155
+
156
+ // Delete document
157
+ await helper.delete('users', { email: 'john@example.com' });
158
+
159
+ // Disconnect
160
+ await helper.disconnect();
161
+ ```
162
+
163
+ **Note:** The connection string database name (if present) is automatically stripped. All operations default to the `'admin'` database unless you specify a different database name as the last parameter.
164
+
165
+ ## API Reference
166
+
167
+ ### Constructor
168
+
169
+ ```typescript
170
+ // Zero-Config Mode (ERC 2.0)
171
+ new SimpleMongoHelper()
172
+
173
+ // Advanced Mode (Config Object)
174
+ new SimpleMongoHelper(config: SimpleMongoHelperConfig)
175
+
176
+ // Legacy Mode (Backward Compatible)
177
+ new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig)
178
+ ```
179
+
180
+ **Parameters:**
181
+
182
+ **Zero-Config Mode:**
183
+ - No parameters - auto-discovers from environment variables
184
+
185
+ **Advanced Mode (Config Object):**
186
+ - `config.connectionString` (optional) - MongoDB connection string (defaults to `MONGO_CONNECTION_STRING` or `MONGODB_URI` env var)
187
+ - `config.retryOptions` (optional) - Retry configuration
188
+ - `maxRetries?: number` - Maximum retry attempts (default: 3, or `MONGO_MAX_RETRIES` env var)
189
+ - `retryDelay?: number` - Initial retry delay in ms (default: 1000, or `MONGO_RETRY_DELAY` env var)
190
+ - `exponentialBackoff?: boolean` - Use exponential backoff (default: true, or `MONGO_EXPONENTIAL_BACKOFF` env var)
191
+ - `config.config` (optional) - HelperConfig for ref-based operations
192
+
193
+ **Legacy Mode:**
194
+ - `connectionString` - MongoDB base connection string (database name is automatically stripped if present)
195
+ - Example: `'mongodb://localhost:27017/'` or `'mongodb://localhost:27017/admin'` (both work the same)
196
+ - `retryOptions` (optional) - Retry configuration
197
+ - `config` (optional) - HelperConfig for ref-based operations
198
+
199
+ **Examples:**
200
+ ```typescript
201
+ // Zero-Config Mode (ERC 2.0)
202
+ const helper = new SimpleMongoHelper(); // Uses MONGO_CONNECTION_STRING from env
203
+
204
+ // Advanced Mode
205
+ const helper = new SimpleMongoHelper({
206
+ connectionString: 'mongodb://localhost:27017/',
207
+ retryOptions: { maxRetries: 5, retryDelay: 2000 }
208
+ });
209
+
210
+ // Legacy Mode (still supported)
211
+ const helper = new SimpleMongoHelper(
212
+ 'mongodb://localhost:27017/',
213
+ { maxRetries: 5, retryDelay: 2000 }
214
+ );
215
+ ```
216
+
217
+ **Important:** The database name in the connection string is automatically stripped. All operations default to the `'admin'` database unless you specify a different database name per operation.
218
+
219
+ ### Connection Methods
220
+
221
+ #### `testConnection(): Promise<{ success: boolean; error?: { type: string; message: string; details?: string } }>`
222
+
223
+ Tests the MongoDB connection and returns detailed error information if it fails. This method does not establish a persistent connection - use `initialize()` for that.
224
+
225
+ **Returns:**
226
+ - `success: boolean` - Whether the connection test succeeded
227
+ - `error?: object` - Error details if connection failed
228
+ - `type` - Error type: `'missing_credentials' | 'invalid_connection_string' | 'connection_failed' | 'authentication_failed' | 'config_error' | 'unknown'`
229
+ - `message` - Human-readable error message
230
+ - `details` - Detailed error information and troubleshooting tips
231
+
232
+ **Example:**
233
+ ```typescript
234
+ const result = await helper.testConnection();
235
+ if (!result.success) {
236
+ console.error('Connection test failed!');
237
+ console.error('Error Type:', result.error?.type);
238
+ console.error('Error Message:', result.error?.message);
239
+ console.error('Error Details:', result.error?.details);
240
+
241
+ // Handle error based on type
242
+ switch (result.error?.type) {
243
+ case 'connection_failed':
244
+ console.error('Cannot connect to MongoDB server. Check if server is running.');
245
+ // On Windows, try using 127.0.0.1 instead of localhost
246
+ break;
247
+ case 'authentication_failed':
248
+ console.error('Invalid credentials. Check username and password.');
249
+ break;
250
+ case 'invalid_connection_string':
251
+ console.error('Connection string format is invalid.');
252
+ break;
253
+ default:
254
+ console.error('Unknown error occurred.');
255
+ }
256
+ } else {
257
+ console.log('Connection test passed!');
258
+ await helper.initialize();
259
+ }
260
+ ```
261
+
262
+ **Error Types:**
263
+ - `missing_credentials` - Username or password missing in connection string
264
+ - `invalid_connection_string` - Connection string format is invalid
265
+ - `connection_failed` - Cannot reach MongoDB server (timeout, DNS, network, etc.)
266
+ - `authentication_failed` - Invalid credentials or insufficient permissions
267
+ - `config_error` - Configuration issues
268
+ - `unknown` - Unexpected error
269
+
270
+ **Troubleshooting Tips:**
271
+ - **Windows users**: If using `localhost` fails, try `127.0.0.1` instead (e.g., `mongodb://127.0.0.1:27017/`) to avoid IPv6 resolution issues
272
+ - **Connection timeout**: Verify MongoDB is running and accessible on the specified host and port
273
+ - **Connection refused**: Check if MongoDB is listening on the correct port (default: 27017)
274
+ - **Authentication failed**: Verify username and password in the connection string
275
+
276
+ #### `initialize(): Promise<void>`
277
+
278
+ Establishes MongoDB connection with automatic retry logic. Must be called before using other methods.
279
+
280
+ ```typescript
281
+ await helper.initialize();
282
+ ```
283
+
284
+ #### `disconnect(): Promise<void>`
285
+
286
+ Closes the MongoDB connection and cleans up resources. **Note:** Connections are automatically closed when your application exits (handles SIGINT, SIGTERM, and beforeExit events), so manual disconnection is optional but recommended for explicit cleanup.
287
+
288
+ ```typescript
289
+ await helper.disconnect();
290
+ ```
291
+
292
+ **Automatic Cleanup:**
293
+ - Connections are automatically closed when the Node.js process receives `SIGINT` (Ctrl+C) or `SIGTERM` signals
294
+ - All `SimpleMongoHelper` instances are cleaned up in parallel with a 5-second timeout
295
+ - Multiple instances are handled gracefully through a global registry
296
+ - Manual `disconnect()` is still recommended for explicit cleanup in your code
297
+
298
+ ### Query Methods
299
+
300
+ #### `loadCollection<T>(collectionName: string, query?: Filter<T>, options?: PaginationOptions, database?: string): Promise<WithId<T>[] | PaginatedResult<T>>`
301
+
302
+ Loads documents from a collection with optional query filter and pagination.
303
+
304
+ **Parameters:**
305
+ - `collectionName` - Name of the collection
306
+ - `query` (optional) - MongoDB query filter
307
+ - `options` (optional) - Pagination and sorting options
308
+ - `page?: number` - Page number (1-indexed)
309
+ - `limit?: number` - Documents per page
310
+ - `sort?: Sort` - Sort specification
311
+ - `database` (optional) - Database name (defaults to `'admin'`)
312
+
313
+ **Returns:**
314
+ - Without pagination: `WithId<T>[]`
315
+ - With pagination: `PaginatedResult<T>` with metadata
316
+
317
+ **Examples:**
318
+ ```typescript
319
+ // Load all documents from 'admin' database (default)
320
+ const allUsers = await helper.loadCollection('users');
321
+
322
+ // Load from specific database
323
+ const mydbUsers = await helper.loadCollection('users', {}, undefined, 'mydb');
324
+
325
+ // Load with query
326
+ const activeUsers = await helper.loadCollection('users', { active: true });
327
+
328
+ // Load with pagination
329
+ const result = await helper.loadCollection('users', {}, {
330
+ page: 1,
331
+ limit: 10,
332
+ sort: { createdAt: -1 }
333
+ });
334
+ // result.data - array of documents
335
+ // result.total - total count
336
+ // result.page - current page
337
+ // result.totalPages - total pages
338
+ // result.hasNext - has next page
339
+ // result.hasPrev - has previous page
340
+
341
+ // Load with pagination from specific database
342
+ const mydbResult = await helper.loadCollection('users', {}, {
343
+ page: 1,
344
+ limit: 10,
345
+ sort: { createdAt: -1 }
346
+ }, 'mydb');
347
+ ```
348
+
349
+ #### `findOne<T>(collectionName: string, query: Filter<T>, options?: { sort?: Sort; projection?: Document }, database?: string): Promise<WithId<T> | null>`
350
+
351
+ Finds a single document in a collection.
352
+
353
+ **Parameters:**
354
+ - `collectionName` - Name of the collection
355
+ - `query` - MongoDB query filter
356
+ - `options` (optional) - Find options
357
+ - `sort?: Sort` - Sort specification
358
+ - `projection?: Document` - Field projection
359
+ - `database` (optional) - Database name (defaults to `'admin'`)
360
+
361
+ **Example:**
362
+ ```typescript
363
+ const user = await helper.findOne('users', { email: 'john@example.com' });
364
+ const latestUser = await helper.findOne('users', {}, { sort: { createdAt: -1 } });
365
+ ```
366
+
367
+ ### Insert Methods
368
+
369
+ #### `insert<T>(collectionName: string, data: T | T[], options?: { session?: ClientSession }, database?: string): Promise<any>`
370
+
371
+ Inserts one or more documents into a collection.
372
+
373
+ **Parameters:**
374
+ - `collectionName` - Name of the collection
375
+ - `data` - Single document or array of documents
376
+ - `options` (optional) - Insert options
377
+ - `session?: ClientSession` - Transaction session
378
+
379
+ **Examples:**
380
+ ```typescript
381
+ // Insert single document
382
+ await helper.insert('users', {
383
+ name: 'John Doe',
384
+ email: 'john@example.com'
385
+ });
386
+
387
+ // Insert multiple documents
388
+ await helper.insert('users', [
389
+ { name: 'John', email: 'john@example.com' },
390
+ { name: 'Jane', email: 'jane@example.com' }
391
+ ]);
392
+
393
+ // Insert within transaction
394
+ const session = helper.startSession();
395
+ await session.withTransaction(async () => {
396
+ await helper.insert('users', { name: 'John' }, { session });
397
+ });
398
+ ```
399
+
400
+ ### Update Methods
401
+
402
+ #### `update<T>(collectionName: string, filter: Filter<T>, updateData: UpdateFilter<T>, options?: { upsert?: boolean; multi?: boolean; session?: ClientSession }, database?: string): Promise<any>`
403
+
404
+ Updates documents in a collection.
405
+
406
+ **Parameters:**
407
+ - `collectionName` - Name of the collection
408
+ - `filter` - MongoDB query filter
409
+ - `updateData` - Update operations
410
+ - `options` (optional) - Update options
411
+ - `upsert?: boolean` - Create if not exists
412
+ - `multi?: boolean` - Update multiple documents (default: false)
413
+ - `session?: ClientSession` - Transaction session
414
+
415
+ **Examples:**
416
+ ```typescript
417
+ // Update single document
418
+ await helper.update(
419
+ 'users',
420
+ { email: 'john@example.com' },
421
+ { $set: { age: 31 } }
422
+ );
423
+
424
+ // Update multiple documents
425
+ await helper.update(
426
+ 'users',
427
+ { role: 'user' },
428
+ { $set: { lastLogin: new Date() } },
429
+ { multi: true }
430
+ );
431
+
432
+ // Upsert (create if not exists)
433
+ await helper.update(
434
+ 'users',
435
+ { email: 'john@example.com' },
436
+ { $set: { name: 'John Doe', email: 'john@example.com' } },
437
+ { upsert: true }
438
+ );
439
+ ```
440
+
441
+ ### Delete Methods
442
+
443
+ #### `delete<T>(collectionName: string, filter: Filter<T>, options?: { multi?: boolean }, database?: string): Promise<any>`
444
+
445
+ Deletes documents from a collection.
446
+
447
+ **Parameters:**
448
+ - `collectionName` - Name of the collection
449
+ - `filter` - MongoDB query filter
450
+ - `options` (optional) - Delete options
451
+ - `multi?: boolean` - Delete multiple documents (default: false)
452
+
453
+ **Examples:**
454
+ ```typescript
455
+ // Delete single document
456
+ await helper.delete('users', { email: 'john@example.com' });
457
+
458
+ // Delete multiple documents
459
+ await helper.delete('users', { role: 'guest' }, { multi: true });
460
+ ```
461
+
462
+ ### Collection Merge Methods
463
+
464
+ #### `mergeCollections(options: MergeCollectionsOptions): Promise<MergeCollectionsResult>`
465
+
466
+ Merges two collections into a new target collection using various strategies (index-based, key-based, or composite-key). Useful for combining original records with assessment results or joining related data.
467
+
468
+ **Parameters:**
469
+ - `sourceCollection1` - Name of first source collection (e.g., original records)
470
+ - `sourceCollection2` - Name of second source collection (e.g., assessment results)
471
+ - `targetCollection` - Name of target collection for merged results
472
+ - `strategy` - Merge strategy: `'index' | 'key' | 'composite'`
473
+ - `key` - (For 'key' strategy) Field name to match on (supports dot notation)
474
+ - `compositeKeys` - (For 'composite' strategy) Array of field names for composite key matching
475
+ - `joinType` - (For 'key' and 'composite' strategies) SQL-style join type: `'inner' | 'left' | 'right' | 'outer'` (optional, overrides onUnmatched flags)
476
+ - `fieldPrefix1` - Prefix for fields from collection 1 (default: 'record')
477
+ - `fieldPrefix2` - Prefix for fields from collection 2 (default: 'assessment')
478
+ - `includeIndex` - Include original index in merged document (default: true for index strategy)
479
+ - `onUnmatched1` - (Deprecated: use `joinType` instead) What to do with unmatched records from collection 1: 'include' | 'skip' (default: 'include')
480
+ - `onUnmatched2` - (Deprecated: use `joinType` instead) What to do with unmatched records from collection 2: 'include' | 'skip' (default: 'include')
481
+ - `session` - Optional transaction session
482
+ - `database` - Optional database name (defaults to `'admin'`)
483
+
484
+ **Returns:**
485
+ ```typescript
486
+ interface MergeCollectionsResult {
487
+ merged: number; // Total merged documents
488
+ unmatched1: number; // Unmatched documents from collection 1
489
+ unmatched2: number; // Unmatched documents from collection 2
490
+ errors: Array<{ index: number; error: Error; doc?: any }>;
491
+ }
492
+ ```
493
+
494
+ **Strategies:**
495
+
496
+ 1. **Index-based** (`strategy: 'index'`): Merges by array position. Assumes both collections are in the same order.
497
+ ```typescript
498
+ const result = await helper.mergeCollections({
499
+ sourceCollection1: 'original_records',
500
+ sourceCollection2: 'assessments',
501
+ targetCollection: 'merged_results',
502
+ strategy: 'index',
503
+ fieldPrefix1: 'record',
504
+ fieldPrefix2: 'assessment',
505
+ includeIndex: true
506
+ });
507
+ // Result: { recordIndex: 0, record: {...}, assessment: {...} }
508
+ ```
509
+
510
+ 2. **Key-based** (`strategy: 'key'`): Merges by matching a single unique field. Supports SQL-style join types.
511
+ ```typescript
512
+ // INNER JOIN - Only matched records
513
+ const result = await helper.mergeCollections({
514
+ sourceCollection1: 'applications',
515
+ sourceCollection2: 'assessments',
516
+ targetCollection: 'merged',
517
+ strategy: 'key',
518
+ key: 'id',
519
+ joinType: 'inner' // Only records with matching assessments
520
+ });
521
+
522
+ // LEFT JOIN - All records, with assessments where available
523
+ const result = await helper.mergeCollections({
524
+ sourceCollection1: 'applications',
525
+ sourceCollection2: 'assessments',
526
+ targetCollection: 'merged',
527
+ strategy: 'key',
528
+ key: 'id',
529
+ joinType: 'left' // All apps, null assessment if no match
530
+ });
531
+
532
+ // RIGHT JOIN - All assessments, with records where available
533
+ const result = await helper.mergeCollections({
534
+ sourceCollection1: 'applications',
535
+ sourceCollection2: 'assessments',
536
+ targetCollection: 'merged',
537
+ strategy: 'key',
538
+ key: 'id',
539
+ joinType: 'right' // All assessments, null record if no match
540
+ });
541
+
542
+ // FULL OUTER JOIN - Everything from both sides
543
+ const result = await helper.mergeCollections({
544
+ sourceCollection1: 'applications',
545
+ sourceCollection2: 'assessments',
546
+ targetCollection: 'merged',
547
+ strategy: 'key',
548
+ key: 'id',
549
+ joinType: 'outer' // All apps and all assessments
550
+ });
551
+ ```
552
+
553
+ 3. **Composite-key** (`strategy: 'composite'`): Merges by matching multiple fields (e.g., name + ports + zones). Also supports join types.
554
+ ```typescript
555
+ const result = await helper.mergeCollections({
556
+ sourceCollection1: 'original_records',
557
+ sourceCollection2: 'assessments',
558
+ targetCollection: 'merged',
559
+ strategy: 'composite',
560
+ compositeKeys: ['name', 'ports[]', 'zones[]'], // Arrays are sorted for matching
561
+ joinType: 'left', // All records, assessments where match
562
+ fieldPrefix1: 'record',
563
+ fieldPrefix2: 'assessment'
564
+ });
565
+ ```
566
+
567
+ **SQL-Style Join Types:**
568
+
569
+ - **`'inner'`** - INNER JOIN: Returns only records that have matches in both collections
570
+ - **`'left'`** - LEFT JOIN: Returns all records from collection 1, with matching records from collection 2 (null if no match)
571
+ - **`'right'`** - RIGHT JOIN: Returns all records from collection 2, with matching records from collection 1 (null if no match)
572
+ - **`'outer'`** - FULL OUTER JOIN: Returns all records from both collections, matching where possible
573
+
574
+ **Multiple Matches:** When a key appears multiple times in collection 2, the merge creates multiple rows (one per match), just like SQL joins. For example, if "app1" has 2 assessments, you'll get 2 merged rows.
575
+
576
+ **Examples:**
577
+
578
+ ```typescript
579
+ // Index-based merge (fast but requires same order)
580
+ const result1 = await helper.mergeCollections({
581
+ sourceCollection1: 'records',
582
+ sourceCollection2: 'assessments',
583
+ targetCollection: 'merged',
584
+ strategy: 'index'
585
+ });
586
+ console.log(`Merged ${result1.merged} documents, ${result1.unmatched1} unmatched from collection 1`);
587
+
588
+ // INNER JOIN - Only complete records (both sides matched)
589
+ const result2 = await helper.mergeCollections({
590
+ sourceCollection1: 'apps',
591
+ sourceCollection2: 'assessments',
592
+ targetCollection: 'merged',
593
+ strategy: 'key',
594
+ key: 'appId',
595
+ joinType: 'inner' // Only apps that have assessments
596
+ });
597
+
598
+ // LEFT JOIN - All apps, assessments where available
599
+ const result3 = await helper.mergeCollections({
600
+ sourceCollection1: 'apps',
601
+ sourceCollection2: 'assessments',
602
+ targetCollection: 'merged',
603
+ strategy: 'key',
604
+ key: 'appId',
605
+ joinType: 'left' // All apps, null assessment if no match
606
+ });
607
+
608
+ // RIGHT JOIN - All assessments, apps where available
609
+ const result4 = await helper.mergeCollections({
610
+ sourceCollection1: 'apps',
611
+ sourceCollection2: 'assessments',
612
+ targetCollection: 'merged',
613
+ strategy: 'key',
614
+ key: 'appId',
615
+ joinType: 'right' // All assessments, null app if no match
616
+ });
617
+
618
+ // FULL OUTER JOIN - Everything from both sides
619
+ const result5 = await helper.mergeCollections({
620
+ sourceCollection1: 'apps',
621
+ sourceCollection2: 'assessments',
622
+ targetCollection: 'merged',
623
+ strategy: 'key',
624
+ key: 'appId',
625
+ joinType: 'outer' // All apps and all assessments
626
+ });
627
+
628
+ // Composite-key merge with LEFT JOIN
629
+ const result6 = await helper.mergeCollections({
630
+ sourceCollection1: 'original',
631
+ sourceCollection2: 'assessments',
632
+ targetCollection: 'merged',
633
+ strategy: 'composite',
634
+ compositeKeys: ['name', 'ports[]', 'zones[]'],
635
+ joinType: 'left',
636
+ fieldPrefix1: 'record',
637
+ fieldPrefix2: 'assessment',
638
+ includeIndex: true
639
+ });
640
+
641
+ // Handling multiple matches (one app, multiple assessments)
642
+ // If "app1" has 2 assessments, you'll get 2 merged rows:
643
+ // - { record: {id: 1, name: "app1"}, assessment: {appId: 1, risk: "high"} }
644
+ // - { record: {id: 1, name: "app1"}, assessment: {appId: 1, risk: "medium"} }
645
+ ```
646
+
647
+ **Notes:**
648
+ - **Index-based merging** is fast but fragile if collections are reordered
649
+ - **Key-based merging** is safer and recommended when you have unique identifiers
650
+ - **Composite-key merging** handles cases where no single unique field exists
651
+ - **SQL-style join types** (`inner`, `left`, `right`, `outer`) provide explicit control over unmatched records
652
+ - **Multiple matches** create multiple rows (SQL-style) - if a key has duplicates, you get one row per match
653
+ - Array fields in composite keys are automatically sorted for consistent matching
654
+ - Supports dot notation for nested fields (e.g., `'meta.id'`, `'ports[]'`)
655
+ - Transaction support available via `session` option
656
+ - Legacy `onUnmatched1`/`onUnmatched2` flags still work but are deprecated in favor of `joinType`
657
+
658
+ ### Count Methods
659
+
660
+ #### `countDocuments<T>(collectionName: string, query?: Filter<T>, database?: string): Promise<number>`
661
+
662
+ Counts documents matching a query (accurate count).
663
+
664
+ **Parameters:**
665
+ - `collectionName` - Name of the collection
666
+ - `query` (optional) - MongoDB query filter
667
+ - `database` (optional) - Database name (defaults to `'admin'`)
668
+
669
+ **Example:**
670
+ ```typescript
671
+ const userCount = await helper.countDocuments('users');
672
+ const activeUserCount = await helper.countDocuments('users', { active: true });
673
+ ```
674
+
675
+ #### `estimatedDocumentCount(collectionName: string): Promise<number>`
676
+
677
+ Gets estimated document count (faster but less accurate).
678
+
679
+ **Example:**
680
+ ```typescript
681
+ const estimatedCount = await helper.estimatedDocumentCount('users');
682
+ ```
683
+
684
+ ### Aggregation Methods
685
+
686
+ #### `aggregate<T>(collectionName: string, pipeline: Document[], database?: string): Promise<T[]>`
687
+
688
+ Runs an aggregation pipeline on a collection.
689
+
690
+ **Parameters:**
691
+ - `collectionName` - Name of the collection
692
+ - `pipeline` - Array of aggregation pipeline stages
693
+ - `database` (optional) - Database name (defaults to `'admin'`)
694
+
695
+ **Example:**
696
+ ```typescript
697
+ const result = await helper.aggregate('orders', [
698
+ { $match: { status: 'completed' } },
699
+ { $group: {
700
+ _id: '$customerId',
701
+ total: { $sum: '$amount' },
702
+ count: { $sum: 1 }
703
+ }},
704
+ { $sort: { total: -1 } }
705
+ ]);
706
+ ```
707
+
708
+ ### Index Methods
709
+
710
+ #### `createIndex(collectionName: string, indexSpec: IndexSpecification, options?: CreateIndexesOptions, database?: string): Promise<string>`
711
+
712
+ Creates an index on a collection.
713
+
714
+ **Parameters:**
715
+ - `collectionName` - Name of the collection
716
+ - `indexSpec` - Index specification
717
+ - `options` (optional) - Index creation options
718
+ - `database` (optional) - Database name (defaults to `'admin'`)
719
+
720
+ **Example:**
721
+ ```typescript
722
+ // Simple index
723
+ await helper.createIndex('users', { email: 1 });
724
+
725
+ // Unique index
726
+ await helper.createIndex('users', { email: 1 }, { unique: true });
727
+
728
+ // Compound index
729
+ await helper.createIndex('users', { email: 1, createdAt: -1 });
730
+ ```
731
+
732
+ #### `dropIndex(collectionName: string, indexName: string, database?: string): Promise<any>`
733
+
734
+ Drops an index from a collection.
735
+
736
+ **Parameters:**
737
+ - `collectionName` - Name of the collection
738
+ - `indexName` - Name of the index to drop
739
+ - `database` (optional) - Database name (defaults to `'admin'`)
740
+
741
+ **Example:**
742
+ ```typescript
743
+ await helper.dropIndex('users', 'email_1');
744
+ ```
745
+
746
+ #### `listIndexes(collectionName: string, database?: string): Promise<Document[]>`
747
+
748
+ Lists all indexes on a collection.
749
+
750
+ **Parameters:**
751
+ - `collectionName` - Name of the collection
752
+ - `database` (optional) - Database name (defaults to `'admin'`)
753
+
754
+ **Example:**
755
+ ```typescript
756
+ const indexes = await helper.listIndexes('users');
757
+ indexes.forEach(idx => console.log(idx.name));
758
+ ```
759
+
760
+ ### Transaction Methods
761
+
762
+ #### `startSession(): ClientSession`
763
+
764
+ Starts a new client session for transactions.
765
+
766
+ **Example:**
767
+ ```typescript
768
+ const session = helper.startSession();
769
+ ```
770
+
771
+ #### `withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>`
772
+
773
+ Executes a function within a transaction.
774
+
775
+ **Example:**
776
+ ```typescript
777
+ await helper.withTransaction(async (session) => {
778
+ await helper.insert('users', { name: 'John' }, { session });
779
+ await helper.update('accounts', { userId: '123' }, { $inc: { balance: 100 } }, { session });
780
+ return 'Transaction completed';
781
+ });
782
+ ```
783
+
784
+ **Note:** Transactions require a MongoDB replica set or sharded cluster.
785
+
786
+ ## Config-driven Ref Mapping and Signature-based Deduplication
787
+
788
+ ### Overview
789
+
790
+ The helper supports config-driven collection mapping and signature-based deduplication. All logic (queries, keys, hashing, append/replace) is generic and built into the helper - applications only pass refs and documents.
791
+
792
+ ### Configuration Schema
793
+
794
+ ```typescript
795
+ interface HelperConfig {
796
+ inputs: Array<{
797
+ ref: string; // Application-level reference name
798
+ collection: string; // MongoDB collection name
799
+ query?: Filter<any>; // Optional MongoDB query filter
800
+ }>;
801
+ outputs: Array<{
802
+ ref: string; // Application-level reference name
803
+ collection: string; // MongoDB collection name
804
+ keys?: string[]; // Optional: dot-paths for signature generation
805
+ mode?: "append" | "replace"; // Optional: write mode (default from global)
806
+ }>;
807
+ output?: {
808
+ mode?: "append" | "replace"; // Global default mode (default: "append")
809
+ };
810
+ progress?: {
811
+ collection?: string; // Progress collection name (default: "progress_states")
812
+ uniqueIndexKeys?: string[]; // Unique index keys (default: ["provider","key"])
813
+ provider?: string; // Default provider namespace for this helper instance
814
+ };
815
+ databases?: Array<{
816
+ ref: string; // Reference identifier
817
+ type: string; // Type identifier
818
+ database: string; // Database name to use
819
+ }>;
820
+ }
821
+ ```
822
+
823
+ **Example Configuration:**
824
+
825
+ ```typescript
826
+ const config = {
827
+ inputs: [
828
+ { ref: "topology", collection: "topology-definition-neo-data", query: {} },
829
+ { ref: "vulnerabilities", collection: "vulnerabilities-data", query: { severity: { "$in": ["high","critical"] } } },
830
+ { ref: "entities", collection: "entities-data" },
831
+ { ref: "crownJewels", collection: "entities-data", query: { type: "crown_jewel" } }
832
+ ],
833
+ outputs: [
834
+ { ref: "paths", collection: "paths-neo-data", keys: ["segments[]","edges[].from","edges[].to","target_role"], mode: "append" },
835
+ { ref: "prioritizedPaths", collection: "prioritized_paths-neo-data", keys: ["segments[]","outside","contains_crown_jewel"], mode: "replace" },
836
+ { ref: "assetPaths", collection: "asset_paths-neo-data", keys: ["asset_ip","segments[]"], mode: "append" }
837
+ ],
838
+ output: { mode: "append" }
839
+ };
840
+ ```
841
+
842
+ ### Constructor with Config
843
+
844
+ ```typescript
845
+ new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig)
846
+ ```
847
+
848
+ **Example:**
849
+
850
+ ```typescript
851
+ const helper = new SimpleMongoHelper(
852
+ 'mongodb://localhost:27017/my-db',
853
+ { maxRetries: 5 },
854
+ config
855
+ );
856
+ ```
857
+
858
+ ### Config Methods
859
+
860
+ #### `useConfig(config: HelperConfig): this`
861
+
862
+ Sets or updates the configuration for ref-based operations.
863
+
864
+ ```typescript
865
+ helper.useConfig(config);
866
+ ```
867
+
868
+ ### Database Selection via Ref/Type Map
869
+
870
+ The helper supports config-driven database selection using `ref` and `type` parameters. This allows you to map logical identifiers to database names without hardcoding them in your application code.
871
+
872
+ **Configuration:**
873
+
874
+ ```typescript
875
+ const config = {
876
+ // ... inputs, outputs, etc.
877
+ databases: [
878
+ { ref: "app1", type: "production", database: "app1_prod" },
879
+ { ref: "app1", type: "staging", database: "app1_staging" },
880
+ { ref: "app2", type: "production", database: "app2_prod" },
881
+ { ref: "app2", type: "staging", database: "app2_staging" },
882
+ ]
883
+ };
884
+ ```
885
+
886
+ **Usage in CRUD Operations:**
887
+
888
+ All CRUD operations now support optional `ref` and `type` parameters for automatic database resolution:
889
+
890
+ ```typescript
891
+ // Priority 1: Direct database parameter (highest priority)
892
+ await helper.insert('users', { name: 'John' }, {}, 'mydb');
893
+
894
+ // Priority 2: Using ref + type (exact match)
895
+ await helper.insert('users', { name: 'John' }, {}, undefined, 'app1', 'production');
896
+ // Resolves to 'app1_prod' database
897
+
898
+ // Priority 3: Using ref alone (must have exactly one match)
899
+ await helper.insert('users', { name: 'John' }, {}, undefined, 'app1');
900
+ // Throws error if multiple matches found
901
+
902
+ // Priority 4: Using type alone (must have exactly one match)
903
+ await helper.insert('users', { name: 'John' }, {}, undefined, undefined, 'production');
904
+ // Throws error if multiple matches found
905
+ ```
906
+
907
+ **Database Resolution Priority:**
908
+
909
+ 1. **Direct `database` parameter** - If provided, it's used immediately (highest priority)
910
+ 2. **`ref` + `type`** - If both provided, finds exact match in databases map
911
+ 3. **`ref` alone** - If only ref provided, finds entries matching ref (must be exactly one)
912
+ 4. **`type` alone** - If only type provided, finds entries matching type (must be exactly one)
913
+ 5. **Default** - If none provided, defaults to `'admin'` database
914
+
915
+ **Error Handling:**
916
+
917
+ - If no match found: throws error with descriptive message
918
+ - If multiple matches found: throws error suggesting to use additional parameter to narrow down
919
+
920
+ **Example:**
921
+
922
+ ```typescript
923
+ const config = {
924
+ databases: [
925
+ { ref: "tenant1", type: "prod", database: "tenant1_prod" },
926
+ { ref: "tenant1", type: "dev", database: "tenant1_dev" },
927
+ { ref: "tenant2", type: "prod", database: "tenant2_prod" },
928
+ ]
929
+ };
930
+
931
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/', undefined, config);
932
+ await helper.initialize();
933
+
934
+ // Use ref + type for exact match
935
+ await helper.insert('users', { name: 'John' }, {}, undefined, 'tenant1', 'prod');
936
+ // Uses 'tenant1_prod' database
937
+
938
+ // Use ref alone (only works if exactly one match)
939
+ // This would throw error because tenant1 has 2 matches (prod and dev)
940
+ // await helper.insert('users', { name: 'John' }, {}, undefined, 'tenant1');
941
+
942
+ // Use type alone (only works if exactly one match)
943
+ // This would throw error because 'prod' has 2 matches (tenant1 and tenant2)
944
+ // await helper.insert('users', { name: 'John' }, {}, undefined, undefined, 'prod');
945
+ ```
946
+
947
+ ### Ref-based Operations
948
+
949
+ #### `loadByRef<T>(ref: string, options?: PaginationOptions & { session?: ClientSession; database?: string; ref?: string; type?: string }): Promise<WithId<T>[] | PaginatedResult<T>>`
950
+
951
+ Loads data from a collection using a ref name from the configuration.
952
+
953
+ **Parameters:**
954
+ - `ref` - Application-level reference name (must exist in config.inputs)
955
+ - `options` (optional) - Pagination and session options
956
+ - `page?: number` - Page number (1-indexed)
957
+ - `limit?: number` - Documents per page
958
+ - `sort?: Sort` - Sort specification
959
+ - `session?: ClientSession` - Transaction session
960
+ - `database?: string` - Database name (defaults to `'admin'`)
961
+ - `ref?: string` - Optional ref for database resolution
962
+ - `type?: string` - Optional type for database resolution
963
+
964
+ **Example:**
965
+
966
+ ```typescript
967
+ // Load using ref (applies query automatically)
968
+ const topology = await helper.loadByRef('topology');
969
+ const vulns = await helper.loadByRef('vulnerabilities');
970
+
971
+ // With pagination
972
+ const result = await helper.loadByRef('topology', {
973
+ page: 1,
974
+ limit: 10,
975
+ sort: { createdAt: -1 }
976
+ });
977
+ ```
978
+
979
+ #### `writeByRef(ref: string, documents: any[], options?: { session?: ClientSession; ensureIndex?: boolean; database?: string; ref?: string; type?: string }): Promise<WriteByRefResult>`
980
+
981
+ Writes documents to a collection using a ref name from the configuration. Supports signature-based deduplication and append/replace modes.
982
+
983
+ **Parameters:**
984
+ - `ref` - Application-level reference name (must exist in config.outputs)
985
+ - `documents` - Array of documents to write
986
+ - `options` (optional) - Write options
987
+ - `session?: ClientSession` - Transaction session
988
+ - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
989
+ - `database?: string` - Database name (defaults to `'admin'`)
990
+ - `ref?: string` - Optional ref for database resolution
991
+ - `type?: string` - Optional type for database resolution
992
+
993
+ **Returns:**
994
+
995
+ ```typescript
996
+ interface WriteByRefResult {
997
+ inserted: number;
998
+ updated: number;
999
+ errors: Array<{ index: number; error: Error; doc?: any }>;
1000
+ indexCreated: boolean;
1001
+ }
1002
+ ```
1003
+
1004
+ **Example:**
1005
+
1006
+ ```typescript
1007
+ // Write using ref (automatic deduplication, uses keys from config)
1008
+ const result = await helper.writeByRef('paths', pathDocuments);
1009
+ console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
1010
+ console.log(`Index created: ${result.indexCreated}`);
1011
+
1012
+ // Replace mode (clears collection first)
1013
+ await helper.writeByRef('prioritizedPaths', prioritizedDocs);
1014
+ ```
1015
+
1016
+ #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
1017
+
1018
+ Writes documents to a collection and optionally marks a stage as complete atomically. See the [Progress Tracking](#progress-tracking) section for details and examples.
1019
+
1020
+ **Parameters:**
1021
+ - `ref` - Application-level reference name (must exist in config.outputs)
1022
+ - `documents` - Array of documents to write
1023
+ - `options` (optional) - Write and completion options
1024
+ - `session?: ClientSession` - Transaction session
1025
+ - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
1026
+ - `database?: string` - Database name (defaults to `'admin'`)
1027
+ - `complete?: object` - Stage completion information (optional)
1028
+
1029
+ **Example:**
1030
+
1031
+ ```typescript
1032
+ // Write and mark stage complete in one call
1033
+ await helper.writeStage('tier1', documents, {
1034
+ complete: {
1035
+ key: 'tier1',
1036
+ process: 'processA',
1037
+ name: 'System Inventory',
1038
+ provider: 'nessus',
1039
+ metadata: { itemCount: documents.length }
1040
+ }
1041
+ });
1042
+ ```
1043
+
1044
+ ### Signature Index Management
1045
+
1046
+ #### `ensureSignatureIndex(collectionName: string, options?: { fieldName?: string; unique?: boolean }): Promise<EnsureSignatureIndexResult>`
1047
+
1048
+ Ensures a unique index exists on the signature field for signature-based deduplication.
1049
+
1050
+ **Parameters:**
1051
+ - `collectionName` - Name of the collection
1052
+ - `options` (optional) - Index configuration
1053
+ - `fieldName?: string` - Field name for signature (default: "_sig")
1054
+ - `unique?: boolean` - Whether index should be unique (default: true)
1055
+
1056
+ **Returns:**
1057
+
1058
+ ```typescript
1059
+ interface EnsureSignatureIndexResult {
1060
+ created: boolean;
1061
+ indexName: string;
1062
+ }
1063
+ ```
1064
+
1065
+ **Example:**
1066
+
1067
+ ```typescript
1068
+ const result = await helper.ensureSignatureIndex('paths-neo-data');
1069
+ console.log(`Index created: ${result.created}, Name: ${result.indexName}`);
1070
+ ```
1071
+
1072
+ ## Progress Tracking
1073
+
1074
+ ### Overview
1075
+
1076
+ The helper provides built-in support for tracking provider-defined pipeline stages. This enables applications to:
1077
+ - Track completion status of different stages (e.g., "tier1", "tier2", "enrichment")
1078
+ - Skip already-completed stages on resumption
1079
+ - Atomically write documents and mark stages complete
1080
+ - Support multi-provider databases with provider namespaces
1081
+
1082
+ ### Configuration
1083
+
1084
+ Progress tracking is configured via the `progress` option in `HelperConfig`:
1085
+
1086
+ ```typescript
1087
+ const config = {
1088
+ // ... inputs and outputs
1089
+ progress: {
1090
+ collection: "progress_states", // Optional: default "progress_states"
1091
+ uniqueIndexKeys: ["process", "provider", "key"], // Optional: default ["process","provider","key"]
1092
+ provider: "nessus" // Optional: default provider for this instance
1093
+ }
1094
+ };
1095
+ ```
1096
+
1097
+ ### Progress API
1098
+
1099
+ The progress API is available via `helper.progress`:
1100
+
1101
+ #### `isCompleted(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<boolean>`
1102
+
1103
+ Checks if a stage is completed. Stages are scoped by process, so the same key can exist in different processes.
1104
+
1105
+ **Example:**
1106
+ ```typescript
1107
+ // Check stage in a specific process
1108
+ if (await helper.progress.isCompleted('tier1', { process: 'processA', provider: 'nessus' })) {
1109
+ console.log('Stage "tier1" in processA already completed, skipping...');
1110
+ }
1111
+
1112
+ // Same key, different process
1113
+ if (await helper.progress.isCompleted('tier1', { process: 'processB', provider: 'nessus' })) {
1114
+ console.log('Stage "tier1" in processB already completed, skipping...');
1115
+ }
1116
+ ```
1117
+
1118
+ #### `start(identity: StageIdentity, options?: { session?: ClientSession }): Promise<void>`
1119
+
1120
+ Marks a stage as started. Idempotent - safe to call multiple times. Stages are scoped by process.
1121
+
1122
+ **Example:**
1123
+ ```typescript
1124
+ await helper.progress.start({
1125
+ key: 'tier1',
1126
+ process: 'processA',
1127
+ name: 'System Inventory',
1128
+ provider: 'nessus'
1129
+ });
1130
+ ```
1131
+
1132
+ #### `complete(identity: StageIdentity & { metadata?: StageMetadata }, options?: { session?: ClientSession }): Promise<void>`
1133
+
1134
+ Marks a stage as completed with optional metadata. Idempotent - safe to call multiple times. Stages are scoped by process.
1135
+
1136
+ **Example:**
1137
+ ```typescript
1138
+ await helper.progress.complete({
1139
+ key: 'tier1',
1140
+ process: 'processA',
1141
+ name: 'System Inventory',
1142
+ provider: 'nessus',
1143
+ metadata: {
1144
+ itemCount: 150,
1145
+ durationMs: 5000
1146
+ }
1147
+ });
1148
+ ```
1149
+
1150
+ #### `getCompleted(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<Array<{ key: string; name?: string; completedAt?: Date }>>`
1151
+
1152
+ Gets a list of all completed stages, optionally filtered by process and/or provider.
1153
+
1154
+ **Example:**
1155
+ ```typescript
1156
+ // Get all completed stages for a specific process
1157
+ const completed = await helper.progress.getCompleted({ process: 'processA', provider: 'nessus' });
1158
+ // → [{ key: 'tier1', name: 'System Inventory', completedAt: Date }, ...]
1159
+
1160
+ // Get all completed stages across all processes for a provider
1161
+ const allCompleted = await helper.progress.getCompleted({ provider: 'nessus' });
1162
+ ```
1163
+
1164
+ #### `getProgress(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<StageRecord[]>`
1165
+
1166
+ Gets all stage records (both completed and in-progress), optionally filtered by process and/or provider.
1167
+
1168
+ **Example:**
1169
+ ```typescript
1170
+ // Get all stages for a specific process
1171
+ const allStages = await helper.progress.getProgress({ process: 'processA', provider: 'nessus' });
1172
+
1173
+ // Get all stages for a provider across all processes
1174
+ const allProviderStages = await helper.progress.getProgress({ provider: 'nessus' });
1175
+ ```
1176
+
1177
+ #### `reset(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<void>`
1178
+
1179
+ Resets a stage to not-started state (clears completion status). Stages are scoped by process.
1180
+
1181
+ **Example:**
1182
+ ```typescript
1183
+ await helper.progress.reset('tier1', { process: 'processA', provider: 'nessus' });
1184
+ ```
1185
+
1186
+ ### Stage-Aware Writes
1187
+
1188
+ #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
1189
+
1190
+ Writes documents to a collection and optionally marks a stage as complete in a single call. If a session is provided, both operations are atomic within the transaction.
1191
+
1192
+ **Parameters:**
1193
+ - `ref` - Application-level reference name (must exist in config.outputs)
1194
+ - `documents` - Array of documents to write
1195
+ - `options` (optional) - Write and completion options
1196
+ - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
1197
+ - `session?: ClientSession` - Transaction session (makes write and complete atomic)
1198
+ - `complete?: { key: string; name?: string; provider?: string; metadata?: StageMetadata }` - Stage completion info
1199
+
1200
+ **Returns:**
1201
+ ```typescript
1202
+ interface WriteStageResult extends WriteByRefResult {
1203
+ completed?: boolean; // true if stage was marked complete
1204
+ }
1205
+ ```
1206
+
1207
+ **Examples:**
1208
+
1209
+ ```typescript
1210
+ // Skip completed stages, then save-and-complete in one call
1211
+ const processName = 'processA';
1212
+ if (!force && (await helper.progress.isCompleted('tier1', { process: processName, provider: 'nessus' }))) {
1213
+ console.log('Skipping stage "tier1" in processA');
1214
+ } else {
1215
+ const docs = [
1216
+ { type: 'server_status', ...status },
1217
+ ...scanners.map(s => ({ type: 'scanner', ...s }))
1218
+ ];
1219
+ await helper.writeStage('tier1', docs, {
1220
+ complete: {
1221
+ key: 'tier1',
1222
+ process: processName,
1223
+ name: 'System Inventory',
1224
+ provider: 'nessus',
1225
+ metadata: { itemCount: docs.length }
1226
+ }
1227
+ });
1228
+ }
1229
+
1230
+ // Transactional multi-write with explicit completion
1231
+ const session = helper.startSession();
1232
+ try {
1233
+ await session.withTransaction(async () => {
1234
+ await helper.writeByRef('tier2_scans', scans, { session });
1235
+ await helper.writeByRef('tier2_hosts', hosts, { session });
1236
+ await helper.progress.complete({
1237
+ key: 'tier2',
1238
+ process: 'processA',
1239
+ name: 'Scan Inventory',
1240
+ provider: 'nessus',
1241
+ metadata: { itemCount: hosts.length }
1242
+ }, { session });
1243
+ });
1244
+ } finally {
1245
+ await session.endSession();
1246
+ }
1247
+ ```
1248
+
1249
+ ### Usage Patterns
1250
+
1251
+ #### Resumption Pattern
1252
+
1253
+ ```typescript
1254
+ const processName = 'processA';
1255
+ const stages = ['tier1', 'tier2', 'tier3'];
1256
+
1257
+ for (const stageKey of stages) {
1258
+ if (await helper.progress.isCompleted(stageKey, { process: processName, provider: 'nessus' })) {
1259
+ console.log(`Skipping completed stage: ${stageKey} in ${processName}`);
1260
+ continue;
1261
+ }
1262
+
1263
+ await helper.progress.start({ key: stageKey, process: processName, provider: 'nessus' });
1264
+
1265
+ try {
1266
+ const docs = await processStage(stageKey);
1267
+ await helper.writeStage(`ref_${stageKey}`, docs, {
1268
+ complete: { key: stageKey, process: processName, provider: 'nessus' }
1269
+ });
1270
+ } catch (error) {
1271
+ console.error(`Stage ${stageKey} in ${processName} failed:`, error);
1272
+ // Stage remains incomplete, can be retried
1273
+ }
1274
+ }
1275
+
1276
+ // Different process can have same stage keys independently
1277
+ const processB = 'processB';
1278
+ if (!await helper.progress.isCompleted('tier1', { process: processB, provider: 'nessus' })) {
1279
+ // Process B's tier1 is independent from Process A's tier1
1280
+ await helper.progress.start({ key: 'tier1', process: processB, provider: 'nessus' });
1281
+ }
1282
+ ```
1283
+
1284
+ ### Utility Functions
1285
+
1286
+ #### `getByDotPath(value: any, path: string): any[]`
1287
+
1288
+ Extracts values from an object using dot-notation paths with array wildcard support.
1289
+
1290
+ **Parameters:**
1291
+ - `value` - The object to extract values from
1292
+ - `path` - Dot-notation path (e.g., "meta.id", "edges[].from", "segments[]")
1293
+
1294
+ **Returns:** Array of extracted values (flattened and deduplicated for arrays)
1295
+
1296
+ **Examples:**
1297
+
1298
+ ```typescript
1299
+ import { getByDotPath } from 'nx-mongo';
1300
+
1301
+ // Simple path
1302
+ getByDotPath({ meta: { id: "123" } }, "meta.id"); // ["123"]
1303
+
1304
+ // Array wildcard
1305
+ getByDotPath({ segments: [1, 2, 3] }, "segments[]"); // [1, 2, 3]
1306
+
1307
+ // Nested array access
1308
+ getByDotPath({ edges: [{ from: "A" }, { from: "B" }] }, "edges[].from"); // ["A", "B"]
1309
+ ```
1310
+
1311
+ #### `computeSignature(doc: any, keys: string[], options?: { algorithm?: "sha256" | "sha1" | "md5" }): string`
1312
+
1313
+ Computes a deterministic signature for a document based on specified keys.
1314
+
1315
+ **Parameters:**
1316
+ - `doc` - The document to compute signature for
1317
+ - `keys` - Array of dot-notation paths to extract values from
1318
+ - `options` (optional) - Configuration
1319
+ - `algorithm?: "sha256" | "sha1" | "md5"` - Hash algorithm (default: "sha256")
1320
+
1321
+ **Returns:** Hex string signature
1322
+
1323
+ **Example:**
1324
+
1325
+ ```typescript
1326
+ import { computeSignature } from 'nx-mongo';
1327
+
1328
+ const sig = computeSignature(
1329
+ { segments: [1, 2], role: "admin" },
1330
+ ["segments[]", "role"]
1331
+ );
1332
+ ```
1333
+
1334
+ ### Signature Algorithm
1335
+
1336
+ The signature generation follows these steps:
1337
+
1338
+ 1. **Extract values** for each key using `getByDotPath`
1339
+ 2. **Normalize values**:
1340
+ - **Strings**: As-is
1341
+ - **Numbers**: `String(value)`
1342
+ - **Booleans**: `"true"` or `"false"`
1343
+ - **Dates**: `value.toISOString()` (UTC)
1344
+ - **Null/Undefined**: `"null"`
1345
+ - **Objects**: `JSON.stringify(value, Object.keys(value).sort())` (sorted keys)
1346
+ - **Arrays**: Flatten recursively, normalize each element, deduplicate, sort lexicographically
1347
+ 3. **Create canonical map**: `{ key1: [normalized values], key2: [normalized values], ... }`
1348
+ 4. **Sort keys** alphabetically
1349
+ 5. **Stringify**: `JSON.stringify(canonicalMap)`
1350
+ 6. **Hash**: SHA-256 (or configurable algorithm)
1351
+ 7. **Return**: Hex string
1352
+
1353
+ ### Usage Examples
1354
+
1355
+ #### Basic Usage with Config
1356
+
1357
+ ```typescript
1358
+ import { SimpleMongoHelper } from 'nx-mongo';
1359
+
1360
+ const config = {
1361
+ inputs: [
1362
+ { ref: "topology", collection: "topology-definition", query: {} },
1363
+ { ref: "vulnerabilities", collection: "vulnerabilities", query: { severity: "high" } }
1364
+ ],
1365
+ outputs: [
1366
+ { ref: "paths", collection: "paths", keys: ["segments[]", "target_role"], mode: "append" },
1367
+ { ref: "prioritizedPaths", collection: "prioritized_paths", keys: ["segments[]"], mode: "replace" }
1368
+ ],
1369
+ output: { mode: "append" }
1370
+ };
1371
+
1372
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/mydb', undefined, config);
1373
+ await helper.initialize();
1374
+
1375
+ // Load using ref (applies query automatically)
1376
+ const topology = await helper.loadByRef('topology');
1377
+ const vulns = await helper.loadByRef('vulnerabilities');
1378
+
1379
+ // Write using ref (automatic deduplication, uses keys from config)
1380
+ const result = await helper.writeByRef('paths', pathDocuments);
1381
+ console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
1382
+
1383
+ // Replace mode (clears collection first)
1384
+ await helper.writeByRef('prioritizedPaths', prioritizedDocs);
1385
+ ```
1386
+
1387
+ #### With Transactions
1388
+
1389
+ ```typescript
1390
+ const session = helper.startSession();
1391
+ try {
1392
+ await session.withTransaction(async () => {
1393
+ await helper.writeByRef('paths', docs, { session });
1394
+ await helper.writeByRef('prioritizedPaths', prioDocs, { session });
1395
+ });
1396
+ } finally {
1397
+ await session.endSession();
1398
+ }
1399
+ ```
1400
+
1401
+ #### Standalone Utilities
1402
+
1403
+ ```typescript
1404
+ import { getByDotPath, computeSignature } from 'nx-mongo';
1405
+
1406
+ // Extract values
1407
+ const values = getByDotPath(doc, "edges[].from"); // ["A", "B", "C"]
1408
+
1409
+ // Compute signature
1410
+ const sig = computeSignature(doc, ["segments[]", "target_role"]);
1411
+ ```
1412
+
1413
+ ## TypeScript Interfaces
1414
+
1415
+ ### PaginationOptions
1416
+
1417
+ ```typescript
1418
+ interface PaginationOptions {
1419
+ page?: number;
1420
+ limit?: number;
1421
+ sort?: Sort;
1422
+ }
1423
+ ```
1424
+
1425
+ ### PaginatedResult
1426
+
1427
+ ```typescript
1428
+ interface PaginatedResult<T> {
1429
+ data: WithId<T>[];
1430
+ total: number;
1431
+ page: number;
1432
+ limit: number;
1433
+ totalPages: number;
1434
+ hasNext: boolean;
1435
+ hasPrev: boolean;
1436
+ }
1437
+ ```
1438
+
1439
+ ### RetryOptions
1440
+
1441
+ ```typescript
1442
+ interface RetryOptions {
1443
+ maxRetries?: number;
1444
+ retryDelay?: number;
1445
+ exponentialBackoff?: boolean;
1446
+ }
1447
+ ```
1448
+
1449
+ ### HelperConfig
1450
+
1451
+ ```typescript
1452
+ interface HelperConfig {
1453
+ inputs: InputConfig[];
1454
+ outputs: OutputConfig[];
1455
+ output?: {
1456
+ mode?: 'append' | 'replace';
1457
+ };
1458
+ progress?: {
1459
+ collection?: string;
1460
+ uniqueIndexKeys?: string[];
1461
+ provider?: string;
1462
+ };
1463
+ }
1464
+
1465
+ interface InputConfig {
1466
+ ref: string;
1467
+ collection: string;
1468
+ query?: Filter<any>;
1469
+ }
1470
+
1471
+ interface OutputConfig {
1472
+ ref: string;
1473
+ collection: string;
1474
+ keys?: string[];
1475
+ mode?: 'append' | 'replace';
1476
+ }
1477
+ ```
1478
+
1479
+ ### WriteByRefResult
1480
+
1481
+ ```typescript
1482
+ interface WriteByRefResult {
1483
+ inserted: number;
1484
+ updated: number;
1485
+ errors: Array<{ index: number; error: Error; doc?: any }>;
1486
+ indexCreated: boolean;
1487
+ }
1488
+ ```
1489
+
1490
+ ### EnsureSignatureIndexResult
1491
+
1492
+ ```typescript
1493
+ interface EnsureSignatureIndexResult {
1494
+ created: boolean;
1495
+ indexName: string;
1496
+ }
1497
+ ```
1498
+
1499
+ ### Progress Tracking Interfaces
1500
+
1501
+ ```typescript
1502
+ interface StageIdentity {
1503
+ key: string;
1504
+ process?: string;
1505
+ provider?: string;
1506
+ name?: string;
1507
+ }
1508
+
1509
+ interface StageMetadata {
1510
+ itemCount?: number;
1511
+ errorCount?: number;
1512
+ durationMs?: number;
1513
+ [key: string]: any;
1514
+ }
1515
+
1516
+ interface StageRecord extends StageIdentity {
1517
+ completed: boolean;
1518
+ startedAt?: Date;
1519
+ completedAt?: Date;
1520
+ metadata?: StageMetadata;
1521
+ }
1522
+
1523
+ interface WriteStageOptions {
1524
+ ensureIndex?: boolean;
1525
+ session?: ClientSession;
1526
+ complete?: {
1527
+ key: string;
1528
+ process?: string;
1529
+ name?: string;
1530
+ provider?: string;
1531
+ metadata?: StageMetadata;
1532
+ };
1533
+ }
1534
+
1535
+ interface WriteStageResult extends WriteByRefResult {
1536
+ completed?: boolean;
1537
+ }
1538
+ ```
1539
+
1540
+ ### Merge Collections Interfaces
1541
+
1542
+ ```typescript
1543
+ interface MergeCollectionsOptions {
1544
+ sourceCollection1: string;
1545
+ sourceCollection2: string;
1546
+ targetCollection: string;
1547
+ strategy: 'index' | 'key' | 'composite';
1548
+ key?: string;
1549
+ compositeKeys?: string[];
1550
+ joinType?: 'inner' | 'left' | 'right' | 'outer'; // SQL-style join type
1551
+ fieldPrefix1?: string;
1552
+ fieldPrefix2?: string;
1553
+ includeIndex?: boolean;
1554
+ onUnmatched1?: 'include' | 'skip'; // Deprecated: use joinType instead
1555
+ onUnmatched2?: 'include' | 'skip'; // Deprecated: use joinType instead
1556
+ session?: ClientSession;
1557
+ }
1558
+
1559
+ interface MergeCollectionsResult {
1560
+ merged: number;
1561
+ unmatched1: number;
1562
+ unmatched2: number;
1563
+ errors: Array<{ index: number; error: Error; doc?: any }>;
1564
+ }
1565
+ ```
1566
+
1567
+ ## Error Handling
1568
+
1569
+ All methods throw errors with descriptive messages. Always wrap operations in try-catch blocks:
1570
+
1571
+ ```typescript
1572
+ try {
1573
+ await helper.initialize();
1574
+ const users = await helper.loadCollection('users');
1575
+ } catch (error) {
1576
+ console.error('Operation failed:', error.message);
1577
+ }
1578
+ ```
1579
+
1580
+ ## Best Practices
1581
+
1582
+ 1. **Always initialize before use:**
1583
+ ```typescript
1584
+ await helper.initialize();
1585
+ ```
1586
+
1587
+ 2. **Use transactions for multi-operation consistency:**
1588
+ ```typescript
1589
+ await helper.withTransaction(async (session) => {
1590
+ // Multiple operations
1591
+ });
1592
+ ```
1593
+
1594
+ 3. **Use pagination for large datasets:**
1595
+ ```typescript
1596
+ const result = await helper.loadCollection('users', {}, { page: 1, limit: 50 });
1597
+ ```
1598
+
1599
+ 4. **Create indexes for frequently queried fields:**
1600
+ ```typescript
1601
+ await helper.createIndex('users', { email: 1 }, { unique: true });
1602
+ ```
1603
+
1604
+ 5. **Disconnect when done (optional but recommended):**
1605
+ ```typescript
1606
+ await helper.disconnect();
1607
+ ```
1608
+ **Note:** Connections automatically close on app exit (SIGINT/SIGTERM), but explicit disconnection is recommended for better control and immediate cleanup.
1609
+
1610
+ ## ERC 2.0 Compliance
1611
+
1612
+ ✅ **Auto-discovers configuration from environment variables**
1613
+ ✅ **Type-safe with automatic coercion and validation**
1614
+ ✅ **All dependency requirements documented**
1615
+ ✅ **Transitive requirements automatically merged**
1616
+
1617
+ **Dependencies:**
1618
+ - ✅ `nx-config2` (Configuration engine)
1619
+ - â„šī¸ `mongodb` (non-ERC) - requirements manually documented
1620
+ - â„šī¸ `micro-logs` (non-ERC) - no environment variables required
1621
+
1622
+ **Verification:**
1623
+ ```bash
1624
+ npx nx-config2 erc-verify
1625
+ ```
1626
+
1627
+ ## License
1628
+
1629
+ ISC
1630
+
1631
+ ## Contributing
1632
+
1633
+ Contributions are welcome! Please feel free to submit a Pull Request.
1634
+
1635
+ ## Changelog
1636
+
1637
+ ### 4.0.1
1638
+ - **ES Module Support**: Converted package to ES module format (`"type": "module"`)
1639
+ - Fixed compatibility issue with ES module dependencies (e.g., `micro-logs`)
1640
+ - Updated TypeScript configuration to use `NodeNext` module resolution
1641
+ - Updated package exports to properly support ES module imports
1642
+ - **Breaking Change**: Package now uses ES module syntax; CommonJS `require()` is no longer supported
1643
+
1644
+ ### 4.0.0
1645
+ - **ERC 2.0 Compliance**: Added zero-config initialization via environment variables
1646
+ - Integrated `nx-config2` with ERC mode for automatic configuration discovery
1647
+ - Added automatic manifest and `.env.example` generation
1648
+ - Support for both zero-config and advanced (programmatic) configuration modes
1649
+
1650
+ ### 3.8.1
1651
+ - **Fixed `testConnection()` error reporting bug**: Improved error extraction from MongoDB error objects to prevent `[object Object]` display
1652
+ - Enhanced error handling to extract MongoDB-specific error properties (`code`, `codeName`, `errmsg`)
1653
+ - Added Windows-specific troubleshooting hints for localhost connection issues (suggests using `127.0.0.1` instead of `localhost`)
1654
+ - Improved error messages with error codes and types for better debugging
1655
+ - Updated documentation with better error handling examples
1656
+
1657
+ ### 3.8.0
1658
+ - **Database selection via ref/type map**: Added config-driven database selection using `ref` and `type` parameters
1659
+ - Added `databases` array to `HelperConfig` for mapping ref/type combinations to database names
1660
+ - All CRUD operations now support optional `ref` and `type` parameters for automatic database resolution
1661
+ - Database resolution priority: direct `database` parameter > `ref` + `type` > `ref` alone > `type` alone
1662
+ - Throws descriptive errors when no match or multiple matches found in database map
1663
+ - Updated Progress API to support database resolution via ref/type
1664
+ - Updated `loadByRef`, `writeByRef`, `writeStage`, and `mergeCollections` to support database map resolution
1665
+
1666
+ ### 3.7.0
1667
+ - **Separated database from connection string**: Database name is now specified per operation, not in connection string
1668
+ - **Multi-database support**: All operations accept optional `database` parameter (defaults to `'admin'`)
1669
+ - Connection string database name is automatically stripped if present (e.g., `mongodb://localhost:27017/admin` becomes `mongodb://localhost:27017/`)
1670
+ - Updated all methods (`insert`, `update`, `delete`, `loadCollection`, `findOne`, `countDocuments`, `aggregate`, `createIndex`, `dropIndex`, `listIndexes`, `writeByRef`, `loadByRef`, `mergeCollections`, `writeStage`, and progress API) to support per-operation database selection
1671
+ - **Breaking change**: No backward compatibility - all code must be updated to use new database parameter
1672
+
1673
+ ### 3.6.0
1674
+ - **Automatic connection cleanup**: Connections now automatically close on app exit (SIGINT, SIGTERM, beforeExit)
1675
+ - **Multi-instance support**: Global registry handles multiple `SimpleMongoHelper` instances gracefully
1676
+ - **Timeout protection**: 5-second timeout prevents hanging during automatic cleanup
1677
+ - Connections are properly managed and cleaned up even if users forget to call `disconnect()`
1678
+
1679
+ ### 3.5.0
1680
+ - Enhanced `mergeCollections()` with SQL-style join types (`inner`, `left`, `right`, `outer`)
1681
+ - **Multiple match handling**: Now creates multiple rows when keys have duplicates (SQL-style behavior)
1682
+ - Improved key-based and composite-key merging to handle one-to-many and many-to-many relationships
1683
+ - Added explicit join type control for better clarity and SQL compatibility
1684
+ - Legacy `onUnmatched1`/`onUnmatched2` flags deprecated in favor of `joinType` parameter
1685
+
1686
+ ### 3.4.0
1687
+ - Added `mergeCollections()` method for merging two collections into a new target collection
1688
+ - Supports three merge strategies: index-based, key-based, and composite-key merging
1689
+ - Index-based merging for same-order collections
1690
+ - Key-based merging using unique identifiers (supports dot notation)
1691
+ - Composite-key merging using multiple fields (e.g., name + ports + zones)
1692
+ - Configurable field prefixes and unmatched record handling
1693
+ - Transaction support for atomic merge operations
1694
+
1695
+ ### 3.3.0
1696
+ - Added `testConnection()` method for detailed connection testing and error diagnostics
1697
+ - Package renamed from `nx-mongodb-helper` to `nx-mongo` (shorter, cleaner name)
1698
+ - Connection test provides detailed error messages for missing credentials, invalid connection strings, authentication failures, and network issues
1699
+
1700
+ ### 3.2.0
1701
+ - Added process-scoped stages support - stages can now be scoped by process identifier
1702
+ - Updated default unique index to include `process` field: `['process', 'provider', 'key']`
1703
+ - All ProgressAPI methods now accept `process` parameter for process-scoped stage tracking
1704
+ - Updated `writeStage()` to support process-scoped completion
1705
+ - Stages with the same key can exist independently in different processes
1706
+
1707
+ ### 3.1.0
1708
+ - Added built-in progress tracking API (`helper.progress`) for provider-defined pipeline stages
1709
+ - Added `writeStage()` method that combines document writing with stage completion
1710
+ - Added progress tracking configuration to `HelperConfig` (collection, uniqueIndexKeys, provider)
1711
+ - Progress API supports idempotent operations, transactions, and provider namespaces
1712
+ - All progress operations support optional transaction sessions for atomicity
1713
+
1714
+ ### 3.0.0
1715
+ - Added config-driven ref mapping (HelperConfig, InputConfig, OutputConfig)
1716
+ - Added signature-based deduplication with automatic index management
1717
+ - Added `loadByRef()` method for loading data by ref name
1718
+ - Added `writeByRef()` method with signature computation, bulk upsert, and append/replace modes
1719
+ - Added `ensureSignatureIndex()` method for signature index management
1720
+ - Added `useConfig()` method for runtime config updates
1721
+ - Added `getByDotPath()` utility function for dot-notation path extraction with array wildcards
1722
+ - Added `computeSignature()` utility function for deterministic document signatures
1723
+ - Enhanced constructor to accept optional config parameter
1724
+ - All new methods support transaction sessions
1725
+
1726
+ ### 2.0.1
1727
+ - Package renamed from `nx-mongodb-helper` to `nx-mongo`
1728
+ - Add version number to README header
1729
+
1730
+ ### 2.0.0
1731
+ - Added delete operations
1732
+ - Added findOne operation
1733
+ - Added count operations (countDocuments, estimatedDocumentCount)
1734
+ - Added pagination support
1735
+ - Added aggregation pipeline support
1736
+ - Added transaction support
1737
+ - Added connection retry logic with exponential backoff
1738
+ - Added index management (createIndex, dropIndex, listIndexes)
1739
+ - Enhanced insert and update methods with session support
1740
+
1741
+ ### 1.0.0
1742
+ - Initial release with basic CRUD operations
1743
+