nx-mongo 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1144 @@
1
+ # nx-mongo
2
+
3
+ **Version:** 3.3.0
4
+
5
+ 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.
6
+
7
+ ## Features
8
+
9
+ - ✅ **Simple API** - Easy-to-use methods for common MongoDB operations
10
+ - ✅ **TypeScript Support** - Full TypeScript support with type safety
11
+ - ✅ **Connection Retry** - Automatic retry with exponential backoff
12
+ - ✅ **Pagination** - Built-in pagination support with metadata
13
+ - ✅ **Transactions** - Full transaction support for multi-operation consistency
14
+ - ✅ **Aggregation** - Complete aggregation pipeline support
15
+ - ✅ **Index Management** - Create, drop, and list indexes
16
+ - ✅ **Count Operations** - Accurate and estimated document counting
17
+ - ✅ **Session Support** - Transaction sessions for complex operations
18
+ - ✅ **Config-driven Ref Mapping** - Map application-level refs to MongoDB collections
19
+ - ✅ **Signature-based Deduplication** - Automatic duplicate prevention using document signatures
20
+ - ✅ **Append/Replace Modes** - Flexible write modes for data pipelines
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install nx-mongo
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import { SimpleMongoHelper } from 'nx-mongo';
32
+
33
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/my-database');
34
+
35
+ // Initialize connection
36
+ await helper.initialize();
37
+
38
+ // Insert a document
39
+ await helper.insert('users', {
40
+ name: 'John Doe',
41
+ email: 'john@example.com',
42
+ age: 30
43
+ });
44
+
45
+ // Find documents
46
+ const users = await helper.loadCollection('users');
47
+
48
+ // Find one document
49
+ const user = await helper.findOne('users', { email: 'john@example.com' });
50
+
51
+ // Update document
52
+ await helper.update(
53
+ 'users',
54
+ { email: 'john@example.com' },
55
+ { $set: { age: 31 } }
56
+ );
57
+
58
+ // Delete document
59
+ await helper.delete('users', { email: 'john@example.com' });
60
+
61
+ // Disconnect
62
+ await helper.disconnect();
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### Constructor
68
+
69
+ ```typescript
70
+ new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions)
71
+ ```
72
+
73
+ **Parameters:**
74
+ - `connectionString` - MongoDB connection string
75
+ - `retryOptions` (optional) - Retry configuration
76
+ - `maxRetries?: number` - Maximum retry attempts (default: 3)
77
+ - `retryDelay?: number` - Initial retry delay in ms (default: 1000)
78
+ - `exponentialBackoff?: boolean` - Use exponential backoff (default: true)
79
+
80
+ **Example:**
81
+ ```typescript
82
+ const helper = new SimpleMongoHelper(
83
+ 'mongodb://localhost:27017/my-db',
84
+ { maxRetries: 5, retryDelay: 2000 }
85
+ );
86
+ ```
87
+
88
+ ### Connection Methods
89
+
90
+ #### `testConnection(): Promise<{ success: boolean; error?: { type: string; message: string; details?: string } }>`
91
+
92
+ Tests the MongoDB connection and returns detailed error information if it fails. This method does not establish a persistent connection - use `initialize()` for that.
93
+
94
+ **Returns:**
95
+ - `success: boolean` - Whether the connection test succeeded
96
+ - `error?: object` - Error details if connection failed
97
+ - `type` - Error type: `'missing_credentials' | 'invalid_connection_string' | 'connection_failed' | 'authentication_failed' | 'config_error' | 'unknown'`
98
+ - `message` - Human-readable error message
99
+ - `details` - Detailed error information and troubleshooting tips
100
+
101
+ **Example:**
102
+ ```typescript
103
+ const result = await helper.testConnection();
104
+ if (!result.success) {
105
+ console.error('Connection test failed:', result.error?.message);
106
+ console.error('Details:', result.error?.details);
107
+ // Handle error based on result.error?.type
108
+ } else {
109
+ console.log('Connection test passed!');
110
+ await helper.initialize();
111
+ }
112
+ ```
113
+
114
+ **Error Types:**
115
+ - `missing_credentials` - Username or password missing in connection string
116
+ - `invalid_connection_string` - Connection string format is invalid
117
+ - `connection_failed` - Cannot reach MongoDB server (timeout, DNS, network, etc.)
118
+ - `authentication_failed` - Invalid credentials or insufficient permissions
119
+ - `config_error` - Configuration issues
120
+ - `unknown` - Unexpected error
121
+
122
+ #### `initialize(): Promise<void>`
123
+
124
+ Establishes MongoDB connection with automatic retry logic. Must be called before using other methods.
125
+
126
+ ```typescript
127
+ await helper.initialize();
128
+ ```
129
+
130
+ #### `disconnect(): Promise<void>`
131
+
132
+ Closes the MongoDB connection and cleans up resources.
133
+
134
+ ```typescript
135
+ await helper.disconnect();
136
+ ```
137
+
138
+ ### Query Methods
139
+
140
+ #### `loadCollection<T>(collectionName: string, query?: Filter<T>, options?: PaginationOptions): Promise<WithId<T>[] | PaginatedResult<T>>`
141
+
142
+ Loads documents from a collection with optional query filter and pagination.
143
+
144
+ **Parameters:**
145
+ - `collectionName` - Name of the collection
146
+ - `query` (optional) - MongoDB query filter
147
+ - `options` (optional) - Pagination and sorting options
148
+ - `page?: number` - Page number (1-indexed)
149
+ - `limit?: number` - Documents per page
150
+ - `sort?: Sort` - Sort specification
151
+
152
+ **Returns:**
153
+ - Without pagination: `WithId<T>[]`
154
+ - With pagination: `PaginatedResult<T>` with metadata
155
+
156
+ **Examples:**
157
+ ```typescript
158
+ // Load all documents
159
+ const allUsers = await helper.loadCollection('users');
160
+
161
+ // Load with query
162
+ const activeUsers = await helper.loadCollection('users', { active: true });
163
+
164
+ // Load with pagination
165
+ const result = await helper.loadCollection('users', {}, {
166
+ page: 1,
167
+ limit: 10,
168
+ sort: { createdAt: -1 }
169
+ });
170
+ // result.data - array of documents
171
+ // result.total - total count
172
+ // result.page - current page
173
+ // result.totalPages - total pages
174
+ // result.hasNext - has next page
175
+ // result.hasPrev - has previous page
176
+ ```
177
+
178
+ #### `findOne<T>(collectionName: string, query: Filter<T>, options?: { sort?: Sort; projection?: Document }): Promise<WithId<T> | null>`
179
+
180
+ Finds a single document in a collection.
181
+
182
+ **Parameters:**
183
+ - `collectionName` - Name of the collection
184
+ - `query` - MongoDB query filter
185
+ - `options` (optional) - Find options
186
+ - `sort?: Sort` - Sort specification
187
+ - `projection?: Document` - Field projection
188
+
189
+ **Example:**
190
+ ```typescript
191
+ const user = await helper.findOne('users', { email: 'john@example.com' });
192
+ const latestUser = await helper.findOne('users', {}, { sort: { createdAt: -1 } });
193
+ ```
194
+
195
+ ### Insert Methods
196
+
197
+ #### `insert<T>(collectionName: string, data: T | T[], options?: { session?: ClientSession }): Promise<any>`
198
+
199
+ Inserts one or more documents into a collection.
200
+
201
+ **Parameters:**
202
+ - `collectionName` - Name of the collection
203
+ - `data` - Single document or array of documents
204
+ - `options` (optional) - Insert options
205
+ - `session?: ClientSession` - Transaction session
206
+
207
+ **Examples:**
208
+ ```typescript
209
+ // Insert single document
210
+ await helper.insert('users', {
211
+ name: 'John Doe',
212
+ email: 'john@example.com'
213
+ });
214
+
215
+ // Insert multiple documents
216
+ await helper.insert('users', [
217
+ { name: 'John', email: 'john@example.com' },
218
+ { name: 'Jane', email: 'jane@example.com' }
219
+ ]);
220
+
221
+ // Insert within transaction
222
+ const session = helper.startSession();
223
+ await session.withTransaction(async () => {
224
+ await helper.insert('users', { name: 'John' }, { session });
225
+ });
226
+ ```
227
+
228
+ ### Update Methods
229
+
230
+ #### `update<T>(collectionName: string, filter: Filter<T>, updateData: UpdateFilter<T>, options?: { upsert?: boolean; multi?: boolean; session?: ClientSession }): Promise<any>`
231
+
232
+ Updates documents in a collection.
233
+
234
+ **Parameters:**
235
+ - `collectionName` - Name of the collection
236
+ - `filter` - MongoDB query filter
237
+ - `updateData` - Update operations
238
+ - `options` (optional) - Update options
239
+ - `upsert?: boolean` - Create if not exists
240
+ - `multi?: boolean` - Update multiple documents (default: false)
241
+ - `session?: ClientSession` - Transaction session
242
+
243
+ **Examples:**
244
+ ```typescript
245
+ // Update single document
246
+ await helper.update(
247
+ 'users',
248
+ { email: 'john@example.com' },
249
+ { $set: { age: 31 } }
250
+ );
251
+
252
+ // Update multiple documents
253
+ await helper.update(
254
+ 'users',
255
+ { role: 'user' },
256
+ { $set: { lastLogin: new Date() } },
257
+ { multi: true }
258
+ );
259
+
260
+ // Upsert (create if not exists)
261
+ await helper.update(
262
+ 'users',
263
+ { email: 'john@example.com' },
264
+ { $set: { name: 'John Doe', email: 'john@example.com' } },
265
+ { upsert: true }
266
+ );
267
+ ```
268
+
269
+ ### Delete Methods
270
+
271
+ #### `delete<T>(collectionName: string, filter: Filter<T>, options?: { multi?: boolean }): Promise<any>`
272
+
273
+ Deletes documents from a collection.
274
+
275
+ **Parameters:**
276
+ - `collectionName` - Name of the collection
277
+ - `filter` - MongoDB query filter
278
+ - `options` (optional) - Delete options
279
+ - `multi?: boolean` - Delete multiple documents (default: false)
280
+
281
+ **Examples:**
282
+ ```typescript
283
+ // Delete single document
284
+ await helper.delete('users', { email: 'john@example.com' });
285
+
286
+ // Delete multiple documents
287
+ await helper.delete('users', { role: 'guest' }, { multi: true });
288
+ ```
289
+
290
+ ### Count Methods
291
+
292
+ #### `countDocuments<T>(collectionName: string, query?: Filter<T>): Promise<number>`
293
+
294
+ Counts documents matching a query (accurate count).
295
+
296
+ **Example:**
297
+ ```typescript
298
+ const userCount = await helper.countDocuments('users');
299
+ const activeUserCount = await helper.countDocuments('users', { active: true });
300
+ ```
301
+
302
+ #### `estimatedDocumentCount(collectionName: string): Promise<number>`
303
+
304
+ Gets estimated document count (faster but less accurate).
305
+
306
+ **Example:**
307
+ ```typescript
308
+ const estimatedCount = await helper.estimatedDocumentCount('users');
309
+ ```
310
+
311
+ ### Aggregation Methods
312
+
313
+ #### `aggregate<T>(collectionName: string, pipeline: Document[]): Promise<T[]>`
314
+
315
+ Runs an aggregation pipeline on a collection.
316
+
317
+ **Example:**
318
+ ```typescript
319
+ const result = await helper.aggregate('orders', [
320
+ { $match: { status: 'completed' } },
321
+ { $group: {
322
+ _id: '$customerId',
323
+ total: { $sum: '$amount' },
324
+ count: { $sum: 1 }
325
+ }},
326
+ { $sort: { total: -1 } }
327
+ ]);
328
+ ```
329
+
330
+ ### Index Methods
331
+
332
+ #### `createIndex(collectionName: string, indexSpec: IndexSpecification, options?: CreateIndexesOptions): Promise<string>`
333
+
334
+ Creates an index on a collection.
335
+
336
+ **Example:**
337
+ ```typescript
338
+ // Simple index
339
+ await helper.createIndex('users', { email: 1 });
340
+
341
+ // Unique index
342
+ await helper.createIndex('users', { email: 1 }, { unique: true });
343
+
344
+ // Compound index
345
+ await helper.createIndex('users', { email: 1, createdAt: -1 });
346
+ ```
347
+
348
+ #### `dropIndex(collectionName: string, indexName: string): Promise<any>`
349
+
350
+ Drops an index from a collection.
351
+
352
+ **Example:**
353
+ ```typescript
354
+ await helper.dropIndex('users', 'email_1');
355
+ ```
356
+
357
+ #### `listIndexes(collectionName: string): Promise<Document[]>`
358
+
359
+ Lists all indexes on a collection.
360
+
361
+ **Example:**
362
+ ```typescript
363
+ const indexes = await helper.listIndexes('users');
364
+ indexes.forEach(idx => console.log(idx.name));
365
+ ```
366
+
367
+ ### Transaction Methods
368
+
369
+ #### `startSession(): ClientSession`
370
+
371
+ Starts a new client session for transactions.
372
+
373
+ **Example:**
374
+ ```typescript
375
+ const session = helper.startSession();
376
+ ```
377
+
378
+ #### `withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>`
379
+
380
+ Executes a function within a transaction.
381
+
382
+ **Example:**
383
+ ```typescript
384
+ await helper.withTransaction(async (session) => {
385
+ await helper.insert('users', { name: 'John' }, { session });
386
+ await helper.update('accounts', { userId: '123' }, { $inc: { balance: 100 } }, { session });
387
+ return 'Transaction completed';
388
+ });
389
+ ```
390
+
391
+ **Note:** Transactions require a MongoDB replica set or sharded cluster.
392
+
393
+ ## Config-driven Ref Mapping and Signature-based Deduplication
394
+
395
+ ### Overview
396
+
397
+ 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.
398
+
399
+ ### Configuration Schema
400
+
401
+ ```typescript
402
+ interface HelperConfig {
403
+ inputs: Array<{
404
+ ref: string; // Application-level reference name
405
+ collection: string; // MongoDB collection name
406
+ query?: Filter<any>; // Optional MongoDB query filter
407
+ }>;
408
+ outputs: Array<{
409
+ ref: string; // Application-level reference name
410
+ collection: string; // MongoDB collection name
411
+ keys?: string[]; // Optional: dot-paths for signature generation
412
+ mode?: "append" | "replace"; // Optional: write mode (default from global)
413
+ }>;
414
+ output?: {
415
+ mode?: "append" | "replace"; // Global default mode (default: "append")
416
+ };
417
+ progress?: {
418
+ collection?: string; // Progress collection name (default: "progress_states")
419
+ uniqueIndexKeys?: string[]; // Unique index keys (default: ["provider","key"])
420
+ provider?: string; // Default provider namespace for this helper instance
421
+ };
422
+ }
423
+ ```
424
+
425
+ **Example Configuration:**
426
+
427
+ ```typescript
428
+ const config = {
429
+ inputs: [
430
+ { ref: "topology", collection: "topology-definition-neo-data", query: {} },
431
+ { ref: "vulnerabilities", collection: "vulnerabilities-data", query: { severity: { "$in": ["high","critical"] } } },
432
+ { ref: "entities", collection: "entities-data" },
433
+ { ref: "crownJewels", collection: "entities-data", query: { type: "crown_jewel" } }
434
+ ],
435
+ outputs: [
436
+ { ref: "paths", collection: "paths-neo-data", keys: ["segments[]","edges[].from","edges[].to","target_role"], mode: "append" },
437
+ { ref: "prioritizedPaths", collection: "prioritized_paths-neo-data", keys: ["segments[]","outside","contains_crown_jewel"], mode: "replace" },
438
+ { ref: "assetPaths", collection: "asset_paths-neo-data", keys: ["asset_ip","segments[]"], mode: "append" }
439
+ ],
440
+ output: { mode: "append" }
441
+ };
442
+ ```
443
+
444
+ ### Constructor with Config
445
+
446
+ ```typescript
447
+ new SimpleMongoHelper(connectionString: string, retryOptions?: RetryOptions, config?: HelperConfig)
448
+ ```
449
+
450
+ **Example:**
451
+
452
+ ```typescript
453
+ const helper = new SimpleMongoHelper(
454
+ 'mongodb://localhost:27017/my-db',
455
+ { maxRetries: 5 },
456
+ config
457
+ );
458
+ ```
459
+
460
+ ### Config Methods
461
+
462
+ #### `useConfig(config: HelperConfig): this`
463
+
464
+ Sets or updates the configuration for ref-based operations.
465
+
466
+ ```typescript
467
+ helper.useConfig(config);
468
+ ```
469
+
470
+ ### Ref-based Operations
471
+
472
+ #### `loadByRef<T>(ref: string, options?: PaginationOptions & { session?: ClientSession }): Promise<WithId<T>[] | PaginatedResult<T>>`
473
+
474
+ Loads data from a collection using a ref name from the configuration.
475
+
476
+ **Parameters:**
477
+ - `ref` - Application-level reference name (must exist in config.inputs)
478
+ - `options` (optional) - Pagination and session options
479
+
480
+ **Example:**
481
+
482
+ ```typescript
483
+ // Load using ref (applies query automatically)
484
+ const topology = await helper.loadByRef('topology');
485
+ const vulns = await helper.loadByRef('vulnerabilities');
486
+
487
+ // With pagination
488
+ const result = await helper.loadByRef('topology', {
489
+ page: 1,
490
+ limit: 10,
491
+ sort: { createdAt: -1 }
492
+ });
493
+ ```
494
+
495
+ #### `writeByRef(ref: string, documents: any[], options?: { session?: ClientSession; ensureIndex?: boolean }): Promise<WriteByRefResult>`
496
+
497
+ Writes documents to a collection using a ref name from the configuration. Supports signature-based deduplication and append/replace modes.
498
+
499
+ **Parameters:**
500
+ - `ref` - Application-level reference name (must exist in config.outputs)
501
+ - `documents` - Array of documents to write
502
+ - `options` (optional) - Write options
503
+ - `session?: ClientSession` - Transaction session
504
+ - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
505
+
506
+ **Returns:**
507
+
508
+ ```typescript
509
+ interface WriteByRefResult {
510
+ inserted: number;
511
+ updated: number;
512
+ errors: Array<{ index: number; error: Error; doc?: any }>;
513
+ indexCreated: boolean;
514
+ }
515
+ ```
516
+
517
+ **Example:**
518
+
519
+ ```typescript
520
+ // Write using ref (automatic deduplication, uses keys from config)
521
+ const result = await helper.writeByRef('paths', pathDocuments);
522
+ console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
523
+ console.log(`Index created: ${result.indexCreated}`);
524
+
525
+ // Replace mode (clears collection first)
526
+ await helper.writeByRef('prioritizedPaths', prioritizedDocs);
527
+ ```
528
+
529
+ #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
530
+
531
+ Writes documents to a collection and optionally marks a stage as complete atomically. See the [Progress Tracking](#progress-tracking) section for details and examples.
532
+
533
+ **Example:**
534
+
535
+ ```typescript
536
+ // Write and mark stage complete in one call
537
+ await helper.writeStage('tier1', documents, {
538
+ complete: {
539
+ key: 'tier1',
540
+ process: 'processA',
541
+ name: 'System Inventory',
542
+ provider: 'nessus',
543
+ metadata: { itemCount: documents.length }
544
+ }
545
+ });
546
+ ```
547
+
548
+ ### Signature Index Management
549
+
550
+ #### `ensureSignatureIndex(collectionName: string, options?: { fieldName?: string; unique?: boolean }): Promise<EnsureSignatureIndexResult>`
551
+
552
+ Ensures a unique index exists on the signature field for signature-based deduplication.
553
+
554
+ **Parameters:**
555
+ - `collectionName` - Name of the collection
556
+ - `options` (optional) - Index configuration
557
+ - `fieldName?: string` - Field name for signature (default: "_sig")
558
+ - `unique?: boolean` - Whether index should be unique (default: true)
559
+
560
+ **Returns:**
561
+
562
+ ```typescript
563
+ interface EnsureSignatureIndexResult {
564
+ created: boolean;
565
+ indexName: string;
566
+ }
567
+ ```
568
+
569
+ **Example:**
570
+
571
+ ```typescript
572
+ const result = await helper.ensureSignatureIndex('paths-neo-data');
573
+ console.log(`Index created: ${result.created}, Name: ${result.indexName}`);
574
+ ```
575
+
576
+ ## Progress Tracking
577
+
578
+ ### Overview
579
+
580
+ The helper provides built-in support for tracking provider-defined pipeline stages. This enables applications to:
581
+ - Track completion status of different stages (e.g., "tier1", "tier2", "enrichment")
582
+ - Skip already-completed stages on resumption
583
+ - Atomically write documents and mark stages complete
584
+ - Support multi-provider databases with provider namespaces
585
+
586
+ ### Configuration
587
+
588
+ Progress tracking is configured via the `progress` option in `HelperConfig`:
589
+
590
+ ```typescript
591
+ const config = {
592
+ // ... inputs and outputs
593
+ progress: {
594
+ collection: "progress_states", // Optional: default "progress_states"
595
+ uniqueIndexKeys: ["process", "provider", "key"], // Optional: default ["process","provider","key"]
596
+ provider: "nessus" // Optional: default provider for this instance
597
+ }
598
+ };
599
+ ```
600
+
601
+ ### Progress API
602
+
603
+ The progress API is available via `helper.progress`:
604
+
605
+ #### `isCompleted(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<boolean>`
606
+
607
+ Checks if a stage is completed. Stages are scoped by process, so the same key can exist in different processes.
608
+
609
+ **Example:**
610
+ ```typescript
611
+ // Check stage in a specific process
612
+ if (await helper.progress.isCompleted('tier1', { process: 'processA', provider: 'nessus' })) {
613
+ console.log('Stage "tier1" in processA already completed, skipping...');
614
+ }
615
+
616
+ // Same key, different process
617
+ if (await helper.progress.isCompleted('tier1', { process: 'processB', provider: 'nessus' })) {
618
+ console.log('Stage "tier1" in processB already completed, skipping...');
619
+ }
620
+ ```
621
+
622
+ #### `start(identity: StageIdentity, options?: { session?: ClientSession }): Promise<void>`
623
+
624
+ Marks a stage as started. Idempotent - safe to call multiple times. Stages are scoped by process.
625
+
626
+ **Example:**
627
+ ```typescript
628
+ await helper.progress.start({
629
+ key: 'tier1',
630
+ process: 'processA',
631
+ name: 'System Inventory',
632
+ provider: 'nessus'
633
+ });
634
+ ```
635
+
636
+ #### `complete(identity: StageIdentity & { metadata?: StageMetadata }, options?: { session?: ClientSession }): Promise<void>`
637
+
638
+ Marks a stage as completed with optional metadata. Idempotent - safe to call multiple times. Stages are scoped by process.
639
+
640
+ **Example:**
641
+ ```typescript
642
+ await helper.progress.complete({
643
+ key: 'tier1',
644
+ process: 'processA',
645
+ name: 'System Inventory',
646
+ provider: 'nessus',
647
+ metadata: {
648
+ itemCount: 150,
649
+ durationMs: 5000
650
+ }
651
+ });
652
+ ```
653
+
654
+ #### `getCompleted(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<Array<{ key: string; name?: string; completedAt?: Date }>>`
655
+
656
+ Gets a list of all completed stages, optionally filtered by process and/or provider.
657
+
658
+ **Example:**
659
+ ```typescript
660
+ // Get all completed stages for a specific process
661
+ const completed = await helper.progress.getCompleted({ process: 'processA', provider: 'nessus' });
662
+ // → [{ key: 'tier1', name: 'System Inventory', completedAt: Date }, ...]
663
+
664
+ // Get all completed stages across all processes for a provider
665
+ const allCompleted = await helper.progress.getCompleted({ provider: 'nessus' });
666
+ ```
667
+
668
+ #### `getProgress(options?: { process?: string; provider?: string; session?: ClientSession }): Promise<StageRecord[]>`
669
+
670
+ Gets all stage records (both completed and in-progress), optionally filtered by process and/or provider.
671
+
672
+ **Example:**
673
+ ```typescript
674
+ // Get all stages for a specific process
675
+ const allStages = await helper.progress.getProgress({ process: 'processA', provider: 'nessus' });
676
+
677
+ // Get all stages for a provider across all processes
678
+ const allProviderStages = await helper.progress.getProgress({ provider: 'nessus' });
679
+ ```
680
+
681
+ #### `reset(key: string, options?: { process?: string; provider?: string; session?: ClientSession }): Promise<void>`
682
+
683
+ Resets a stage to not-started state (clears completion status). Stages are scoped by process.
684
+
685
+ **Example:**
686
+ ```typescript
687
+ await helper.progress.reset('tier1', { process: 'processA', provider: 'nessus' });
688
+ ```
689
+
690
+ ### Stage-Aware Writes
691
+
692
+ #### `writeStage(ref: string, documents: any[], options?: WriteStageOptions): Promise<WriteStageResult>`
693
+
694
+ 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.
695
+
696
+ **Parameters:**
697
+ - `ref` - Application-level reference name (must exist in config.outputs)
698
+ - `documents` - Array of documents to write
699
+ - `options` (optional) - Write and completion options
700
+ - `ensureIndex?: boolean` - Whether to ensure signature index exists (default: true)
701
+ - `session?: ClientSession` - Transaction session (makes write and complete atomic)
702
+ - `complete?: { key: string; name?: string; provider?: string; metadata?: StageMetadata }` - Stage completion info
703
+
704
+ **Returns:**
705
+ ```typescript
706
+ interface WriteStageResult extends WriteByRefResult {
707
+ completed?: boolean; // true if stage was marked complete
708
+ }
709
+ ```
710
+
711
+ **Examples:**
712
+
713
+ ```typescript
714
+ // Skip completed stages, then save-and-complete in one call
715
+ const processName = 'processA';
716
+ if (!force && (await helper.progress.isCompleted('tier1', { process: processName, provider: 'nessus' }))) {
717
+ console.log('Skipping stage "tier1" in processA');
718
+ } else {
719
+ const docs = [
720
+ { type: 'server_status', ...status },
721
+ ...scanners.map(s => ({ type: 'scanner', ...s }))
722
+ ];
723
+ await helper.writeStage('tier1', docs, {
724
+ complete: {
725
+ key: 'tier1',
726
+ process: processName,
727
+ name: 'System Inventory',
728
+ provider: 'nessus',
729
+ metadata: { itemCount: docs.length }
730
+ }
731
+ });
732
+ }
733
+
734
+ // Transactional multi-write with explicit completion
735
+ const session = helper.startSession();
736
+ try {
737
+ await session.withTransaction(async () => {
738
+ await helper.writeByRef('tier2_scans', scans, { session });
739
+ await helper.writeByRef('tier2_hosts', hosts, { session });
740
+ await helper.progress.complete({
741
+ key: 'tier2',
742
+ process: 'processA',
743
+ name: 'Scan Inventory',
744
+ provider: 'nessus',
745
+ metadata: { itemCount: hosts.length }
746
+ }, { session });
747
+ });
748
+ } finally {
749
+ await session.endSession();
750
+ }
751
+ ```
752
+
753
+ ### Usage Patterns
754
+
755
+ #### Resumption Pattern
756
+
757
+ ```typescript
758
+ const processName = 'processA';
759
+ const stages = ['tier1', 'tier2', 'tier3'];
760
+
761
+ for (const stageKey of stages) {
762
+ if (await helper.progress.isCompleted(stageKey, { process: processName, provider: 'nessus' })) {
763
+ console.log(`Skipping completed stage: ${stageKey} in ${processName}`);
764
+ continue;
765
+ }
766
+
767
+ await helper.progress.start({ key: stageKey, process: processName, provider: 'nessus' });
768
+
769
+ try {
770
+ const docs = await processStage(stageKey);
771
+ await helper.writeStage(`ref_${stageKey}`, docs, {
772
+ complete: { key: stageKey, process: processName, provider: 'nessus' }
773
+ });
774
+ } catch (error) {
775
+ console.error(`Stage ${stageKey} in ${processName} failed:`, error);
776
+ // Stage remains incomplete, can be retried
777
+ }
778
+ }
779
+
780
+ // Different process can have same stage keys independently
781
+ const processB = 'processB';
782
+ if (!await helper.progress.isCompleted('tier1', { process: processB, provider: 'nessus' })) {
783
+ // Process B's tier1 is independent from Process A's tier1
784
+ await helper.progress.start({ key: 'tier1', process: processB, provider: 'nessus' });
785
+ }
786
+ ```
787
+
788
+ ### Utility Functions
789
+
790
+ #### `getByDotPath(value: any, path: string): any[]`
791
+
792
+ Extracts values from an object using dot-notation paths with array wildcard support.
793
+
794
+ **Parameters:**
795
+ - `value` - The object to extract values from
796
+ - `path` - Dot-notation path (e.g., "meta.id", "edges[].from", "segments[]")
797
+
798
+ **Returns:** Array of extracted values (flattened and deduplicated for arrays)
799
+
800
+ **Examples:**
801
+
802
+ ```typescript
803
+ import { getByDotPath } from 'nx-mongo';
804
+
805
+ // Simple path
806
+ getByDotPath({ meta: { id: "123" } }, "meta.id"); // ["123"]
807
+
808
+ // Array wildcard
809
+ getByDotPath({ segments: [1, 2, 3] }, "segments[]"); // [1, 2, 3]
810
+
811
+ // Nested array access
812
+ getByDotPath({ edges: [{ from: "A" }, { from: "B" }] }, "edges[].from"); // ["A", "B"]
813
+ ```
814
+
815
+ #### `computeSignature(doc: any, keys: string[], options?: { algorithm?: "sha256" | "sha1" | "md5" }): string`
816
+
817
+ Computes a deterministic signature for a document based on specified keys.
818
+
819
+ **Parameters:**
820
+ - `doc` - The document to compute signature for
821
+ - `keys` - Array of dot-notation paths to extract values from
822
+ - `options` (optional) - Configuration
823
+ - `algorithm?: "sha256" | "sha1" | "md5"` - Hash algorithm (default: "sha256")
824
+
825
+ **Returns:** Hex string signature
826
+
827
+ **Example:**
828
+
829
+ ```typescript
830
+ import { computeSignature } from 'nx-mongo';
831
+
832
+ const sig = computeSignature(
833
+ { segments: [1, 2], role: "admin" },
834
+ ["segments[]", "role"]
835
+ );
836
+ ```
837
+
838
+ ### Signature Algorithm
839
+
840
+ The signature generation follows these steps:
841
+
842
+ 1. **Extract values** for each key using `getByDotPath`
843
+ 2. **Normalize values**:
844
+ - **Strings**: As-is
845
+ - **Numbers**: `String(value)`
846
+ - **Booleans**: `"true"` or `"false"`
847
+ - **Dates**: `value.toISOString()` (UTC)
848
+ - **Null/Undefined**: `"null"`
849
+ - **Objects**: `JSON.stringify(value, Object.keys(value).sort())` (sorted keys)
850
+ - **Arrays**: Flatten recursively, normalize each element, deduplicate, sort lexicographically
851
+ 3. **Create canonical map**: `{ key1: [normalized values], key2: [normalized values], ... }`
852
+ 4. **Sort keys** alphabetically
853
+ 5. **Stringify**: `JSON.stringify(canonicalMap)`
854
+ 6. **Hash**: SHA-256 (or configurable algorithm)
855
+ 7. **Return**: Hex string
856
+
857
+ ### Usage Examples
858
+
859
+ #### Basic Usage with Config
860
+
861
+ ```typescript
862
+ import { SimpleMongoHelper } from 'nx-mongo';
863
+
864
+ const config = {
865
+ inputs: [
866
+ { ref: "topology", collection: "topology-definition", query: {} },
867
+ { ref: "vulnerabilities", collection: "vulnerabilities", query: { severity: "high" } }
868
+ ],
869
+ outputs: [
870
+ { ref: "paths", collection: "paths", keys: ["segments[]", "target_role"], mode: "append" },
871
+ { ref: "prioritizedPaths", collection: "prioritized_paths", keys: ["segments[]"], mode: "replace" }
872
+ ],
873
+ output: { mode: "append" }
874
+ };
875
+
876
+ const helper = new SimpleMongoHelper('mongodb://localhost:27017/mydb', undefined, config);
877
+ await helper.initialize();
878
+
879
+ // Load using ref (applies query automatically)
880
+ const topology = await helper.loadByRef('topology');
881
+ const vulns = await helper.loadByRef('vulnerabilities');
882
+
883
+ // Write using ref (automatic deduplication, uses keys from config)
884
+ const result = await helper.writeByRef('paths', pathDocuments);
885
+ console.log(`Inserted: ${result.inserted}, Updated: ${result.updated}`);
886
+
887
+ // Replace mode (clears collection first)
888
+ await helper.writeByRef('prioritizedPaths', prioritizedDocs);
889
+ ```
890
+
891
+ #### With Transactions
892
+
893
+ ```typescript
894
+ const session = helper.startSession();
895
+ try {
896
+ await session.withTransaction(async () => {
897
+ await helper.writeByRef('paths', docs, { session });
898
+ await helper.writeByRef('prioritizedPaths', prioDocs, { session });
899
+ });
900
+ } finally {
901
+ await session.endSession();
902
+ }
903
+ ```
904
+
905
+ #### Standalone Utilities
906
+
907
+ ```typescript
908
+ import { getByDotPath, computeSignature } from 'nx-mongo';
909
+
910
+ // Extract values
911
+ const values = getByDotPath(doc, "edges[].from"); // ["A", "B", "C"]
912
+
913
+ // Compute signature
914
+ const sig = computeSignature(doc, ["segments[]", "target_role"]);
915
+ ```
916
+
917
+ ## TypeScript Interfaces
918
+
919
+ ### PaginationOptions
920
+
921
+ ```typescript
922
+ interface PaginationOptions {
923
+ page?: number;
924
+ limit?: number;
925
+ sort?: Sort;
926
+ }
927
+ ```
928
+
929
+ ### PaginatedResult
930
+
931
+ ```typescript
932
+ interface PaginatedResult<T> {
933
+ data: WithId<T>[];
934
+ total: number;
935
+ page: number;
936
+ limit: number;
937
+ totalPages: number;
938
+ hasNext: boolean;
939
+ hasPrev: boolean;
940
+ }
941
+ ```
942
+
943
+ ### RetryOptions
944
+
945
+ ```typescript
946
+ interface RetryOptions {
947
+ maxRetries?: number;
948
+ retryDelay?: number;
949
+ exponentialBackoff?: boolean;
950
+ }
951
+ ```
952
+
953
+ ### HelperConfig
954
+
955
+ ```typescript
956
+ interface HelperConfig {
957
+ inputs: InputConfig[];
958
+ outputs: OutputConfig[];
959
+ output?: {
960
+ mode?: 'append' | 'replace';
961
+ };
962
+ progress?: {
963
+ collection?: string;
964
+ uniqueIndexKeys?: string[];
965
+ provider?: string;
966
+ };
967
+ }
968
+
969
+ interface InputConfig {
970
+ ref: string;
971
+ collection: string;
972
+ query?: Filter<any>;
973
+ }
974
+
975
+ interface OutputConfig {
976
+ ref: string;
977
+ collection: string;
978
+ keys?: string[];
979
+ mode?: 'append' | 'replace';
980
+ }
981
+ ```
982
+
983
+ ### WriteByRefResult
984
+
985
+ ```typescript
986
+ interface WriteByRefResult {
987
+ inserted: number;
988
+ updated: number;
989
+ errors: Array<{ index: number; error: Error; doc?: any }>;
990
+ indexCreated: boolean;
991
+ }
992
+ ```
993
+
994
+ ### EnsureSignatureIndexResult
995
+
996
+ ```typescript
997
+ interface EnsureSignatureIndexResult {
998
+ created: boolean;
999
+ indexName: string;
1000
+ }
1001
+ ```
1002
+
1003
+ ### Progress Tracking Interfaces
1004
+
1005
+ ```typescript
1006
+ interface StageIdentity {
1007
+ key: string;
1008
+ process?: string;
1009
+ provider?: string;
1010
+ name?: string;
1011
+ }
1012
+
1013
+ interface StageMetadata {
1014
+ itemCount?: number;
1015
+ errorCount?: number;
1016
+ durationMs?: number;
1017
+ [key: string]: any;
1018
+ }
1019
+
1020
+ interface StageRecord extends StageIdentity {
1021
+ completed: boolean;
1022
+ startedAt?: Date;
1023
+ completedAt?: Date;
1024
+ metadata?: StageMetadata;
1025
+ }
1026
+
1027
+ interface WriteStageOptions {
1028
+ ensureIndex?: boolean;
1029
+ session?: ClientSession;
1030
+ complete?: {
1031
+ key: string;
1032
+ process?: string;
1033
+ name?: string;
1034
+ provider?: string;
1035
+ metadata?: StageMetadata;
1036
+ };
1037
+ }
1038
+
1039
+ interface WriteStageResult extends WriteByRefResult {
1040
+ completed?: boolean;
1041
+ }
1042
+ ```
1043
+
1044
+ ## Error Handling
1045
+
1046
+ All methods throw errors with descriptive messages. Always wrap operations in try-catch blocks:
1047
+
1048
+ ```typescript
1049
+ try {
1050
+ await helper.initialize();
1051
+ const users = await helper.loadCollection('users');
1052
+ } catch (error) {
1053
+ console.error('Operation failed:', error.message);
1054
+ }
1055
+ ```
1056
+
1057
+ ## Best Practices
1058
+
1059
+ 1. **Always initialize before use:**
1060
+ ```typescript
1061
+ await helper.initialize();
1062
+ ```
1063
+
1064
+ 2. **Use transactions for multi-operation consistency:**
1065
+ ```typescript
1066
+ await helper.withTransaction(async (session) => {
1067
+ // Multiple operations
1068
+ });
1069
+ ```
1070
+
1071
+ 3. **Use pagination for large datasets:**
1072
+ ```typescript
1073
+ const result = await helper.loadCollection('users', {}, { page: 1, limit: 50 });
1074
+ ```
1075
+
1076
+ 4. **Create indexes for frequently queried fields:**
1077
+ ```typescript
1078
+ await helper.createIndex('users', { email: 1 }, { unique: true });
1079
+ ```
1080
+
1081
+ 5. **Always disconnect when done:**
1082
+ ```typescript
1083
+ await helper.disconnect();
1084
+ ```
1085
+
1086
+ ## License
1087
+
1088
+ ISC
1089
+
1090
+ ## Contributing
1091
+
1092
+ Contributions are welcome! Please feel free to submit a Pull Request.
1093
+
1094
+ ## Changelog
1095
+
1096
+ ### 3.3.0
1097
+ - Added `testConnection()` method for detailed connection testing and error diagnostics
1098
+ - Package renamed from `nx-mongodb-helper` to `nx-mongo` (shorter, cleaner name)
1099
+ - Connection test provides detailed error messages for missing credentials, invalid connection strings, authentication failures, and network issues
1100
+
1101
+ ### 3.2.0
1102
+ - Added process-scoped stages support - stages can now be scoped by process identifier
1103
+ - Updated default unique index to include `process` field: `['process', 'provider', 'key']`
1104
+ - All ProgressAPI methods now accept `process` parameter for process-scoped stage tracking
1105
+ - Updated `writeStage()` to support process-scoped completion
1106
+ - Stages with the same key can exist independently in different processes
1107
+
1108
+ ### 3.1.0
1109
+ - Added built-in progress tracking API (`helper.progress`) for provider-defined pipeline stages
1110
+ - Added `writeStage()` method that combines document writing with stage completion
1111
+ - Added progress tracking configuration to `HelperConfig` (collection, uniqueIndexKeys, provider)
1112
+ - Progress API supports idempotent operations, transactions, and provider namespaces
1113
+ - All progress operations support optional transaction sessions for atomicity
1114
+
1115
+ ### 3.0.0
1116
+ - Added config-driven ref mapping (HelperConfig, InputConfig, OutputConfig)
1117
+ - Added signature-based deduplication with automatic index management
1118
+ - Added `loadByRef()` method for loading data by ref name
1119
+ - Added `writeByRef()` method with signature computation, bulk upsert, and append/replace modes
1120
+ - Added `ensureSignatureIndex()` method for signature index management
1121
+ - Added `useConfig()` method for runtime config updates
1122
+ - Added `getByDotPath()` utility function for dot-notation path extraction with array wildcards
1123
+ - Added `computeSignature()` utility function for deterministic document signatures
1124
+ - Enhanced constructor to accept optional config parameter
1125
+ - All new methods support transaction sessions
1126
+
1127
+ ### 2.0.1
1128
+ - Package renamed from `nx-mongodb-helper` to `nx-mongo`
1129
+ - Add version number to README header
1130
+
1131
+ ### 2.0.0
1132
+ - Added delete operations
1133
+ - Added findOne operation
1134
+ - Added count operations (countDocuments, estimatedDocumentCount)
1135
+ - Added pagination support
1136
+ - Added aggregation pipeline support
1137
+ - Added transaction support
1138
+ - Added connection retry logic with exponential backoff
1139
+ - Added index management (createIndex, dropIndex, listIndexes)
1140
+ - Enhanced insert and update methods with session support
1141
+
1142
+ ### 1.0.0
1143
+ - Initial release with basic CRUD operations
1144
+