@memberjunction/metadata-sync 2.84.0 → 2.85.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.
@@ -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,398 @@
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
+ // Handle special case where candidate has @parent:ID and lookup has resolved value
227
+ if (candidateValue === '@parent:ID' && candidate.parentContext && !lookupValue.startsWith('@')) {
228
+ // Get the parent record's ID
229
+ const parentRecord = candidate.parentContext.record;
230
+ candidateValue = parentRecord.primaryKey?.ID || parentRecord.fields?.ID;
231
+ // If parent ID is not yet set (new record), we can't match yet
232
+ if (!candidateValue) {
233
+ allMatch = false;
234
+ break;
235
+ }
236
+ }
237
+ // Also check if @parent:<field> syntax
238
+ if (typeof candidateValue === 'string' && candidateValue.startsWith('@parent:') && candidate.parentContext) {
239
+ const parentField = candidateValue.substring(8);
240
+ const parentRecord = candidate.parentContext.record;
241
+ candidateValue = parentRecord.fields?.[parentField] || parentRecord.primaryKey?.[parentField];
242
+ }
243
+ if (candidateValue !== lookupValue) {
244
+ allMatch = false;
245
+ break;
246
+ }
247
+ }
248
+ if (allMatch) {
249
+ return candidate.id;
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+ /**
255
+ * Finds the root record for a given record
256
+ */
257
+ findRootDependency(record) {
258
+ // If this record has no parent, it IS the root, no dependency
259
+ if (!record.parentContext) {
260
+ return null;
261
+ }
262
+ // Walk up the parent chain to find the root
263
+ let current = record;
264
+ while (current.parentContext) {
265
+ // Try to find the parent record in our flattened list
266
+ const parentRecord = this.flattenedRecords.find(r => r.record === current.parentContext.record &&
267
+ r.entityName === current.parentContext.entityName);
268
+ if (!parentRecord) {
269
+ // Parent not found, something is wrong
270
+ return null;
271
+ }
272
+ // If this parent has no parent, it's the root
273
+ if (!parentRecord.parentContext) {
274
+ return parentRecord.id;
275
+ }
276
+ current = parentRecord;
277
+ }
278
+ return null;
279
+ }
280
+ /**
281
+ * Finds a record by its primary key value
282
+ */
283
+ findRecordByPrimaryKey(entityName, primaryKeyValue, entityInfo) {
284
+ // Get primary key field name
285
+ const primaryKeyField = entityInfo.PrimaryKeys[0]?.Name;
286
+ if (!primaryKeyField)
287
+ return null;
288
+ for (const candidate of this.flattenedRecords) {
289
+ if (candidate.entityName !== entityName)
290
+ continue;
291
+ const candidateValue = candidate.record.primaryKey?.[primaryKeyField] ||
292
+ candidate.record.fields?.[primaryKeyField];
293
+ if (candidateValue === primaryKeyValue) {
294
+ return candidate.id;
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+ /**
300
+ * Gets EntityInfo from cache or metadata
301
+ */
302
+ getEntityInfo(entityName) {
303
+ if (!this.entityInfoCache.has(entityName)) {
304
+ const info = this.metadata.EntityByName(entityName);
305
+ if (info) {
306
+ this.entityInfoCache.set(entityName, info);
307
+ }
308
+ }
309
+ return this.entityInfoCache.get(entityName) || null;
310
+ }
311
+ /**
312
+ * Detects circular dependencies in the dependency graph
313
+ */
314
+ detectCircularDependencies() {
315
+ const cycles = [];
316
+ const visited = new Set();
317
+ const recursionStack = new Set();
318
+ const detectCycle = (recordId, path) => {
319
+ visited.add(recordId);
320
+ recursionStack.add(recordId);
321
+ path.push(recordId);
322
+ const record = this.recordIdMap.get(recordId);
323
+ if (record) {
324
+ for (const depId of record.dependencies) {
325
+ if (!visited.has(depId)) {
326
+ if (detectCycle(depId, [...path])) {
327
+ return true;
328
+ }
329
+ }
330
+ else if (recursionStack.has(depId)) {
331
+ // Found a cycle
332
+ const cycleStart = path.indexOf(depId);
333
+ const cycle = path.slice(cycleStart);
334
+ cycle.push(depId); // Complete the cycle
335
+ cycles.push(cycle);
336
+ return true;
337
+ }
338
+ }
339
+ }
340
+ recursionStack.delete(recordId);
341
+ return false;
342
+ };
343
+ // Check all records for cycles
344
+ for (const record of this.flattenedRecords) {
345
+ if (!visited.has(record.id)) {
346
+ detectCycle(record.id, []);
347
+ }
348
+ }
349
+ return cycles;
350
+ }
351
+ /**
352
+ * Performs topological sort on the dependency graph
353
+ */
354
+ topologicalSort() {
355
+ const result = [];
356
+ const visited = new Set();
357
+ const tempStack = new Set();
358
+ const visit = (recordId) => {
359
+ if (tempStack.has(recordId)) {
360
+ // Circular dependency - we've already detected these
361
+ return false;
362
+ }
363
+ if (visited.has(recordId)) {
364
+ return true;
365
+ }
366
+ tempStack.add(recordId);
367
+ const record = this.recordIdMap.get(recordId);
368
+ if (record) {
369
+ // Visit dependencies first
370
+ for (const depId of record.dependencies) {
371
+ visit(depId);
372
+ }
373
+ }
374
+ tempStack.delete(recordId);
375
+ visited.add(recordId);
376
+ if (record) {
377
+ result.push(record);
378
+ }
379
+ return true;
380
+ };
381
+ // Process all records, starting with those that have no dependencies
382
+ // First, process records with no dependencies
383
+ for (const record of this.flattenedRecords) {
384
+ if (record.dependencies.size === 0 && !visited.has(record.id)) {
385
+ visit(record.id);
386
+ }
387
+ }
388
+ // Then process any remaining records (handles disconnected components)
389
+ for (const record of this.flattenedRecords) {
390
+ if (!visited.has(record.id)) {
391
+ visit(record.id);
392
+ }
393
+ }
394
+ return result;
395
+ }
396
+ }
397
+ exports.RecordDependencyAnalyzer = RecordDependencyAnalyzer;
398
+ //# 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":";;;AAAA,+CAA4D;AA8B5D;;GAEG;AACH,MAAa,wBAAwB;IAC3B,QAAQ,CAAW;IACnB,gBAAgB,GAAsB,EAAE,CAAC;IACzC,WAAW,GAAiC,IAAI,GAAG,EAAE,CAAC;IACtD,eAAe,GAA4B,IAAI,GAAG,EAAE,CAAC;IACrD,aAAa,GAAW,CAAC,CAAC;IAElC;QACE,IAAI,CAAC,QAAQ,GAAG,IAAI,eAAQ,EAAE,CAAC;IACjC,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,kBAAkB,CAC7B,OAAqB,EACrB,UAAkB;QAElB,cAAc;QACd,IAAI,CAAC,gBAAgB,GAAG,EAAE,CAAC;QAC3B,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QAEvB,iEAAiE;QACjE,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAEzC,6DAA6D;QAC7D,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAE3B,uCAAuC;QACvC,MAAM,YAAY,GAAG,IAAI,CAAC,0BAA0B,EAAE,CAAC;QAEvD,mCAAmC;QACnC,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAE7C,+CAA+C;QAC/C,MAAM,eAAe,GAAG,IAAI,GAAG,EAAuB,CAAC;QACvD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;QACtD,CAAC;QAED,OAAO;YACL,aAAa;YACb,oBAAoB,EAAE,YAAY;YAClC,eAAe;SAChB,CAAC;IACJ,CAAC;IAED;;OAEG;IACK,cAAc,CACpB,OAAqB,EACrB,UAAkB,EAClB,aAAgD,EAChD,QAAgB,CAAC,EACjB,aAAqB,EAAE,EACvB,cAAuB;QAEvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YAC1B,MAAM,QAAQ,GAAG,GAAG,UAAU,IAAI,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,UAAU,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,CAAC,GAAG,CAAC;YAEtF,MAAM,eAAe,GAAoB;gBACvC,MAAM;gBACN,UAAU;gBACV,aAAa;gBACb,KAAK;gBACL,IAAI;gBACJ,YAAY,EAAE,IAAI,GAAG,EAAE;gBACvB,EAAE,EAAE,QAAQ;gBACZ,aAAa,EAAE,CAAC;aACjB,CAAC;YAEF,qDAAqD;YACrD,IAAI,cAAc,EAAE,CAAC;gBACnB,eAAe,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACnD,CAAC;YAED,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;YAEhD,uCAAuC;YACvC,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;oBACzF,IAAI,CAAC,cAAc,CACjB,cAAc,EACd,iBAAiB,EACjB;wBACE,UAAU;wBACV,MAAM;wBACN,WAAW,EAAE,CAAC;qBACf,EACD,KAAK,GAAG,CAAC,EACT,IAAI,EACJ,QAAQ,CAAE,gDAAgD;qBAC3D,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACK,mBAAmB;QACzB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,gDAAgD;YAChD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACzD,IAAI,CAAC,UAAU;gBAAE,SAAS;YAE1B,6BAA6B;YAC7B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACzB,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC3E,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;wBACnC,4BAA4B;wBAC5B,IAAI,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;4BACtC,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;4BACjE,IAAI,UAAU,EAAE,CAAC;gCACf,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;4BACtC,CAAC;wBACH,CAAC;wBACD,yEAAyE;6BACpE,IAAI,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;4BACzC,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;4BACvD,IAAI,cAAc,EAAE,CAAC;gCACnB,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;4BAC1C,CAAC;wBACH,CAAC;wBACD,mFAAmF;wBACnF,8EAA8E;wBAC9E,+DAA+D;oBACjE,CAAC;gBACH,CAAC;YACH,CAAC;YAED,iCAAiC;YACjC,IAAI,CAAC,6BAA6B,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;OAEG;IACK,6BAA6B,CAAC,MAAuB,EAAE,UAAsB;QACnF,+BAA+B;QAC/B,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChF,iEAAiE;gBACjE,MAAM,iBAAiB,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;gBAClE,IAAI,iBAAiB,EAAE,CAAC;oBACtB,MAAM,UAAU,GAAG,IAAI,CAAC,sBAAsB,CAC5C,KAAK,CAAC,aAAa,EACnB,UAAU,EACV,iBAAiB,CAClB,CAAC;oBACF,IAAI,UAAU,EAAE,CAAC;wBACf,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;oBACtC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACK,oBAAoB,CAAC,WAAmB,EAAE,aAA8B;QAC9E,6EAA6E;QAC7E,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,oBAAoB;QAEhE,2CAA2C;QAC3C,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5C,+BAA+B;QAC/B,IAAI,YAAoB,CAAC;QACzB,IAAI,QAAgB,CAAC;QAErB,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrC,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACxB,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,+BAA+B;YAC/B,YAAY,GAAG,aAAa,CAAC,UAAU,CAAC;YACxC,QAAQ,GAAG,WAAW,CAAC;QACzB,CAAC;QAED,0CAA0C;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC9C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;gBACnB,IAAI,aAAa,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;gBAEjC,oEAAoE;gBACpE,oDAAoD;gBACpD,IAAI,aAAa,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;oBACzC,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;oBACjF,IAAI,gBAAgB,EAAE,CAAC;wBACrB,iDAAiD;wBACjD,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;wBACjD,+DAA+D;wBAC/D,oCAAoC;oBACtC,CAAC;gBACH,CAAC;gBACD,2DAA2D;qBACtD,IAAI,aAAa,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC5C,gCAAgC;oBAChC,MAAM,OAAO,GAAG,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;oBACvD,IAAI,OAAO,EAAE,CAAC;wBACZ,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBAC1C,CAAC;oBACD,kFAAkF;gBACpF,CAAC;gBACD,6DAA6D;gBAC7D,+EAA+E;qBAC1E,IAAI,aAAa,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,aAAa,CAAC,aAAa,EAAE,CAAC;oBAC7E,MAAM,WAAW,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;oBAE/C,qCAAqC;oBACrC,MAAM,WAAW,GAAG,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC;wBACzD,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,WAAW,CAAC,CAAC;oBAEhF,IAAI,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;wBACnD,yEAAyE;wBACzE,IAAI,WAAW,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;4BACvC,mDAAmD;4BACnD,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAClD,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,aAAc,CAAC,MAAM;gCAChD,CAAC,CAAC,UAAU,KAAK,aAAa,CAAC,aAAc,CAAC,UAAU,CACzD,CAAC;4BAEF,IAAI,YAAY,IAAI,YAAY,CAAC,aAAa,EAAE,CAAC;gCAC/C,MAAM,gBAAgB,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gCAClD,MAAM,gBAAgB,GAAG,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,gBAAgB,CAAC;oCAC7D,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,gBAAgB,CAAC,CAAC;gCACzF,IAAI,gBAAgB,IAAI,OAAO,gBAAgB,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oCAClG,aAAa,GAAG,gBAAgB,CAAC;gCACnC,CAAC;4BACH,CAAC;wBACH,CAAC;6BAAM,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;4BACxC,aAAa,GAAG,WAAW,CAAC;wBAC9B,CAAC;oBACH,CAAC;gBACH,CAAC;gBAED,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,aAAa,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QAED,6CAA6C;QAC7C,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,SAAS,CAAC,UAAU,KAAK,YAAY;gBAAE,SAAS;YACpD,IAAI,SAAS,CAAC,EAAE,KAAK,aAAa,CAAC,EAAE;gBAAE,SAAS,CAAC,YAAY;YAE7D,8BAA8B;YAC9B,IAAI,QAAQ,GAAG,IAAI,CAAC;YACpB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC;gBACzC,IAAI,cAAc,GAAG,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC;oBAChC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC1D,IAAI,WAAW,GAAG,KAAK,CAAC;gBAExB,mFAAmF;gBACnF,IAAI,cAAc,KAAK,YAAY,IAAI,SAAS,CAAC,aAAa,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC/F,6BAA6B;oBAC7B,MAAM,YAAY,GAAG,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC;oBACpD,cAAc,GAAG,YAAY,CAAC,UAAU,EAAE,EAAE,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC;oBAExE,+DAA+D;oBAC/D,IAAI,CAAC,cAAc,EAAE,CAAC;wBACpB,QAAQ,GAAG,KAAK,CAAC;wBACjB,MAAM;oBACR,CAAC;gBACH,CAAC;gBAED,uCAAuC;gBACvC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC,aAAa,EAAE,CAAC;oBAC3G,MAAM,WAAW,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;oBAChD,MAAM,YAAY,GAAG,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC;oBACpD,cAAc,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,IAAI,YAAY,CAAC,UAAU,EAAE,CAAC,WAAW,CAAC,CAAC;gBAChG,CAAC;gBAED,IAAI,cAAc,KAAK,WAAW,EAAE,CAAC;oBACnC,QAAQ,GAAG,KAAK,CAAC;oBACjB,MAAM;gBACR,CAAC;YACH,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,SAAS,CAAC,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,kBAAkB,CAAC,MAAuB;QAChD,8DAA8D;QAC9D,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,4CAA4C;QAC5C,IAAI,OAAO,GAAG,MAAM,CAAC;QACrB,OAAO,OAAO,CAAC,aAAa,EAAE,CAAC;YAC7B,sDAAsD;YACtD,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAClD,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,aAAc,CAAC,MAAM;gBAC1C,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,aAAc,CAAC,UAAU,CACnD,CAAC;YAEF,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,uCAAuC;gBACvC,OAAO,IAAI,CAAC;YACd,CAAC;YAED,8CAA8C;YAC9C,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;gBAChC,OAAO,YAAY,CAAC,EAAE,CAAC;YACzB,CAAC;YAED,OAAO,GAAG,YAAY,CAAC;QACzB,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,sBAAsB,CAC5B,UAAkB,EAClB,eAAuB,EACvB,UAAsB;QAEtB,6BAA6B;QAC7B,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QACxD,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QAElC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,SAAS,CAAC,UAAU,KAAK,UAAU;gBAAE,SAAS;YAElD,MAAM,cAAc,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,eAAe,CAAC;gBAC/C,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,CAAC;YACjE,IAAI,cAAc,KAAK,eAAe,EAAE,CAAC;gBACvC,OAAO,SAAS,CAAC,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,aAAa,CAAC,UAAkB;QACtC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YACpD,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;IACtD,CAAC;IAED;;OAEG;IACK,0BAA0B;QAChC,MAAM,MAAM,GAAe,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;QAEzC,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAE,IAAc,EAAW,EAAE;YAChE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtB,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC9C,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;oBACxC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;wBACxB,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;4BAClC,OAAO,IAAI,CAAC;wBACd,CAAC;oBACH,CAAC;yBAAM,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;wBACrC,gBAAgB;wBAChB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;wBACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;wBACrC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB;wBACxC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;wBACnB,OAAO,IAAI,CAAC;oBACd,CAAC;gBACH,CAAC;YACH,CAAC;YAED,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAChC,OAAO,KAAK,CAAC;QACf,CAAC,CAAC;QAEF,+BAA+B;QAC/B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5B,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACK,eAAe;QACrB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;QAEpC,MAAM,KAAK,GAAG,CAAC,QAAgB,EAAW,EAAE;YAC1C,IAAI,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5B,qDAAqD;gBACrD,OAAO,KAAK,CAAC;YACf,CAAC;YAED,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC;YACd,CAAC;YAED,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAExB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC9C,IAAI,MAAM,EAAE,CAAC;gBACX,2BAA2B;gBAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;oBACxC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACf,CAAC;YACH,CAAC;YAED,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAEtB,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACtB,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,qEAAqE;QACrE,8CAA8C;QAC9C,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC9D,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;QAED,uEAAuE;QACvE,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAvdD,4DAudC","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 // Handle special case where candidate has @parent:ID and lookup has resolved value\n if (candidateValue === '@parent:ID' && candidate.parentContext && !lookupValue.startsWith('@')) {\n // Get the parent record's ID\n const parentRecord = candidate.parentContext.record;\n candidateValue = parentRecord.primaryKey?.ID || parentRecord.fields?.ID;\n \n // If parent ID is not yet set (new record), we can't match yet\n if (!candidateValue) {\n allMatch = false;\n break;\n }\n }\n \n // Also check if @parent:<field> syntax\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 \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}"]}
@@ -34,7 +34,6 @@ export interface RecordData {
34
34
  * @example
35
35
  * ```typescript
36
36
  * const syncEngine = new SyncEngine(systemUser);
37
- * await syncEngine.initialize();
38
37
  *
39
38
  * // Process a field value with special references
40
39
  * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');
@@ -52,7 +51,7 @@ export declare class SyncEngine {
52
51
  * Initializes the sync engine by refreshing metadata cache
53
52
  * @returns Promise that resolves when initialization is complete
54
53
  */
55
- initialize(forceRefresh?: boolean): Promise<void>;
54
+ initialize(): Promise<void>;
56
55
  /**
57
56
  * Process special references in field values and handle complex objects
58
57
  *
@@ -88,7 +87,7 @@ export declare class SyncEngine {
88
87
  * // Returns: '{\n "items": [\n {\n "id": 1\n },\n {\n "id": 2\n }\n ]\n}'
89
88
  * ```
90
89
  */
91
- processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null, depth?: number): Promise<any>;
90
+ processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null, depth?: number, batchContext?: Map<string, BaseEntity>): Promise<any>;
92
91
  /**
93
92
  * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist
94
93
  *
@@ -115,7 +114,7 @@ export declare class SyncEngine {
115
114
  resolveLookup(entityName: string, lookupFields: Array<{
116
115
  fieldName: string;
117
116
  fieldValue: string;
118
- }>, autoCreate?: boolean, createFields?: Record<string, any>): Promise<string>;
117
+ }>, autoCreate?: boolean, createFields?: Record<string, any>, batchContext?: Map<string, BaseEntity>): Promise<string>;
119
118
  /**
120
119
  * Build cascading defaults for a file path and process field values
121
120
  *
@@ -244,49 +243,6 @@ export declare class SyncEngine {
244
243
  * ```
245
244
  */
246
245
  loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null>;
247
- /**
248
- * Process JSON object with template references
249
- *
250
- * Recursively processes JSON data structures to resolve `@template` references.
251
- * Templates can be defined at any level and support:
252
- * - Single template references: `"@template:path/to/template.json"`
253
- * - Object with @template field: `{ "@template": "file.json", "override": "value" }`
254
- * - Array of templates for merging: `{ "@template": ["base.json", "overrides.json"] }`
255
- * - Nested template references within templates
256
- *
257
- * @param data - JSON data structure to process
258
- * @param baseDir - Base directory for resolving relative template paths
259
- * @returns Promise resolving to the processed data with all templates resolved
260
- * @throws Error if template file is not found or contains invalid JSON
261
- *
262
- * @example
263
- * ```typescript
264
- * // Input data with template reference
265
- * const data = {
266
- * "@template": "defaults/ai-prompt.json",
267
- * "Name": "Custom Prompt",
268
- * "Prompt": "Override the template prompt"
269
- * };
270
- *
271
- * // Resolves template and merges with overrides
272
- * const result = await syncEngine.processTemplates(data, '/path/to/dir');
273
- * ```
274
- */
275
- processTemplates(data: any, baseDir: string): Promise<any>;
276
- /**
277
- * Load and process a template file
278
- *
279
- * Loads a JSON template file from the filesystem and recursively processes any
280
- * nested template references within it. Template paths are resolved relative to
281
- * the template file's directory, enabling template composition.
282
- *
283
- * @param templatePath - Path to the template file (relative or absolute)
284
- * @param baseDir - Base directory for resolving relative paths
285
- * @returns Promise resolving to the processed template content
286
- * @throws Error if template file not found or contains invalid JSON
287
- * @private
288
- */
289
- private loadAndProcessTemplate;
290
246
  /**
291
247
  * Process file content with {@include} references
292
248
  *
@@ -297,8 +253,8 @@ export declare class SyncEngine {
297
253
  * - Circular reference detection to prevent infinite loops
298
254
  * - Seamless content substitution maintaining surrounding text
299
255
  *
300
- * @param filePath - Path to the file being processed
301
256
  * @param content - The file content to process
257
+ * @param filePath - Path to the file being processed
302
258
  * @param visitedPaths - Set of already visited file paths for circular reference detection
303
259
  * @returns Promise resolving to the content with all includes resolved
304
260
  * @throws Error if circular reference detected or included file not found
@@ -314,33 +270,4 @@ export declare class SyncEngine {
314
270
  * ```
315
271
  */
316
272
  private processFileContentWithIncludes;
317
- /**
318
- * Deep merge two objects with target taking precedence
319
- *
320
- * Recursively merges two objects, with values from the target object overriding
321
- * values from the source object. Arrays and primitive values are not merged but
322
- * replaced entirely by the target value. Undefined values in target are skipped.
323
- *
324
- * @param source - Base object to merge from
325
- * @param target - Object with values that override source
326
- * @returns New object with merged values
327
- * @private
328
- *
329
- * @example
330
- * ```typescript
331
- * const source = {
332
- * a: 1,
333
- * b: { x: 10, y: 20 },
334
- * c: [1, 2, 3]
335
- * };
336
- * const target = {
337
- * a: 2,
338
- * b: { y: 30, z: 40 },
339
- * d: 'new'
340
- * };
341
- * const result = deepMerge(source, target);
342
- * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }
343
- * ```
344
- */
345
- private deepMerge;
346
273
  }