/** * Datenbank-Restore Script * * Stellt Daten aus einem JSON-Backup wieder her. * * Verwendung: * npm run db:restore # Letztes Backup * npx tsx prisma/restore-data.ts # Bestimmtes Backup * * WICHTIG: Führe vorher 'npx prisma db push' oder 'npx prisma migrate deploy' aus, * damit das Schema zur DB passt! */ import { PrismaClient, Prisma } from '@prisma/client'; import * as fs from 'fs'; import * as path from 'path'; const prisma = new PrismaClient(); // Hilfsfunktion: JSON-Datei lesen (leer bei fehlender Datei) function readJsonFile(filePath: string): T[] { if (!fs.existsSync(filePath)) return []; try { const content = fs.readFileSync(filePath, 'utf-8'); return JSON.parse(content); } catch { return []; } } // Hilfsfunktion: ISO-Datum-Strings rekursiv zu Date-Objekten function convertDates(obj: any): any { if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) { return new Date(obj); } return obj; } if (Array.isArray(obj)) return obj.map(convertDates); if (typeof obj === 'object') { const result: any = {}; for (const [key, value] of Object.entries(obj)) { result[key] = convertDates(value); } return result; } return obj; } /** * Generischer Restore-Helper: nutzt createMany mit skipDuplicates * wenn möglich, sonst einzelnes upsert per ID. */ async function restoreTable( tableName: string, data: T[], model: any, options: { useCreateMany?: boolean; compositeKey?: string[] } = {}, ): Promise { if (data.length === 0) return 0; const converted = data.map(convertDates) as T[]; // Bei einfachen Tabellen: createMany mit skipDuplicates if (options.useCreateMany) { try { const result = await model.createMany({ data: converted, skipDuplicates: true, }); return result.count; } catch (err: any) { // Fallback auf einzeln } } // Upsert per ID (oder Composite-Key) let count = 0; for (const item of converted) { try { if (options.compositeKey) { const where: any = {}; const compositeWhere: any = {}; for (const key of options.compositeKey) { compositeWhere[key] = (item as any)[key]; } where[options.compositeKey.join('_')] = compositeWhere; await model.upsert({ where, update: {}, create: item, }); } else { await model.upsert({ where: { id: (item as any).id }, update: item, create: item, }); } count++; } catch (err: any) { console.log(` ⚠️ Eintrag in ${tableName} (id=${(item as any).id}): ${err.message?.slice(0, 80)}`); } } return count; } async function main() { const backupsDir = path.join(__dirname, 'backups'); let backupName = process.argv[2]; if (!backupName) { if (!fs.existsSync(backupsDir)) { console.error('❌ Kein Backup-Ordner gefunden!'); process.exit(1); } const backups = fs.readdirSync(backupsDir) .filter((f) => fs.statSync(path.join(backupsDir, f)).isDirectory()) .sort() .reverse(); if (backups.length === 0) { console.error('❌ Keine Backups gefunden!'); process.exit(1); } backupName = backups[0]; console.log(`📦 Verwende neuestes Backup: ${backupName}`); } const backupDir = path.join(backupsDir, backupName); if (!fs.existsSync(backupDir)) { console.error(`❌ Backup-Ordner nicht gefunden: ${backupDir}`); process.exit(1); } const infoPath = path.join(backupDir, '_backup-info.json'); if (fs.existsSync(infoPath)) { const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8')); console.log(`\n📅 Backup vom: ${new Date(info.timestamp).toLocaleString('de-DE')}`); console.log(`📊 ${info.totalRecords} Datensätze\n`); } console.log(`🔄 Starte Wiederherstellung aus: ${backupDir}\n`); // Foreign Key Checks deaktivieren für MySQL await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0'); try { // Tabellen in Abhängigkeitsreihenfolge (gleich wie im Backup) const order: Array<{ name: string; model: any; compositeKey?: string[]; }> = [ // Level 0 { name: 'Permission', model: prisma.permission }, { name: 'Role', model: prisma.role }, { name: 'SalesPlatform', model: prisma.salesPlatform }, { name: 'ContractCategory', model: prisma.contractCategory }, { name: 'CancellationPeriod', model: prisma.cancellationPeriod }, { name: 'ContractDuration', model: prisma.contractDuration }, { name: 'AppSetting', model: prisma.appSetting }, { name: 'EmailProviderConfig', model: prisma.emailProviderConfig }, { name: 'Provider', model: prisma.provider }, { name: 'PdfTemplate', model: prisma.pdfTemplate }, { name: 'AuditRetentionPolicy', model: prisma.auditRetentionPolicy }, // Level 1 { name: 'RolePermission', model: prisma.rolePermission, compositeKey: ['roleId', 'permissionId'] }, { name: 'User', model: prisma.user }, { name: 'Customer', model: prisma.customer }, { name: 'Tariff', model: prisma.tariff }, // Level 2: Customer-abhängig { name: 'UserRole', model: prisma.userRole, compositeKey: ['userId', 'roleId'] }, { name: 'Address', model: prisma.address }, { name: 'BankCard', model: prisma.bankCard }, { name: 'IdentityDocument', model: prisma.identityDocument }, { name: 'Meter', model: prisma.meter }, { name: 'StressfreiEmail', model: prisma.stressfreiEmail }, { name: 'CustomerRepresentative', model: prisma.customerRepresentative }, { name: 'CustomerConsent', model: prisma.customerConsent }, { name: 'DataDeletionRequest', model: prisma.dataDeletionRequest }, // Level 3: Contracts { name: 'Contract', model: prisma.contract }, { name: 'RepresentativeAuthorization', model: prisma.representativeAuthorization }, // Level 4: Vertragstyp-Details { name: 'EnergyContractDetails', model: prisma.energyContractDetails }, { name: 'InternetContractDetails', model: prisma.internetContractDetails }, { name: 'MobileContractDetails', model: prisma.mobileContractDetails }, { name: 'TvContractDetails', model: prisma.tvContractDetails }, { name: 'CarInsuranceDetails', model: prisma.carInsuranceDetails }, { name: 'ContractMeter', model: prisma.contractMeter }, { name: 'ContractDocument', model: prisma.contractDocument }, { name: 'ContractHistoryEntry', model: prisma.contractHistoryEntry }, { name: 'ContractTask', model: prisma.contractTask }, { name: 'Invoice', model: prisma.invoice }, { name: 'MeterReading', model: prisma.meterReading }, // Level 5: Sub-Tabellen { name: 'ContractTaskSubtask', model: prisma.contractTaskSubtask }, { name: 'PhoneNumber', model: prisma.phoneNumber }, { name: 'SimCard', model: prisma.simCard }, // Level 6: Logs & Emails { name: 'CachedEmail', model: prisma.cachedEmail }, { name: 'EmailLog', model: prisma.emailLog }, { name: 'AuditLog', model: prisma.auditLog }, ]; let totalRestored = 0; const skipped: string[] = []; for (const table of order) { const filePath = path.join(backupDir, `${table.name}.json`); const data = readJsonFile(filePath); if (data.length === 0) { console.log(`⚪ ${table.name}: Keine Daten`); continue; } try { const count = await restoreTable(table.name, data, table.model, { compositeKey: table.compositeKey, }); totalRestored += count; console.log(`✅ ${table.name}: ${count}/${data.length} Einträge wiederhergestellt`); } catch (error: any) { skipped.push(table.name); console.log(`⚠️ ${table.name}: Fehler - ${error.message?.slice(0, 100)}`); } } console.log(`\n✅ Wiederherstellung abgeschlossen!`); console.log(` 📊 ${totalRestored} Datensätze wiederhergestellt`); if (skipped.length > 0) { console.log(` ⚠️ ${skipped.length} Tabellen mit Fehlern: ${skipped.join(', ')}`); } console.log(''); } finally { // Foreign Key Checks wieder aktivieren await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); } } main() .catch((e) => { console.error('❌ Wiederherstellung fehlgeschlagen:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });