@memberjunction/metadata-sync 3.4.0 → 4.1.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 (130) hide show
  1. package/README.md +907 -2200
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +17 -27
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants/metadata-keywords.d.ts +1 -0
  7. package/dist/constants/metadata-keywords.d.ts.map +1 -0
  8. package/dist/constants/metadata-keywords.js +31 -42
  9. package/dist/constants/metadata-keywords.js.map +1 -1
  10. package/dist/index.d.ts +39 -37
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +26 -62
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/EntityPropertyExtractor.d.ts +1 -0
  15. package/dist/lib/EntityPropertyExtractor.d.ts.map +1 -0
  16. package/dist/lib/EntityPropertyExtractor.js +1 -5
  17. package/dist/lib/EntityPropertyExtractor.js.map +1 -1
  18. package/dist/lib/FieldExternalizer.d.ts +1 -0
  19. package/dist/lib/FieldExternalizer.d.ts.map +1 -0
  20. package/dist/lib/FieldExternalizer.js +14 -21
  21. package/dist/lib/FieldExternalizer.js.map +1 -1
  22. package/dist/lib/RecordProcessor.d.ts +3 -2
  23. package/dist/lib/RecordProcessor.d.ts.map +1 -0
  24. package/dist/lib/RecordProcessor.js +16 -25
  25. package/dist/lib/RecordProcessor.js.map +1 -1
  26. package/dist/lib/RelatedEntityHandler.d.ts +3 -2
  27. package/dist/lib/RelatedEntityHandler.d.ts.map +1 -0
  28. package/dist/lib/RelatedEntityHandler.js +3 -9
  29. package/dist/lib/RelatedEntityHandler.js.map +1 -1
  30. package/dist/lib/config-manager.d.ts +2 -1
  31. package/dist/lib/config-manager.d.ts.map +1 -0
  32. package/dist/lib/config-manager.js +10 -15
  33. package/dist/lib/config-manager.js.map +1 -1
  34. package/dist/lib/database-reference-scanner.d.ts +3 -2
  35. package/dist/lib/database-reference-scanner.d.ts.map +1 -0
  36. package/dist/lib/database-reference-scanner.js +7 -13
  37. package/dist/lib/database-reference-scanner.js.map +1 -1
  38. package/dist/lib/deletion-auditor.d.ts +18 -3
  39. package/dist/lib/deletion-auditor.d.ts.map +1 -0
  40. package/dist/lib/deletion-auditor.js +111 -20
  41. package/dist/lib/deletion-auditor.js.map +1 -1
  42. package/dist/lib/deletion-report-generator.d.ts +2 -1
  43. package/dist/lib/deletion-report-generator.d.ts.map +1 -0
  44. package/dist/lib/deletion-report-generator.js +1 -5
  45. package/dist/lib/deletion-report-generator.js.map +1 -1
  46. package/dist/lib/entity-foreign-key-helper.d.ts +1 -0
  47. package/dist/lib/entity-foreign-key-helper.d.ts.map +1 -0
  48. package/dist/lib/entity-foreign-key-helper.js +1 -5
  49. package/dist/lib/entity-foreign-key-helper.js.map +1 -1
  50. package/dist/lib/file-backup-manager.d.ts +1 -0
  51. package/dist/lib/file-backup-manager.d.ts.map +1 -0
  52. package/dist/lib/file-backup-manager.js +22 -27
  53. package/dist/lib/file-backup-manager.js.map +1 -1
  54. package/dist/lib/file-write-batch.d.ts +2 -1
  55. package/dist/lib/file-write-batch.d.ts.map +1 -0
  56. package/dist/lib/file-write-batch.js +16 -21
  57. package/dist/lib/file-write-batch.js.map +1 -1
  58. package/dist/lib/json-preprocessor.d.ts +1 -0
  59. package/dist/lib/json-preprocessor.d.ts.map +1 -0
  60. package/dist/lib/json-preprocessor.js +21 -26
  61. package/dist/lib/json-preprocessor.js.map +1 -1
  62. package/dist/lib/json-write-helper.d.ts +2 -1
  63. package/dist/lib/json-write-helper.d.ts.map +1 -0
  64. package/dist/lib/json-write-helper.js +4 -11
  65. package/dist/lib/json-write-helper.js.map +1 -1
  66. package/dist/lib/provider-utils.d.ts +2 -1
  67. package/dist/lib/provider-utils.d.ts.map +1 -0
  68. package/dist/lib/provider-utils.js +15 -46
  69. package/dist/lib/provider-utils.js.map +1 -1
  70. package/dist/lib/record-dependency-analyzer.d.ts +41 -2
  71. package/dist/lib/record-dependency-analyzer.d.ts.map +1 -0
  72. package/dist/lib/record-dependency-analyzer.js +108 -40
  73. package/dist/lib/record-dependency-analyzer.js.map +1 -1
  74. package/dist/lib/singleton-manager.d.ts +2 -1
  75. package/dist/lib/singleton-manager.d.ts.map +1 -0
  76. package/dist/lib/singleton-manager.js +4 -9
  77. package/dist/lib/singleton-manager.js.map +1 -1
  78. package/dist/lib/sql-logger.d.ts +2 -1
  79. package/dist/lib/sql-logger.d.ts.map +1 -0
  80. package/dist/lib/sql-logger.js +8 -16
  81. package/dist/lib/sql-logger.js.map +1 -1
  82. package/dist/lib/sync-engine.d.ts +2 -1
  83. package/dist/lib/sync-engine.d.ts.map +1 -0
  84. package/dist/lib/sync-engine.js +58 -76
  85. package/dist/lib/sync-engine.js.map +1 -1
  86. package/dist/lib/transaction-manager.d.ts +2 -1
  87. package/dist/lib/transaction-manager.d.ts.map +1 -0
  88. package/dist/lib/transaction-manager.js +6 -11
  89. package/dist/lib/transaction-manager.js.map +1 -1
  90. package/dist/services/FileResetService.d.ts +1 -0
  91. package/dist/services/FileResetService.d.ts.map +1 -0
  92. package/dist/services/FileResetService.js +17 -24
  93. package/dist/services/FileResetService.js.map +1 -1
  94. package/dist/services/FormattingService.d.ts +2 -1
  95. package/dist/services/FormattingService.d.ts.map +1 -0
  96. package/dist/services/FormattingService.js +68 -73
  97. package/dist/services/FormattingService.js.map +1 -1
  98. package/dist/services/InitService.d.ts +1 -0
  99. package/dist/services/InitService.d.ts.map +1 -0
  100. package/dist/services/InitService.js +12 -19
  101. package/dist/services/InitService.js.map +1 -1
  102. package/dist/services/PullService.d.ts +2 -1
  103. package/dist/services/PullService.d.ts.map +1 -0
  104. package/dist/services/PullService.js +49 -60
  105. package/dist/services/PullService.js.map +1 -1
  106. package/dist/services/PushService.d.ts +3 -1
  107. package/dist/services/PushService.d.ts.map +1 -0
  108. package/dist/services/PushService.js +144 -95
  109. package/dist/services/PushService.js.map +1 -1
  110. package/dist/services/StatusService.d.ts +2 -1
  111. package/dist/services/StatusService.d.ts.map +1 -0
  112. package/dist/services/StatusService.js +14 -22
  113. package/dist/services/StatusService.js.map +1 -1
  114. package/dist/services/ValidationService.d.ts +2 -1
  115. package/dist/services/ValidationService.d.ts.map +1 -0
  116. package/dist/services/ValidationService.js +41 -71
  117. package/dist/services/ValidationService.js.map +1 -1
  118. package/dist/services/WatchService.d.ts +4 -3
  119. package/dist/services/WatchService.d.ts.map +1 -0
  120. package/dist/services/WatchService.js +35 -43
  121. package/dist/services/WatchService.js.map +1 -1
  122. package/dist/services/index.d.ts +9 -8
  123. package/dist/services/index.d.ts.map +1 -0
  124. package/dist/services/index.js +8 -19
  125. package/dist/services/index.js.map +1 -1
  126. package/dist/types/validation.d.ts +2 -1
  127. package/dist/types/validation.d.ts.map +1 -0
  128. package/dist/types/validation.js +2 -23
  129. package/dist/types/validation.js.map +1 -1
  130. package/package.json +23 -21
@@ -1,35 +1,27 @@
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 sync_engine_1 = require("../lib/sync-engine");
12
- const config_1 = require("../config");
13
- const file_backup_manager_1 = require("../lib/file-backup-manager");
14
- const config_manager_1 = require("../lib/config-manager");
15
- const sql_logger_1 = require("../lib/sql-logger");
16
- const transaction_manager_1 = require("../lib/transaction-manager");
17
- const json_write_helper_1 = require("../lib/json-write-helper");
18
- const record_dependency_analyzer_1 = require("../lib/record-dependency-analyzer");
19
- const json_preprocessor_1 = require("../lib/json-preprocessor");
20
- const provider_utils_1 = require("../lib/provider-utils");
21
- const deletion_auditor_1 = require("../lib/deletion-auditor");
22
- const deletion_report_generator_1 = require("../lib/deletion-report-generator");
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import fastGlob from 'fast-glob';
4
+ import { Metadata } from '@memberjunction/core';
5
+ import { DeferrableLookupError } from '../lib/sync-engine.js';
6
+ import { loadEntityConfig, loadSyncConfig } from '../config.js';
7
+ import { FileBackupManager } from '../lib/file-backup-manager.js';
8
+ import { configManager } from '../lib/config-manager.js';
9
+ import { SQLLogger } from '../lib/sql-logger.js';
10
+ import { TransactionManager } from '../lib/transaction-manager.js';
11
+ import { JsonWriteHelper } from '../lib/json-write-helper.js';
12
+ import { RecordDependencyAnalyzer } from '../lib/record-dependency-analyzer.js';
13
+ import { JsonPreprocessor } from '../lib/json-preprocessor.js';
14
+ import { findEntityDirectories } from '../lib/provider-utils.js';
15
+ import { DeletionAuditor } from '../lib/deletion-auditor.js';
16
+ import { DeletionReportGenerator } from '../lib/deletion-report-generator.js';
23
17
  // Configuration for parallel processing
24
18
  const PARALLEL_BATCH_SIZE = 1; // Number of records to process in parallel at each dependency level
25
- class PushService {
26
- syncEngine;
27
- contextUser;
28
- warnings = [];
29
- syncConfig = null;
30
- deferredFileWrites = new Map();
31
- deferredRecords = [];
19
+ export class PushService {
32
20
  constructor(syncEngine, contextUser) {
21
+ this.warnings = [];
22
+ this.syncConfig = null;
23
+ this.deferredFileWrites = new Map();
24
+ this.deferredRecords = [];
33
25
  this.syncEngine = syncEngine;
34
26
  this.contextUser = contextUser;
35
27
  }
@@ -56,11 +48,11 @@ class PushService {
56
48
  // Reset deferred tracking for this push operation
57
49
  this.deferredFileWrites.clear();
58
50
  this.deferredRecords = [];
59
- const fileBackupManager = new file_backup_manager_1.FileBackupManager();
51
+ const fileBackupManager = new FileBackupManager();
60
52
  // Load sync config for SQL logging settings and autoCreateMissingRecords flag
61
53
  // If dir option is specified, load from that directory, otherwise use original CWD
62
- const configDir = options.dir ? path_1.default.resolve(config_manager_1.configManager.getOriginalCwd(), options.dir) : config_manager_1.configManager.getOriginalCwd();
63
- this.syncConfig = await (0, config_1.loadSyncConfig)(configDir);
54
+ const configDir = options.dir ? path.resolve(configManager.getOriginalCwd(), options.dir) : configManager.getOriginalCwd();
55
+ this.syncConfig = await loadSyncConfig(configDir);
64
56
  // Display warnings for special flags that are enabled
65
57
  if (this.syncConfig?.push?.alwaysPush && !options.dryRun) {
66
58
  callbacks?.onWarn?.('\n⚡ WARNING: alwaysPush is enabled - ALL records will be saved to database regardless of changes\n');
@@ -69,14 +61,14 @@ class PushService {
69
61
  callbacks?.onWarn?.('\n🔧 WARNING: autoCreateMissingRecords is enabled - Missing records with primaryKey will be created\n');
70
62
  }
71
63
  if (options.verbose) {
72
- callbacks?.onLog?.(`Original working directory: ${config_manager_1.configManager.getOriginalCwd()}`);
64
+ callbacks?.onLog?.(`Original working directory: ${configManager.getOriginalCwd()}`);
73
65
  callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);
74
- callbacks?.onLog?.(`Config file path: ${path_1.default.join(configDir, '.mj-sync.json')}`);
66
+ callbacks?.onLog?.(`Config file path: ${path.join(configDir, '.mj-sync.json')}`);
75
67
  callbacks?.onLog?.(`Full sync config loaded: ${JSON.stringify(this.syncConfig, null, 2)}`);
76
68
  callbacks?.onLog?.(`SQL logging config: ${JSON.stringify(this.syncConfig?.sqlLogging)}`);
77
69
  }
78
- const sqlLogger = new sql_logger_1.SQLLogger(this.syncConfig);
79
- const transactionManager = new transaction_manager_1.TransactionManager(sqlLogger);
70
+ const sqlLogger = new SQLLogger(this.syncConfig);
71
+ const transactionManager = new TransactionManager(sqlLogger);
80
72
  if (options.verbose) {
81
73
  callbacks?.onLog?.(`SQLLogger enabled status: ${sqlLogger.enabled}`);
82
74
  }
@@ -85,7 +77,7 @@ class PushService {
85
77
  try {
86
78
  // Initialize SQL logger if enabled and not dry-run
87
79
  if (sqlLogger.enabled && !options.dryRun) {
88
- const provider = core_1.Metadata.Provider;
80
+ const provider = Metadata.Provider;
89
81
  if (options.verbose) {
90
82
  callbacks?.onLog?.(`SQL logging enabled: ${sqlLogger.enabled}`);
91
83
  callbacks?.onLog?.(`Provider type: ${provider?.constructor?.name || 'Unknown'}`);
@@ -98,10 +90,10 @@ class PushService {
98
90
  ? `MetadataSync_Push_${timestamp}.sql`
99
91
  : `push_${timestamp}.sql`;
100
92
  // Use .sql-log-push directory in the config directory (where sync was initiated)
101
- const outputDir = path_1.default.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');
102
- const filepath = path_1.default.join(outputDir, filename);
93
+ const outputDir = path.join(configDir, this.syncConfig?.sqlLogging?.outputDirectory || './sql-log-push');
94
+ const filepath = path.join(outputDir, filename);
103
95
  // Ensure the directory exists
104
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filepath));
96
+ await fs.ensureDir(path.dirname(filepath));
105
97
  // Create the SQL logging session
106
98
  sqlLoggingSession = await provider.CreateSqlLogger(filepath, {
107
99
  formatAsMigration: this.syncConfig?.sqlLogging?.formatAsMigration || false,
@@ -125,7 +117,7 @@ class PushService {
125
117
  // Find entity directories to process
126
118
  // Note: If options.dir is specified, configDir already points to that directory
127
119
  // So we don't need to pass it as specificDir
128
- const entityDirs = (0, provider_utils_1.findEntityDirectories)(configDir, undefined, this.syncConfig?.directoryOrder, this.syncConfig?.ignoreDirectories, options.include, options.exclude);
120
+ const entityDirs = findEntityDirectories(configDir, undefined, this.syncConfig?.directoryOrder, this.syncConfig?.ignoreDirectories, options.include, options.exclude);
129
121
  if (entityDirs.length === 0) {
130
122
  throw new Error('No entity directories found');
131
123
  }
@@ -167,8 +159,8 @@ class PushService {
167
159
  try {
168
160
  await sqlLoggingSession.dispose();
169
161
  // Delete the empty SQL log file since no operations occurred
170
- if (await fs_extra_1.default.pathExists(sqlLogPath)) {
171
- await fs_extra_1.default.remove(sqlLogPath);
162
+ if (await fs.pathExists(sqlLogPath)) {
163
+ await fs.remove(sqlLogPath);
172
164
  if (options.verbose) {
173
165
  callbacks?.onLog?.(`🗑️ Removed empty SQL log file: ${sqlLogPath}`);
174
166
  }
@@ -198,7 +190,7 @@ class PushService {
198
190
  // PHASE 1: Process creates/updates for all entities
199
191
  callbacks?.onLog?.('📝 Processing creates and updates...\n');
200
192
  for (const entityDir of entityDirs) {
201
- const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
193
+ const entityConfig = await loadEntityConfig(entityDir);
202
194
  if (!entityConfig) {
203
195
  const warning = `Skipping ${entityDir} - no valid entity configuration`;
204
196
  this.warnings.push(warning);
@@ -207,7 +199,7 @@ class PushService {
207
199
  continue;
208
200
  }
209
201
  // Show folder with spinner at start
210
- const dirName = path_1.default.relative(process.cwd(), entityDir) || '.';
202
+ const dirName = path.relative(process.cwd(), entityDir) || '.';
211
203
  callbacks?.onLog?.(`\n📁 ${dirName}:`);
212
204
  // Use onProgress for animated spinner if available
213
205
  if (callbacks?.onProgress) {
@@ -350,7 +342,7 @@ class PushService {
350
342
  let errors = 0;
351
343
  // Find all JSON files in the directory
352
344
  const pattern = entityConfig.filePattern || '*.json';
353
- const files = await (0, fast_glob_1.default)(pattern, {
345
+ const files = await fastGlob(pattern, {
354
346
  cwd: entityDir,
355
347
  absolute: true,
356
348
  onlyFiles: true,
@@ -368,7 +360,7 @@ class PushService {
368
360
  await fileBackupManager.backupFile(filePath);
369
361
  }
370
362
  // Read the raw file data first
371
- const rawFileData = await fs_extra_1.default.readJson(filePath);
363
+ const rawFileData = await fs.readJson(filePath);
372
364
  // Keep unprocessed data to write back (preserves @file: references)
373
365
  const unprocessedRecords = Array.isArray(rawFileData) ? rawFileData : [rawFileData];
374
366
  const isArray = Array.isArray(rawFileData);
@@ -379,12 +371,12 @@ class PushService {
379
371
  if (hasIncludes) {
380
372
  // Preprocess the JSON file to handle @include directives
381
373
  // Create a new preprocessor instance for each file to ensure clean state
382
- const jsonPreprocessor = new json_preprocessor_1.JsonPreprocessor();
374
+ const jsonPreprocessor = new JsonPreprocessor();
383
375
  fileData = await jsonPreprocessor.processFile(filePath);
384
376
  }
385
377
  const records = Array.isArray(fileData) ? fileData : [fileData];
386
378
  // Analyze dependencies and get sorted records
387
- const analyzer = new record_dependency_analyzer_1.RecordDependencyAnalyzer();
379
+ const analyzer = new RecordDependencyAnalyzer();
388
380
  const analysisResult = await analyzer.analyzeFileRecords(records, entityConfig.entity);
389
381
  if (analysisResult.circularDependencies.length > 0) {
390
382
  callbacks?.onWarn?.(`⚠️ Circular dependencies detected in ${filePath}`);
@@ -521,10 +513,10 @@ class PushService {
521
513
  else {
522
514
  // Write immediately for files without deletions
523
515
  if (isArray) {
524
- await json_write_helper_1.JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords);
516
+ await JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords);
525
517
  }
526
518
  else {
527
- await json_write_helper_1.JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords[0]);
519
+ await JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords[0]);
528
520
  }
529
521
  }
530
522
  }
@@ -537,7 +529,7 @@ class PushService {
537
529
  return { created, updated, unchanged, deleted, skipped, deferred, errors };
538
530
  }
539
531
  async processFlattenedRecord(flattenedRecord, entityDir, options, batchContext, callbacks, entityConfig, allowDefer = true) {
540
- const metadata = new core_1.Metadata();
532
+ const metadata = new Metadata();
541
533
  const { record, entityName, parentContext, id: recordId } = flattenedRecord;
542
534
  // Skip deletion records - they're handled in Phase 2
543
535
  // File writing is deferred for files containing deletions
@@ -584,7 +576,7 @@ class PushService {
584
576
  }
585
577
  catch (pkError) {
586
578
  // Check if this is a deferrable lookup error
587
- if (pkError instanceof sync_engine_1.DeferrableLookupError) {
579
+ if (pkError instanceof DeferrableLookupError) {
588
580
  throw new Error(`Cannot defer lookup in primaryKey field '${pkField}': ${pkError.message}. Primary key lookups must resolve immediately.`);
589
581
  }
590
582
  throw pkError;
@@ -660,7 +652,7 @@ class PushService {
660
652
  }
661
653
  catch (fieldError) {
662
654
  // Check if this is a deferrable lookup error first
663
- if (fieldError instanceof sync_engine_1.DeferrableLookupError) {
655
+ if (fieldError instanceof DeferrableLookupError) {
664
656
  // If allowDefer is false, we're in deferred processing mode - can't defer again
665
657
  if (!allowDefer) {
666
658
  const err = fieldError;
@@ -999,7 +991,7 @@ class PushService {
999
991
  }
1000
992
  return String(value);
1001
993
  }
1002
- async processDeleteRecord(flattenedRecord, _entityDir, options, callbacks) {
994
+ async processDeleteRecord(flattenedRecord, _entityDir, options, callbacks, isDbOnly = false) {
1003
995
  const { record, entityName } = flattenedRecord;
1004
996
  // Validate that we have a primary key for deletion
1005
997
  if (!record.primaryKey || Object.keys(record.primaryKey).length === 0) {
@@ -1046,7 +1038,12 @@ class PushService {
1046
1038
  primaryKeyDisplay.push(`${pk.Name}: ${existingEntity.Get(pk.Name)}`);
1047
1039
  }
1048
1040
  }
1049
- callbacks?.onLog?.(`🗑️ Deleting ${entityName} record:`);
1041
+ if (isDbOnly) {
1042
+ callbacks?.onLog?.(`🗑️ Deleting database-only ${entityName} record:`);
1043
+ }
1044
+ else {
1045
+ callbacks?.onLog?.(`🗑️ Deleting ${entityName} record:`);
1046
+ }
1050
1047
  if (primaryKeyDisplay.length > 0) {
1051
1048
  callbacks?.onLog?.(` Primary Key: ${primaryKeyDisplay.join(', ')}`);
1052
1049
  }
@@ -1074,15 +1071,22 @@ class PushService {
1074
1071
  }
1075
1072
  throw new Error(`Failed to delete ${entityName} record: ${errorMessage}`);
1076
1073
  }
1077
- // Set deletedAt timestamp after successful deletion
1078
- if (!record.deleteRecord) {
1079
- record.deleteRecord = { delete: true };
1074
+ // Set deletedAt timestamp after successful deletion (only for metadata records)
1075
+ if (!isDbOnly) {
1076
+ if (!record.deleteRecord) {
1077
+ record.deleteRecord = { delete: true };
1078
+ }
1079
+ record.deleteRecord.deletedAt = new Date().toISOString();
1080
+ // Update the corresponding record in deferred file writes
1081
+ this.updateDeferredFileRecord(flattenedRecord);
1080
1082
  }
1081
- record.deleteRecord.deletedAt = new Date().toISOString();
1082
- // Update the corresponding record in deferred file writes
1083
- this.updateDeferredFileRecord(flattenedRecord);
1084
1083
  if (options.verbose) {
1085
- callbacks?.onLog?.(` ✓ Successfully deleted ${entityName} record`);
1084
+ if (isDbOnly) {
1085
+ callbacks?.onLog?.(` ✓ Successfully deleted database-only ${entityName} record`);
1086
+ }
1087
+ else {
1088
+ callbacks?.onLog?.(` ✓ Successfully deleted ${entityName} record`);
1089
+ }
1086
1090
  }
1087
1091
  return { status: 'deleted', isDuplicate: false };
1088
1092
  }
@@ -1116,9 +1120,17 @@ class PushService {
1116
1120
  messages.push(' • Create new records');
1117
1121
  messages.push(' • Update existing records');
1118
1122
  if (deletionAudit) {
1119
- const totalDeletes = deletionAudit.explicitDeletes.size + deletionAudit.implicitDeletes.size;
1120
- messages.push(` • Delete ${totalDeletes} record${totalDeletes > 1 ? 's' : ''} (${deletionAudit.explicitDeletes.size} explicit, ${deletionAudit.implicitDeletes.size} implicit)`);
1121
- if (deletionAudit.orphanedReferences.length > 0) {
1123
+ const metadataDeletes = deletionAudit.explicitDeletes.size + deletionAudit.implicitDeletes.size;
1124
+ const dbOnlyDeletes = deletionAudit.databaseOnlyDeletions?.length ?? 0;
1125
+ const totalDeletes = metadataDeletes + dbOnlyDeletes;
1126
+ if (dbOnlyDeletes > 0) {
1127
+ messages.push(` • Delete ${totalDeletes} record${totalDeletes > 1 ? 's' : ''} (${deletionAudit.explicitDeletes.size} explicit, ${deletionAudit.implicitDeletes.size} implicit, ${dbOnlyDeletes} database-only)`);
1128
+ }
1129
+ else {
1130
+ messages.push(` • Delete ${metadataDeletes} record${metadataDeletes > 1 ? 's' : ''} (${deletionAudit.explicitDeletes.size} explicit, ${deletionAudit.implicitDeletes.size} implicit)`);
1131
+ }
1132
+ if (deletionAudit.orphanedReferences.length > 0 && dbOnlyDeletes === 0) {
1133
+ // Only show warning if not deleting DB-only records
1122
1134
  messages.push(` ⚠️ ${deletionAudit.orphanedReferences.length} database-only reference${deletionAudit.orphanedReferences.length > 1 ? 's' : ''} detected (may cause FK errors)`);
1123
1135
  }
1124
1136
  }
@@ -1156,12 +1168,12 @@ class PushService {
1156
1168
  for (const entityDir of entityDirs) {
1157
1169
  if (hasAnyDeletions)
1158
1170
  break; // Early exit once we find any deletion
1159
- const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
1171
+ const entityConfig = await loadEntityConfig(entityDir);
1160
1172
  if (!entityConfig) {
1161
1173
  continue;
1162
1174
  }
1163
1175
  const pattern = entityConfig.filePattern || '*.json';
1164
- const files = await (0, fast_glob_1.default)(pattern, {
1176
+ const files = await fastGlob(pattern, {
1165
1177
  cwd: entityDir,
1166
1178
  absolute: true,
1167
1179
  onlyFiles: true,
@@ -1171,7 +1183,7 @@ class PushService {
1171
1183
  // Quick scan for delete directives without full processing
1172
1184
  for (const filePath of files) {
1173
1185
  try {
1174
- const content = await fs_extra_1.default.readFile(filePath, 'utf-8');
1186
+ const content = await fs.readFile(filePath, 'utf-8');
1175
1187
  // Fast string check for delete directives
1176
1188
  if (content.includes('"delete"') && content.includes('true')) {
1177
1189
  // More precise check - parse JSON to confirm
@@ -1198,39 +1210,43 @@ class PushService {
1198
1210
  }
1199
1211
  // Deletions exist - proceed with full audit
1200
1212
  callbacks?.onLog?.('\n🔍 Analyzing deletion operations...\n');
1201
- // Load all records from all entity directories
1202
- const allRecords = [];
1203
- const analyzer = new record_dependency_analyzer_1.RecordDependencyAnalyzer();
1213
+ // Two-phase approach for cross-file dependency detection:
1214
+ // Phase 1: Flatten all records from all files (no dependency analysis yet)
1215
+ // Phase 2: Analyze dependencies globally across all records
1216
+ const analyzer = new RecordDependencyAnalyzer();
1217
+ analyzer.reset(); // Ensure clean state before processing multiple files
1218
+ // Phase 1: Flatten all records from all files
1219
+ const allFlattenedRecords = [];
1204
1220
  for (const entityDir of entityDirs) {
1205
- const entityConfig = await (0, config_1.loadEntityConfig)(entityDir);
1221
+ const entityConfig = await loadEntityConfig(entityDir);
1206
1222
  if (!entityConfig) {
1207
1223
  continue;
1208
1224
  }
1209
1225
  // Find all JSON files
1210
1226
  const pattern = entityConfig.filePattern || '*.json';
1211
- const files = await (0, fast_glob_1.default)(pattern, {
1227
+ const files = await fastGlob(pattern, {
1212
1228
  cwd: entityDir,
1213
1229
  absolute: true,
1214
1230
  onlyFiles: true,
1215
1231
  dot: true,
1216
1232
  ignore: ['**/node_modules/**', '**/.mj-*.json']
1217
1233
  });
1218
- // Load and flatten records from each file
1234
+ // Load and flatten records from each file (no dependency analysis yet)
1219
1235
  for (const filePath of files) {
1220
1236
  try {
1221
- const rawFileData = await fs_extra_1.default.readJson(filePath);
1237
+ const rawFileData = await fs.readJson(filePath);
1222
1238
  // Handle @include directives if present
1223
1239
  let fileData = rawFileData;
1224
1240
  const jsonString = JSON.stringify(rawFileData);
1225
1241
  const hasIncludes = jsonString.includes('"@include"') || jsonString.includes('"@include.');
1226
1242
  if (hasIncludes) {
1227
- const jsonPreprocessor = new json_preprocessor_1.JsonPreprocessor();
1243
+ const jsonPreprocessor = new JsonPreprocessor();
1228
1244
  fileData = await jsonPreprocessor.processFile(filePath);
1229
1245
  }
1230
1246
  const records = Array.isArray(fileData) ? fileData : [fileData];
1231
- // Analyze and flatten records
1232
- const analysisResult = await analyzer.analyzeFileRecords(records, entityConfig.entity);
1233
- allRecords.push(...analysisResult.sortedRecords);
1247
+ // Phase 1: Flatten only - accumulates into analyzer's internal state
1248
+ const flattenedRecords = analyzer.flattenFileRecords(records, entityConfig.entity);
1249
+ allFlattenedRecords.push(...flattenedRecords);
1234
1250
  }
1235
1251
  catch (error) {
1236
1252
  if (options.verbose) {
@@ -1239,10 +1255,18 @@ class PushService {
1239
1255
  }
1240
1256
  }
1241
1257
  }
1258
+ // Phase 2: Analyze dependencies globally across ALL records
1259
+ // This enables cross-file dependency detection (e.g., AIPromptModel -> AIConfiguration)
1260
+ const analysisResult = analyzer.analyzeAllDependencies(allFlattenedRecords);
1261
+ const allRecords = analysisResult.sortedRecords;
1262
+ // Log any circular dependencies detected across files
1263
+ if (analysisResult.circularDependencies.length > 0 && options.verbose) {
1264
+ callbacks?.onWarn?.(`⚠️ Detected ${analysisResult.circularDependencies.length} circular dependencies across metadata files`);
1265
+ }
1242
1266
  // Perform comprehensive deletion audit
1243
- const md = new core_1.Metadata();
1244
- const auditor = new deletion_auditor_1.DeletionAuditor(md, this.contextUser);
1245
- const audit = await auditor.auditDeletions(allRecords);
1267
+ const md = new Metadata();
1268
+ const auditor = new DeletionAuditor(md, this.contextUser);
1269
+ const audit = await auditor.auditDeletions(allRecords, options.deleteDbOnly ?? false);
1246
1270
  // Check if any records actually need deletion
1247
1271
  const totalMarkedForDeletion = audit.explicitDeletes.size + audit.implicitDeletes.size;
1248
1272
  const needDeletion = audit.deletionLevels.flat().length; // Only records that exist in DB
@@ -1255,7 +1279,7 @@ class PushService {
1255
1279
  return null; // Signal that no deletion audit is needed
1256
1280
  }
1257
1281
  // Generate and display report (only if records need deletion)
1258
- const report = deletion_report_generator_1.DeletionReportGenerator.generateReport(audit, options.verbose);
1282
+ const report = DeletionReportGenerator.generateReport(audit, options.verbose);
1259
1283
  callbacks?.onLog?.(report);
1260
1284
  callbacks?.onLog?.('');
1261
1285
  // Check for blocking issues (only circular dependencies block execution)
@@ -1265,13 +1289,21 @@ class PushService {
1265
1289
  callbacks?.onError?.(error);
1266
1290
  throw new Error(error);
1267
1291
  }
1268
- // Warn about database-only references (non-blocking)
1269
- // These may be handled by cascade delete rules at the database level
1292
+ // Warn about database-only references
1270
1293
  if (audit.orphanedReferences.length > 0) {
1271
- callbacks?.onWarn?.(`⚠️ WARNING: ${audit.orphanedReferences.length} database-only reference${audit.orphanedReferences.length > 1 ? 's' : ''} found.`);
1272
- callbacks?.onWarn?.(` These records exist in the database but not in metadata.`);
1273
- callbacks?.onWarn?.(` If your database has cascade delete rules, these will be handled automatically.`);
1274
- callbacks?.onWarn?.(` Otherwise, deletion may fail with FK constraint errors.\n`);
1294
+ if (options.deleteDbOnly) {
1295
+ // When deleteDbOnly is enabled, these will be deleted
1296
+ callbacks?.onLog?.(`ℹ️ ${audit.databaseOnlyDeletions.length} database-only record${audit.databaseOnlyDeletions.length > 1 ? 's' : ''} will be deleted.`);
1297
+ callbacks?.onLog?.(` These records exist in the database but not in metadata files.`);
1298
+ callbacks?.onLog?.(` They reference records being deleted and will be removed first.\n`);
1299
+ }
1300
+ else {
1301
+ // When deleteDbOnly is NOT enabled, warn about potential FK errors
1302
+ callbacks?.onWarn?.(`⚠️ WARNING: ${audit.orphanedReferences.length} database-only reference${audit.orphanedReferences.length > 1 ? 's' : ''} found.`);
1303
+ callbacks?.onWarn?.(` These records exist in the database but not in metadata.`);
1304
+ callbacks?.onWarn?.(` Deletion may fail with FK constraint errors.`);
1305
+ callbacks?.onWarn?.(` Use --delete-db-only flag to automatically delete these records first.\n`);
1306
+ }
1275
1307
  }
1276
1308
  // Warn about implicit deletes
1277
1309
  if (audit.implicitDeletes.size > 0) {
@@ -1288,17 +1320,35 @@ class PushService {
1288
1320
  let deleted = 0;
1289
1321
  let errors = 0;
1290
1322
  callbacks?.onLog?.('🗑️ Processing deletions in reverse dependency order...\n');
1323
+ // Count database-only records for summary
1324
+ const dbOnlyCount = audit.databaseOnlyDeletions?.length ?? 0;
1325
+ let dbOnlyDeleted = 0;
1291
1326
  // Process deletion levels in order (highest dependency level first)
1292
1327
  for (let i = 0; i < audit.deletionLevels.length; i++) {
1293
1328
  const level = audit.deletionLevels[i];
1294
1329
  const levelNumber = audit.deletionLevels.length - i; // Reverse numbering for clarity
1295
- callbacks?.onLog?.(` Level ${levelNumber}: Deleting ${level.length} record${level.length > 1 ? 's' : ''}...`);
1330
+ // Count DB-only vs metadata records at this level
1331
+ const dbOnlyAtLevel = level.filter(r => r.path === '<DATABASE>').length;
1332
+ const metadataAtLevel = level.length - dbOnlyAtLevel;
1333
+ if (dbOnlyAtLevel > 0 && metadataAtLevel > 0) {
1334
+ callbacks?.onLog?.(` Level ${levelNumber}: Deleting ${level.length} records (${dbOnlyAtLevel} database-only, ${metadataAtLevel} metadata)...`);
1335
+ }
1336
+ else if (dbOnlyAtLevel > 0) {
1337
+ callbacks?.onLog?.(` Level ${levelNumber}: Deleting ${dbOnlyAtLevel} database-only record${dbOnlyAtLevel > 1 ? 's' : ''}...`);
1338
+ }
1339
+ else {
1340
+ callbacks?.onLog?.(` Level ${levelNumber}: Deleting ${level.length} record${level.length > 1 ? 's' : ''}...`);
1341
+ }
1296
1342
  // Process records within same level (can be done in parallel in the future)
1297
1343
  for (const record of level) {
1344
+ const isDbOnly = record.path === '<DATABASE>';
1298
1345
  try {
1299
- const result = await this.processDeleteRecord(record, '', options, callbacks);
1346
+ const result = await this.processDeleteRecord(record, '', options, callbacks, isDbOnly);
1300
1347
  if (result.status === 'deleted') {
1301
1348
  deleted++;
1349
+ if (isDbOnly) {
1350
+ dbOnlyDeleted++;
1351
+ }
1302
1352
  }
1303
1353
  else if (result.status === 'skipped') {
1304
1354
  // Record not found, already handled in processDeleteRecord
@@ -1419,10 +1469,10 @@ class PushService {
1419
1469
  for (const deferredWrite of this.deferredFileWrites.values()) {
1420
1470
  try {
1421
1471
  if (deferredWrite.isArray) {
1422
- await json_write_helper_1.JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records);
1472
+ await JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records);
1423
1473
  }
1424
1474
  else {
1425
- await json_write_helper_1.JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records[0]);
1475
+ await JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records[0]);
1426
1476
  }
1427
1477
  }
1428
1478
  catch (error) {
@@ -1517,5 +1567,4 @@ class PushService {
1517
1567
  return keyParts.join('|');
1518
1568
  }
1519
1569
  }
1520
- exports.PushService = PushService;
1521
1570
  //# sourceMappingURL=PushService.js.map