@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.
- package/README.md +907 -2200
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +17 -27
- package/dist/config.js.map +1 -1
- package/dist/constants/metadata-keywords.d.ts +1 -0
- package/dist/constants/metadata-keywords.d.ts.map +1 -0
- package/dist/constants/metadata-keywords.js +31 -42
- package/dist/constants/metadata-keywords.js.map +1 -1
- package/dist/index.d.ts +39 -37
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -62
- package/dist/index.js.map +1 -1
- package/dist/lib/EntityPropertyExtractor.d.ts +1 -0
- package/dist/lib/EntityPropertyExtractor.d.ts.map +1 -0
- package/dist/lib/EntityPropertyExtractor.js +1 -5
- package/dist/lib/EntityPropertyExtractor.js.map +1 -1
- package/dist/lib/FieldExternalizer.d.ts +1 -0
- package/dist/lib/FieldExternalizer.d.ts.map +1 -0
- package/dist/lib/FieldExternalizer.js +14 -21
- package/dist/lib/FieldExternalizer.js.map +1 -1
- package/dist/lib/RecordProcessor.d.ts +3 -2
- package/dist/lib/RecordProcessor.d.ts.map +1 -0
- package/dist/lib/RecordProcessor.js +16 -25
- package/dist/lib/RecordProcessor.js.map +1 -1
- package/dist/lib/RelatedEntityHandler.d.ts +3 -2
- package/dist/lib/RelatedEntityHandler.d.ts.map +1 -0
- package/dist/lib/RelatedEntityHandler.js +3 -9
- package/dist/lib/RelatedEntityHandler.js.map +1 -1
- package/dist/lib/config-manager.d.ts +2 -1
- package/dist/lib/config-manager.d.ts.map +1 -0
- package/dist/lib/config-manager.js +10 -15
- package/dist/lib/config-manager.js.map +1 -1
- package/dist/lib/database-reference-scanner.d.ts +3 -2
- package/dist/lib/database-reference-scanner.d.ts.map +1 -0
- package/dist/lib/database-reference-scanner.js +7 -13
- package/dist/lib/database-reference-scanner.js.map +1 -1
- package/dist/lib/deletion-auditor.d.ts +18 -3
- package/dist/lib/deletion-auditor.d.ts.map +1 -0
- package/dist/lib/deletion-auditor.js +111 -20
- package/dist/lib/deletion-auditor.js.map +1 -1
- package/dist/lib/deletion-report-generator.d.ts +2 -1
- package/dist/lib/deletion-report-generator.d.ts.map +1 -0
- package/dist/lib/deletion-report-generator.js +1 -5
- package/dist/lib/deletion-report-generator.js.map +1 -1
- package/dist/lib/entity-foreign-key-helper.d.ts +1 -0
- package/dist/lib/entity-foreign-key-helper.d.ts.map +1 -0
- package/dist/lib/entity-foreign-key-helper.js +1 -5
- package/dist/lib/entity-foreign-key-helper.js.map +1 -1
- package/dist/lib/file-backup-manager.d.ts +1 -0
- package/dist/lib/file-backup-manager.d.ts.map +1 -0
- package/dist/lib/file-backup-manager.js +22 -27
- package/dist/lib/file-backup-manager.js.map +1 -1
- package/dist/lib/file-write-batch.d.ts +2 -1
- package/dist/lib/file-write-batch.d.ts.map +1 -0
- package/dist/lib/file-write-batch.js +16 -21
- package/dist/lib/file-write-batch.js.map +1 -1
- package/dist/lib/json-preprocessor.d.ts +1 -0
- package/dist/lib/json-preprocessor.d.ts.map +1 -0
- package/dist/lib/json-preprocessor.js +21 -26
- package/dist/lib/json-preprocessor.js.map +1 -1
- package/dist/lib/json-write-helper.d.ts +2 -1
- package/dist/lib/json-write-helper.d.ts.map +1 -0
- package/dist/lib/json-write-helper.js +4 -11
- package/dist/lib/json-write-helper.js.map +1 -1
- package/dist/lib/provider-utils.d.ts +2 -1
- package/dist/lib/provider-utils.d.ts.map +1 -0
- package/dist/lib/provider-utils.js +15 -46
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/record-dependency-analyzer.d.ts +41 -2
- package/dist/lib/record-dependency-analyzer.d.ts.map +1 -0
- package/dist/lib/record-dependency-analyzer.js +108 -40
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/lib/singleton-manager.d.ts +2 -1
- package/dist/lib/singleton-manager.d.ts.map +1 -0
- package/dist/lib/singleton-manager.js +4 -9
- package/dist/lib/singleton-manager.js.map +1 -1
- package/dist/lib/sql-logger.d.ts +2 -1
- package/dist/lib/sql-logger.d.ts.map +1 -0
- package/dist/lib/sql-logger.js +8 -16
- package/dist/lib/sql-logger.js.map +1 -1
- package/dist/lib/sync-engine.d.ts +2 -1
- package/dist/lib/sync-engine.d.ts.map +1 -0
- package/dist/lib/sync-engine.js +58 -76
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/lib/transaction-manager.d.ts +2 -1
- package/dist/lib/transaction-manager.d.ts.map +1 -0
- package/dist/lib/transaction-manager.js +6 -11
- package/dist/lib/transaction-manager.js.map +1 -1
- package/dist/services/FileResetService.d.ts +1 -0
- package/dist/services/FileResetService.d.ts.map +1 -0
- package/dist/services/FileResetService.js +17 -24
- package/dist/services/FileResetService.js.map +1 -1
- package/dist/services/FormattingService.d.ts +2 -1
- package/dist/services/FormattingService.d.ts.map +1 -0
- package/dist/services/FormattingService.js +68 -73
- package/dist/services/FormattingService.js.map +1 -1
- package/dist/services/InitService.d.ts +1 -0
- package/dist/services/InitService.d.ts.map +1 -0
- package/dist/services/InitService.js +12 -19
- package/dist/services/InitService.js.map +1 -1
- package/dist/services/PullService.d.ts +2 -1
- package/dist/services/PullService.d.ts.map +1 -0
- package/dist/services/PullService.js +49 -60
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.d.ts +3 -1
- package/dist/services/PushService.d.ts.map +1 -0
- package/dist/services/PushService.js +144 -95
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/StatusService.d.ts +2 -1
- package/dist/services/StatusService.d.ts.map +1 -0
- package/dist/services/StatusService.js +14 -22
- package/dist/services/StatusService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +2 -1
- package/dist/services/ValidationService.d.ts.map +1 -0
- package/dist/services/ValidationService.js +41 -71
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/services/WatchService.d.ts +4 -3
- package/dist/services/WatchService.d.ts.map +1 -0
- package/dist/services/WatchService.js +35 -43
- package/dist/services/WatchService.js.map +1 -1
- package/dist/services/index.d.ts +9 -8
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +8 -19
- package/dist/services/index.js.map +1 -1
- package/dist/types/validation.d.ts +2 -1
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +2 -23
- package/dist/types/validation.js.map +1 -1
- package/package.json +23 -21
|
@@ -1,35 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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 ?
|
|
63
|
-
this.syncConfig = await
|
|
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: ${
|
|
64
|
+
callbacks?.onLog?.(`Original working directory: ${configManager.getOriginalCwd()}`);
|
|
73
65
|
callbacks?.onLog?.(`Config directory (with dir option): ${configDir}`);
|
|
74
|
-
callbacks?.onLog?.(`Config file path: ${
|
|
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
|
|
79
|
-
const transactionManager = new
|
|
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 =
|
|
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 =
|
|
102
|
-
const filepath =
|
|
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
|
|
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 =
|
|
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
|
|
171
|
-
await
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
516
|
+
await JsonWriteHelper.writeOrderedRecordData(filePath, unprocessedRecords);
|
|
525
517
|
}
|
|
526
518
|
else {
|
|
527
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
1079
|
-
record.deleteRecord
|
|
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
|
-
|
|
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
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
1243
|
+
const jsonPreprocessor = new JsonPreprocessor();
|
|
1228
1244
|
fileData = await jsonPreprocessor.processFile(filePath);
|
|
1229
1245
|
}
|
|
1230
1246
|
const records = Array.isArray(fileData) ? fileData : [fileData];
|
|
1231
|
-
//
|
|
1232
|
-
const
|
|
1233
|
-
|
|
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
|
|
1244
|
-
const auditor = new
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
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
|
|
1472
|
+
await JsonWriteHelper.writeOrderedRecordData(deferredWrite.filePath, deferredWrite.records);
|
|
1423
1473
|
}
|
|
1424
1474
|
else {
|
|
1425
|
-
await
|
|
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
|