@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.
- package/README.md +218 -147
- 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
|
|
15
|
-
- Configurable change detection
|
|
16
|
-
- Ability to replay/apply detected changes
|
|
17
|
-
- Built-in optimization for
|
|
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 {
|
|
37
|
-
import {
|
|
37
|
+
import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
|
|
38
|
+
import { Metadata } from '@memberjunction/core';
|
|
38
39
|
|
|
39
|
-
async function
|
|
40
|
-
//
|
|
41
|
-
const detector =
|
|
40
|
+
async function detectAndReplayChanges() {
|
|
41
|
+
// Get the engine instance
|
|
42
|
+
const detector = ExternalChangeDetectorEngine.Instance;
|
|
42
43
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
50
|
-
|
|
51
|
+
// Detect changes for the entity
|
|
52
|
+
const result = await detector.DetectChangesForEntity(entityInfo);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
66
|
+
## API Documentation
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
### ExternalChangeDetectorEngine
|
|
64
69
|
|
|
65
|
-
|
|
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
|
-
|
|
72
|
+
#### Configuration
|
|
70
73
|
|
|
71
74
|
```typescript
|
|
72
|
-
|
|
75
|
+
// Configure the engine - this loads eligible entities
|
|
76
|
+
await ExternalChangeDetectorEngine.Instance.Config();
|
|
77
|
+
```
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
const isEligible = await detector.isEntityEligibleForChangeDetection('User');
|
|
79
|
+
#### Properties
|
|
76
80
|
|
|
77
|
-
|
|
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
|
-
|
|
84
|
+
#### Methods
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
##### DetectChangesForEntity
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
|
|
88
|
+
Detects changes for a single entity.
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
##### DetectChangesForAllEligibleEntities
|
|
109
|
+
|
|
110
|
+
Detects changes for all eligible entities.
|
|
108
111
|
|
|
109
112
|
```typescript
|
|
110
|
-
|
|
113
|
+
const result = await detector.DetectChangesForAllEligibleEntities();
|
|
114
|
+
```
|
|
111
115
|
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
+
Parameters:
|
|
125
|
+
- `changes`: ChangeDetectionItem[] - Changes to replay
|
|
126
|
+
- `batchSize`: number (optional, default: 20) - Number of concurrent replays
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
### Data Types
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
import { ExternalChangeDetector } from '@memberjunction/external-change-detection';
|
|
130
|
+
#### ChangeDetectionItem
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
+
#### FieldChange
|
|
155
148
|
|
|
156
|
-
|
|
149
|
+
Represents a change to a single field:
|
|
157
150
|
|
|
158
151
|
```typescript
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
errorMessage?: string;
|
|
152
|
+
class FieldChange {
|
|
153
|
+
FieldName: string;
|
|
154
|
+
OldValue: any;
|
|
155
|
+
NewValue: any;
|
|
164
156
|
}
|
|
165
157
|
```
|
|
166
158
|
|
|
167
|
-
|
|
159
|
+
#### ChangeDetectionResult
|
|
160
|
+
|
|
161
|
+
Result of a change detection operation:
|
|
168
162
|
|
|
169
163
|
```typescript
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
class ChangeDetectionResult {
|
|
165
|
+
Success: boolean;
|
|
166
|
+
ErrorMessage?: string;
|
|
167
|
+
Changes: ChangeDetectionItem[];
|
|
174
168
|
}
|
|
175
169
|
```
|
|
176
170
|
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
212
|
+
// Detect changes for both entities
|
|
213
|
+
const result = await detector.DetectChangesForEntities([customerEntity, orderEntity]);
|
|
189
214
|
|
|
190
|
-
|
|
215
|
+
console.log(`Found ${result.Changes.length} total changes`);
|
|
216
|
+
```
|
|
191
217
|
|
|
192
|
-
|
|
218
|
+
### Process Changes with Error Handling
|
|
193
219
|
|
|
194
220
|
```typescript
|
|
195
|
-
|
|
196
|
-
|
|
221
|
+
const detector = ExternalChangeDetectorEngine.Instance;
|
|
222
|
+
await detector.Config();
|
|
223
|
+
|
|
224
|
+
const result = await detector.DetectChangesForAllEligibleEntities();
|
|
197
225
|
|
|
198
|
-
|
|
199
|
-
|
|
226
|
+
if (result.Success && result.Changes.length > 0) {
|
|
227
|
+
console.log(`Processing ${result.Changes.length} changes...`);
|
|
200
228
|
|
|
201
|
-
//
|
|
202
|
-
const
|
|
203
|
-
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 (
|
|
211
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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.
|
|
23
|
-
"@memberjunction/core-entities": "2.
|
|
24
|
-
"@memberjunction/global": "2.
|
|
25
|
-
"@memberjunction/sqlserver-dataprovider": "2.
|
|
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
|
}
|