@memberjunction/metadata-sync 2.117.0 → 2.118.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 +24 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database-reference-scanner.d.ts +56 -0
- package/dist/lib/database-reference-scanner.js +175 -0
- package/dist/lib/database-reference-scanner.js.map +1 -0
- package/dist/lib/deletion-auditor.d.ts +76 -0
- package/dist/lib/deletion-auditor.js +219 -0
- package/dist/lib/deletion-auditor.js.map +1 -0
- package/dist/lib/deletion-report-generator.d.ts +58 -0
- package/dist/lib/deletion-report-generator.js +287 -0
- package/dist/lib/deletion-report-generator.js.map +1 -0
- package/dist/lib/entity-foreign-key-helper.d.ts +51 -0
- package/dist/lib/entity-foreign-key-helper.js +83 -0
- package/dist/lib/entity-foreign-key-helper.js.map +1 -0
- package/dist/lib/provider-utils.d.ts +9 -1
- package/dist/lib/provider-utils.js +42 -5
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/record-dependency-analyzer.d.ts +44 -0
- package/dist/lib/record-dependency-analyzer.js +133 -0
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/services/PullService.d.ts +2 -0
- package/dist/services/PullService.js +4 -0
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.d.ts +42 -2
- package/dist/services/PushService.js +451 -109
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/StatusService.d.ts +2 -0
- package/dist/services/StatusService.js +5 -1
- package/dist/services/StatusService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +4 -0
- package/dist/services/ValidationService.js +32 -2
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/types/validation.d.ts +2 -0
- package/dist/types/validation.js.map +1 -1
- package/package.json +9 -8
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeletionAuditor = void 0;
|
|
4
|
+
const record_dependency_analyzer_1 = require("./record-dependency-analyzer");
|
|
5
|
+
const database_reference_scanner_1 = require("./database-reference-scanner");
|
|
6
|
+
const entity_foreign_key_helper_1 = require("./entity-foreign-key-helper");
|
|
7
|
+
const sync_engine_1 = require("./sync-engine");
|
|
8
|
+
/**
|
|
9
|
+
* Performs comprehensive deletion auditing across all metadata files
|
|
10
|
+
* Identifies all records that need to be deleted, in what order, and potential issues
|
|
11
|
+
*/
|
|
12
|
+
class DeletionAuditor {
|
|
13
|
+
metadata;
|
|
14
|
+
contextUser;
|
|
15
|
+
constructor(metadata, contextUser) {
|
|
16
|
+
this.metadata = metadata;
|
|
17
|
+
this.contextUser = contextUser;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Perform comprehensive deletion audit across all metadata files
|
|
21
|
+
*
|
|
22
|
+
* This analyzes:
|
|
23
|
+
* 1. Which records are explicitly marked for deletion
|
|
24
|
+
* 2. Which records must be implicitly deleted (due to FK constraints)
|
|
25
|
+
* 3. Database-only references that will prevent deletion
|
|
26
|
+
* 4. Correct deletion order (reverse topological sort)
|
|
27
|
+
* 5. Circular dependencies
|
|
28
|
+
*
|
|
29
|
+
* @param allRecords All flattened records from all metadata files
|
|
30
|
+
* @returns Complete deletion audit
|
|
31
|
+
*/
|
|
32
|
+
async auditDeletions(allRecords) {
|
|
33
|
+
// Step 1: Identify records explicitly marked for deletion
|
|
34
|
+
const explicitDeletes = this.findExplicitDeletes(allRecords);
|
|
35
|
+
// Step 2: Build reverse dependency map for all records
|
|
36
|
+
const analyzer = new record_dependency_analyzer_1.RecordDependencyAnalyzer();
|
|
37
|
+
const reverseDependencies = analyzer.buildReverseDependencyMap(allRecords);
|
|
38
|
+
// Step 3: Find implicit deletes (records that depend on explicit deletes)
|
|
39
|
+
const implicitDeletes = this.findImplicitDeletes(explicitDeletes, reverseDependencies, allRecords);
|
|
40
|
+
// Step 4: Check which records actually exist in the database
|
|
41
|
+
const allDeletes = new Map([...explicitDeletes, ...implicitDeletes]);
|
|
42
|
+
const { existingRecords, alreadyDeleted } = await this.checkRecordExistence(Array.from(allDeletes.values()));
|
|
43
|
+
// Step 5: Scan database for existing references (only for records that still exist)
|
|
44
|
+
const reverseFKMap = entity_foreign_key_helper_1.EntityForeignKeyHelper.buildReverseFKMap(this.metadata);
|
|
45
|
+
const scanner = new database_reference_scanner_1.DatabaseReferenceScanner(this.metadata, this.contextUser);
|
|
46
|
+
const databaseReferences = await scanner.scanForReferences(existingRecords, reverseFKMap, allRecords // Pass all metadata records for correct exists-in-metadata check
|
|
47
|
+
);
|
|
48
|
+
// Step 6: Identify orphaned references (DB records not in metadata)
|
|
49
|
+
const orphanedReferences = this.findOrphanedReferences(databaseReferences, allRecords);
|
|
50
|
+
// Step 7: Calculate deletion order (reverse topological sort) - only for existing records
|
|
51
|
+
const deletionLevels = analyzer.reverseTopologicalSort(existingRecords, reverseDependencies);
|
|
52
|
+
// Step 8: Check for circular dependencies among records to delete
|
|
53
|
+
const circularDependencies = this.findCircularDependencies(existingRecords);
|
|
54
|
+
return {
|
|
55
|
+
explicitDeletes,
|
|
56
|
+
implicitDeletes,
|
|
57
|
+
alreadyDeleted,
|
|
58
|
+
databaseOnlyReferences: databaseReferences,
|
|
59
|
+
reverseDependencies,
|
|
60
|
+
deletionLevels,
|
|
61
|
+
circularDependencies,
|
|
62
|
+
orphanedReferences
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find all records marked with delete: true
|
|
67
|
+
*/
|
|
68
|
+
findExplicitDeletes(records) {
|
|
69
|
+
const deletes = new Map();
|
|
70
|
+
for (const record of records) {
|
|
71
|
+
if (record.record.deleteRecord?.delete === true) {
|
|
72
|
+
deletes.set(record.id, record);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return deletes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find all records that depend on records being deleted (implicit deletes)
|
|
79
|
+
* Uses BFS to find transitive dependents
|
|
80
|
+
*/
|
|
81
|
+
findImplicitDeletes(explicitDeletes, reverseDependencies, allRecords) {
|
|
82
|
+
const implicitDeletes = new Map();
|
|
83
|
+
const visited = new Set();
|
|
84
|
+
// BFS to find all transitive dependents
|
|
85
|
+
const queue = Array.from(explicitDeletes.keys());
|
|
86
|
+
while (queue.length > 0) {
|
|
87
|
+
const recordId = queue.shift();
|
|
88
|
+
if (visited.has(recordId))
|
|
89
|
+
continue;
|
|
90
|
+
visited.add(recordId);
|
|
91
|
+
const dependents = reverseDependencies.get(recordId) || [];
|
|
92
|
+
for (const dep of dependents) {
|
|
93
|
+
// Add dependent to implicit deletes if not already explicit
|
|
94
|
+
if (!explicitDeletes.has(dep.dependentId) &&
|
|
95
|
+
!implicitDeletes.has(dep.dependentId)) {
|
|
96
|
+
const record = allRecords.find(r => r.id === dep.dependentId);
|
|
97
|
+
if (record) {
|
|
98
|
+
implicitDeletes.set(dep.dependentId, record);
|
|
99
|
+
queue.push(dep.dependentId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return implicitDeletes;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Check which records actually exist in the database
|
|
108
|
+
* Returns records separated into existing (need deletion) and already deleted
|
|
109
|
+
*/
|
|
110
|
+
async checkRecordExistence(records) {
|
|
111
|
+
const existingRecords = [];
|
|
112
|
+
const alreadyDeleted = new Map();
|
|
113
|
+
// Import SyncEngine to check record existence
|
|
114
|
+
const syncEngine = new sync_engine_1.SyncEngine(this.contextUser);
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
try {
|
|
117
|
+
// Check if record exists in database
|
|
118
|
+
const existingEntity = await syncEngine.loadEntity(record.entityName, record.record.primaryKey || {});
|
|
119
|
+
if (existingEntity) {
|
|
120
|
+
existingRecords.push(record);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
alreadyDeleted.set(record.id, record);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// If we can't check, assume it exists to be safe
|
|
128
|
+
existingRecords.push(record);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { existingRecords, alreadyDeleted };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Find orphaned references (database records not in metadata files)
|
|
135
|
+
* These will prevent deletion unless handled
|
|
136
|
+
*/
|
|
137
|
+
findOrphanedReferences(databaseReferences, allRecords) {
|
|
138
|
+
const orphaned = [];
|
|
139
|
+
for (const ref of databaseReferences) {
|
|
140
|
+
if (!ref.existsInMetadata) {
|
|
141
|
+
orphaned.push({
|
|
142
|
+
recordId: ref.referencedKey.ToConcatenatedString(),
|
|
143
|
+
dependentId: ref.primaryKey.ToConcatenatedString(),
|
|
144
|
+
entityName: ref.entityName,
|
|
145
|
+
fieldName: ref.referencingField,
|
|
146
|
+
filePath: '<DATABASE ONLY>'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return orphaned;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check for circular dependencies among records to delete
|
|
154
|
+
* This would prevent safe deletion order
|
|
155
|
+
*/
|
|
156
|
+
findCircularDependencies(records) {
|
|
157
|
+
const cycles = [];
|
|
158
|
+
const visited = new Set();
|
|
159
|
+
const recursionStack = new Set();
|
|
160
|
+
const detectCycle = (record, path) => {
|
|
161
|
+
visited.add(record.id);
|
|
162
|
+
recursionStack.add(record.id);
|
|
163
|
+
path.push(`${record.entityName}:${this.getRecordDisplayName(record)}`);
|
|
164
|
+
for (const depId of record.dependencies) {
|
|
165
|
+
// Only check dependencies among records being deleted
|
|
166
|
+
const depRecord = records.find(r => r.id === depId);
|
|
167
|
+
if (!depRecord)
|
|
168
|
+
continue;
|
|
169
|
+
if (!visited.has(depId)) {
|
|
170
|
+
if (detectCycle(depRecord, [...path])) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (recursionStack.has(depId)) {
|
|
175
|
+
// Found a cycle
|
|
176
|
+
const cycleStart = path.findIndex(p => p.startsWith(depRecord.entityName));
|
|
177
|
+
const cycle = path.slice(cycleStart);
|
|
178
|
+
cycle.push(`${depRecord.entityName}:${this.getRecordDisplayName(depRecord)}`);
|
|
179
|
+
cycles.push(cycle);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
recursionStack.delete(record.id);
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
// Check all records for cycles
|
|
187
|
+
for (const record of records) {
|
|
188
|
+
if (!visited.has(record.id)) {
|
|
189
|
+
detectCycle(record, []);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return cycles;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get a display name for a record (for error messages)
|
|
196
|
+
*/
|
|
197
|
+
getRecordDisplayName(record) {
|
|
198
|
+
return record.record.fields?.Name ||
|
|
199
|
+
record.record.primaryKey?.ID ||
|
|
200
|
+
record.record.fields?.ID ||
|
|
201
|
+
record.id;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Check if deletion audit has blocking issues
|
|
205
|
+
*/
|
|
206
|
+
isValid(audit) {
|
|
207
|
+
return audit.orphanedReferences.length === 0 &&
|
|
208
|
+
audit.circularDependencies.length === 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if deletion audit requires user confirmation
|
|
212
|
+
* (due to implicit deletes)
|
|
213
|
+
*/
|
|
214
|
+
requiresConfirmation(audit) {
|
|
215
|
+
return audit.implicitDeletes.size > 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.DeletionAuditor = DeletionAuditor;
|
|
219
|
+
//# sourceMappingURL=deletion-auditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deletion-auditor.js","sourceRoot":"","sources":["../../src/lib/deletion-auditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { Metadata, UserInfo } from '@memberjunction/core';\nimport {\n FlattenedRecord,\n ReverseDependency,\n RecordDependencyAnalyzer\n} from './record-dependency-analyzer';\nimport { DatabaseReference, DatabaseReferenceScanner } from './database-reference-scanner';\nimport { EntityForeignKeyHelper } from './entity-foreign-key-helper';\nimport { SyncEngine } from './sync-engine';\n/**\n * Complete audit result for deletion operations\n */\nexport interface DeletionAudit {\n // Records explicitly marked for deletion (delete: true in metadata)\n explicitDeletes: Map<string, FlattenedRecord>;\n\n // Records that must be deleted due to FK dependencies\n implicitDeletes: Map<string, FlattenedRecord>;\n\n // Records marked for deletion that already don't exist in the database\n alreadyDeleted: Map<string, FlattenedRecord>;\n\n // Database records that reference records being deleted (not in metadata)\n databaseOnlyReferences: DatabaseReference[];\n\n // Reverse dependency map: recordId -> records that depend on it\n reverseDependencies: Map<string, ReverseDependency[]>;\n\n // Deletion order (reverse topological sort) - highest dependency level first\n deletionLevels: FlattenedRecord[][];\n\n // Errors/warnings\n circularDependencies: string[][];\n orphanedReferences: ReverseDependency[];\n}\n\n/**\n * Performs comprehensive deletion auditing across all metadata files\n * Identifies all records that need to be deleted, in what order, and potential issues\n */\nexport class DeletionAuditor {\n constructor(\n private metadata: Metadata,\n private contextUser: UserInfo\n ) {}\n\n /**\n * Perform comprehensive deletion audit across all metadata files\n *\n * This analyzes:\n * 1. Which records are explicitly marked for deletion\n * 2. Which records must be implicitly deleted (due to FK constraints)\n * 3. Database-only references that will prevent deletion\n * 4. Correct deletion order (reverse topological sort)\n * 5. Circular dependencies\n *\n * @param allRecords All flattened records from all metadata files\n * @returns Complete deletion audit\n */\n async auditDeletions(allRecords: FlattenedRecord[]): Promise<DeletionAudit> {\n // Step 1: Identify records explicitly marked for deletion\n const explicitDeletes = this.findExplicitDeletes(allRecords);\n\n // Step 2: Build reverse dependency map for all records\n const analyzer = new RecordDependencyAnalyzer();\n const reverseDependencies = analyzer.buildReverseDependencyMap(allRecords);\n\n // Step 3: Find implicit deletes (records that depend on explicit deletes)\n const implicitDeletes = this.findImplicitDeletes(\n explicitDeletes,\n reverseDependencies,\n allRecords\n );\n\n // Step 4: Check which records actually exist in the database\n const allDeletes = new Map([...explicitDeletes, ...implicitDeletes]);\n const { existingRecords, alreadyDeleted } = await this.checkRecordExistence(\n Array.from(allDeletes.values())\n );\n\n // Step 5: Scan database for existing references (only for records that still exist)\n const reverseFKMap = EntityForeignKeyHelper.buildReverseFKMap(this.metadata);\n const scanner = new DatabaseReferenceScanner(this.metadata, this.contextUser);\n\n const databaseReferences = await scanner.scanForReferences(\n existingRecords,\n reverseFKMap,\n allRecords // Pass all metadata records for correct exists-in-metadata check\n );\n\n // Step 6: Identify orphaned references (DB records not in metadata)\n const orphanedReferences = this.findOrphanedReferences(\n databaseReferences,\n allRecords\n );\n\n // Step 7: Calculate deletion order (reverse topological sort) - only for existing records\n const deletionLevels = analyzer.reverseTopologicalSort(\n existingRecords,\n reverseDependencies\n );\n\n // Step 8: Check for circular dependencies among records to delete\n const circularDependencies = this.findCircularDependencies(existingRecords);\n\n return {\n explicitDeletes,\n implicitDeletes,\n alreadyDeleted,\n databaseOnlyReferences: databaseReferences,\n reverseDependencies,\n deletionLevels,\n circularDependencies,\n orphanedReferences\n };\n }\n\n /**\n * Find all records marked with delete: true\n */\n private findExplicitDeletes(records: FlattenedRecord[]): Map<string, FlattenedRecord> {\n const deletes = new Map<string, FlattenedRecord>();\n\n for (const record of records) {\n if (record.record.deleteRecord?.delete === true) {\n deletes.set(record.id, record);\n }\n }\n\n return deletes;\n }\n\n /**\n * Find all records that depend on records being deleted (implicit deletes)\n * Uses BFS to find transitive dependents\n */\n private findImplicitDeletes(\n explicitDeletes: Map<string, FlattenedRecord>,\n reverseDependencies: Map<string, ReverseDependency[]>,\n allRecords: FlattenedRecord[]\n ): Map<string, FlattenedRecord> {\n const implicitDeletes = new Map<string, FlattenedRecord>();\n const visited = new Set<string>();\n\n // BFS to find all transitive dependents\n const queue = Array.from(explicitDeletes.keys());\n\n while (queue.length > 0) {\n const recordId = queue.shift()!;\n if (visited.has(recordId)) continue;\n visited.add(recordId);\n\n const dependents = reverseDependencies.get(recordId) || [];\n\n for (const dep of dependents) {\n // Add dependent to implicit deletes if not already explicit\n if (!explicitDeletes.has(dep.dependentId) &&\n !implicitDeletes.has(dep.dependentId)) {\n\n const record = allRecords.find(r => r.id === dep.dependentId);\n if (record) {\n implicitDeletes.set(dep.dependentId, record);\n queue.push(dep.dependentId);\n }\n }\n }\n }\n\n return implicitDeletes;\n }\n\n /**\n * Check which records actually exist in the database\n * Returns records separated into existing (need deletion) and already deleted\n */\n private async checkRecordExistence(\n records: FlattenedRecord[]\n ): Promise<{ existingRecords: FlattenedRecord[]; alreadyDeleted: Map<string, FlattenedRecord> }> {\n const existingRecords: FlattenedRecord[] = [];\n const alreadyDeleted = new Map<string, FlattenedRecord>();\n\n // Import SyncEngine to check record existence\n const syncEngine = new SyncEngine(this.contextUser);\n\n for (const record of records) {\n try {\n // Check if record exists in database\n const existingEntity = await syncEngine.loadEntity(\n record.entityName,\n record.record.primaryKey || {}\n );\n\n if (existingEntity) {\n existingRecords.push(record);\n } else {\n alreadyDeleted.set(record.id, record);\n }\n } catch (error) {\n // If we can't check, assume it exists to be safe\n existingRecords.push(record);\n }\n }\n\n return { existingRecords, alreadyDeleted };\n }\n\n /**\n * Find orphaned references (database records not in metadata files)\n * These will prevent deletion unless handled\n */\n private findOrphanedReferences(\n databaseReferences: DatabaseReference[],\n allRecords: FlattenedRecord[]\n ): ReverseDependency[] {\n const orphaned: ReverseDependency[] = [];\n\n for (const ref of databaseReferences) {\n if (!ref.existsInMetadata) {\n orphaned.push({\n recordId: ref.referencedKey.ToConcatenatedString(),\n dependentId: ref.primaryKey.ToConcatenatedString(),\n entityName: ref.entityName,\n fieldName: ref.referencingField,\n filePath: '<DATABASE ONLY>'\n });\n }\n }\n\n return orphaned;\n }\n\n /**\n * Check for circular dependencies among records to delete\n * This would prevent safe deletion order\n */\n private findCircularDependencies(records: FlattenedRecord[]): string[][] {\n const cycles: string[][] = [];\n const visited = new Set<string>();\n const recursionStack = new Set<string>();\n\n const detectCycle = (record: FlattenedRecord, path: string[]): boolean => {\n visited.add(record.id);\n recursionStack.add(record.id);\n path.push(`${record.entityName}:${this.getRecordDisplayName(record)}`);\n\n for (const depId of record.dependencies) {\n // Only check dependencies among records being deleted\n const depRecord = records.find(r => r.id === depId);\n if (!depRecord) continue;\n\n if (!visited.has(depId)) {\n if (detectCycle(depRecord, [...path])) {\n return true;\n }\n } else if (recursionStack.has(depId)) {\n // Found a cycle\n const cycleStart = path.findIndex(p => p.startsWith(depRecord.entityName));\n const cycle = path.slice(cycleStart);\n cycle.push(`${depRecord.entityName}:${this.getRecordDisplayName(depRecord)}`);\n cycles.push(cycle);\n return true;\n }\n }\n\n recursionStack.delete(record.id);\n return false;\n };\n\n // Check all records for cycles\n for (const record of records) {\n if (!visited.has(record.id)) {\n detectCycle(record, []);\n }\n }\n\n return cycles;\n }\n\n /**\n * Get a display name for a record (for error messages)\n */\n private getRecordDisplayName(record: FlattenedRecord): string {\n return record.record.fields?.Name ||\n record.record.primaryKey?.ID ||\n record.record.fields?.ID ||\n record.id;\n }\n\n /**\n * Check if deletion audit has blocking issues\n */\n isValid(audit: DeletionAudit): boolean {\n return audit.orphanedReferences.length === 0 &&\n audit.circularDependencies.length === 0;\n }\n\n /**\n * Check if deletion audit requires user confirmation\n * (due to implicit deletes)\n */\n requiresConfirmation(audit: DeletionAudit): boolean {\n return audit.implicitDeletes.size > 0;\n }\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DeletionAudit } from './deletion-auditor';
|
|
2
|
+
/**
|
|
3
|
+
* Generates human-readable reports for deletion audits
|
|
4
|
+
*/
|
|
5
|
+
export declare class DeletionReportGenerator {
|
|
6
|
+
/**
|
|
7
|
+
* Generate comprehensive deletion plan report
|
|
8
|
+
*/
|
|
9
|
+
static generateReport(audit: DeletionAudit, verbose?: boolean): string;
|
|
10
|
+
/**
|
|
11
|
+
* Generate summary section
|
|
12
|
+
*/
|
|
13
|
+
private static addSummary;
|
|
14
|
+
/**
|
|
15
|
+
* Add explicit deletes section
|
|
16
|
+
*/
|
|
17
|
+
private static addExplicitDeletes;
|
|
18
|
+
/**
|
|
19
|
+
* Add implicit deletes section
|
|
20
|
+
*/
|
|
21
|
+
private static addImplicitDeletes;
|
|
22
|
+
/**
|
|
23
|
+
* Add already deleted records section
|
|
24
|
+
*/
|
|
25
|
+
private static addAlreadyDeleted;
|
|
26
|
+
/**
|
|
27
|
+
* Add orphaned references section
|
|
28
|
+
*/
|
|
29
|
+
private static addOrphanedReferences;
|
|
30
|
+
/**
|
|
31
|
+
* Add deletion order section
|
|
32
|
+
*/
|
|
33
|
+
private static addDeletionOrder;
|
|
34
|
+
/**
|
|
35
|
+
* Add circular dependencies section
|
|
36
|
+
*/
|
|
37
|
+
private static addCircularDependencies;
|
|
38
|
+
/**
|
|
39
|
+
* Add conclusion section
|
|
40
|
+
*/
|
|
41
|
+
private static addConclusion;
|
|
42
|
+
/**
|
|
43
|
+
* Format a record ID for display
|
|
44
|
+
*/
|
|
45
|
+
private static formatRecordId;
|
|
46
|
+
/**
|
|
47
|
+
* Group records by entity name
|
|
48
|
+
*/
|
|
49
|
+
private static groupByEntity;
|
|
50
|
+
/**
|
|
51
|
+
* Find what an implicit delete depends on (for display)
|
|
52
|
+
*/
|
|
53
|
+
private static findDependencies;
|
|
54
|
+
/**
|
|
55
|
+
* Generate a concise summary for logging
|
|
56
|
+
*/
|
|
57
|
+
static generateSummary(audit: DeletionAudit): string;
|
|
58
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeletionReportGenerator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Generates human-readable reports for deletion audits
|
|
6
|
+
*/
|
|
7
|
+
class DeletionReportGenerator {
|
|
8
|
+
/**
|
|
9
|
+
* Generate comprehensive deletion plan report
|
|
10
|
+
*/
|
|
11
|
+
static generateReport(audit, verbose = false) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push('═'.repeat(80));
|
|
14
|
+
lines.push('DELETION AUDIT REPORT');
|
|
15
|
+
lines.push('═'.repeat(80));
|
|
16
|
+
lines.push('');
|
|
17
|
+
// Summary
|
|
18
|
+
this.addSummary(lines, audit);
|
|
19
|
+
// Explicit deletes (verbose only)
|
|
20
|
+
if (verbose && audit.explicitDeletes.size > 0) {
|
|
21
|
+
this.addExplicitDeletes(lines, audit);
|
|
22
|
+
}
|
|
23
|
+
// Implicit deletes
|
|
24
|
+
if (audit.implicitDeletes.size > 0) {
|
|
25
|
+
this.addImplicitDeletes(lines, audit);
|
|
26
|
+
}
|
|
27
|
+
// Already deleted records
|
|
28
|
+
if (audit.alreadyDeleted.size > 0) {
|
|
29
|
+
this.addAlreadyDeleted(lines, audit);
|
|
30
|
+
}
|
|
31
|
+
// Database-only references
|
|
32
|
+
if (audit.orphanedReferences.length > 0) {
|
|
33
|
+
this.addOrphanedReferences(lines, audit);
|
|
34
|
+
}
|
|
35
|
+
// Deletion order (verbose only)
|
|
36
|
+
if (verbose && audit.deletionLevels.length > 0) {
|
|
37
|
+
this.addDeletionOrder(lines, audit);
|
|
38
|
+
}
|
|
39
|
+
// Circular dependencies
|
|
40
|
+
if (audit.circularDependencies.length > 0) {
|
|
41
|
+
this.addCircularDependencies(lines, audit);
|
|
42
|
+
}
|
|
43
|
+
// Conclusion
|
|
44
|
+
this.addConclusion(lines, audit);
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate summary section
|
|
49
|
+
*/
|
|
50
|
+
static addSummary(lines, audit) {
|
|
51
|
+
const totalMarkedForDeletion = audit.explicitDeletes.size + audit.implicitDeletes.size;
|
|
52
|
+
const needDeletion = totalMarkedForDeletion - audit.alreadyDeleted.size;
|
|
53
|
+
lines.push('SUMMARY:');
|
|
54
|
+
lines.push(` Records Marked for Deletion: ${totalMarkedForDeletion}`);
|
|
55
|
+
lines.push(` • Will be deleted: ${needDeletion}`);
|
|
56
|
+
lines.push(` • Already deleted: ${audit.alreadyDeleted.size}`);
|
|
57
|
+
lines.push(` Explicit Deletes (marked in metadata): ${audit.explicitDeletes.size}`);
|
|
58
|
+
lines.push(` Implicit Deletes (required by FK): ${audit.implicitDeletes.size}`);
|
|
59
|
+
lines.push(` Database-Only References: ${audit.orphanedReferences.length}`);
|
|
60
|
+
lines.push(` Deletion Levels: ${audit.deletionLevels.length}`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Add explicit deletes section
|
|
65
|
+
*/
|
|
66
|
+
static addExplicitDeletes(lines, audit) {
|
|
67
|
+
lines.push('EXPLICIT DELETES (marked with delete: true):');
|
|
68
|
+
// Group by entity
|
|
69
|
+
const byEntity = this.groupByEntity(Array.from(audit.explicitDeletes.values()));
|
|
70
|
+
for (const [entityName, records] of byEntity) {
|
|
71
|
+
lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);
|
|
72
|
+
for (const record of records) {
|
|
73
|
+
lines.push(` ✓ ${this.formatRecordId(record)}`);
|
|
74
|
+
lines.push(` File: ${record.path}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Add implicit deletes section
|
|
81
|
+
*/
|
|
82
|
+
static addImplicitDeletes(lines, audit) {
|
|
83
|
+
lines.push('⚠️ IMPLICIT DELETES (required to satisfy FK constraints):');
|
|
84
|
+
lines.push(' These records will be deleted because they depend on explicitly deleted records.');
|
|
85
|
+
lines.push('');
|
|
86
|
+
// Group by entity
|
|
87
|
+
const byEntity = this.groupByEntity(Array.from(audit.implicitDeletes.values()));
|
|
88
|
+
for (const [entityName, records] of byEntity) {
|
|
89
|
+
lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);
|
|
90
|
+
for (const record of records) {
|
|
91
|
+
lines.push(` → ${this.formatRecordId(record)}`);
|
|
92
|
+
lines.push(` File: ${record.path}`);
|
|
93
|
+
// Show what it depends on
|
|
94
|
+
const deps = this.findDependencies(record, audit);
|
|
95
|
+
if (deps.length > 0) {
|
|
96
|
+
lines.push(` Depends on: ${deps.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Add already deleted records section
|
|
104
|
+
*/
|
|
105
|
+
static addAlreadyDeleted(lines, audit) {
|
|
106
|
+
lines.push('ℹ️ ALREADY DELETED (exist in metadata but not in database):');
|
|
107
|
+
lines.push(' These records are marked for deletion but already don\'t exist in the database.');
|
|
108
|
+
lines.push(' No action will be taken for these records.');
|
|
109
|
+
lines.push('');
|
|
110
|
+
// Group by entity
|
|
111
|
+
const byEntity = this.groupByEntity(Array.from(audit.alreadyDeleted.values()));
|
|
112
|
+
for (const [entityName, records] of byEntity) {
|
|
113
|
+
lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
lines.push(` ✓ ${this.formatRecordId(record)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Add orphaned references section
|
|
122
|
+
*/
|
|
123
|
+
static addOrphanedReferences(lines, audit) {
|
|
124
|
+
lines.push('⚠️ DATABASE-ONLY REFERENCES (not in metadata files):');
|
|
125
|
+
lines.push(' These records exist in the database but not in metadata.');
|
|
126
|
+
lines.push(' May be handled by database cascade delete rules.');
|
|
127
|
+
lines.push(' If not, deletion will fail with FK constraint errors.');
|
|
128
|
+
lines.push('');
|
|
129
|
+
// Group by entity
|
|
130
|
+
const byEntity = new Map();
|
|
131
|
+
for (const ref of audit.orphanedReferences) {
|
|
132
|
+
if (!byEntity.has(ref.entityName)) {
|
|
133
|
+
byEntity.set(ref.entityName, []);
|
|
134
|
+
}
|
|
135
|
+
byEntity.get(ref.entityName).push(ref);
|
|
136
|
+
}
|
|
137
|
+
for (const [entityName, refs] of byEntity) {
|
|
138
|
+
lines.push(` ${entityName} (${refs.length} record${refs.length > 1 ? 's' : ''}):`);
|
|
139
|
+
for (const ref of refs) {
|
|
140
|
+
lines.push(` ⚠️ ID: ${ref.dependentId}`);
|
|
141
|
+
lines.push(` References: ${ref.recordId} via ${ref.fieldName}`);
|
|
142
|
+
lines.push(` Note: May be handled by cascade delete OR require manual handling`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Add deletion order section
|
|
149
|
+
*/
|
|
150
|
+
static addDeletionOrder(lines, audit) {
|
|
151
|
+
lines.push('DELETION ORDER (reverse topological sort):');
|
|
152
|
+
lines.push('Records will be deleted in this order (highest dependency level first):');
|
|
153
|
+
lines.push('');
|
|
154
|
+
for (let i = 0; i < audit.deletionLevels.length; i++) {
|
|
155
|
+
const level = audit.deletionLevels[i];
|
|
156
|
+
const levelNumber = audit.deletionLevels.length - i; // Reverse numbering for clarity
|
|
157
|
+
lines.push(` Level ${levelNumber} (${level.length} record${level.length > 1 ? 's' : ''}):`);
|
|
158
|
+
// Group by entity within level
|
|
159
|
+
const byEntity = this.groupByEntity(level);
|
|
160
|
+
for (const [entityName, records] of byEntity) {
|
|
161
|
+
if (records.length === 1) {
|
|
162
|
+
lines.push(` - ${entityName}: ${this.formatRecordId(records[0])}`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
lines.push(` - ${entityName} (${records.length} records):`);
|
|
166
|
+
for (const record of records) {
|
|
167
|
+
lines.push(` ${this.formatRecordId(record)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Add circular dependencies section
|
|
176
|
+
*/
|
|
177
|
+
static addCircularDependencies(lines, audit) {
|
|
178
|
+
lines.push('❌ CIRCULAR DEPENDENCIES DETECTED:');
|
|
179
|
+
lines.push(' The following circular dependencies prevent safe deletion:');
|
|
180
|
+
lines.push('');
|
|
181
|
+
for (let i = 0; i < audit.circularDependencies.length; i++) {
|
|
182
|
+
const cycle = audit.circularDependencies[i];
|
|
183
|
+
lines.push(` Cycle ${i + 1}:`);
|
|
184
|
+
lines.push(` ${cycle.join(' → ')}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push(' To resolve:');
|
|
188
|
+
lines.push(' 1. Review the relationships creating the cycle');
|
|
189
|
+
lines.push(' 2. Break the cycle by removing one of the dependencies');
|
|
190
|
+
lines.push(' 3. Consider using NULL values or restructuring relationships');
|
|
191
|
+
lines.push('');
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Add conclusion section
|
|
195
|
+
*/
|
|
196
|
+
static addConclusion(lines, audit) {
|
|
197
|
+
lines.push('═'.repeat(80));
|
|
198
|
+
if (audit.circularDependencies.length > 0) {
|
|
199
|
+
lines.push('STATUS: ❌ CANNOT PROCEED');
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push('Circular dependencies detected. Please resolve the cycles above.');
|
|
202
|
+
}
|
|
203
|
+
else if (audit.implicitDeletes.size > 0 || audit.orphanedReferences.length > 0) {
|
|
204
|
+
lines.push('STATUS: ⚠️ REVIEW REQUIRED');
|
|
205
|
+
lines.push('');
|
|
206
|
+
if (audit.implicitDeletes.size > 0) {
|
|
207
|
+
lines.push(`${audit.implicitDeletes.size} implicit deletion${audit.implicitDeletes.size > 1 ? 's' : ''} will occur.`);
|
|
208
|
+
lines.push('Review the implicit deletes above and confirm this is intended.');
|
|
209
|
+
}
|
|
210
|
+
if (audit.orphanedReferences.length > 0) {
|
|
211
|
+
if (audit.implicitDeletes.size > 0) {
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
lines.push(`${audit.orphanedReferences.length} database-only reference${audit.orphanedReferences.length > 1 ? 's' : ''} detected.`);
|
|
215
|
+
lines.push('These may be handled by cascade delete rules or may cause FK errors.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
lines.push('STATUS: ✓ SAFE TO PROCEED');
|
|
220
|
+
lines.push('');
|
|
221
|
+
lines.push('All deletions are explicitly marked and have no dependencies.');
|
|
222
|
+
}
|
|
223
|
+
lines.push('═'.repeat(80));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Format a record ID for display
|
|
227
|
+
*/
|
|
228
|
+
static formatRecordId(record) {
|
|
229
|
+
return record.record.fields?.Name ||
|
|
230
|
+
record.record.primaryKey?.ID ||
|
|
231
|
+
record.record.fields?.ID ||
|
|
232
|
+
record.id;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Group records by entity name
|
|
236
|
+
*/
|
|
237
|
+
static groupByEntity(records) {
|
|
238
|
+
const byEntity = new Map();
|
|
239
|
+
for (const record of records) {
|
|
240
|
+
if (!byEntity.has(record.entityName)) {
|
|
241
|
+
byEntity.set(record.entityName, []);
|
|
242
|
+
}
|
|
243
|
+
byEntity.get(record.entityName).push(record);
|
|
244
|
+
}
|
|
245
|
+
return byEntity;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Find what an implicit delete depends on (for display)
|
|
249
|
+
*/
|
|
250
|
+
static findDependencies(record, audit) {
|
|
251
|
+
const deps = [];
|
|
252
|
+
for (const depId of record.dependencies) {
|
|
253
|
+
const explicitRecord = audit.explicitDeletes.get(depId);
|
|
254
|
+
if (explicitRecord) {
|
|
255
|
+
deps.push(`${explicitRecord.entityName}:${this.formatRecordId(explicitRecord)}`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const implicitRecord = audit.implicitDeletes.get(depId);
|
|
259
|
+
if (implicitRecord) {
|
|
260
|
+
deps.push(`${implicitRecord.entityName}:${this.formatRecordId(implicitRecord)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return deps;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Generate a concise summary for logging
|
|
268
|
+
*/
|
|
269
|
+
static generateSummary(audit) {
|
|
270
|
+
const parts = [];
|
|
271
|
+
if (audit.explicitDeletes.size > 0) {
|
|
272
|
+
parts.push(`${audit.explicitDeletes.size} explicit`);
|
|
273
|
+
}
|
|
274
|
+
if (audit.implicitDeletes.size > 0) {
|
|
275
|
+
parts.push(`${audit.implicitDeletes.size} implicit`);
|
|
276
|
+
}
|
|
277
|
+
if (audit.orphanedReferences.length > 0) {
|
|
278
|
+
parts.push(`${audit.orphanedReferences.length} orphaned refs`);
|
|
279
|
+
}
|
|
280
|
+
if (audit.circularDependencies.length > 0) {
|
|
281
|
+
parts.push(`${audit.circularDependencies.length} cycles`);
|
|
282
|
+
}
|
|
283
|
+
return parts.length > 0 ? parts.join(', ') : 'no deletions';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.DeletionReportGenerator = DeletionReportGenerator;
|
|
287
|
+
//# sourceMappingURL=deletion-report-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deletion-report-generator.js","sourceRoot":"","sources":["../../src/lib/deletion-report-generator.ts"],"names":[],"mappings":"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","sourcesContent":["import { DeletionAudit } from './deletion-auditor';\nimport { FlattenedRecord } from './record-dependency-analyzer';\n\n/**\n * Generates human-readable reports for deletion audits\n */\nexport class DeletionReportGenerator {\n /**\n * Generate comprehensive deletion plan report\n */\n static generateReport(audit: DeletionAudit, verbose = false): string {\n const lines: string[] = [];\n\n lines.push('═'.repeat(80));\n lines.push('DELETION AUDIT REPORT');\n lines.push('═'.repeat(80));\n lines.push('');\n\n // Summary\n this.addSummary(lines, audit);\n\n // Explicit deletes (verbose only)\n if (verbose && audit.explicitDeletes.size > 0) {\n this.addExplicitDeletes(lines, audit);\n }\n\n // Implicit deletes\n if (audit.implicitDeletes.size > 0) {\n this.addImplicitDeletes(lines, audit);\n }\n\n // Already deleted records\n if (audit.alreadyDeleted.size > 0) {\n this.addAlreadyDeleted(lines, audit);\n }\n\n // Database-only references\n if (audit.orphanedReferences.length > 0) {\n this.addOrphanedReferences(lines, audit);\n }\n\n // Deletion order (verbose only)\n if (verbose && audit.deletionLevels.length > 0) {\n this.addDeletionOrder(lines, audit);\n }\n\n // Circular dependencies\n if (audit.circularDependencies.length > 0) {\n this.addCircularDependencies(lines, audit);\n }\n\n // Conclusion\n this.addConclusion(lines, audit);\n\n return lines.join('\\n');\n }\n\n /**\n * Generate summary section\n */\n private static addSummary(lines: string[], audit: DeletionAudit): void {\n const totalMarkedForDeletion = audit.explicitDeletes.size + audit.implicitDeletes.size;\n const needDeletion = totalMarkedForDeletion - audit.alreadyDeleted.size;\n\n lines.push('SUMMARY:');\n lines.push(` Records Marked for Deletion: ${totalMarkedForDeletion}`);\n lines.push(` • Will be deleted: ${needDeletion}`);\n lines.push(` • Already deleted: ${audit.alreadyDeleted.size}`);\n lines.push(` Explicit Deletes (marked in metadata): ${audit.explicitDeletes.size}`);\n lines.push(` Implicit Deletes (required by FK): ${audit.implicitDeletes.size}`);\n lines.push(` Database-Only References: ${audit.orphanedReferences.length}`);\n lines.push(` Deletion Levels: ${audit.deletionLevels.length}`);\n lines.push('');\n }\n\n /**\n * Add explicit deletes section\n */\n private static addExplicitDeletes(lines: string[], audit: DeletionAudit): void {\n lines.push('EXPLICIT DELETES (marked with delete: true):');\n\n // Group by entity\n const byEntity = this.groupByEntity(Array.from(audit.explicitDeletes.values()));\n\n for (const [entityName, records] of byEntity) {\n lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);\n for (const record of records) {\n lines.push(` ✓ ${this.formatRecordId(record)}`);\n lines.push(` File: ${record.path}`);\n }\n }\n lines.push('');\n }\n\n /**\n * Add implicit deletes section\n */\n private static addImplicitDeletes(lines: string[], audit: DeletionAudit): void {\n lines.push('⚠️ IMPLICIT DELETES (required to satisfy FK constraints):');\n lines.push(' These records will be deleted because they depend on explicitly deleted records.');\n lines.push('');\n\n // Group by entity\n const byEntity = this.groupByEntity(Array.from(audit.implicitDeletes.values()));\n\n for (const [entityName, records] of byEntity) {\n lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);\n for (const record of records) {\n lines.push(` → ${this.formatRecordId(record)}`);\n lines.push(` File: ${record.path}`);\n\n // Show what it depends on\n const deps = this.findDependencies(record, audit);\n if (deps.length > 0) {\n lines.push(` Depends on: ${deps.join(', ')}`);\n }\n }\n }\n lines.push('');\n }\n\n /**\n * Add already deleted records section\n */\n private static addAlreadyDeleted(lines: string[], audit: DeletionAudit): void {\n lines.push('ℹ️ ALREADY DELETED (exist in metadata but not in database):');\n lines.push(' These records are marked for deletion but already don\\'t exist in the database.');\n lines.push(' No action will be taken for these records.');\n lines.push('');\n\n // Group by entity\n const byEntity = this.groupByEntity(Array.from(audit.alreadyDeleted.values()));\n\n for (const [entityName, records] of byEntity) {\n lines.push(` ${entityName} (${records.length} record${records.length > 1 ? 's' : ''}):`);\n for (const record of records) {\n lines.push(` ✓ ${this.formatRecordId(record)}`);\n }\n }\n lines.push('');\n }\n\n /**\n * Add orphaned references section\n */\n private static addOrphanedReferences(lines: string[], audit: DeletionAudit): void {\n lines.push('⚠️ DATABASE-ONLY REFERENCES (not in metadata files):');\n lines.push(' These records exist in the database but not in metadata.');\n lines.push(' May be handled by database cascade delete rules.');\n lines.push(' If not, deletion will fail with FK constraint errors.');\n lines.push('');\n\n // Group by entity\n const byEntity = new Map<string, typeof audit.orphanedReferences>();\n for (const ref of audit.orphanedReferences) {\n if (!byEntity.has(ref.entityName)) {\n byEntity.set(ref.entityName, []);\n }\n byEntity.get(ref.entityName)!.push(ref);\n }\n\n for (const [entityName, refs] of byEntity) {\n lines.push(` ${entityName} (${refs.length} record${refs.length > 1 ? 's' : ''}):`);\n for (const ref of refs) {\n lines.push(` ⚠️ ID: ${ref.dependentId}`);\n lines.push(` References: ${ref.recordId} via ${ref.fieldName}`);\n lines.push(` Note: May be handled by cascade delete OR require manual handling`);\n }\n }\n lines.push('');\n }\n\n /**\n * Add deletion order section\n */\n private static addDeletionOrder(lines: string[], audit: DeletionAudit): void {\n lines.push('DELETION ORDER (reverse topological sort):');\n lines.push('Records will be deleted in this order (highest dependency level first):');\n lines.push('');\n\n for (let i = 0; i < audit.deletionLevels.length; i++) {\n const level = audit.deletionLevels[i];\n const levelNumber = audit.deletionLevels.length - i; // Reverse numbering for clarity\n\n lines.push(` Level ${levelNumber} (${level.length} record${level.length > 1 ? 's' : ''}):`);\n\n // Group by entity within level\n const byEntity = this.groupByEntity(level);\n for (const [entityName, records] of byEntity) {\n if (records.length === 1) {\n lines.push(` - ${entityName}: ${this.formatRecordId(records[0])}`);\n } else {\n lines.push(` - ${entityName} (${records.length} records):`);\n for (const record of records) {\n lines.push(` ${this.formatRecordId(record)}`);\n }\n }\n }\n }\n lines.push('');\n }\n\n /**\n * Add circular dependencies section\n */\n private static addCircularDependencies(lines: string[], audit: DeletionAudit): void {\n lines.push('❌ CIRCULAR DEPENDENCIES DETECTED:');\n lines.push(' The following circular dependencies prevent safe deletion:');\n lines.push('');\n\n for (let i = 0; i < audit.circularDependencies.length; i++) {\n const cycle = audit.circularDependencies[i];\n lines.push(` Cycle ${i + 1}:`);\n lines.push(` ${cycle.join(' → ')}`);\n }\n lines.push('');\n lines.push(' To resolve:');\n lines.push(' 1. Review the relationships creating the cycle');\n lines.push(' 2. Break the cycle by removing one of the dependencies');\n lines.push(' 3. Consider using NULL values or restructuring relationships');\n lines.push('');\n }\n\n /**\n * Add conclusion section\n */\n private static addConclusion(lines: string[], audit: DeletionAudit): void {\n lines.push('═'.repeat(80));\n\n if (audit.circularDependencies.length > 0) {\n lines.push('STATUS: ❌ CANNOT PROCEED');\n lines.push('');\n lines.push('Circular dependencies detected. Please resolve the cycles above.');\n } else if (audit.implicitDeletes.size > 0 || audit.orphanedReferences.length > 0) {\n lines.push('STATUS: ⚠️ REVIEW REQUIRED');\n lines.push('');\n if (audit.implicitDeletes.size > 0) {\n lines.push(`${audit.implicitDeletes.size} implicit deletion${audit.implicitDeletes.size > 1 ? 's' : ''} will occur.`);\n lines.push('Review the implicit deletes above and confirm this is intended.');\n }\n if (audit.orphanedReferences.length > 0) {\n if (audit.implicitDeletes.size > 0) {\n lines.push('');\n }\n lines.push(`${audit.orphanedReferences.length} database-only reference${audit.orphanedReferences.length > 1 ? 's' : ''} detected.`);\n lines.push('These may be handled by cascade delete rules or may cause FK errors.');\n }\n } else {\n lines.push('STATUS: ✓ SAFE TO PROCEED');\n lines.push('');\n lines.push('All deletions are explicitly marked and have no dependencies.');\n }\n\n lines.push('═'.repeat(80));\n }\n\n /**\n * Format a record ID for display\n */\n private static formatRecordId(record: FlattenedRecord): string {\n return record.record.fields?.Name ||\n record.record.primaryKey?.ID ||\n record.record.fields?.ID ||\n record.id;\n }\n\n /**\n * Group records by entity name\n */\n private static groupByEntity(records: FlattenedRecord[]): Map<string, FlattenedRecord[]> {\n const byEntity = new Map<string, FlattenedRecord[]>();\n\n for (const record of records) {\n if (!byEntity.has(record.entityName)) {\n byEntity.set(record.entityName, []);\n }\n byEntity.get(record.entityName)!.push(record);\n }\n\n return byEntity;\n }\n\n /**\n * Find what an implicit delete depends on (for display)\n */\n private static findDependencies(\n record: FlattenedRecord,\n audit: DeletionAudit\n ): string[] {\n const deps: string[] = [];\n\n for (const depId of record.dependencies) {\n const explicitRecord = audit.explicitDeletes.get(depId);\n if (explicitRecord) {\n deps.push(`${explicitRecord.entityName}:${this.formatRecordId(explicitRecord)}`);\n } else {\n const implicitRecord = audit.implicitDeletes.get(depId);\n if (implicitRecord) {\n deps.push(`${implicitRecord.entityName}:${this.formatRecordId(implicitRecord)}`);\n }\n }\n }\n\n return deps;\n }\n\n /**\n * Generate a concise summary for logging\n */\n static generateSummary(audit: DeletionAudit): string {\n const parts: string[] = [];\n\n if (audit.explicitDeletes.size > 0) {\n parts.push(`${audit.explicitDeletes.size} explicit`);\n }\n\n if (audit.implicitDeletes.size > 0) {\n parts.push(`${audit.implicitDeletes.size} implicit`);\n }\n\n if (audit.orphanedReferences.length > 0) {\n parts.push(`${audit.orphanedReferences.length} orphaned refs`);\n }\n\n if (audit.circularDependencies.length > 0) {\n parts.push(`${audit.circularDependencies.length} cycles`);\n }\n\n return parts.length > 0 ? parts.join(', ') : 'no deletions';\n }\n}\n"]}
|