# @memberjunction/external-change-detection Detects and reconciles changes made to MemberJunction entities by external systems, third-party integrations, or direct database modifications that bypass the MemberJunction application layer. ## Overview The `@memberjunction/external-change-detection` package detects when records have been modified outside of MemberJunction, generates detailed change reports with field-level differences, and can replay those changes through the MemberJunction entity system to trigger all business logic, validations, and audit tracking. ```mermaid graph TD A["ExternalChangeDetectorEngine
(Singleton)"] --> B["Create Detection"] A --> C["Update Detection"] A --> D["Delete Detection"] B --> E["Records without
Create in RecordChanges"] C --> F["__mj_UpdatedAt newer than
latest RecordChange"] D --> G["RecordChanges entries
with no matching record"] A --> H["ReplayChanges"] H --> I["Load Entity via MJ"] I --> J["Save/Delete with ReplayOnly"] J --> K["Business Logic
Audit Trail
Triggers"] style A fill:#2d6a9f,stroke:#1a4971,color:#fff style B fill:#2d8659,stroke:#1a5c3a,color:#fff style C fill:#2d8659,stroke:#1a5c3a,color:#fff style D fill:#2d8659,stroke:#1a5c3a,color:#fff style H fill:#7c5295,stroke:#563a6b,color:#fff style K fill:#b8762f,stroke:#8a5722,color:#fff ``` ## Key Features - Detect external changes to entity records (creates, updates, and deletes) - Compare current state with previous snapshots stored in RecordChanges - Generate detailed change reports with field-level differences - Support for composite primary keys - Configurable change detection with parallel processing - Ability to replay/apply detected changes through MemberJunction - Built-in optimization for batch loading records - Track change replay runs for audit purposes ## Installation ```bash npm install @memberjunction/external-change-detection ``` ## Dependencies This package relies on the following MemberJunction packages: - `@memberjunction/core` - Core MemberJunction functionality - `@memberjunction/core-entities` - Entity definitions - `@memberjunction/global` - Global utilities - `@memberjunction/sqlserver-dataprovider` - SQL Server data provider ## Basic Usage ```typescript import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection'; import { Metadata } from '@memberjunction/core'; async function detectAndReplayChanges() { // Get the engine instance const detector = ExternalChangeDetectorEngine.Instance; // Configure the engine (loads eligible entities) await detector.Config(); // Get a specific entity const md = new Metadata(); const entityInfo = md.Entities.find(e => e.Name === 'Customer'); // Detect changes for the entity const result = await detector.DetectChangesForEntity(entityInfo); if (result.Success) { console.log(`Detected ${result.Changes.length} changes`); // Replay the changes if any were found if (result.Changes.length > 0) { const replaySuccess = await detector.ReplayChanges(result.Changes); console.log(`Replay ${replaySuccess ? 'succeeded' : 'failed'}`); } } } ``` ## API Documentation ### ExternalChangeDetectorEngine The main class for detecting and replaying external changes. This is a singleton that extends BaseEngine. #### Configuration ```typescript // Configure the engine - this loads eligible entities await ExternalChangeDetectorEngine.Instance.Config(); ``` #### Properties - `EligibleEntities`: EntityInfo[] - List of entities eligible for change detection - `IneligibleEntities`: string[] - List of entity names to exclude from detection #### Methods ##### DetectChangesForEntity Detects changes for a single entity. ```typescript const result = await detector.DetectChangesForEntity(entityInfo); ``` Returns a `ChangeDetectionResult` with: - `Success`: boolean - `ErrorMessage`: string (if failed) - `Changes`: ChangeDetectionItem[] ##### DetectChangesForEntities Detects changes for multiple entities in parallel. ```typescript const entities = [entity1, entity2, entity3]; const result = await detector.DetectChangesForEntities(entities); ``` ##### DetectChangesForAllEligibleEntities Detects changes for all eligible entities. ```typescript const result = await detector.DetectChangesForAllEligibleEntities(); ``` ##### ReplayChanges Replays detected changes through MemberJunction to trigger all business logic. ```typescript const success = await detector.ReplayChanges(changes, batchSize); ``` Parameters: - `changes`: ChangeDetectionItem[] - Changes to replay - `batchSize`: number (optional, default: 20) - Number of concurrent replays ### Data Types #### ChangeDetectionItem Represents a single detected change: ```typescript class ChangeDetectionItem { Entity: EntityInfo; // The entity that changed PrimaryKey: CompositeKey; // Primary key of the record Type: 'Create' | 'Update' | 'Delete'; // Type of change ChangedAt: Date; // When the change occurred Changes: FieldChange[]; // Field-level changes (for updates) LatestRecord?: BaseEntity; // Current record data (for creates/updates) LegacyKey?: boolean; // For backward compatibility LegacyKeyValue?: string; // Legacy single-value key } ``` #### FieldChange Represents a change to a single field: ```typescript class FieldChange { FieldName: string; OldValue: any; NewValue: any; } ``` #### ChangeDetectionResult Result of a change detection operation: ```typescript class ChangeDetectionResult { Success: boolean; ErrorMessage?: string; Changes: ChangeDetectionItem[]; } ``` ## Eligible Entities For an entity to be eligible for external change detection, **all** of the following must be true: 1. `TrackRecordChanges` is set to 1 (entity has audit logging enabled) 2. `DetectExternalChanges` is set to 1 (entity has opted into external change scanning) 3. The entity has `__mj_UpdatedAt` and `__mj_CreatedAt` fields (automatically added by CodeGen) 4. The entity is not in the `IneligibleEntities` list The eligible entities are determined by the database view `vwEntitiesWithExternalChangeTracking`. ### Opt-In Model External change detection is **opt-in by default** (`DetectExternalChanges = 0`). Most entities — especially `__mj` schema metadata tables — are managed exclusively by SQL migrations and CodeGen. Scanning them produces false positives because their records are intentionally created outside the MJ `Save()` flow. To enable external change detection for specific entities, create a metadata JSON file in `metadata/entities/` and push it with `mj sync`. For example, create `metadata/entities/.external-change-detection.json`: ```json [ { "fields": { "Name": "Contacts", "DetectExternalChanges": true }, "primaryKey": { "ID": "@lookup:MJ: Entities.Name=Contacts" } }, { "fields": { "Name": "Companies", "DetectExternalChanges": true }, "primaryKey": { "ID": "@lookup:MJ: Entities.Name=Companies" } }, { "fields": { "Name": "Invoices", "DetectExternalChanges": true }, "primaryKey": { "ID": "@lookup:MJ: Entities.Name=Invoices" } } ] ``` Then push from the repository root: ```bash npx mj sync push --dir=metadata --include="entities" ``` Alternatively, you can toggle `DetectExternalChanges` directly in the Entity form within MJ Explorer. ### Good Candidates - **User-facing data** that may be modified by third-party integrations (CRM sync, ETL tools) - **Entities loaded via bulk import** that bypass the MJ application layer - **Tables with direct SQL edits** from administrative tools or scripts ### Bad Candidates - **`__mj` schema tables** — managed by migrations/CodeGen, no external changes by design - **System configuration entities** — changes should go through the MJ API - **High-volume logging tables** — detection scans would be expensive with no benefit ## How It Works ### Change Detection Process 1. **Create Detection**: Finds records in the entity table that don't have a corresponding 'Create' entry in RecordChanges 2. **Update Detection**: Compares `__mj_UpdatedAt` timestamps between entity records and their latest RecordChanges entry 3. **Delete Detection**: Finds RecordChanges entries where the corresponding entity record no longer exists ### Change Replay Process 1. Creates a new RecordChangeReplayRun to track the replay session 2. For each change: - Creates a new RecordChange record with status 'Pending' - Loads the entity using MemberJunction's entity system - Calls Save() or Delete() with the `ReplayOnly` option - Updates the RecordChange status to 'Complete' or 'Error' 3. Updates the RecordChangeReplayRun status when finished ## Examples ### Detect Changes for Specific Entities ```typescript const detector = ExternalChangeDetectorEngine.Instance; await detector.Config(); // Get specific entities const md = new Metadata(); const customerEntity = md.Entities.find(e => e.Name === 'Customer'); const orderEntity = md.Entities.find(e => e.Name === 'Order'); // Detect changes for both entities const result = await detector.DetectChangesForEntities([customerEntity, orderEntity]); console.log(`Found ${result.Changes.length} total changes`); ``` ### Process Changes with Error Handling ```typescript const detector = ExternalChangeDetectorEngine.Instance; await detector.Config(); const result = await detector.DetectChangesForAllEligibleEntities(); if (result.Success && result.Changes.length > 0) { console.log(`Processing ${result.Changes.length} changes...`); // Group changes by entity for reporting const changesByEntity = result.Changes.reduce((acc, change) => { const entityName = change.Entity.Name; if (!acc[entityName]) acc[entityName] = []; acc[entityName].push(change); return acc; }, {}); // Log summary Object.entries(changesByEntity).forEach(([entityName, changes]) => { console.log(`${entityName}: ${changes.length} changes`); }); // Replay with smaller batch size for critical entities const success = await detector.ReplayChanges(result.Changes, 10); if (!success) { console.error('Some changes failed to replay'); } } ``` ### Scheduled Change Detection Job ```typescript import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection'; import { UserInfo } from '@memberjunction/core'; async function runScheduledChangeDetection(contextUser: UserInfo) { const detector = ExternalChangeDetectorEngine.Instance; try { // Configure with specific user context await detector.Config(false, contextUser); // Detect all changes const detectResult = await detector.DetectChangesForAllEligibleEntities(); if (!detectResult.Success) { throw new Error(`Detection failed: ${detectResult.ErrorMessage}`); } console.log(`Detection complete: ${detectResult.Changes.length} changes found`); // Replay changes if any were found if (detectResult.Changes.length > 0) { const replaySuccess = await detector.ReplayChanges(detectResult.Changes); if (!replaySuccess) { console.error('Some changes failed during replay'); // Could implement retry logic or notifications here } } } catch (error) { console.error('Change detection job failed:', error); // Implement alerting/logging as needed } } ``` ## Performance Considerations 1. **Batch Processing**: The engine processes multiple entities in parallel and loads records in batches 2. **Efficient Queries**: Uses optimized SQL queries with proper joins and filters 3. **Composite Key Support**: Handles both simple and composite primary keys efficiently 4. **Configurable Batch Size**: Adjust the replay batch size based on your system's capacity ### Best Practices - Run change detection during off-peak hours - Monitor the RecordChangeReplayRuns table for failed runs - Set appropriate batch sizes for replay based on your data volume - Consider entity-specific scheduling for high-volume entities - Implement proper error handling and alerting ## Database Requirements This package requires the following database objects: - `__mj.vwEntitiesWithExternalChangeTracking` - View listing eligible entities - `__mj.vwRecordChanges` - View of record change history - `__mj.RecordChange` - Table storing change records - `__mj.RecordChangeReplayRun` - Table tracking replay runs ## License ISC