@memberjunction/metadata-sync 2.84.0 → 2.86.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/dist/lib/record-dependency-analyzer.d.ts +77 -0
- package/dist/lib/record-dependency-analyzer.js +427 -0
- package/dist/lib/record-dependency-analyzer.js.map +1 -0
- package/dist/lib/sync-engine.d.ts +4 -77
- package/dist/lib/sync-engine.js +41 -171
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/PushService.d.ts +2 -16
- package/dist/services/PushService.js +139 -401
- package/dist/services/PushService.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { RecordData } from './sync-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a flattened record with its context and dependencies
|
|
4
|
+
*/
|
|
5
|
+
export interface FlattenedRecord {
|
|
6
|
+
record: RecordData;
|
|
7
|
+
entityName: string;
|
|
8
|
+
parentContext?: {
|
|
9
|
+
entityName: string;
|
|
10
|
+
record: RecordData;
|
|
11
|
+
recordIndex: number;
|
|
12
|
+
};
|
|
13
|
+
depth: number;
|
|
14
|
+
path: string;
|
|
15
|
+
dependencies: Set<string>;
|
|
16
|
+
id: string;
|
|
17
|
+
originalIndex: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of dependency analysis
|
|
21
|
+
*/
|
|
22
|
+
export interface DependencyAnalysisResult {
|
|
23
|
+
sortedRecords: FlattenedRecord[];
|
|
24
|
+
circularDependencies: string[][];
|
|
25
|
+
dependencyGraph: Map<string, Set<string>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Analyzes and sorts records based on their dependencies
|
|
29
|
+
*/
|
|
30
|
+
export declare class RecordDependencyAnalyzer {
|
|
31
|
+
private metadata;
|
|
32
|
+
private flattenedRecords;
|
|
33
|
+
private recordIdMap;
|
|
34
|
+
private entityInfoCache;
|
|
35
|
+
private recordCounter;
|
|
36
|
+
constructor();
|
|
37
|
+
/**
|
|
38
|
+
* Main entry point: analyzes all records in a file and returns them in dependency order
|
|
39
|
+
*/
|
|
40
|
+
analyzeFileRecords(records: RecordData[], entityName: string): Promise<DependencyAnalysisResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Flattens all records including nested relatedEntities
|
|
43
|
+
*/
|
|
44
|
+
private flattenRecords;
|
|
45
|
+
/**
|
|
46
|
+
* Analyzes dependencies between all flattened records
|
|
47
|
+
*/
|
|
48
|
+
private analyzeDependencies;
|
|
49
|
+
/**
|
|
50
|
+
* Analyzes foreign key dependencies based on EntityInfo
|
|
51
|
+
*/
|
|
52
|
+
private analyzeForeignKeyDependencies;
|
|
53
|
+
/**
|
|
54
|
+
* Finds a record that matches a @lookup reference
|
|
55
|
+
*/
|
|
56
|
+
private findLookupDependency;
|
|
57
|
+
/**
|
|
58
|
+
* Finds the root record for a given record
|
|
59
|
+
*/
|
|
60
|
+
private findRootDependency;
|
|
61
|
+
/**
|
|
62
|
+
* Finds a record by its primary key value
|
|
63
|
+
*/
|
|
64
|
+
private findRecordByPrimaryKey;
|
|
65
|
+
/**
|
|
66
|
+
* Gets EntityInfo from cache or metadata
|
|
67
|
+
*/
|
|
68
|
+
private getEntityInfo;
|
|
69
|
+
/**
|
|
70
|
+
* Detects circular dependencies in the dependency graph
|
|
71
|
+
*/
|
|
72
|
+
private detectCircularDependencies;
|
|
73
|
+
/**
|
|
74
|
+
* Performs topological sort on the dependency graph
|
|
75
|
+
*/
|
|
76
|
+
private topologicalSort;
|
|
77
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecordDependencyAnalyzer = void 0;
|
|
4
|
+
const core_1 = require("@memberjunction/core");
|
|
5
|
+
/**
|
|
6
|
+
* Analyzes and sorts records based on their dependencies
|
|
7
|
+
*/
|
|
8
|
+
class RecordDependencyAnalyzer {
|
|
9
|
+
metadata;
|
|
10
|
+
flattenedRecords = [];
|
|
11
|
+
recordIdMap = new Map();
|
|
12
|
+
entityInfoCache = new Map();
|
|
13
|
+
recordCounter = 0;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.metadata = new core_1.Metadata();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Main entry point: analyzes all records in a file and returns them in dependency order
|
|
19
|
+
*/
|
|
20
|
+
async analyzeFileRecords(records, entityName) {
|
|
21
|
+
// Reset state
|
|
22
|
+
this.flattenedRecords = [];
|
|
23
|
+
this.recordIdMap.clear();
|
|
24
|
+
this.recordCounter = 0;
|
|
25
|
+
// Step 1: Flatten all records (including nested relatedEntities)
|
|
26
|
+
this.flattenRecords(records, entityName);
|
|
27
|
+
// Step 2: Analyze dependencies between all flattened records
|
|
28
|
+
this.analyzeDependencies();
|
|
29
|
+
// Step 3: Detect circular dependencies
|
|
30
|
+
const circularDeps = this.detectCircularDependencies();
|
|
31
|
+
// Step 4: Perform topological sort
|
|
32
|
+
const sortedRecords = this.topologicalSort();
|
|
33
|
+
// Step 5: Build dependency graph for debugging
|
|
34
|
+
const dependencyGraph = new Map();
|
|
35
|
+
for (const record of this.flattenedRecords) {
|
|
36
|
+
dependencyGraph.set(record.id, record.dependencies);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
sortedRecords,
|
|
40
|
+
circularDependencies: circularDeps,
|
|
41
|
+
dependencyGraph
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Flattens all records including nested relatedEntities
|
|
46
|
+
*/
|
|
47
|
+
flattenRecords(records, entityName, parentContext, depth = 0, pathPrefix = '', parentRecordId) {
|
|
48
|
+
for (let i = 0; i < records.length; i++) {
|
|
49
|
+
const record = records[i];
|
|
50
|
+
const recordId = `${entityName}_${this.recordCounter++}`;
|
|
51
|
+
const path = pathPrefix ? `${pathPrefix}/${entityName}[${i}]` : `${entityName}[${i}]`;
|
|
52
|
+
const flattenedRecord = {
|
|
53
|
+
record,
|
|
54
|
+
entityName,
|
|
55
|
+
parentContext,
|
|
56
|
+
depth,
|
|
57
|
+
path,
|
|
58
|
+
dependencies: new Set(),
|
|
59
|
+
id: recordId,
|
|
60
|
+
originalIndex: i
|
|
61
|
+
};
|
|
62
|
+
// If this has a parent, add dependency on the parent
|
|
63
|
+
if (parentRecordId) {
|
|
64
|
+
flattenedRecord.dependencies.add(parentRecordId);
|
|
65
|
+
}
|
|
66
|
+
this.flattenedRecords.push(flattenedRecord);
|
|
67
|
+
this.recordIdMap.set(recordId, flattenedRecord);
|
|
68
|
+
// Recursively flatten related entities
|
|
69
|
+
if (record.relatedEntities) {
|
|
70
|
+
for (const [relatedEntityName, relatedRecords] of Object.entries(record.relatedEntities)) {
|
|
71
|
+
this.flattenRecords(relatedRecords, relatedEntityName, {
|
|
72
|
+
entityName,
|
|
73
|
+
record,
|
|
74
|
+
recordIndex: i
|
|
75
|
+
}, depth + 1, path, recordId // Pass current record ID as parent for children
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Analyzes dependencies between all flattened records
|
|
83
|
+
*/
|
|
84
|
+
analyzeDependencies() {
|
|
85
|
+
for (const record of this.flattenedRecords) {
|
|
86
|
+
// Get entity info for foreign key relationships
|
|
87
|
+
const entityInfo = this.getEntityInfo(record.entityName);
|
|
88
|
+
if (!entityInfo)
|
|
89
|
+
continue;
|
|
90
|
+
// Analyze field dependencies
|
|
91
|
+
if (record.record.fields) {
|
|
92
|
+
for (const [fieldName, fieldValue] of Object.entries(record.record.fields)) {
|
|
93
|
+
if (typeof fieldValue === 'string') {
|
|
94
|
+
// Handle @lookup references
|
|
95
|
+
if (fieldValue.startsWith('@lookup:')) {
|
|
96
|
+
const dependency = this.findLookupDependency(fieldValue, record);
|
|
97
|
+
if (dependency) {
|
|
98
|
+
record.dependencies.add(dependency);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Handle @root references - these create dependencies on the root record
|
|
102
|
+
else if (fieldValue.startsWith('@root:')) {
|
|
103
|
+
const rootDependency = this.findRootDependency(record);
|
|
104
|
+
if (rootDependency) {
|
|
105
|
+
record.dependencies.add(rootDependency);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// @parent references don't create explicit dependencies in our flattened structure
|
|
109
|
+
// because parent is guaranteed to be processed before children due to the way
|
|
110
|
+
// we flatten records (parent always comes before its children)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check foreign key dependencies
|
|
115
|
+
this.analyzeForeignKeyDependencies(record, entityInfo);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Analyzes foreign key dependencies based on EntityInfo
|
|
120
|
+
*/
|
|
121
|
+
analyzeForeignKeyDependencies(record, entityInfo) {
|
|
122
|
+
// Check all foreign key fields
|
|
123
|
+
for (const field of entityInfo.ForeignKeys) {
|
|
124
|
+
const fieldValue = record.record.fields?.[field.Name];
|
|
125
|
+
if (fieldValue && typeof fieldValue === 'string' && !fieldValue.startsWith('@')) {
|
|
126
|
+
// This is a direct foreign key value, find the referenced record
|
|
127
|
+
const relatedEntityInfo = this.getEntityInfo(field.RelatedEntity);
|
|
128
|
+
if (relatedEntityInfo) {
|
|
129
|
+
const dependency = this.findRecordByPrimaryKey(field.RelatedEntity, fieldValue, relatedEntityInfo);
|
|
130
|
+
if (dependency) {
|
|
131
|
+
record.dependencies.add(dependency);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Finds a record that matches a @lookup reference
|
|
139
|
+
*/
|
|
140
|
+
findLookupDependency(lookupValue, currentRecord) {
|
|
141
|
+
// Parse lookup format: @lookup:EntityName.Field=Value or @lookup:Field=Value
|
|
142
|
+
const lookupStr = lookupValue.substring(8); // Remove '@lookup:'
|
|
143
|
+
// Handle the ?create syntax by removing it
|
|
144
|
+
const cleanLookup = lookupStr.split('?')[0];
|
|
145
|
+
// Parse entity name if present
|
|
146
|
+
let targetEntity;
|
|
147
|
+
let criteria;
|
|
148
|
+
if (cleanLookup.includes('.')) {
|
|
149
|
+
const parts = cleanLookup.split('.');
|
|
150
|
+
targetEntity = parts[0];
|
|
151
|
+
criteria = parts.slice(1).join('.');
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Same entity if not specified
|
|
155
|
+
targetEntity = currentRecord.entityName;
|
|
156
|
+
criteria = cleanLookup;
|
|
157
|
+
}
|
|
158
|
+
// Parse criteria (can be multiple with &)
|
|
159
|
+
const criteriaMap = new Map();
|
|
160
|
+
for (const pair of criteria.split('&')) {
|
|
161
|
+
const [field, value] = pair.split('=');
|
|
162
|
+
if (field && value) {
|
|
163
|
+
let resolvedValue = value.trim();
|
|
164
|
+
// Special handling for nested @lookup references in lookup criteria
|
|
165
|
+
// This creates a dependency on the looked-up record
|
|
166
|
+
if (resolvedValue.startsWith('@lookup:')) {
|
|
167
|
+
const nestedDependency = this.findLookupDependency(resolvedValue, currentRecord);
|
|
168
|
+
if (nestedDependency) {
|
|
169
|
+
// Add this as a dependency of the current record
|
|
170
|
+
currentRecord.dependencies.add(nestedDependency);
|
|
171
|
+
// Continue processing - we can't resolve the actual value here
|
|
172
|
+
// but we've recorded the dependency
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Special handling for @root references in lookup criteria
|
|
176
|
+
else if (resolvedValue.startsWith('@root:')) {
|
|
177
|
+
// Add dependency on root record
|
|
178
|
+
const rootDep = this.findRootDependency(currentRecord);
|
|
179
|
+
if (rootDep) {
|
|
180
|
+
currentRecord.dependencies.add(rootDep);
|
|
181
|
+
}
|
|
182
|
+
// Note: We can't resolve the actual value here, but we've recorded the dependency
|
|
183
|
+
}
|
|
184
|
+
// Special handling for @parent references in lookup criteria
|
|
185
|
+
// If the value is @parent:field, we need to resolve it from the parent context
|
|
186
|
+
else if (resolvedValue.startsWith('@parent:') && currentRecord.parentContext) {
|
|
187
|
+
const parentField = resolvedValue.substring(8);
|
|
188
|
+
// Try to resolve from parent context
|
|
189
|
+
const parentValue = currentRecord.parentContext.record.fields?.[parentField] ||
|
|
190
|
+
currentRecord.parentContext.record.primaryKey?.[parentField];
|
|
191
|
+
if (parentValue && typeof parentValue === 'string') {
|
|
192
|
+
// Check if parent value is also a @parent reference (nested parent refs)
|
|
193
|
+
if (parentValue.startsWith('@parent:')) {
|
|
194
|
+
// Find the parent record to get its parent context
|
|
195
|
+
const parentRecord = this.flattenedRecords.find(r => r.record === currentRecord.parentContext.record &&
|
|
196
|
+
r.entityName === currentRecord.parentContext.entityName);
|
|
197
|
+
if (parentRecord && parentRecord.parentContext) {
|
|
198
|
+
const grandParentField = parentValue.substring(8);
|
|
199
|
+
const grandParentValue = parentRecord.parentContext.record.fields?.[grandParentField] ||
|
|
200
|
+
parentRecord.parentContext.record.primaryKey?.[grandParentField];
|
|
201
|
+
if (grandParentValue && typeof grandParentValue === 'string' && !grandParentValue.startsWith('@')) {
|
|
202
|
+
resolvedValue = grandParentValue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (!parentValue.startsWith('@')) {
|
|
207
|
+
resolvedValue = parentValue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
criteriaMap.set(field.trim(), resolvedValue);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Find matching record in our flattened list
|
|
215
|
+
for (const candidate of this.flattenedRecords) {
|
|
216
|
+
if (candidate.entityName !== targetEntity)
|
|
217
|
+
continue;
|
|
218
|
+
if (candidate.id === currentRecord.id)
|
|
219
|
+
continue; // Skip self
|
|
220
|
+
// Check if all criteria match
|
|
221
|
+
let allMatch = true;
|
|
222
|
+
for (const [field, value] of criteriaMap) {
|
|
223
|
+
let candidateValue = candidate.record.fields?.[field] ||
|
|
224
|
+
candidate.record.primaryKey?.[field];
|
|
225
|
+
let lookupValue = value;
|
|
226
|
+
// Resolve candidate value if it's a @parent reference
|
|
227
|
+
if (typeof candidateValue === 'string' && candidateValue.startsWith('@parent:') && candidate.parentContext) {
|
|
228
|
+
const parentField = candidateValue.substring(8);
|
|
229
|
+
const parentRecord = candidate.parentContext.record;
|
|
230
|
+
candidateValue = parentRecord.fields?.[parentField] || parentRecord.primaryKey?.[parentField];
|
|
231
|
+
// If the parent field is also a @parent reference, resolve it recursively
|
|
232
|
+
if (typeof candidateValue === 'string' && candidateValue.startsWith('@parent:')) {
|
|
233
|
+
// Find the candidate's parent in our flattened list
|
|
234
|
+
const candidateParent = this.flattenedRecords.find(r => r.record === candidate.parentContext.record &&
|
|
235
|
+
r.entityName === candidate.parentContext.entityName);
|
|
236
|
+
if (candidateParent?.parentContext) {
|
|
237
|
+
const grandParentField = candidateValue.substring(8);
|
|
238
|
+
candidateValue = candidateParent.parentContext.record.fields?.[grandParentField] ||
|
|
239
|
+
candidateParent.parentContext.record.primaryKey?.[grandParentField];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Resolve lookup value if it contains @parent reference
|
|
244
|
+
if (typeof lookupValue === 'string' && lookupValue.includes('@parent:')) {
|
|
245
|
+
// Handle cases like "@parent:AgentID" or embedded references
|
|
246
|
+
if (lookupValue.startsWith('@parent:') && currentRecord.parentContext) {
|
|
247
|
+
const parentField = lookupValue.substring(8);
|
|
248
|
+
lookupValue = currentRecord.parentContext.record.fields?.[parentField] ||
|
|
249
|
+
currentRecord.parentContext.record.primaryKey?.[parentField];
|
|
250
|
+
// If still a reference, try to resolve from the parent's parent
|
|
251
|
+
if (typeof lookupValue === 'string' && lookupValue.startsWith('@parent:')) {
|
|
252
|
+
const currentParent = this.flattenedRecords.find(r => r.record === currentRecord.parentContext.record &&
|
|
253
|
+
r.entityName === currentRecord.parentContext.entityName);
|
|
254
|
+
if (currentParent?.parentContext) {
|
|
255
|
+
const grandParentField = lookupValue.substring(8);
|
|
256
|
+
lookupValue = currentParent.parentContext.record.fields?.[grandParentField] ||
|
|
257
|
+
currentParent.parentContext.record.primaryKey?.[grandParentField];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Special case: if both values are @parent references pointing to the same parent field,
|
|
263
|
+
// and they have the same parent context, they match
|
|
264
|
+
if (value.startsWith('@parent:') && candidateValue === value &&
|
|
265
|
+
currentRecord.parentContext && candidate.parentContext) {
|
|
266
|
+
// Check if they share the same parent
|
|
267
|
+
if (currentRecord.parentContext.record === candidate.parentContext.record) {
|
|
268
|
+
// Same parent, same reference - they will resolve to the same value
|
|
269
|
+
continue; // This criterion matches
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (candidateValue !== lookupValue) {
|
|
273
|
+
allMatch = false;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (allMatch) {
|
|
278
|
+
return candidate.id;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Finds the root record for a given record
|
|
285
|
+
*/
|
|
286
|
+
findRootDependency(record) {
|
|
287
|
+
// If this record has no parent, it IS the root, no dependency
|
|
288
|
+
if (!record.parentContext) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
// Walk up the parent chain to find the root
|
|
292
|
+
let current = record;
|
|
293
|
+
while (current.parentContext) {
|
|
294
|
+
// Try to find the parent record in our flattened list
|
|
295
|
+
const parentRecord = this.flattenedRecords.find(r => r.record === current.parentContext.record &&
|
|
296
|
+
r.entityName === current.parentContext.entityName);
|
|
297
|
+
if (!parentRecord) {
|
|
298
|
+
// Parent not found, something is wrong
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
// If this parent has no parent, it's the root
|
|
302
|
+
if (!parentRecord.parentContext) {
|
|
303
|
+
return parentRecord.id;
|
|
304
|
+
}
|
|
305
|
+
current = parentRecord;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Finds a record by its primary key value
|
|
311
|
+
*/
|
|
312
|
+
findRecordByPrimaryKey(entityName, primaryKeyValue, entityInfo) {
|
|
313
|
+
// Get primary key field name
|
|
314
|
+
const primaryKeyField = entityInfo.PrimaryKeys[0]?.Name;
|
|
315
|
+
if (!primaryKeyField)
|
|
316
|
+
return null;
|
|
317
|
+
for (const candidate of this.flattenedRecords) {
|
|
318
|
+
if (candidate.entityName !== entityName)
|
|
319
|
+
continue;
|
|
320
|
+
const candidateValue = candidate.record.primaryKey?.[primaryKeyField] ||
|
|
321
|
+
candidate.record.fields?.[primaryKeyField];
|
|
322
|
+
if (candidateValue === primaryKeyValue) {
|
|
323
|
+
return candidate.id;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Gets EntityInfo from cache or metadata
|
|
330
|
+
*/
|
|
331
|
+
getEntityInfo(entityName) {
|
|
332
|
+
if (!this.entityInfoCache.has(entityName)) {
|
|
333
|
+
const info = this.metadata.EntityByName(entityName);
|
|
334
|
+
if (info) {
|
|
335
|
+
this.entityInfoCache.set(entityName, info);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return this.entityInfoCache.get(entityName) || null;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Detects circular dependencies in the dependency graph
|
|
342
|
+
*/
|
|
343
|
+
detectCircularDependencies() {
|
|
344
|
+
const cycles = [];
|
|
345
|
+
const visited = new Set();
|
|
346
|
+
const recursionStack = new Set();
|
|
347
|
+
const detectCycle = (recordId, path) => {
|
|
348
|
+
visited.add(recordId);
|
|
349
|
+
recursionStack.add(recordId);
|
|
350
|
+
path.push(recordId);
|
|
351
|
+
const record = this.recordIdMap.get(recordId);
|
|
352
|
+
if (record) {
|
|
353
|
+
for (const depId of record.dependencies) {
|
|
354
|
+
if (!visited.has(depId)) {
|
|
355
|
+
if (detectCycle(depId, [...path])) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (recursionStack.has(depId)) {
|
|
360
|
+
// Found a cycle
|
|
361
|
+
const cycleStart = path.indexOf(depId);
|
|
362
|
+
const cycle = path.slice(cycleStart);
|
|
363
|
+
cycle.push(depId); // Complete the cycle
|
|
364
|
+
cycles.push(cycle);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
recursionStack.delete(recordId);
|
|
370
|
+
return false;
|
|
371
|
+
};
|
|
372
|
+
// Check all records for cycles
|
|
373
|
+
for (const record of this.flattenedRecords) {
|
|
374
|
+
if (!visited.has(record.id)) {
|
|
375
|
+
detectCycle(record.id, []);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return cycles;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Performs topological sort on the dependency graph
|
|
382
|
+
*/
|
|
383
|
+
topologicalSort() {
|
|
384
|
+
const result = [];
|
|
385
|
+
const visited = new Set();
|
|
386
|
+
const tempStack = new Set();
|
|
387
|
+
const visit = (recordId) => {
|
|
388
|
+
if (tempStack.has(recordId)) {
|
|
389
|
+
// Circular dependency - we've already detected these
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (visited.has(recordId)) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
tempStack.add(recordId);
|
|
396
|
+
const record = this.recordIdMap.get(recordId);
|
|
397
|
+
if (record) {
|
|
398
|
+
// Visit dependencies first
|
|
399
|
+
for (const depId of record.dependencies) {
|
|
400
|
+
visit(depId);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
tempStack.delete(recordId);
|
|
404
|
+
visited.add(recordId);
|
|
405
|
+
if (record) {
|
|
406
|
+
result.push(record);
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
};
|
|
410
|
+
// Process all records, starting with those that have no dependencies
|
|
411
|
+
// First, process records with no dependencies
|
|
412
|
+
for (const record of this.flattenedRecords) {
|
|
413
|
+
if (record.dependencies.size === 0 && !visited.has(record.id)) {
|
|
414
|
+
visit(record.id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Then process any remaining records (handles disconnected components)
|
|
418
|
+
for (const record of this.flattenedRecords) {
|
|
419
|
+
if (!visited.has(record.id)) {
|
|
420
|
+
visit(record.id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
exports.RecordDependencyAnalyzer = RecordDependencyAnalyzer;
|
|
427
|
+
//# sourceMappingURL=record-dependency-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-dependency-analyzer.js","sourceRoot":"","sources":["../../src/lib/record-dependency-analyzer.ts"],"names":[],"mappings":"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","sourcesContent":["import { Metadata, EntityInfo } from '@memberjunction/core';\nimport { RecordData } from './sync-engine';\n\n/**\n * Represents a flattened record with its context and dependencies\n */\nexport interface FlattenedRecord {\n record: RecordData;\n entityName: string;\n parentContext?: {\n entityName: string;\n record: RecordData;\n recordIndex: number;\n };\n depth: number;\n path: string; // Path to this record for debugging\n dependencies: Set<string>; // Set of record IDs this record depends on\n id: string; // Unique identifier for this record in the flattened list\n originalIndex: number; // Original index in the source array\n}\n\n/**\n * Result of dependency analysis\n */\nexport interface DependencyAnalysisResult {\n sortedRecords: FlattenedRecord[];\n circularDependencies: string[][];\n dependencyGraph: Map<string, Set<string>>;\n}\n\n/**\n * Analyzes and sorts records based on their dependencies\n */\nexport class RecordDependencyAnalyzer {\n private metadata: Metadata;\n private flattenedRecords: FlattenedRecord[] = [];\n private recordIdMap: Map<string, FlattenedRecord> = new Map();\n private entityInfoCache: Map<string, EntityInfo> = new Map();\n private recordCounter: number = 0;\n\n constructor() {\n this.metadata = new Metadata();\n }\n\n /**\n * Main entry point: analyzes all records in a file and returns them in dependency order\n */\n public async analyzeFileRecords(\n records: RecordData[],\n entityName: string\n ): Promise<DependencyAnalysisResult> {\n // Reset state\n this.flattenedRecords = [];\n this.recordIdMap.clear();\n this.recordCounter = 0;\n\n // Step 1: Flatten all records (including nested relatedEntities)\n this.flattenRecords(records, entityName);\n\n // Step 2: Analyze dependencies between all flattened records\n this.analyzeDependencies();\n\n // Step 3: Detect circular dependencies\n const circularDeps = this.detectCircularDependencies();\n\n // Step 4: Perform topological sort\n const sortedRecords = this.topologicalSort();\n\n // Step 5: Build dependency graph for debugging\n const dependencyGraph = new Map<string, Set<string>>();\n for (const record of this.flattenedRecords) {\n dependencyGraph.set(record.id, record.dependencies);\n }\n\n return {\n sortedRecords,\n circularDependencies: circularDeps,\n dependencyGraph\n };\n }\n\n /**\n * Flattens all records including nested relatedEntities\n */\n private flattenRecords(\n records: RecordData[],\n entityName: string,\n parentContext?: FlattenedRecord['parentContext'],\n depth: number = 0,\n pathPrefix: string = '',\n parentRecordId?: string\n ): void {\n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n const recordId = `${entityName}_${this.recordCounter++}`;\n const path = pathPrefix ? `${pathPrefix}/${entityName}[${i}]` : `${entityName}[${i}]`;\n\n const flattenedRecord: FlattenedRecord = {\n record,\n entityName,\n parentContext,\n depth,\n path,\n dependencies: new Set(),\n id: recordId,\n originalIndex: i\n };\n\n // If this has a parent, add dependency on the parent\n if (parentRecordId) {\n flattenedRecord.dependencies.add(parentRecordId);\n }\n\n this.flattenedRecords.push(flattenedRecord);\n this.recordIdMap.set(recordId, flattenedRecord);\n\n // Recursively flatten related entities\n if (record.relatedEntities) {\n for (const [relatedEntityName, relatedRecords] of Object.entries(record.relatedEntities)) {\n this.flattenRecords(\n relatedRecords,\n relatedEntityName,\n {\n entityName,\n record,\n recordIndex: i\n },\n depth + 1,\n path,\n recordId // Pass current record ID as parent for children\n );\n }\n }\n }\n }\n\n /**\n * Analyzes dependencies between all flattened records\n */\n private analyzeDependencies(): void {\n for (const record of this.flattenedRecords) {\n // Get entity info for foreign key relationships\n const entityInfo = this.getEntityInfo(record.entityName);\n if (!entityInfo) continue;\n\n // Analyze field dependencies\n if (record.record.fields) {\n for (const [fieldName, fieldValue] of Object.entries(record.record.fields)) {\n if (typeof fieldValue === 'string') {\n // Handle @lookup references\n if (fieldValue.startsWith('@lookup:')) {\n const dependency = this.findLookupDependency(fieldValue, record);\n if (dependency) {\n record.dependencies.add(dependency);\n }\n }\n // Handle @root references - these create dependencies on the root record\n else if (fieldValue.startsWith('@root:')) {\n const rootDependency = this.findRootDependency(record);\n if (rootDependency) {\n record.dependencies.add(rootDependency);\n }\n }\n // @parent references don't create explicit dependencies in our flattened structure\n // because parent is guaranteed to be processed before children due to the way\n // we flatten records (parent always comes before its children)\n }\n }\n }\n\n // Check foreign key dependencies\n this.analyzeForeignKeyDependencies(record, entityInfo);\n }\n }\n\n /**\n * Analyzes foreign key dependencies based on EntityInfo\n */\n private analyzeForeignKeyDependencies(record: FlattenedRecord, entityInfo: EntityInfo): void {\n // Check all foreign key fields\n for (const field of entityInfo.ForeignKeys) {\n const fieldValue = record.record.fields?.[field.Name];\n if (fieldValue && typeof fieldValue === 'string' && !fieldValue.startsWith('@')) {\n // This is a direct foreign key value, find the referenced record\n const relatedEntityInfo = this.getEntityInfo(field.RelatedEntity);\n if (relatedEntityInfo) {\n const dependency = this.findRecordByPrimaryKey(\n field.RelatedEntity,\n fieldValue,\n relatedEntityInfo\n );\n if (dependency) {\n record.dependencies.add(dependency);\n }\n }\n }\n }\n }\n\n /**\n * Finds a record that matches a @lookup reference\n */\n private findLookupDependency(lookupValue: string, currentRecord: FlattenedRecord): string | null {\n // Parse lookup format: @lookup:EntityName.Field=Value or @lookup:Field=Value\n const lookupStr = lookupValue.substring(8); // Remove '@lookup:'\n \n // Handle the ?create syntax by removing it\n const cleanLookup = lookupStr.split('?')[0];\n \n // Parse entity name if present\n let targetEntity: string;\n let criteria: string;\n \n if (cleanLookup.includes('.')) {\n const parts = cleanLookup.split('.');\n targetEntity = parts[0];\n criteria = parts.slice(1).join('.');\n } else {\n // Same entity if not specified\n targetEntity = currentRecord.entityName;\n criteria = cleanLookup;\n }\n\n // Parse criteria (can be multiple with &)\n const criteriaMap = new Map<string, string>();\n for (const pair of criteria.split('&')) {\n const [field, value] = pair.split('=');\n if (field && value) {\n let resolvedValue = value.trim();\n \n // Special handling for nested @lookup references in lookup criteria\n // This creates a dependency on the looked-up record\n if (resolvedValue.startsWith('@lookup:')) {\n const nestedDependency = this.findLookupDependency(resolvedValue, currentRecord);\n if (nestedDependency) {\n // Add this as a dependency of the current record\n currentRecord.dependencies.add(nestedDependency);\n // Continue processing - we can't resolve the actual value here\n // but we've recorded the dependency\n }\n }\n // Special handling for @root references in lookup criteria\n else if (resolvedValue.startsWith('@root:')) {\n // Add dependency on root record\n const rootDep = this.findRootDependency(currentRecord);\n if (rootDep) {\n currentRecord.dependencies.add(rootDep);\n }\n // Note: We can't resolve the actual value here, but we've recorded the dependency\n }\n // Special handling for @parent references in lookup criteria\n // If the value is @parent:field, we need to resolve it from the parent context\n else if (resolvedValue.startsWith('@parent:') && currentRecord.parentContext) {\n const parentField = resolvedValue.substring(8);\n \n // Try to resolve from parent context\n const parentValue = currentRecord.parentContext.record.fields?.[parentField] ||\n currentRecord.parentContext.record.primaryKey?.[parentField];\n \n if (parentValue && typeof parentValue === 'string') {\n // Check if parent value is also a @parent reference (nested parent refs)\n if (parentValue.startsWith('@parent:')) {\n // Find the parent record to get its parent context\n const parentRecord = this.flattenedRecords.find(r => \n r.record === currentRecord.parentContext!.record && \n r.entityName === currentRecord.parentContext!.entityName\n );\n \n if (parentRecord && parentRecord.parentContext) {\n const grandParentField = parentValue.substring(8);\n const grandParentValue = parentRecord.parentContext.record.fields?.[grandParentField] ||\n parentRecord.parentContext.record.primaryKey?.[grandParentField];\n if (grandParentValue && typeof grandParentValue === 'string' && !grandParentValue.startsWith('@')) {\n resolvedValue = grandParentValue;\n }\n }\n } else if (!parentValue.startsWith('@')) {\n resolvedValue = parentValue;\n }\n }\n }\n \n criteriaMap.set(field.trim(), resolvedValue);\n }\n }\n\n // Find matching record in our flattened list\n for (const candidate of this.flattenedRecords) {\n if (candidate.entityName !== targetEntity) continue;\n if (candidate.id === currentRecord.id) continue; // Skip self\n\n // Check if all criteria match\n let allMatch = true;\n for (const [field, value] of criteriaMap) {\n let candidateValue = candidate.record.fields?.[field] || \n candidate.record.primaryKey?.[field];\n let lookupValue = value;\n \n // Resolve candidate value if it's a @parent reference\n if (typeof candidateValue === 'string' && candidateValue.startsWith('@parent:') && candidate.parentContext) {\n const parentField = candidateValue.substring(8);\n const parentRecord = candidate.parentContext.record;\n candidateValue = parentRecord.fields?.[parentField] || parentRecord.primaryKey?.[parentField];\n \n // If the parent field is also a @parent reference, resolve it recursively\n if (typeof candidateValue === 'string' && candidateValue.startsWith('@parent:')) {\n // Find the candidate's parent in our flattened list\n const candidateParent = this.flattenedRecords.find(r => \n r.record === candidate.parentContext!.record && \n r.entityName === candidate.parentContext!.entityName\n );\n if (candidateParent?.parentContext) {\n const grandParentField = candidateValue.substring(8);\n candidateValue = candidateParent.parentContext.record.fields?.[grandParentField] || \n candidateParent.parentContext.record.primaryKey?.[grandParentField];\n }\n }\n }\n \n // Resolve lookup value if it contains @parent reference\n if (typeof lookupValue === 'string' && lookupValue.includes('@parent:')) {\n // Handle cases like \"@parent:AgentID\" or embedded references\n if (lookupValue.startsWith('@parent:') && currentRecord.parentContext) {\n const parentField = lookupValue.substring(8);\n lookupValue = currentRecord.parentContext.record.fields?.[parentField] || \n currentRecord.parentContext.record.primaryKey?.[parentField];\n \n // If still a reference, try to resolve from the parent's parent\n if (typeof lookupValue === 'string' && lookupValue.startsWith('@parent:')) {\n const currentParent = this.flattenedRecords.find(r => \n r.record === currentRecord.parentContext!.record && \n r.entityName === currentRecord.parentContext!.entityName\n );\n if (currentParent?.parentContext) {\n const grandParentField = lookupValue.substring(8);\n lookupValue = currentParent.parentContext.record.fields?.[grandParentField] || \n currentParent.parentContext.record.primaryKey?.[grandParentField];\n }\n }\n }\n }\n \n // Special case: if both values are @parent references pointing to the same parent field,\n // and they have the same parent context, they match\n if (value.startsWith('@parent:') && candidateValue === value && \n currentRecord.parentContext && candidate.parentContext) {\n // Check if they share the same parent\n if (currentRecord.parentContext.record === candidate.parentContext.record) {\n // Same parent, same reference - they will resolve to the same value\n continue; // This criterion matches\n }\n }\n \n if (candidateValue !== lookupValue) {\n allMatch = false;\n break;\n }\n }\n\n if (allMatch) {\n return candidate.id;\n }\n }\n\n return null;\n }\n\n /**\n * Finds the root record for a given record\n */\n private findRootDependency(record: FlattenedRecord): string | null {\n // If this record has no parent, it IS the root, no dependency\n if (!record.parentContext) {\n return null;\n }\n \n // Walk up the parent chain to find the root\n let current = record;\n while (current.parentContext) {\n // Try to find the parent record in our flattened list\n const parentRecord = this.flattenedRecords.find(r => \n r.record === current.parentContext!.record && \n r.entityName === current.parentContext!.entityName\n );\n \n if (!parentRecord) {\n // Parent not found, something is wrong\n return null;\n }\n \n // If this parent has no parent, it's the root\n if (!parentRecord.parentContext) {\n return parentRecord.id;\n }\n \n current = parentRecord;\n }\n \n return null;\n }\n\n /**\n * Finds a record by its primary key value\n */\n private findRecordByPrimaryKey(\n entityName: string,\n primaryKeyValue: string,\n entityInfo: EntityInfo\n ): string | null {\n // Get primary key field name\n const primaryKeyField = entityInfo.PrimaryKeys[0]?.Name;\n if (!primaryKeyField) return null;\n\n for (const candidate of this.flattenedRecords) {\n if (candidate.entityName !== entityName) continue;\n\n const candidateValue = candidate.record.primaryKey?.[primaryKeyField] ||\n candidate.record.fields?.[primaryKeyField];\n if (candidateValue === primaryKeyValue) {\n return candidate.id;\n }\n }\n\n return null;\n }\n\n /**\n * Gets EntityInfo from cache or metadata\n */\n private getEntityInfo(entityName: string): EntityInfo | null {\n if (!this.entityInfoCache.has(entityName)) {\n const info = this.metadata.EntityByName(entityName);\n if (info) {\n this.entityInfoCache.set(entityName, info);\n }\n }\n return this.entityInfoCache.get(entityName) || null;\n }\n\n /**\n * Detects circular dependencies in the dependency graph\n */\n private detectCircularDependencies(): string[][] {\n const cycles: string[][] = [];\n const visited = new Set<string>();\n const recursionStack = new Set<string>();\n\n const detectCycle = (recordId: string, path: string[]): boolean => {\n visited.add(recordId);\n recursionStack.add(recordId);\n path.push(recordId);\n\n const record = this.recordIdMap.get(recordId);\n if (record) {\n for (const depId of record.dependencies) {\n if (!visited.has(depId)) {\n if (detectCycle(depId, [...path])) {\n return true;\n }\n } else if (recursionStack.has(depId)) {\n // Found a cycle\n const cycleStart = path.indexOf(depId);\n const cycle = path.slice(cycleStart);\n cycle.push(depId); // Complete the cycle\n cycles.push(cycle);\n return true;\n }\n }\n }\n\n recursionStack.delete(recordId);\n return false;\n };\n\n // Check all records for cycles\n for (const record of this.flattenedRecords) {\n if (!visited.has(record.id)) {\n detectCycle(record.id, []);\n }\n }\n\n return cycles;\n }\n\n /**\n * Performs topological sort on the dependency graph\n */\n private topologicalSort(): FlattenedRecord[] {\n const result: FlattenedRecord[] = [];\n const visited = new Set<string>();\n const tempStack = new Set<string>();\n\n const visit = (recordId: string): boolean => {\n if (tempStack.has(recordId)) {\n // Circular dependency - we've already detected these\n return false;\n }\n\n if (visited.has(recordId)) {\n return true;\n }\n\n tempStack.add(recordId);\n\n const record = this.recordIdMap.get(recordId);\n if (record) {\n // Visit dependencies first\n for (const depId of record.dependencies) {\n visit(depId);\n }\n }\n\n tempStack.delete(recordId);\n visited.add(recordId);\n \n if (record) {\n result.push(record);\n }\n\n return true;\n };\n\n // Process all records, starting with those that have no dependencies\n // First, process records with no dependencies\n for (const record of this.flattenedRecords) {\n if (record.dependencies.size === 0 && !visited.has(record.id)) {\n visit(record.id);\n }\n }\n\n // Then process any remaining records (handles disconnected components)\n for (const record of this.flattenedRecords) {\n if (!visited.has(record.id)) {\n visit(record.id);\n }\n }\n\n return result;\n }\n}"]}
|