/** * Datenbank-Backup Script * * Exportiert ALLE Daten als JSON-Dateien für die Wiederherstellung nach Migrationen. * * Verwendung: * npm run db:backup * * 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'; import * as fs from 'fs'; import * as path from 'path'; const prisma = new PrismaClient(); async function main() { // Backup-Ordner mit Zeitstempel erstellen const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const backupDir = path.join(__dirname, 'backups', timestamp); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } console.log(`\n📦 Starte Datenbank-Backup nach: ${backupDir}\n`); // Tabellen in Abhängigkeitsreihenfolge (unabhängige zuerst) const tables = [ // ============ Level 0: Reine Stammdaten/Kataloge ============ { name: 'Permission', query: () => prisma.permission.findMany() }, { name: 'Role', query: () => prisma.role.findMany() }, { name: 'SalesPlatform', query: () => prisma.salesPlatform.findMany() }, { name: 'ContractCategory', query: () => prisma.contractCategory.findMany() }, { name: 'CancellationPeriod', query: () => prisma.cancellationPeriod.findMany() }, { name: 'ContractDuration', query: () => prisma.contractDuration.findMany() }, { name: 'AppSetting', query: () => prisma.appSetting.findMany() }, { name: 'EmailProviderConfig', query: () => prisma.emailProviderConfig.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 ============ { 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 Customer ============ { name: 'UserRole', query: () => prisma.userRole.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: Contracts + abhängige ============ { name: 'Contract', query: () => prisma.contract.findMany() }, { name: 'RepresentativeAuthorization', query: () => prisma.representativeAuthorization.findMany() }, // ============ Level 4: Vertragstyp-Details + Sub-Tabellen ============ { name: 'EnergyContractDetails', query: () => prisma.energyContractDetails.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 { const data = await table.query(); const count = data.length; totalRecords += count; stats.push({ table: table.name, count }); // JSON-Datei schreiben (Date-Felder als ISO-String) const filePath = path.join(backupDir, `${table.name}.json`); fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); const status = count > 0 ? '✅' : '⚪'; console.log(`${status} ${table.name}: ${count} Einträge`); } catch (error: any) { // Tabelle existiert möglicherweise nicht (bei älteren Schema-Versionen) 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`); } main() .catch((e) => { console.error('❌ Backup fehlgeschlagen:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });