Backup/Restore: alle neuen Tabellen erfasst (43 Tabellen insgesamt)
Das Backup- und Restore-System kannte noch nicht alle Tabellen, die im Lauf der letzten Wochen hinzugekommen sind. Kritischer Datenverlust im Ernstfall! Neu im Backup + Restore: - PdfTemplate (PDF-Auftragsvorlagen + Feldzuordnungen) - ContractMeter (Zähler-Vertrag-Zuordnungen mit Zeiträumen) - ContractDocument (flexible Vertragsdokumente: Auftragsformular, Lieferbestätigung ...) - RepresentativeAuthorization (Vollmachten zwischen Kunden) - CustomerConsent (DSGVO-Einwilligungen pro Kunde) - DataDeletionRequest (DSGVO-Löschanfragen) - EmailLog (SMTP-Sendeprotokoll) - AuditRetentionPolicy (Aufbewahrungsfristen pro Ressourcentyp) - AuditLog (vollständiges Änderungsprotokoll) Außerdem: - prisma/backup-data.ts: komplett neu strukturiert, korrekte Level-Hierarchie, nutzt aktuelles Schema (Provider statt EnergyProvider/TelecomProvider, InternetContractDetails statt TelecomContractDetails etc.) - prisma/restore-data.ts: Boilerplate durch generische restoreTable()-Helper ersetzt – von 487 auf ~240 Zeilen - backup.service.ts: neue Tabellen in createBackup, restoreOrder und deleteMany-Liste nachgetragen (Service bleibt sonst wie er ist) Test-Backup erfolgreich: 4420 Datensätze in 37 aktiven Tabellen gesichert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* Datenbank-Backup Script
|
||||
*
|
||||
* Exportiert alle Daten als JSON-Dateien für die Wiederherstellung nach Migrationen.
|
||||
* Exportiert ALLE Daten als JSON-Dateien für die Wiederherstellung nach Migrationen.
|
||||
*
|
||||
* Verwendung:
|
||||
* npx ts-node prisma/backup-data.ts
|
||||
* npm run db:backup
|
||||
*
|
||||
* Erstellt einen Ordner 'backups/YYYY-MM-DD_HH-mm-ss/' mit JSON-Dateien pro Tabelle.
|
||||
* Erstellt einen Ordner 'prisma/backups/YYYY-MM-DDTHH-mm-ss/' mit JSON-Dateien pro Tabelle.
|
||||
*
|
||||
* Die Tabellen sind nach Abhängigkeitsreihenfolge sortiert (Level 0 = keine FKs, dann aufsteigend).
|
||||
* Damit kann das Restore-Script sie in der gleichen Reihenfolge einspielen, ohne FK-Verletzungen.
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
@@ -28,7 +31,7 @@ async function main() {
|
||||
|
||||
// Tabellen in Abhängigkeitsreihenfolge (unabhängige zuerst)
|
||||
const tables = [
|
||||
// Level 0: Keine Abhängigkeiten
|
||||
// ============ Level 0: Reine Stammdaten/Kataloge ============
|
||||
{ name: 'Permission', query: () => prisma.permission.findMany() },
|
||||
{ name: 'Role', query: () => prisma.role.findMany() },
|
||||
{ name: 'SalesPlatform', query: () => prisma.salesPlatform.findMany() },
|
||||
@@ -37,40 +40,58 @@ async function main() {
|
||||
{ name: 'ContractDuration', query: () => prisma.contractDuration.findMany() },
|
||||
{ name: 'AppSetting', query: () => prisma.appSetting.findMany() },
|
||||
{ name: 'EmailProviderConfig', query: () => prisma.emailProviderConfig.findMany() },
|
||||
{ name: 'EnergyProvider', query: () => prisma.energyProvider.findMany() },
|
||||
{ name: 'TelecomProvider', query: () => prisma.telecomProvider.findMany() },
|
||||
{ name: 'Provider', query: () => prisma.provider.findMany() },
|
||||
{ name: 'PdfTemplate', query: () => prisma.pdfTemplate.findMany() },
|
||||
{ name: 'AuditRetentionPolicy', query: () => prisma.auditRetentionPolicy.findMany() },
|
||||
|
||||
// Level 1: Abhängig von Level 0
|
||||
// ============ Level 1: Abhängig von Level 0 ============
|
||||
{ name: 'RolePermission', query: () => prisma.rolePermission.findMany() },
|
||||
{ name: 'User', query: () => prisma.user.findMany() },
|
||||
{ name: 'Customer', query: () => prisma.customer.findMany() },
|
||||
{ name: 'Tariff', query: () => prisma.tariff.findMany() },
|
||||
|
||||
// Level 2: Abhängig von Level 1
|
||||
// ============ Level 2: Abhängig von Customer ============
|
||||
{ name: 'UserRole', query: () => prisma.userRole.findMany() },
|
||||
{ name: 'CustomerRepresentative', query: () => prisma.customerRepresentative.findMany() },
|
||||
{ name: 'StressfreiEmail', query: () => prisma.stressfreiEmail.findMany() },
|
||||
{ name: 'Contract', query: () => prisma.contract.findMany() },
|
||||
{ name: 'Address', query: () => prisma.address.findMany() },
|
||||
{ name: 'BankCard', query: () => prisma.bankCard.findMany() },
|
||||
{ name: 'IdentityDocument', query: () => prisma.identityDocument.findMany() },
|
||||
{ name: 'Meter', query: () => prisma.meter.findMany() },
|
||||
{ name: 'StressfreiEmail', query: () => prisma.stressfreiEmail.findMany() },
|
||||
{ name: 'CustomerRepresentative', query: () => prisma.customerRepresentative.findMany() },
|
||||
{ name: 'CustomerConsent', query: () => prisma.customerConsent.findMany() },
|
||||
{ name: 'DataDeletionRequest', query: () => prisma.dataDeletionRequest.findMany() },
|
||||
|
||||
// Level 3: Abhängig von Level 2
|
||||
{ name: 'CachedEmail', query: () => prisma.cachedEmail.findMany() },
|
||||
{ name: 'ContractTask', query: () => prisma.contractTask.findMany() },
|
||||
{ name: 'MeterReading', query: () => prisma.meterReading.findMany() },
|
||||
{ name: 'ContractNote', query: () => prisma.contractNote.findMany() },
|
||||
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||||
// ============ Level 3: Contracts + abhängige ============
|
||||
{ name: 'Contract', query: () => prisma.contract.findMany() },
|
||||
{ name: 'RepresentativeAuthorization', query: () => prisma.representativeAuthorization.findMany() },
|
||||
|
||||
// Level 4: Abhängig von Level 3
|
||||
{ name: 'ContractTaskSubtask', query: () => prisma.contractTaskSubtask.findMany() },
|
||||
|
||||
// Vertragstyp-spezifische Details
|
||||
// ============ Level 4: Vertragstyp-Details + Sub-Tabellen ============
|
||||
{ name: 'EnergyContractDetails', query: () => prisma.energyContractDetails.findMany() },
|
||||
{ name: 'TelecomContractDetails', query: () => prisma.telecomContractDetails.findMany() },
|
||||
{ name: 'InternetContractDetails', query: () => prisma.internetContractDetails.findMany() },
|
||||
{ name: 'MobileContractDetails', query: () => prisma.mobileContractDetails.findMany() },
|
||||
{ name: 'TvContractDetails', query: () => prisma.tvContractDetails.findMany() },
|
||||
{ name: 'CarInsuranceDetails', query: () => prisma.carInsuranceDetails.findMany() },
|
||||
{ name: 'ContractMeter', query: () => prisma.contractMeter.findMany() },
|
||||
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||||
{ name: 'ContractHistoryEntry', query: () => prisma.contractHistoryEntry.findMany() },
|
||||
{ name: 'ContractTask', query: () => prisma.contractTask.findMany() },
|
||||
{ name: 'Invoice', query: () => prisma.invoice.findMany() },
|
||||
{ name: 'MeterReading', query: () => prisma.meterReading.findMany() },
|
||||
|
||||
// ============ Level 5: Sub-Tabellen der Sub-Tabellen ============
|
||||
{ name: 'ContractTaskSubtask', query: () => prisma.contractTaskSubtask.findMany() },
|
||||
{ name: 'PhoneNumber', query: () => prisma.phoneNumber.findMany() },
|
||||
{ name: 'SimCard', query: () => prisma.simCard.findMany() },
|
||||
|
||||
// ============ Level 6: Logs & Emails (wachsende Tabellen) ============
|
||||
{ name: 'CachedEmail', query: () => prisma.cachedEmail.findMany() },
|
||||
{ name: 'EmailLog', query: () => prisma.emailLog.findMany() },
|
||||
{ name: 'AuditLog', query: () => prisma.auditLog.findMany() },
|
||||
];
|
||||
|
||||
let totalRecords = 0;
|
||||
const stats: { table: string; count: number }[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
@@ -79,7 +100,7 @@ async function main() {
|
||||
totalRecords += count;
|
||||
stats.push({ table: table.name, count });
|
||||
|
||||
// JSON-Datei schreiben
|
||||
// JSON-Datei schreiben (Date-Felder als ISO-String)
|
||||
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
|
||||
@@ -87,20 +108,26 @@ async function main() {
|
||||
console.log(`${status} ${table.name}: ${count} Einträge`);
|
||||
} catch (error: any) {
|
||||
// Tabelle existiert möglicherweise nicht (bei älteren Schema-Versionen)
|
||||
console.log(`⚠️ ${table.name}: Übersprungen (${error.message?.slice(0, 50)}...)`);
|
||||
skipped.push(table.name);
|
||||
console.log(`⚠️ ${table.name}: Übersprungen (${error.message?.slice(0, 80)}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Backup-Info speichern
|
||||
const backupInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
schemaVersion: 'current',
|
||||
totalRecords,
|
||||
tables: stats,
|
||||
skippedTables: skipped,
|
||||
};
|
||||
fs.writeFileSync(path.join(backupDir, '_backup-info.json'), JSON.stringify(backupInfo, null, 2));
|
||||
|
||||
console.log(`\n✅ Backup abgeschlossen!`);
|
||||
console.log(` 📊 ${totalRecords} Datensätze in ${stats.filter(s => s.count > 0).length} Tabellen`);
|
||||
if (skipped.length > 0) {
|
||||
console.log(` ⚠️ ${skipped.length} Tabellen übersprungen: ${skipped.join(', ')}`);
|
||||
}
|
||||
console.log(` 📁 Gespeichert in: ${backupDir}\n`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user