@memberjunction/metadata-sync 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +92 -51
  2. package/dist/index.d.ts +21 -1
  3. package/dist/index.js +43 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/file-backup-manager.js +2 -2
  6. package/dist/lib/file-backup-manager.js.map +1 -1
  7. package/dist/lib/provider-utils.d.ts +2 -2
  8. package/dist/lib/provider-utils.js.map +1 -1
  9. package/dist/lib/sql-logger.d.ts +44 -0
  10. package/dist/lib/sql-logger.js +140 -0
  11. package/dist/lib/sql-logger.js.map +1 -0
  12. package/dist/lib/sync-engine.d.ts +25 -0
  13. package/dist/lib/sync-engine.js +72 -2
  14. package/dist/lib/sync-engine.js.map +1 -1
  15. package/dist/lib/transaction-manager.d.ts +35 -0
  16. package/dist/lib/transaction-manager.js +100 -0
  17. package/dist/lib/transaction-manager.js.map +1 -0
  18. package/dist/services/FileResetService.d.ts +30 -0
  19. package/dist/services/FileResetService.js +183 -0
  20. package/dist/services/FileResetService.js.map +1 -0
  21. package/dist/services/InitService.d.ts +17 -0
  22. package/dist/services/InitService.js +118 -0
  23. package/dist/services/InitService.js.map +1 -0
  24. package/dist/services/PullService.d.ts +45 -0
  25. package/dist/services/PullService.js +564 -0
  26. package/dist/services/PullService.js.map +1 -0
  27. package/dist/services/PushService.d.ts +47 -0
  28. package/dist/services/PushService.js +558 -0
  29. package/dist/services/PushService.js.map +1 -0
  30. package/dist/services/StatusService.d.ts +32 -0
  31. package/dist/services/StatusService.js +138 -0
  32. package/dist/services/StatusService.js.map +1 -0
  33. package/dist/services/WatchService.d.ts +34 -0
  34. package/dist/services/WatchService.js +296 -0
  35. package/dist/services/WatchService.js.map +1 -0
  36. package/dist/services/index.d.ts +16 -0
  37. package/dist/services/index.js +28 -0
  38. package/dist/services/index.js.map +1 -0
  39. package/package.json +14 -45
  40. package/bin/debug.js +0 -7
  41. package/bin/run +0 -17
  42. package/bin/run.js +0 -6
  43. package/dist/commands/file-reset/index.d.ts +0 -15
  44. package/dist/commands/file-reset/index.js +0 -221
  45. package/dist/commands/file-reset/index.js.map +0 -1
  46. package/dist/commands/init/index.d.ts +0 -7
  47. package/dist/commands/init/index.js +0 -155
  48. package/dist/commands/init/index.js.map +0 -1
  49. package/dist/commands/pull/index.d.ts +0 -246
  50. package/dist/commands/pull/index.js +0 -1448
  51. package/dist/commands/pull/index.js.map +0 -1
  52. package/dist/commands/push/index.d.ts +0 -41
  53. package/dist/commands/push/index.js +0 -1129
  54. package/dist/commands/push/index.js.map +0 -1
  55. package/dist/commands/status/index.d.ts +0 -10
  56. package/dist/commands/status/index.js +0 -199
  57. package/dist/commands/status/index.js.map +0 -1
  58. package/dist/commands/validate/index.d.ts +0 -15
  59. package/dist/commands/validate/index.js +0 -149
  60. package/dist/commands/validate/index.js.map +0 -1
  61. package/dist/commands/watch/index.d.ts +0 -15
  62. package/dist/commands/watch/index.js +0 -300
  63. package/dist/commands/watch/index.js.map +0 -1
  64. package/dist/hooks/init.d.ts +0 -3
  65. package/dist/hooks/init.js +0 -59
  66. package/dist/hooks/init.js.map +0 -1
  67. package/oclif.manifest.json +0 -376
@@ -0,0 +1,558 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PushService = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fast_glob_1 = __importDefault(require("fast-glob"));
10
+ const core_1 = require("@memberjunction/core");
11
+ const config_1 = require("../config");
12
+ const file_backup_manager_1 = require("../lib/file-backup-manager");
13
+ const config_manager_1 = require("../lib/config-manager");
14
+ const sql_logger_1 = require("../lib/sql-logger");
15
+ const transaction_manager_1 = require("../lib/transaction-manager");
16
+ class PushService {
17
+ syncEngine;
18
+ contextUser;
19
+ warnings = [];
20
+ processedRecords = new Map();
21
+ syncConfig;
22
+ constructor(syncEngine, contextUser) {
23
+ this.syncEngine = syncEngine;
24
+ this.contextUser = contextUser;
25
+ }
26
+ async push(options, callbacks) {
27
+ this.warnings = [];
28
+ this.processedRecords.clear();
29
+ const fileBackupManager = new file_backup_manager_1.FileBackupManager();
30
+ // Load sync config for SQL logging settings and autoCreateMissingRecords flag
31
+ // If dir option is specified, load from that directory, otherwise use original CWD
32
+ const configDir = options.dir ? path_1.default.resolve(config_manager_1.configManager.getOriginalCwd(), options.dir) : config_manager_1.configManager.getOriginalCwd();
33
+ this.syncConfig = await (0, config_1.loadSyncConfig)(configDir);
34
+ if (options.verbose) {
35
+ callbacks?.onLog?.(`Original working directory: ${config_manager_1.configManager.getOriginalCwd()}`);
36
+ callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);
37
+ callbacks?.onLog?.(`Config file path: ${path_1.default.join(configDir, '.mj-sync.json')}`);
38
+ callbacks?.onLog?.(`Full sync config loaded: ${JSON.stringify(this.syncConfig, null, 2)}`);
39
+ callbacks?.onLog?.(`SQL logging config: ${JSON.stringify(this.syncConfig?.sqlLogging)}`);
40
+ }
41
+ const sqlLogger = new sql_logger_1.SQLLogger(this.syncConfig);
42
+ const transactionManager = new transaction_manager_1.TransactionManager(sqlLogger);
43
+ if (options.verbose) {
44
+ callbacks?.onLog?.(`SQLLogger enabled status: ${sqlLogger.enabled}`);
45
+ }
46
+ // Setup SQL logging session with the provider if enabled
47
+ let sqlLoggingSession = null;
48
+ try {
49
+ // Initialize SQL logger if enabled and not dry-run
50
+ if (sqlLogger.enabled && !options.dryRun) {
51
+ const provider = core_1.Metadata.Provider;
52
+ if (options.verbose) {
53
+ callbacks?.onLog?.(`SQL logging enabled: ${sqlLogger.enabled}`);
54
+ callbacks?.onLog?.(`Provider type: ${provider?.constructor?.name || 'Unknown'}`);
55
+ callbacks?.onLog?.(`Has CreateSqlLogger: ${typeof provider?.CreateSqlLogger === 'function'}`);
56
+ }
57
+ if (provider && typeof provider.CreateSqlLogger === 'function') {
58
+ // Generate filename with timestamp
59
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
60
+ const filename = this.syncConfig.sqlLogging?.formatAsMigration
61
+ ? `MetadataSync_Push_${timestamp}.sql`
62
+ : `push_${timestamp}.sql`;
63
+ // Use .sql-log-push directory in the config directory (where sync was initiated)
64
+ const outputDir = path_1.default.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');
65
+ const filepath = path_1.default.join(outputDir, filename);
66
+ // Ensure the directory exists
67
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filepath));
68
+ // Create the SQL logging session
69
+ sqlLoggingSession = await provider.CreateSqlLogger(filepath, {
70
+ formatAsMigration: this.syncConfig.sqlLogging?.formatAsMigration || false,
71
+ description: 'MetadataSync push operation',
72
+ statementTypes: "mutations",
73
+ prettyPrint: true,
74
+ });
75
+ if (options.verbose) {
76
+ callbacks?.onLog?.(`šŸ“ SQL logging enabled: ${filepath}`);
77
+ }
78
+ }
79
+ else {
80
+ if (options.verbose) {
81
+ callbacks?.onWarn?.('SQL logging requested but provider does not support it');
82
+ }
83
+ }
84
+ }
85
+ // Find entity directories to process
86
+ const entityDirs = this.findEntityDirectories(configDir);
87
+ if (entityDirs.length === 0) {
88
+ throw new Error('No entity directories found');
89
+ }
90
+ if (options.verbose) {
91
+ callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to process`);
92
+ }
93
+ // Initialize file backup manager (unless in dry-run mode)
94
+ if (!options.dryRun) {
95
+ await fileBackupManager.initialize();
96
+ if (options.verbose) {
97
+ callbacks?.onLog?.('šŸ“ File backup manager initialized');
98
+ }
99
+ }
100
+ // Process each entity directory
101
+ let totalCreated = 0;
102
+ let totalUpdated = 0;
103
+ let totalUnchanged = 0;
104
+ let totalErrors = 0;
105
+ // Begin transaction if not in dry-run mode
106
+ if (!options.dryRun) {
107
+ await transactionManager.beginTransaction();
108
+ }
109
+ try {
110
+ for (const entityDir of entityDirs) {
111
+ const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
112
+ if (!entityConfig) {
113
+ const warning = `Skipping ${entityDir} - no valid entity configuration`;
114
+ this.warnings.push(warning);
115
+ callbacks?.onWarn?.(warning);
116
+ continue;
117
+ }
118
+ if (options.verbose) {
119
+ callbacks?.onLog?.(`\nProcessing ${entityConfig.entity} in ${entityDir}`);
120
+ }
121
+ const result = await this.processEntityDirectory(entityDir, entityConfig, options, fileBackupManager, callbacks, sqlLogger);
122
+ // Show per-directory summary
123
+ const dirName = path_1.default.relative(process.cwd(), entityDir) || '.';
124
+ const dirTotal = result.created + result.updated + result.unchanged;
125
+ if (dirTotal > 0 || result.errors > 0) {
126
+ callbacks?.onLog?.(`\nšŸ“ ${dirName}:`);
127
+ callbacks?.onLog?.(` Total processed: ${dirTotal} unique records`);
128
+ if (result.created > 0) {
129
+ callbacks?.onLog?.(` āœ“ Created: ${result.created}`);
130
+ }
131
+ if (result.updated > 0) {
132
+ callbacks?.onLog?.(` āœ“ Updated: ${result.updated}`);
133
+ }
134
+ if (result.unchanged > 0) {
135
+ callbacks?.onLog?.(` - Unchanged: ${result.unchanged}`);
136
+ }
137
+ if (result.errors > 0) {
138
+ callbacks?.onLog?.(` āœ— Errors: ${result.errors}`);
139
+ }
140
+ }
141
+ totalCreated += result.created;
142
+ totalUpdated += result.updated;
143
+ totalUnchanged += result.unchanged;
144
+ totalErrors += result.errors;
145
+ }
146
+ // Commit transaction if successful
147
+ if (!options.dryRun && totalErrors === 0) {
148
+ await transactionManager.commitTransaction();
149
+ }
150
+ }
151
+ catch (error) {
152
+ // Rollback transaction on error
153
+ if (!options.dryRun) {
154
+ await transactionManager.rollbackTransaction();
155
+ }
156
+ throw error;
157
+ }
158
+ // Commit file backups if successful and not in dry-run mode
159
+ if (!options.dryRun && totalErrors === 0) {
160
+ await fileBackupManager.cleanup();
161
+ if (options.verbose) {
162
+ callbacks?.onLog?.('āœ… File backups committed');
163
+ }
164
+ }
165
+ // Close SQL logging session if it was created
166
+ let sqlLogPath;
167
+ if (sqlLoggingSession) {
168
+ sqlLogPath = sqlLoggingSession.filePath;
169
+ await sqlLoggingSession.dispose();
170
+ if (options.verbose) {
171
+ callbacks?.onLog?.(`šŸ“ SQL log written to: ${sqlLogPath}`);
172
+ }
173
+ }
174
+ return {
175
+ created: totalCreated,
176
+ updated: totalUpdated,
177
+ unchanged: totalUnchanged,
178
+ errors: totalErrors,
179
+ warnings: this.warnings,
180
+ sqlLogPath
181
+ };
182
+ }
183
+ catch (error) {
184
+ // Rollback file backups on error
185
+ if (!options.dryRun) {
186
+ try {
187
+ await fileBackupManager.rollback();
188
+ callbacks?.onWarn?.('File backups rolled back due to error');
189
+ }
190
+ catch (rollbackError) {
191
+ callbacks?.onWarn?.(`Failed to rollback file backups: ${rollbackError}`);
192
+ }
193
+ }
194
+ // Close SQL logging session on error
195
+ if (sqlLoggingSession) {
196
+ try {
197
+ await sqlLoggingSession.dispose();
198
+ }
199
+ catch (disposeError) {
200
+ callbacks?.onWarn?.(`Failed to close SQL logging session: ${disposeError}`);
201
+ }
202
+ }
203
+ throw error;
204
+ }
205
+ }
206
+ async processEntityDirectory(entityDir, entityConfig, options, fileBackupManager, callbacks, sqlLogger) {
207
+ let created = 0;
208
+ let updated = 0;
209
+ let unchanged = 0;
210
+ let errors = 0;
211
+ // Find all JSON files in the directory
212
+ const pattern = entityConfig.filePattern || '*.json';
213
+ const files = await (0, fast_glob_1.default)(pattern, {
214
+ cwd: entityDir,
215
+ absolute: true,
216
+ onlyFiles: true,
217
+ dot: true,
218
+ ignore: ['**/node_modules/**', '**/.mj-*.json']
219
+ });
220
+ if (options.verbose) {
221
+ callbacks?.onLog?.(`Found ${files.length} files to process`);
222
+ }
223
+ // Process each file
224
+ for (const filePath of files) {
225
+ try {
226
+ // Backup the file before any modifications (unless dry-run)
227
+ if (!options.dryRun) {
228
+ await fileBackupManager.backupFile(filePath);
229
+ }
230
+ const fileData = await fs_extra_1.default.readJson(filePath);
231
+ const records = Array.isArray(fileData) ? fileData : [fileData];
232
+ const isArray = Array.isArray(fileData);
233
+ for (let i = 0; i < records.length; i++) {
234
+ const recordData = records[i];
235
+ if (!this.isValidRecordData(recordData)) {
236
+ callbacks?.onWarn?.(`Invalid record format in ${filePath}${isArray ? ` at index ${i}` : ''}`);
237
+ errors++;
238
+ continue;
239
+ }
240
+ try {
241
+ // For arrays, work with a deep copy to avoid modifying the original
242
+ const recordToProcess = isArray ? JSON.parse(JSON.stringify(recordData)) : recordData;
243
+ const result = await this.processRecord(recordToProcess, entityConfig, entityDir, options, callbacks, filePath, isArray ? i : undefined);
244
+ if (result === 'created')
245
+ created++;
246
+ else if (result === 'updated')
247
+ updated++;
248
+ else if (result === 'unchanged')
249
+ unchanged++;
250
+ // For arrays, update the original record's primaryKey and sync only
251
+ if (isArray) {
252
+ // Update primaryKey if it exists (for new records)
253
+ if (recordToProcess.primaryKey) {
254
+ records[i].primaryKey = recordToProcess.primaryKey;
255
+ }
256
+ // Update sync metadata only if it was updated (dirty records only)
257
+ if (recordToProcess.sync) {
258
+ records[i].sync = recordToProcess.sync;
259
+ }
260
+ }
261
+ // Track processed record
262
+ const recordKey = this.getRecordKey(recordData, entityConfig.entity);
263
+ this.processedRecords.set(recordKey, {
264
+ filePath,
265
+ arrayIndex: isArray ? i : undefined,
266
+ lineNumber: i + 1 // Simple line number approximation
267
+ });
268
+ }
269
+ catch (recordError) {
270
+ const errorMsg = `Error processing record in ${filePath}${isArray ? ` at index ${i}` : ''}: ${recordError}`;
271
+ callbacks?.onError?.(errorMsg);
272
+ errors++;
273
+ }
274
+ }
275
+ // Write back the entire file if it's an array (after processing all records)
276
+ if (isArray && !options.dryRun) {
277
+ await fs_extra_1.default.writeJson(filePath, records, { spaces: 2 });
278
+ }
279
+ }
280
+ catch (fileError) {
281
+ const errorMsg = `Error reading file ${filePath}: ${fileError}`;
282
+ callbacks?.onError?.(errorMsg);
283
+ errors++;
284
+ }
285
+ }
286
+ return { created, updated, unchanged, errors };
287
+ }
288
+ async processRecord(recordData, entityConfig, entityDir, options, callbacks, filePath, arrayIndex) {
289
+ const metadata = new core_1.Metadata();
290
+ // Get or create entity instance
291
+ let entity = await metadata.GetEntityObject(entityConfig.entity, this.contextUser);
292
+ if (!entity) {
293
+ throw new Error(`Failed to create entity object for ${entityConfig.entity}`);
294
+ }
295
+ // Apply defaults from configuration
296
+ const defaults = { ...entityConfig.defaults };
297
+ // Build full record data - keep original values for file writing
298
+ const originalFields = { ...recordData.fields };
299
+ const fullData = {
300
+ ...defaults,
301
+ ...recordData.fields
302
+ };
303
+ // Process field values for database operations
304
+ const processedData = {};
305
+ for (const [fieldName, fieldValue] of Object.entries(fullData)) {
306
+ const processedValue = await this.syncEngine.processFieldValue(fieldValue, entityDir, null, // parentRecord
307
+ null // rootRecord
308
+ );
309
+ processedData[fieldName] = processedValue;
310
+ }
311
+ // Check if record exists
312
+ const primaryKey = recordData.primaryKey;
313
+ let exists = false;
314
+ let isNew = false;
315
+ if (primaryKey && Object.keys(primaryKey).length > 0) {
316
+ // Try to load existing record
317
+ const compositeKey = new core_1.CompositeKey();
318
+ compositeKey.LoadFromSimpleObject(primaryKey);
319
+ exists = await entity.InnerLoad(compositeKey);
320
+ // Check autoCreateMissingRecords flag if record not found
321
+ if (!exists) {
322
+ const autoCreate = this.syncConfig?.push?.autoCreateMissingRecords ?? false;
323
+ const pkDisplay = Object.entries(primaryKey)
324
+ .map(([key, value]) => `${key}=${value}`)
325
+ .join(', ');
326
+ if (!autoCreate) {
327
+ const warning = `Record not found: ${entityConfig.entity} with primaryKey {${pkDisplay}}. To auto-create missing records, set push.autoCreateMissingRecords=true in .mj-sync.json`;
328
+ this.warnings.push(warning);
329
+ callbacks?.onWarn?.(warning);
330
+ return 'error';
331
+ }
332
+ else if (options.verbose) {
333
+ callbacks?.onLog?.(`Auto-creating missing ${entityConfig.entity} record with primaryKey {${pkDisplay}}`);
334
+ }
335
+ }
336
+ }
337
+ if (options.dryRun) {
338
+ if (exists) {
339
+ callbacks?.onLog?.(`[DRY RUN] Would update ${entityConfig.entity} record`);
340
+ return 'updated';
341
+ }
342
+ else {
343
+ callbacks?.onLog?.(`[DRY RUN] Would create ${entityConfig.entity} record`);
344
+ return 'created';
345
+ }
346
+ }
347
+ if (!exists) {
348
+ entity.NewRecord(); // make sure our record starts out fresh
349
+ isNew = true;
350
+ // Set primary key values for new records if provided, this is important for the auto-create logic
351
+ if (primaryKey) {
352
+ for (const [pkField, pkValue] of Object.entries(primaryKey)) {
353
+ entity.Set(pkField, pkValue);
354
+ }
355
+ }
356
+ }
357
+ // Set field values
358
+ for (const [fieldName, fieldValue] of Object.entries(processedData)) {
359
+ entity.Set(fieldName, fieldValue);
360
+ }
361
+ // Handle related entities
362
+ if (recordData.relatedEntities) {
363
+ // Store related entities to process after parent save
364
+ entity.__pendingRelatedEntities = recordData.relatedEntities;
365
+ }
366
+ // Check if the record is actually dirty before considering it changed
367
+ let isDirty = entity.Dirty;
368
+ // Also check if file content has changed (for @file references)
369
+ if (!isDirty && !isNew && recordData.sync) {
370
+ const currentChecksum = await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir);
371
+ if (currentChecksum !== recordData.sync.checksum) {
372
+ isDirty = true;
373
+ if (options.verbose) {
374
+ callbacks?.onLog?.(`šŸ“„ File content changed for ${entityConfig.entity} record (checksum mismatch)`);
375
+ }
376
+ }
377
+ }
378
+ // If updating an existing record that's dirty, show what changed
379
+ if (!isNew && isDirty) {
380
+ const changes = entity.GetChangesSinceLastSave();
381
+ const changeKeys = Object.keys(changes);
382
+ if (changeKeys.length > 0) {
383
+ // Get primary key info for display
384
+ const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
385
+ const primaryKeyDisplay = [];
386
+ if (entityInfo) {
387
+ for (const pk of entityInfo.PrimaryKeys) {
388
+ primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);
389
+ }
390
+ }
391
+ callbacks?.onLog?.(`šŸ“ Updating ${entityConfig.entity} record:`);
392
+ if (primaryKeyDisplay.length > 0) {
393
+ callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);
394
+ }
395
+ callbacks?.onLog?.(` Changes:`);
396
+ for (const fieldName of changeKeys) {
397
+ const field = entity.GetFieldByName(fieldName);
398
+ const oldValue = field ? field.OldValue : undefined;
399
+ const newValue = changes[fieldName];
400
+ callbacks?.onLog?.(` ${fieldName}: ${this.formatFieldValue(oldValue)} → ${this.formatFieldValue(newValue)}`);
401
+ }
402
+ }
403
+ }
404
+ // Save the record (always call Save, but track if it was actually dirty)
405
+ const saveResult = await entity.Save();
406
+ if (!saveResult) {
407
+ throw new Error(`Failed to save ${entityConfig.entity} record: ${entity.LatestResult?.Message || 'Unknown error'}`);
408
+ }
409
+ // Update primaryKey for new records
410
+ if (isNew) {
411
+ const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);
412
+ if (entityInfo) {
413
+ const newPrimaryKey = {};
414
+ for (const pk of entityInfo.PrimaryKeys) {
415
+ newPrimaryKey[pk.Name] = entity.Get(pk.Name);
416
+ }
417
+ recordData.primaryKey = newPrimaryKey;
418
+ }
419
+ }
420
+ // Only update sync metadata if the record was actually dirty (changed)
421
+ if (isNew || isDirty) {
422
+ recordData.sync = {
423
+ lastModified: new Date().toISOString(),
424
+ checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir)
425
+ };
426
+ }
427
+ // Restore original field values to preserve @ references
428
+ recordData.fields = originalFields;
429
+ // Write back to file only if it's a single record (not part of an array)
430
+ if (filePath && arrayIndex === undefined && !options.dryRun) {
431
+ await fs_extra_1.default.writeJson(filePath, recordData, { spaces: 2 });
432
+ }
433
+ // Process related entities after parent save
434
+ if (recordData.relatedEntities) {
435
+ await this.processRelatedEntities(entity, recordData.relatedEntities, entityDir, options, callbacks);
436
+ }
437
+ // Return the actual status based on whether the record was dirty
438
+ if (isNew) {
439
+ return 'created';
440
+ }
441
+ else if (isDirty) {
442
+ return 'updated';
443
+ }
444
+ else {
445
+ return 'unchanged';
446
+ }
447
+ }
448
+ async processRelatedEntities(parentEntity, relatedEntities, entityDir, options, callbacks) {
449
+ // TODO: Complete implementation for processing related entities
450
+ // This is a simplified version - full implementation would:
451
+ // 1. Create entity objects for each related entity type
452
+ // 2. Apply field values with proper parent/root references
453
+ // 3. Save related entities with proper error handling
454
+ // 4. Support nested related entities recursively
455
+ for (const [key, records] of Object.entries(relatedEntities)) {
456
+ for (const relatedRecord of records) {
457
+ // Process @parent references but DON'T modify the original fields
458
+ const processedFields = {};
459
+ for (const [fieldName, fieldValue] of Object.entries(relatedRecord.fields)) {
460
+ if (typeof fieldValue === 'string' && fieldValue.startsWith('@parent:')) {
461
+ const parentField = fieldValue.substring(8);
462
+ processedFields[fieldName] = parentEntity.Get(parentField);
463
+ }
464
+ else {
465
+ processedFields[fieldName] = await this.syncEngine.processFieldValue(fieldValue, entityDir, parentEntity, null);
466
+ }
467
+ }
468
+ // TODO: Actually save the related entity with processedFields
469
+ // For now, we're just processing the values but not saving
470
+ // This needs to be implemented to actually create/update the related entities
471
+ }
472
+ }
473
+ }
474
+ isValidRecordData(data) {
475
+ return data &&
476
+ typeof data === 'object' &&
477
+ 'fields' in data &&
478
+ typeof data.fields === 'object';
479
+ }
480
+ getRecordKey(recordData, entityName) {
481
+ if (recordData.primaryKey) {
482
+ const keys = Object.entries(recordData.primaryKey)
483
+ .sort(([a], [b]) => a.localeCompare(b))
484
+ .map(([k, v]) => `${k}:${v}`)
485
+ .join('|');
486
+ return `${entityName}|${keys}`;
487
+ }
488
+ // Generate a key from fields if no primary key
489
+ const fieldKeys = Object.keys(recordData.fields).sort().join(',');
490
+ return `${entityName}|fields:${fieldKeys}`;
491
+ }
492
+ formatFieldValue(value, maxLength = 50) {
493
+ let strValue = JSON.stringify(value);
494
+ strValue = strValue.trim();
495
+ if (strValue.length > maxLength) {
496
+ return strValue.substring(0, maxLength) + '...';
497
+ }
498
+ return strValue;
499
+ }
500
+ findEntityDirectories(baseDir, specificDir) {
501
+ const dirs = [];
502
+ if (specificDir) {
503
+ // Process specific directory
504
+ const fullPath = path_1.default.resolve(baseDir, specificDir);
505
+ if (fs_extra_1.default.existsSync(fullPath) && fs_extra_1.default.statSync(fullPath).isDirectory()) {
506
+ // Check if this directory has an entity configuration
507
+ const configPath = path_1.default.join(fullPath, '.mj-sync.json');
508
+ if (fs_extra_1.default.existsSync(configPath)) {
509
+ try {
510
+ const config = fs_extra_1.default.readJsonSync(configPath);
511
+ if (config.entity) {
512
+ // It's an entity directory, add it
513
+ dirs.push(fullPath);
514
+ }
515
+ else {
516
+ // It's a container directory, search its subdirectories
517
+ this.findEntityDirectoriesRecursive(fullPath, dirs);
518
+ }
519
+ }
520
+ catch {
521
+ // Invalid config, skip
522
+ }
523
+ }
524
+ }
525
+ }
526
+ else {
527
+ // Find all entity directories
528
+ this.findEntityDirectoriesRecursive(baseDir, dirs);
529
+ }
530
+ return dirs;
531
+ }
532
+ findEntityDirectoriesRecursive(dir, dirs) {
533
+ const entries = fs_extra_1.default.readdirSync(dir, { withFileTypes: true });
534
+ for (const entry of entries) {
535
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
536
+ const fullPath = path_1.default.join(dir, entry.name);
537
+ const configPath = path_1.default.join(fullPath, '.mj-sync.json');
538
+ if (fs_extra_1.default.existsSync(configPath)) {
539
+ try {
540
+ const config = fs_extra_1.default.readJsonSync(configPath);
541
+ if (config.entity) {
542
+ dirs.push(fullPath);
543
+ }
544
+ }
545
+ catch {
546
+ // Skip invalid config files
547
+ }
548
+ }
549
+ else {
550
+ // Recurse into subdirectories
551
+ this.findEntityDirectoriesRecursive(fullPath, dirs);
552
+ }
553
+ }
554
+ }
555
+ }
556
+ }
557
+ exports.PushService = PushService;
558
+ //# sourceMappingURL=PushService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PushService.js","sourceRoot":"","sources":["../../src/services/PushService.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\nimport fastGlob from 'fast-glob';\nimport { BaseEntity, LogStatus, Metadata, UserInfo, CompositeKey } from '@memberjunction/core';\nimport { SyncEngine, RecordData } from '../lib/sync-engine';\nimport { loadEntityConfig, loadSyncConfig } from '../config';\nimport { FileBackupManager } from '../lib/file-backup-manager';\nimport { configManager } from '../lib/config-manager';\nimport { SQLLogger } from '../lib/sql-logger';\nimport { TransactionManager } from '../lib/transaction-manager';\nimport type { SqlLoggingSession, SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';\n\nexport interface PushOptions {\n dir?: string;\n dryRun?: boolean;\n verbose?: boolean;\n noValidate?: boolean;\n}\n\nexport interface PushCallbacks {\n onProgress?: (message: string) => void;\n onSuccess?: (message: string) => void;\n onError?: (message: string) => void;\n onWarn?: (message: string) => void;\n onLog?: (message: string) => void;\n onConfirm?: (message: string) => Promise<boolean>;\n}\n\nexport interface PushResult {\n created: number;\n updated: number;\n unchanged: number;\n errors: number;\n warnings: string[];\n sqlLogPath?: string;\n}\n\nexport interface EntityPushResult {\n created: number;\n updated: number;\n unchanged: number;\n errors: number;\n}\n\nexport class PushService {\n private syncEngine: SyncEngine;\n private contextUser: UserInfo;\n private warnings: string[] = [];\n private processedRecords: Map<string, { filePath: string; arrayIndex?: number; lineNumber?: number }> = new Map();\n private syncConfig: any;\n \n constructor(syncEngine: SyncEngine, contextUser: UserInfo) {\n this.syncEngine = syncEngine;\n this.contextUser = contextUser;\n }\n \n async push(options: PushOptions, callbacks?: PushCallbacks): Promise<PushResult> {\n this.warnings = [];\n this.processedRecords.clear();\n \n const fileBackupManager = new FileBackupManager();\n \n // Load sync config for SQL logging settings and autoCreateMissingRecords flag\n // If dir option is specified, load from that directory, otherwise use original CWD\n const configDir = options.dir ? path.resolve(configManager.getOriginalCwd(), options.dir) : configManager.getOriginalCwd();\n this.syncConfig = await loadSyncConfig(configDir);\n \n if (options.verbose) {\n callbacks?.onLog?.(`Original working directory: ${configManager.getOriginalCwd()}`);\n callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);\n callbacks?.onLog?.(`Config file path: ${path.join(configDir, '.mj-sync.json')}`);\n callbacks?.onLog?.(`Full sync config loaded: ${JSON.stringify(this.syncConfig, null, 2)}`);\n callbacks?.onLog?.(`SQL logging config: ${JSON.stringify(this.syncConfig?.sqlLogging)}`);\n }\n \n const sqlLogger = new SQLLogger(this.syncConfig);\n const transactionManager = new TransactionManager(sqlLogger);\n \n if (options.verbose) {\n callbacks?.onLog?.(`SQLLogger enabled status: ${sqlLogger.enabled}`);\n }\n \n // Setup SQL logging session with the provider if enabled\n let sqlLoggingSession: SqlLoggingSession | null = null;\n \n try {\n // Initialize SQL logger if enabled and not dry-run\n if (sqlLogger.enabled && !options.dryRun) {\n const provider = Metadata.Provider as SQLServerDataProvider;\n \n if (options.verbose) {\n callbacks?.onLog?.(`SQL logging enabled: ${sqlLogger.enabled}`);\n callbacks?.onLog?.(`Provider type: ${provider?.constructor?.name || 'Unknown'}`);\n callbacks?.onLog?.(`Has CreateSqlLogger: ${typeof provider?.CreateSqlLogger === 'function'}`);\n }\n \n if (provider && typeof provider.CreateSqlLogger === 'function') {\n // Generate filename with timestamp\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const filename = this.syncConfig.sqlLogging?.formatAsMigration \n ? `MetadataSync_Push_${timestamp}.sql`\n : `push_${timestamp}.sql`;\n \n // Use .sql-log-push directory in the config directory (where sync was initiated)\n const outputDir = path.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');\n const filepath = path.join(outputDir, filename);\n \n // Ensure the directory exists\n await fs.ensureDir(path.dirname(filepath));\n \n // Create the SQL logging session\n sqlLoggingSession = await provider.CreateSqlLogger(filepath, {\n formatAsMigration: this.syncConfig.sqlLogging?.formatAsMigration || false,\n description: 'MetadataSync push operation',\n statementTypes: \"mutations\",\n prettyPrint: true, \n });\n \n if (options.verbose) {\n callbacks?.onLog?.(`šŸ“ SQL logging enabled: ${filepath}`);\n }\n } else {\n if (options.verbose) {\n callbacks?.onWarn?.('SQL logging requested but provider does not support it');\n }\n }\n }\n \n // Find entity directories to process\n const entityDirs = this.findEntityDirectories(configDir);\n \n if (entityDirs.length === 0) {\n throw new Error('No entity directories found');\n }\n \n if (options.verbose) {\n callbacks?.onLog?.(`Found ${entityDirs.length} entity ${entityDirs.length === 1 ? 'directory' : 'directories'} to process`);\n }\n \n // Initialize file backup manager (unless in dry-run mode)\n if (!options.dryRun) {\n await fileBackupManager.initialize();\n if (options.verbose) {\n callbacks?.onLog?.('šŸ“ File backup manager initialized');\n }\n }\n \n // Process each entity directory\n let totalCreated = 0;\n let totalUpdated = 0;\n let totalUnchanged = 0;\n let totalErrors = 0;\n \n // Begin transaction if not in dry-run mode\n if (!options.dryRun) {\n await transactionManager.beginTransaction();\n }\n \n try {\n for (const entityDir of entityDirs) {\n const entityConfig = await loadEntityConfig(entityDir);\n if (!entityConfig) {\n const warning = `Skipping ${entityDir} - no valid entity configuration`;\n this.warnings.push(warning);\n callbacks?.onWarn?.(warning);\n continue;\n }\n \n if (options.verbose) {\n callbacks?.onLog?.(`\\nProcessing ${entityConfig.entity} in ${entityDir}`);\n }\n \n const result = await this.processEntityDirectory(\n entityDir,\n entityConfig,\n options,\n fileBackupManager,\n callbacks,\n sqlLogger\n );\n \n // Show per-directory summary\n const dirName = path.relative(process.cwd(), entityDir) || '.';\n const dirTotal = result.created + result.updated + result.unchanged;\n if (dirTotal > 0 || result.errors > 0) {\n callbacks?.onLog?.(`\\nšŸ“ ${dirName}:`);\n callbacks?.onLog?.(` Total processed: ${dirTotal} unique records`);\n if (result.created > 0) {\n callbacks?.onLog?.(` āœ“ Created: ${result.created}`);\n }\n if (result.updated > 0) {\n callbacks?.onLog?.(` āœ“ Updated: ${result.updated}`);\n }\n if (result.unchanged > 0) {\n callbacks?.onLog?.(` - Unchanged: ${result.unchanged}`);\n }\n if (result.errors > 0) {\n callbacks?.onLog?.(` āœ— Errors: ${result.errors}`);\n }\n }\n \n totalCreated += result.created;\n totalUpdated += result.updated;\n totalUnchanged += result.unchanged;\n totalErrors += result.errors;\n }\n \n // Commit transaction if successful\n if (!options.dryRun && totalErrors === 0) {\n await transactionManager.commitTransaction();\n }\n } catch (error) {\n // Rollback transaction on error\n if (!options.dryRun) {\n await transactionManager.rollbackTransaction();\n }\n throw error;\n }\n \n // Commit file backups if successful and not in dry-run mode\n if (!options.dryRun && totalErrors === 0) {\n await fileBackupManager.cleanup();\n if (options.verbose) {\n callbacks?.onLog?.('āœ… File backups committed');\n }\n }\n \n // Close SQL logging session if it was created\n let sqlLogPath: string | undefined;\n if (sqlLoggingSession) {\n sqlLogPath = sqlLoggingSession.filePath;\n await sqlLoggingSession.dispose();\n if (options.verbose) {\n callbacks?.onLog?.(`šŸ“ SQL log written to: ${sqlLogPath}`);\n }\n }\n \n return {\n created: totalCreated,\n updated: totalUpdated,\n unchanged: totalUnchanged,\n errors: totalErrors,\n warnings: this.warnings,\n sqlLogPath\n };\n \n } catch (error) {\n // Rollback file backups on error\n if (!options.dryRun) {\n try {\n await fileBackupManager.rollback();\n callbacks?.onWarn?.('File backups rolled back due to error');\n } catch (rollbackError) {\n callbacks?.onWarn?.(`Failed to rollback file backups: ${rollbackError}`);\n }\n }\n \n // Close SQL logging session on error\n if (sqlLoggingSession) {\n try {\n await sqlLoggingSession.dispose();\n } catch (disposeError) {\n callbacks?.onWarn?.(`Failed to close SQL logging session: ${disposeError}`);\n }\n }\n \n throw error;\n }\n }\n \n private async processEntityDirectory(\n entityDir: string,\n entityConfig: any,\n options: PushOptions,\n fileBackupManager: FileBackupManager,\n callbacks?: PushCallbacks,\n sqlLogger?: SQLLogger\n ): Promise<EntityPushResult> {\n let created = 0;\n let updated = 0;\n let unchanged = 0;\n let errors = 0;\n \n // Find all JSON files in the directory\n const pattern = entityConfig.filePattern || '*.json';\n const files = await fastGlob(pattern, {\n cwd: entityDir,\n absolute: true,\n onlyFiles: true,\n dot: true,\n ignore: ['**/node_modules/**', '**/.mj-*.json']\n });\n \n if (options.verbose) {\n callbacks?.onLog?.(`Found ${files.length} files to process`);\n }\n \n // Process each file\n for (const filePath of files) {\n try {\n // Backup the file before any modifications (unless dry-run)\n if (!options.dryRun) {\n await fileBackupManager.backupFile(filePath);\n }\n \n const fileData = await fs.readJson(filePath);\n const records = Array.isArray(fileData) ? fileData : [fileData];\n const isArray = Array.isArray(fileData);\n \n for (let i = 0; i < records.length; i++) {\n const recordData = records[i];\n \n if (!this.isValidRecordData(recordData)) {\n callbacks?.onWarn?.(`Invalid record format in ${filePath}${isArray ? ` at index ${i}` : ''}`);\n errors++;\n continue;\n }\n \n try {\n // For arrays, work with a deep copy to avoid modifying the original\n const recordToProcess = isArray ? JSON.parse(JSON.stringify(recordData)) : recordData;\n \n const result = await this.processRecord(\n recordToProcess,\n entityConfig,\n entityDir,\n options,\n callbacks,\n filePath,\n isArray ? i : undefined\n );\n \n if (result === 'created') created++;\n else if (result === 'updated') updated++;\n else if (result === 'unchanged') unchanged++;\n \n // For arrays, update the original record's primaryKey and sync only\n if (isArray) {\n // Update primaryKey if it exists (for new records)\n if (recordToProcess.primaryKey) {\n records[i].primaryKey = recordToProcess.primaryKey;\n }\n // Update sync metadata only if it was updated (dirty records only)\n if (recordToProcess.sync) {\n records[i].sync = recordToProcess.sync;\n }\n }\n \n // Track processed record\n const recordKey = this.getRecordKey(recordData, entityConfig.entity);\n this.processedRecords.set(recordKey, {\n filePath,\n arrayIndex: isArray ? i : undefined,\n lineNumber: i + 1 // Simple line number approximation\n });\n \n } catch (recordError) {\n const errorMsg = `Error processing record in ${filePath}${isArray ? ` at index ${i}` : ''}: ${recordError}`;\n callbacks?.onError?.(errorMsg);\n errors++;\n }\n }\n \n // Write back the entire file if it's an array (after processing all records)\n if (isArray && !options.dryRun) {\n await fs.writeJson(filePath, records, { spaces: 2 });\n }\n } catch (fileError) {\n const errorMsg = `Error reading file ${filePath}: ${fileError}`;\n callbacks?.onError?.(errorMsg);\n errors++;\n }\n }\n \n return { created, updated, unchanged, errors };\n }\n \n private async processRecord(\n recordData: RecordData,\n entityConfig: any,\n entityDir: string,\n options: PushOptions,\n callbacks?: PushCallbacks,\n filePath?: string,\n arrayIndex?: number\n ): Promise<'created' | 'updated' | 'unchanged' | 'error'> {\n const metadata = new Metadata();\n \n // Get or create entity instance\n let entity = await metadata.GetEntityObject(entityConfig.entity, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for ${entityConfig.entity}`);\n }\n \n // Apply defaults from configuration\n const defaults = { ...entityConfig.defaults };\n \n // Build full record data - keep original values for file writing\n const originalFields = { ...recordData.fields };\n const fullData = {\n ...defaults,\n ...recordData.fields\n };\n \n // Process field values for database operations\n const processedData: Record<string, any> = {};\n for (const [fieldName, fieldValue] of Object.entries(fullData)) {\n const processedValue = await this.syncEngine.processFieldValue(\n fieldValue,\n entityDir,\n null, // parentRecord\n null // rootRecord\n );\n processedData[fieldName] = processedValue;\n }\n \n // Check if record exists\n const primaryKey = recordData.primaryKey;\n let exists = false;\n let isNew = false;\n \n if (primaryKey && Object.keys(primaryKey).length > 0) {\n // Try to load existing record\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n exists = await entity.InnerLoad(compositeKey);\n \n // Check autoCreateMissingRecords flag if record not found\n if (!exists) {\n const autoCreate = this.syncConfig?.push?.autoCreateMissingRecords ?? false;\n const pkDisplay = Object.entries(primaryKey)\n .map(([key, value]) => `${key}=${value}`)\n .join(', ');\n \n if (!autoCreate) {\n const warning = `Record not found: ${entityConfig.entity} with primaryKey {${pkDisplay}}. To auto-create missing records, set push.autoCreateMissingRecords=true in .mj-sync.json`;\n this.warnings.push(warning);\n callbacks?.onWarn?.(warning);\n return 'error';\n } else if (options.verbose) {\n callbacks?.onLog?.(`Auto-creating missing ${entityConfig.entity} record with primaryKey {${pkDisplay}}`);\n }\n }\n }\n \n if (options.dryRun) {\n if (exists) {\n callbacks?.onLog?.(`[DRY RUN] Would update ${entityConfig.entity} record`);\n return 'updated';\n } else {\n callbacks?.onLog?.(`[DRY RUN] Would create ${entityConfig.entity} record`);\n return 'created';\n }\n }\n \n if (!exists) {\n entity.NewRecord(); // make sure our record starts out fresh\n isNew = true;\n // Set primary key values for new records if provided, this is important for the auto-create logic\n if (primaryKey) {\n for (const [pkField, pkValue] of Object.entries(primaryKey)) {\n entity.Set(pkField, pkValue);\n }\n }\n }\n \n // Set field values\n for (const [fieldName, fieldValue] of Object.entries(processedData)) {\n entity.Set(fieldName, fieldValue);\n }\n\n // Handle related entities\n if (recordData.relatedEntities) {\n // Store related entities to process after parent save\n (entity as any).__pendingRelatedEntities = recordData.relatedEntities;\n }\n \n // Check if the record is actually dirty before considering it changed\n let isDirty = entity.Dirty;\n \n // Also check if file content has changed (for @file references)\n if (!isDirty && !isNew && recordData.sync) {\n const currentChecksum = await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir);\n if (currentChecksum !== recordData.sync.checksum) {\n isDirty = true;\n if (options.verbose) {\n callbacks?.onLog?.(`šŸ“„ File content changed for ${entityConfig.entity} record (checksum mismatch)`);\n }\n }\n }\n \n // If updating an existing record that's dirty, show what changed\n if (!isNew && isDirty) {\n const changes = entity.GetChangesSinceLastSave();\n const changeKeys = Object.keys(changes);\n if (changeKeys.length > 0) {\n // Get primary key info for display\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n const primaryKeyDisplay: string[] = [];\n if (entityInfo) {\n for (const pk of entityInfo.PrimaryKeys) {\n primaryKeyDisplay.push(`${pk.Name}: ${entity.Get(pk.Name)}`);\n }\n }\n \n callbacks?.onLog?.(`šŸ“ Updating ${entityConfig.entity} record:`);\n if (primaryKeyDisplay.length > 0) {\n callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);\n }\n callbacks?.onLog?.(` Changes:`);\n for (const fieldName of changeKeys) {\n const field = entity.GetFieldByName(fieldName);\n const oldValue = field ? field.OldValue : undefined;\n const newValue = (changes as any)[fieldName];\n callbacks?.onLog?.(` ${fieldName}: ${this.formatFieldValue(oldValue)} → ${this.formatFieldValue(newValue)}`);\n }\n }\n }\n \n // Save the record (always call Save, but track if it was actually dirty)\n const saveResult = await entity.Save();\n \n if (!saveResult) {\n throw new Error(`Failed to save ${entityConfig.entity} record: ${entity.LatestResult?.Message || 'Unknown error'}`);\n }\n \n // Update primaryKey for new records\n if (isNew) {\n const entityInfo = this.syncEngine.getEntityInfo(entityConfig.entity);\n if (entityInfo) {\n const newPrimaryKey: Record<string, any> = {};\n for (const pk of entityInfo.PrimaryKeys) {\n newPrimaryKey[pk.Name] = entity.Get(pk.Name);\n }\n recordData.primaryKey = newPrimaryKey;\n }\n }\n \n // Only update sync metadata if the record was actually dirty (changed)\n if (isNew || isDirty) {\n recordData.sync = {\n lastModified: new Date().toISOString(),\n checksum: await this.syncEngine.calculateChecksumWithFileContent(originalFields, entityDir)\n };\n }\n \n // Restore original field values to preserve @ references\n recordData.fields = originalFields;\n \n // Write back to file only if it's a single record (not part of an array)\n if (filePath && arrayIndex === undefined && !options.dryRun) {\n await fs.writeJson(filePath, recordData, { spaces: 2 });\n }\n \n // Process related entities after parent save\n if (recordData.relatedEntities) {\n await this.processRelatedEntities(\n entity,\n recordData.relatedEntities,\n entityDir,\n options,\n callbacks\n );\n }\n \n // Return the actual status based on whether the record was dirty\n if (isNew) {\n return 'created';\n } else if (isDirty) {\n return 'updated';\n } else {\n return 'unchanged';\n }\n }\n \n private async processRelatedEntities(\n parentEntity: BaseEntity,\n relatedEntities: Record<string, RecordData[]>,\n entityDir: string,\n options: PushOptions,\n callbacks?: PushCallbacks\n ): Promise<void> {\n // TODO: Complete implementation for processing related entities\n // This is a simplified version - full implementation would:\n // 1. Create entity objects for each related entity type\n // 2. Apply field values with proper parent/root references\n // 3. Save related entities with proper error handling\n // 4. Support nested related entities recursively\n for (const [key, records] of Object.entries(relatedEntities)) {\n for (const relatedRecord of records) {\n // Process @parent references but DON'T modify the original fields\n const processedFields: Record<string, any> = {};\n for (const [fieldName, fieldValue] of Object.entries(relatedRecord.fields)) {\n if (typeof fieldValue === 'string' && fieldValue.startsWith('@parent:')) {\n const parentField = fieldValue.substring(8);\n processedFields[fieldName] = parentEntity.Get(parentField);\n } else {\n processedFields[fieldName] = await this.syncEngine.processFieldValue(\n fieldValue,\n entityDir,\n parentEntity,\n null\n );\n }\n }\n \n // TODO: Actually save the related entity with processedFields\n // For now, we're just processing the values but not saving\n // This needs to be implemented to actually create/update the related entities\n }\n }\n }\n \n private isValidRecordData(data: any): data is RecordData {\n return data && \n typeof data === 'object' && \n 'fields' in data &&\n typeof data.fields === 'object';\n }\n \n private getRecordKey(recordData: RecordData, entityName: string): string {\n if (recordData.primaryKey) {\n const keys = Object.entries(recordData.primaryKey)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n return `${entityName}|${keys}`;\n }\n \n // Generate a key from fields if no primary key\n const fieldKeys = Object.keys(recordData.fields).sort().join(',');\n return `${entityName}|fields:${fieldKeys}`;\n }\n \n private formatFieldValue(value: any, maxLength: number = 50): string {\n let strValue = JSON.stringify(value);\n strValue = strValue.trim();\n\n if (strValue.length > maxLength) {\n return strValue.substring(0, maxLength) + '...';\n }\n\n return strValue;\n }\n \n private findEntityDirectories(baseDir: string, specificDir?: string): string[] {\n const dirs: string[] = [];\n \n if (specificDir) {\n // Process specific directory\n const fullPath = path.resolve(baseDir, specificDir);\n if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {\n // Check if this directory has an entity configuration\n const configPath = path.join(fullPath, '.mj-sync.json');\n if (fs.existsSync(configPath)) {\n try {\n const config = fs.readJsonSync(configPath);\n if (config.entity) {\n // It's an entity directory, add it\n dirs.push(fullPath);\n } else {\n // It's a container directory, search its subdirectories\n this.findEntityDirectoriesRecursive(fullPath, dirs);\n }\n } catch {\n // Invalid config, skip\n }\n }\n }\n } else {\n // Find all entity directories\n this.findEntityDirectoriesRecursive(baseDir, dirs);\n }\n \n return dirs;\n }\n\n private findEntityDirectoriesRecursive(dir: string, dirs: string[]): void {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n const fullPath = path.join(dir, entry.name);\n const configPath = path.join(fullPath, '.mj-sync.json');\n \n if (fs.existsSync(configPath)) {\n try {\n const config = fs.readJsonSync(configPath);\n if (config.entity) {\n dirs.push(fullPath);\n }\n } catch {\n // Skip invalid config files\n }\n } else {\n // Recurse into subdirectories\n this.findEntityDirectoriesRecursive(fullPath, dirs);\n }\n }\n }\n }\n}"]}