@memberjunction/external-change-detection 2.43.0 → 2.45.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.
Files changed (2) hide show
  1. package/README.md +218 -147
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -8,13 +8,14 @@ The `@memberjunction/external-change-detection` package provides functionality t
8
8
 
9
9
  ## Key Features
10
10
 
11
- - Detect external changes to entity records
12
- - Compare current state with previous snapshots
13
- - Generate detailed change reports
14
- - Support for field-level change detection
15
- - Configurable change detection criteria
16
- - Ability to replay/apply detected changes
17
- - Built-in optimization for large datasets
11
+ - Detect external changes to entity records (creates, updates, and deletes)
12
+ - Compare current state with previous snapshots stored in RecordChanges
13
+ - Generate detailed change reports with field-level differences
14
+ - Support for composite primary keys
15
+ - Configurable change detection with parallel processing
16
+ - Ability to replay/apply detected changes through MemberJunction
17
+ - Built-in optimization for batch loading records
18
+ - Track change replay runs for audit purposes
18
19
 
19
20
  ## Installation
20
21
 
@@ -25,218 +26,288 @@ npm install @memberjunction/external-change-detection
25
26
  ## Dependencies
26
27
 
27
28
  This package relies on the following MemberJunction packages:
28
- - `@memberjunction/core`
29
- - `@memberjunction/core-entities`
30
- - `@memberjunction/global`
31
- - `@memberjunction/sqlserver-dataprovider`
29
+ - `@memberjunction/core` - Core MemberJunction functionality
30
+ - `@memberjunction/core-entities` - Entity definitions
31
+ - `@memberjunction/global` - Global utilities
32
+ - `@memberjunction/sqlserver-dataprovider` - SQL Server data provider
32
33
 
33
34
  ## Basic Usage
34
35
 
35
36
  ```typescript
36
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
37
- import { User } from '@memberjunction/core-entities';
37
+ import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
38
+ import { Metadata } from '@memberjunction/core';
38
39
 
39
- async function detectChanges() {
40
- // Create change detector instance
41
- const detector = new ExternalChangeDetector();
40
+ async function detectAndReplayChanges() {
41
+ // Get the engine instance
42
+ const detector = ExternalChangeDetectorEngine.Instance;
42
43
 
43
- // Run detection for the User entity
44
- const changes = await detector.detectChanges({
45
- entityName: 'User',
46
- captureTimeLimit: 30, // minutes
47
- });
44
+ // Configure the engine (loads eligible entities)
45
+ await detector.Config();
46
+
47
+ // Get a specific entity
48
+ const md = new Metadata();
49
+ const entityInfo = md.Entities.find(e => e.Name === 'Customer');
48
50
 
49
- // Process detected changes
50
- console.log(`Detected ${changes.length} changes in User entity`);
51
+ // Detect changes for the entity
52
+ const result = await detector.DetectChangesForEntity(entityInfo);
51
53
 
52
- // Replay changes if needed
53
- if (changes.length > 0) {
54
- await detector.replayChanges(changes);
54
+ if (result.Success) {
55
+ console.log(`Detected ${result.Changes.length} changes`);
56
+
57
+ // Replay the changes if any were found
58
+ if (result.Changes.length > 0) {
59
+ const replaySuccess = await detector.ReplayChanges(result.Changes);
60
+ console.log(`Replay ${replaySuccess ? 'succeeded' : 'failed'}`);
61
+ }
55
62
  }
56
63
  }
57
-
58
- detectChanges();
59
64
  ```
60
65
 
61
- ## Eligible Entities
66
+ ## API Documentation
62
67
 
63
- Not all entities support external change detection. For an entity to be eligible for change detection:
68
+ ### ExternalChangeDetectorEngine
64
69
 
65
- 1. The entity must have a TrackChanges property set to true in the metadata
66
- 2. The entity must have a LastUpdated or LastModifiedDate field
67
- 3. The entity must have the required fields for tracking history
70
+ The main class for detecting and replaying external changes. This is a singleton that extends BaseEngine.
68
71
 
69
- You can check if an entity is eligible using:
72
+ #### Configuration
70
73
 
71
74
  ```typescript
72
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
75
+ // Configure the engine - this loads eligible entities
76
+ await ExternalChangeDetectorEngine.Instance.Config();
77
+ ```
73
78
 
74
- const detector = new ExternalChangeDetector();
75
- const isEligible = await detector.isEntityEligibleForChangeDetection('User');
79
+ #### Properties
76
80
 
77
- console.log(`User entity is eligible for change detection: ${isEligible}`);
78
- ```
81
+ - `EligibleEntities`: EntityInfo[] - List of entities eligible for change detection
82
+ - `IneligibleEntities`: string[] - List of entity names to exclude from detection
79
83
 
80
- ## Detecting Changes
84
+ #### Methods
81
85
 
82
- ### Simple Detection
86
+ ##### DetectChangesForEntity
83
87
 
84
- ```typescript
85
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
88
+ Detects changes for a single entity.
86
89
 
87
- const detector = new ExternalChangeDetector();
88
- const changes = await detector.detectChanges({
89
- entityName: 'Customer',
90
- captureTimeLimit: 60 // Look back 60 minutes
91
- });
90
+ ```typescript
91
+ const result = await detector.DetectChangesForEntity(entityInfo);
92
92
  ```
93
93
 
94
- ### Filtering by Record IDs
94
+ Returns a `ChangeDetectionResult` with:
95
+ - `Success`: boolean
96
+ - `ErrorMessage`: string (if failed)
97
+ - `Changes`: ChangeDetectionItem[]
98
+
99
+ ##### DetectChangesForEntities
100
+
101
+ Detects changes for multiple entities in parallel.
95
102
 
96
103
  ```typescript
97
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
98
-
99
- const detector = new ExternalChangeDetector();
100
- const changes = await detector.detectChanges({
101
- entityName: 'Product',
102
- recordIDs: [1001, 1002, 1003], // Only check these specific records
103
- captureTimeLimit: 24 * 60 // Look back 24 hours
104
- });
104
+ const entities = [entity1, entity2, entity3];
105
+ const result = await detector.DetectChangesForEntities(entities);
105
106
  ```
106
107
 
107
- ### Setting Change Criteria
108
+ ##### DetectChangesForAllEligibleEntities
109
+
110
+ Detects changes for all eligible entities.
108
111
 
109
112
  ```typescript
110
- import { ExternalChangeDetector, ExternalChangeDetectorCriteria } from '@memberjunction/external-change-detection';
113
+ const result = await detector.DetectChangesForAllEligibleEntities();
114
+ ```
111
115
 
112
- const criteria: ExternalChangeDetectorCriteria = {
113
- entityName: 'Order',
114
- captureTimeLimit: 120, // 2 hours
115
- includeFieldNames: ['Status', 'TotalAmount', 'CustomerID'], // Only check these fields
116
- excludeFieldNames: ['UpdatedBy', 'InternalNotes'] // Ignore changes to these fields
117
- };
116
+ ##### ReplayChanges
118
117
 
119
- const detector = new ExternalChangeDetector();
120
- const changes = await detector.detectChanges(criteria);
118
+ Replays detected changes through MemberJunction to trigger all business logic.
119
+
120
+ ```typescript
121
+ const success = await detector.ReplayChanges(changes, batchSize);
121
122
  ```
122
123
 
123
- ## Replaying Changes
124
+ Parameters:
125
+ - `changes`: ChangeDetectionItem[] - Changes to replay
126
+ - `batchSize`: number (optional, default: 20) - Number of concurrent replays
124
127
 
125
- Once changes are detected, you can replay or apply them through the MemberJunction application to ensure that all business logic is properly executed:
128
+ ### Data Types
126
129
 
127
- ```typescript
128
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
130
+ #### ChangeDetectionItem
129
131
 
130
- async function syncChanges() {
131
- const detector = new ExternalChangeDetector();
132
-
133
- // Detect changes
134
- const changes = await detector.detectChanges({
135
- entityName: 'Invoice',
136
- captureTimeLimit: 720 // 12 hours
137
- });
138
-
139
- if (changes.length > 0) {
140
- // Apply the detected changes through MemberJunction
141
- const results = await detector.replayChanges(changes);
142
-
143
- // Log results
144
- console.log(`Applied ${results.successCount} changes successfully`);
145
- console.log(`Failed to apply ${results.failureCount} changes`);
146
-
147
- if (results.failureCount > 0) {
148
- console.error('Failures:', results.failures);
149
- }
150
- }
132
+ Represents a single detected change:
133
+
134
+ ```typescript
135
+ class ChangeDetectionItem {
136
+ Entity: EntityInfo; // The entity that changed
137
+ PrimaryKey: CompositeKey; // Primary key of the record
138
+ Type: 'Create' | 'Update' | 'Delete'; // Type of change
139
+ ChangedAt: Date; // When the change occurred
140
+ Changes: FieldChange[]; // Field-level changes (for updates)
141
+ LatestRecord?: BaseEntity; // Current record data (for creates/updates)
142
+ LegacyKey?: boolean; // For backward compatibility
143
+ LegacyKeyValue?: string; // Legacy single-value key
151
144
  }
152
145
  ```
153
146
 
154
- ## Return Types
147
+ #### FieldChange
155
148
 
156
- ### ExternalChangeResult
149
+ Represents a change to a single field:
157
150
 
158
151
  ```typescript
159
- interface ExternalChangeResult {
160
- entityName: string;
161
- recordID: number;
162
- fieldChanges: ExternalFieldChange[];
163
- errorMessage?: string;
152
+ class FieldChange {
153
+ FieldName: string;
154
+ OldValue: any;
155
+ NewValue: any;
164
156
  }
165
157
  ```
166
158
 
167
- ### ExternalFieldChange
159
+ #### ChangeDetectionResult
160
+
161
+ Result of a change detection operation:
168
162
 
169
163
  ```typescript
170
- interface ExternalFieldChange {
171
- fieldName: string;
172
- oldValue: any;
173
- newValue: any;
164
+ class ChangeDetectionResult {
165
+ Success: boolean;
166
+ ErrorMessage?: string;
167
+ Changes: ChangeDetectionItem[];
174
168
  }
175
169
  ```
176
170
 
177
- ### ReplayChangesResult
171
+ ## Eligible Entities
172
+
173
+ For an entity to be eligible for external change detection:
174
+
175
+ 1. The entity must have `TrackRecordChanges` property set to 1
176
+ 2. The entity must have the special `__mj_UpdatedAt` and `__mj_CreatedAt` fields (automatically added by CodeGen)
177
+ 3. The entity must not be in the `IneligibleEntities` list
178
+
179
+ The eligible entities are determined by the database view `vwEntitiesWithExternalChangeTracking`.
180
+
181
+ ## How It Works
182
+
183
+ ### Change Detection Process
184
+
185
+ 1. **Create Detection**: Finds records in the entity table that don't have a corresponding 'Create' entry in RecordChanges
186
+ 2. **Update Detection**: Compares `__mj_UpdatedAt` timestamps between entity records and their latest RecordChanges entry
187
+ 3. **Delete Detection**: Finds RecordChanges entries where the corresponding entity record no longer exists
188
+
189
+ ### Change Replay Process
190
+
191
+ 1. Creates a new RecordChangeReplayRun to track the replay session
192
+ 2. For each change:
193
+ - Creates a new RecordChange record with status 'Pending'
194
+ - Loads the entity using MemberJunction's entity system
195
+ - Calls Save() or Delete() with the `ReplayOnly` option
196
+ - Updates the RecordChange status to 'Complete' or 'Error'
197
+ 3. Updates the RecordChangeReplayRun status when finished
198
+
199
+ ## Examples
200
+
201
+ ### Detect Changes for Specific Entities
178
202
 
179
203
  ```typescript
180
- interface ReplayChangesResult {
181
- successCount: number;
182
- failureCount: number;
183
- successes: ReplayChangeSuccess[];
184
- failures: ReplayChangeFailure[];
185
- }
186
- ```
204
+ const detector = ExternalChangeDetectorEngine.Instance;
205
+ await detector.Config();
206
+
207
+ // Get specific entities
208
+ const md = new Metadata();
209
+ const customerEntity = md.Entities.find(e => e.Name === 'Customer');
210
+ const orderEntity = md.Entities.find(e => e.Name === 'Order');
187
211
 
188
- ## Server-Side Usage
212
+ // Detect changes for both entities
213
+ const result = await detector.DetectChangesForEntities([customerEntity, orderEntity]);
189
214
 
190
- This library is primarily intended for server-side applications, often running as scheduled jobs or services that periodically check for external changes and reconcile them.
215
+ console.log(`Found ${result.Changes.length} total changes`);
216
+ ```
191
217
 
192
- Example of setting up a scheduled check:
218
+ ### Process Changes with Error Handling
193
219
 
194
220
  ```typescript
195
- import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
196
- import { EntityInfo } from '@memberjunction/core';
221
+ const detector = ExternalChangeDetectorEngine.Instance;
222
+ await detector.Config();
223
+
224
+ const result = await detector.DetectChangesForAllEligibleEntities();
197
225
 
198
- async function scheduleChangeDetection() {
199
- const detector = new ExternalChangeDetector();
226
+ if (result.Success && result.Changes.length > 0) {
227
+ console.log(`Processing ${result.Changes.length} changes...`);
200
228
 
201
- // Get all entities that support change detection
202
- const metadata = new EntityInfo();
203
- const entities = await metadata.getEntitiesWithTrackChanges();
229
+ // Group changes by entity for reporting
230
+ const changesByEntity = result.Changes.reduce((acc, change) => {
231
+ const entityName = change.Entity.Name;
232
+ if (!acc[entityName]) acc[entityName] = [];
233
+ acc[entityName].push(change);
234
+ return acc;
235
+ }, {});
204
236
 
205
- // Check each eligible entity
206
- for (const entity of entities) {
207
- try {
208
- const isEligible = await detector.isEntityEligibleForChangeDetection(entity.Name);
237
+ // Log summary
238
+ Object.entries(changesByEntity).forEach(([entityName, changes]) => {
239
+ console.log(`${entityName}: ${changes.length} changes`);
240
+ });
241
+
242
+ // Replay with smaller batch size for critical entities
243
+ const success = await detector.ReplayChanges(result.Changes, 10);
244
+
245
+ if (!success) {
246
+ console.error('Some changes failed to replay');
247
+ }
248
+ }
249
+ ```
250
+
251
+ ### Scheduled Change Detection Job
252
+
253
+ ```typescript
254
+ import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
255
+ import { UserInfo } from '@memberjunction/core';
256
+
257
+ async function runScheduledChangeDetection(contextUser: UserInfo) {
258
+ const detector = ExternalChangeDetectorEngine.Instance;
259
+
260
+ try {
261
+ // Configure with specific user context
262
+ await detector.Config(false, contextUser);
263
+
264
+ // Detect all changes
265
+ const detectResult = await detector.DetectChangesForAllEligibleEntities();
266
+
267
+ if (!detectResult.Success) {
268
+ throw new Error(`Detection failed: ${detectResult.ErrorMessage}`);
269
+ }
270
+
271
+ console.log(`Detection complete: ${detectResult.Changes.length} changes found`);
272
+
273
+ // Replay changes if any were found
274
+ if (detectResult.Changes.length > 0) {
275
+ const replaySuccess = await detector.ReplayChanges(detectResult.Changes);
209
276
 
210
- if (isEligible) {
211
- console.log(`Checking ${entity.Name} for external changes...`);
212
-
213
- const changes = await detector.detectChanges({
214
- entityName: entity.Name,
215
- captureTimeLimit: 24 * 60 // Daily check
216
- });
217
-
218
- if (changes.length > 0) {
219
- await detector.replayChanges(changes);
220
- console.log(`Applied ${changes.length} changes to ${entity.Name}`);
221
- }
277
+ if (!replaySuccess) {
278
+ console.error('Some changes failed during replay');
279
+ // Could implement retry logic or notifications here
222
280
  }
223
- } catch (error) {
224
- console.error(`Error processing ${entity.Name}:`, error);
225
281
  }
282
+ } catch (error) {
283
+ console.error('Change detection job failed:', error);
284
+ // Implement alerting/logging as needed
226
285
  }
227
286
  }
228
287
  ```
229
288
 
230
289
  ## Performance Considerations
231
290
 
232
- For large entities with many records, change detection can be resource-intensive. Consider using these optimization strategies:
291
+ 1. **Batch Processing**: The engine processes multiple entities in parallel and loads records in batches
292
+ 2. **Efficient Queries**: Uses optimized SQL queries with proper joins and filters
293
+ 3. **Composite Key Support**: Handles both simple and composite primary keys efficiently
294
+ 4. **Configurable Batch Size**: Adjust the replay batch size based on your system's capacity
295
+
296
+ ### Best Practices
297
+
298
+ - Run change detection during off-peak hours
299
+ - Monitor the RecordChangeReplayRuns table for failed runs
300
+ - Set appropriate batch sizes for replay based on your data volume
301
+ - Consider entity-specific scheduling for high-volume entities
302
+ - Implement proper error handling and alerting
303
+
304
+ ## Database Requirements
233
305
 
234
- 1. Use smaller `captureTimeLimit` values
235
- 2. Filter by specific `recordIDs` when possible
236
- 3. Use `includeFieldNames` to limit which fields are checked
237
- 4. Schedule detection jobs during off-peak hours
238
- 5. Process entities in batches
239
- 6. Implement error handling and retry logic
306
+ This package requires the following database objects:
307
+ - `__mj.vwEntitiesWithExternalChangeTracking` - View listing eligible entities
308
+ - `__mj.vwRecordChanges` - View of record change history
309
+ - `__mj.RecordChange` - Table storing change records
310
+ - `__mj.RecordChangeReplayRun` - Table tracking replay runs
240
311
 
241
312
  ## License
242
313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/external-change-detection",
3
- "version": "2.43.0",
3
+ "version": "2.45.0",
4
4
  "description": "Library used by server side applications to determine if changes have been made to entities by external systems/integrations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,9 +19,9 @@
19
19
  "typescript": "^5.4.5"
20
20
  },
21
21
  "dependencies": {
22
- "@memberjunction/core": "2.43.0",
23
- "@memberjunction/core-entities": "2.43.0",
24
- "@memberjunction/global": "2.43.0",
25
- "@memberjunction/sqlserver-dataprovider": "2.43.0"
22
+ "@memberjunction/core": "2.45.0",
23
+ "@memberjunction/core-entities": "2.45.0",
24
+ "@memberjunction/global": "2.45.0",
25
+ "@memberjunction/sqlserver-dataprovider": "2.45.0"
26
26
  }
27
27
  }