258 lines
8.5 KiB
TypeScript
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();
|
|
});
|