opencrm/backend/prisma/restore-data.ts

258 lines
8.5 KiB
TypeScript

/**
* Datenbank-Restore Script
*
* Stellt Daten aus einem JSON-Backup wieder her.
*
* Verwendung:
* npm run db:restore # Letztes Backup
* npx tsx prisma/restore-data.ts <ordner> # 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<T>(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<T extends { id?: any }>(
tableName: string,
data: T[],
model: any,
options: { useCreateMany?: boolean; compositeKey?: string[] } = {},
): Promise<number> {
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<any>(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();
});