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:
parent
b8a3c0d11a
commit
51a25f8b0b
|
|
@ -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`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,43 +4,40 @@
|
|||
* Stellt Daten aus einem JSON-Backup wieder her.
|
||||
*
|
||||
* Verwendung:
|
||||
* npx ts-node prisma/restore-data.ts [backup-ordner]
|
||||
* npm run db:restore # Letztes Backup
|
||||
* npx tsx prisma/restore-data.ts <ordner> # Bestimmtes Backup
|
||||
*
|
||||
* Beispiele:
|
||||
* npx ts-node prisma/restore-data.ts # Letztes Backup
|
||||
* npx ts-node prisma/restore-data.ts 2025-01-31_14-30-00 # Bestimmtes Backup
|
||||
*
|
||||
* WICHTIG: Führe vorher 'npx prisma migrate deploy' oder 'npx prisma db push' aus!
|
||||
* WICHTIG: Führe vorher 'npx prisma db push' oder 'npx prisma migrate deploy' aus,
|
||||
* damit das Schema zur DB passt!
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Hilfsfunktion: JSON-Datei lesen
|
||||
// Hilfsfunktion: JSON-Datei lesen (leer bei fehlender Datei)
|
||||
function readJsonFile<T>(filePath: string): T[] {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
if (!fs.existsSync(filePath)) return [];
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
// Hilfsfunktion: Datum-Strings zu Date-Objekten konvertieren
|
||||
// Hilfsfunktion: ISO-Datum-Strings rekursiv zu Date-Objekten
|
||||
function convertDates(obj: any): any {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (typeof obj === 'string') {
|
||||
// ISO-Datumsformat erkennen
|
||||
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 (Array.isArray(obj)) return obj.map(convertDates);
|
||||
if (typeof obj === 'object') {
|
||||
const result: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
|
|
@ -51,19 +48,75 @@ function convertDates(obj: any): any {
|
|||
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() {
|
||||
// Backup-Ordner bestimmen
|
||||
const backupsDir = path.join(__dirname, 'backups');
|
||||
let backupName = process.argv[2];
|
||||
|
||||
if (!backupName) {
|
||||
// Neuestes Backup finden
|
||||
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())
|
||||
.filter((f) => fs.statSync(path.join(backupsDir, f)).isDirectory())
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
|
|
@ -76,18 +129,16 @@ async function main() {
|
|||
}
|
||||
|
||||
const backupDir = path.join(backupsDir, backupName);
|
||||
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
console.error(`❌ Backup-Ordner nicht gefunden: ${backupDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Backup-Info lesen
|
||||
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 in ${info.tables.filter((t: any) => t.count > 0).length} Tabellen\n`);
|
||||
console.log(`📊 ${info.totalRecords} Datensätze\n`);
|
||||
}
|
||||
|
||||
console.log(`🔄 Starte Wiederherstellung aus: ${backupDir}\n`);
|
||||
|
|
@ -96,362 +147,76 @@ async function main() {
|
|||
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
try {
|
||||
// Tabellen in Abhängigkeitsreihenfolge wiederherstellen
|
||||
const restoreOrder = [
|
||||
// Level 0: Keine Abhängigkeiten
|
||||
{
|
||||
name: 'Permission',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.permission.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Role',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.role.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SalesPlatform',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.salesPlatform.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractCategory',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractCategory.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CancellationPeriod',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.cancellationPeriod.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractDuration',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractDuration.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AppSetting',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.appSetting.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EmailProviderConfig',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.emailProviderConfig.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EnergyProvider',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.energyProvider.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TelecomProvider',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.telecomProvider.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// 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',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.rolePermission.upsert({
|
||||
where: { roleId_permissionId: { roleId: item.roleId, permissionId: item.permissionId } },
|
||||
update: {},
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.user.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Customer',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.customer.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Tariff',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.tariff.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{ 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
|
||||
{
|
||||
name: 'UserRole',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.userRole.upsert({
|
||||
where: { userId_roleId: { userId: item.userId, roleId: item.roleId } },
|
||||
update: {},
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CustomerRepresentative',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.customerRepresentative.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'StressfreiEmail',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.stressfreiEmail.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Contract',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contract.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Meter',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.meter.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// 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
|
||||
{
|
||||
name: 'CachedEmail',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.cachedEmail.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractTask',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractTask.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MeterReading',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.meterReading.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractNote',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractNote.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractDocument',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractDocument.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// Level 3: Contracts
|
||||
{ name: 'Contract', model: prisma.contract },
|
||||
{ name: 'RepresentativeAuthorization', model: prisma.representativeAuthorization },
|
||||
|
||||
// Level 4
|
||||
{
|
||||
name: 'ContractTaskSubtask',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractTaskSubtask.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// 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 },
|
||||
|
||||
// Vertragsdetails
|
||||
{
|
||||
name: 'EnergyContractDetails',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.energyContractDetails.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TelecomContractDetails',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.telecomContractDetails.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CarInsuranceDetails',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.carInsuranceDetails.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// 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 restoreOrder) {
|
||||
for (const table of order) {
|
||||
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||
const data = readJsonFile(filePath);
|
||||
const data = readJsonFile<any>(filePath);
|
||||
|
||||
if (data.length === 0) {
|
||||
console.log(`⚪ ${table.name}: Keine Daten`);
|
||||
|
|
@ -459,17 +224,23 @@ async function main() {
|
|||
}
|
||||
|
||||
try {
|
||||
await table.restore(data);
|
||||
totalRestored += data.length;
|
||||
console.log(`✅ ${table.name}: ${data.length} Einträge wiederhergestellt`);
|
||||
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) {
|
||||
console.log(`⚠️ ${table.name}: Fehler - ${error.message?.slice(0, 80)}`);
|
||||
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\n`);
|
||||
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -239,6 +239,16 @@ export async function createBackup(): Promise<BackupResult> {
|
|||
{ name: 'Address', query: () => prisma.address.findMany() },
|
||||
{ name: 'BankCard', query: () => prisma.bankCard.findMany() },
|
||||
{ name: 'IdentityDocument', query: () => prisma.identityDocument.findMany() },
|
||||
// Neue Tabellen
|
||||
{ name: 'PdfTemplate', query: () => prisma.pdfTemplate.findMany() },
|
||||
{ name: 'ContractMeter', query: () => prisma.contractMeter.findMany() },
|
||||
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||||
{ name: 'RepresentativeAuthorization', query: () => prisma.representativeAuthorization.findMany() },
|
||||
{ name: 'CustomerConsent', query: () => prisma.customerConsent.findMany() },
|
||||
{ name: 'DataDeletionRequest', query: () => prisma.dataDeletionRequest.findMany() },
|
||||
{ name: 'EmailLog', query: () => prisma.emailLog.findMany() },
|
||||
{ name: 'AuditRetentionPolicy', query: () => prisma.auditRetentionPolicy.findMany() },
|
||||
{ name: 'AuditLog', query: () => prisma.auditLog.findMany() },
|
||||
];
|
||||
|
||||
let totalRecords = 0;
|
||||
|
|
@ -297,6 +307,10 @@ export async function restoreBackup(backupName: string): Promise<RestoreResult>
|
|||
// WICHTIG: Alle Tabellen vor dem Restore leeren, damit keine alten Daten übrig bleiben
|
||||
console.log('[Restore] Lösche alle bestehenden Daten...');
|
||||
|
||||
// Logs & Audit zuerst (hängen an allem)
|
||||
await prisma.auditLog.deleteMany({});
|
||||
await prisma.emailLog.deleteMany({});
|
||||
|
||||
// Detail-Tabellen
|
||||
await prisma.carInsuranceDetails.deleteMany({});
|
||||
await prisma.tvContractDetails.deleteMany({});
|
||||
|
|
@ -309,12 +323,21 @@ export async function restoreBackup(backupName: string): Promise<RestoreResult>
|
|||
await prisma.meterReading.deleteMany({});
|
||||
await prisma.contractHistoryEntry.deleteMany({});
|
||||
|
||||
// Neue Contract-bezogene Tabellen
|
||||
await prisma.contractDocument.deleteMany({});
|
||||
await prisma.contractMeter.deleteMany({});
|
||||
|
||||
// E-Mail & Verträge
|
||||
await prisma.cachedEmail.deleteMany({});
|
||||
await prisma.contractTaskSubtask.deleteMany({});
|
||||
await prisma.contractTask.deleteMany({});
|
||||
await prisma.contract.deleteMany({});
|
||||
|
||||
// DSGVO + Vollmachten (abhängig von Customer)
|
||||
await prisma.representativeAuthorization.deleteMany({});
|
||||
await prisma.customerConsent.deleteMany({});
|
||||
await prisma.dataDeletionRequest.deleteMany({});
|
||||
|
||||
// Kunden-bezogene Daten
|
||||
await prisma.stressfreiEmail.deleteMany({});
|
||||
await prisma.meter.deleteMany({});
|
||||
|
|
@ -328,7 +351,8 @@ export async function restoreBackup(backupName: string): Promise<RestoreResult>
|
|||
await prisma.user.deleteMany({});
|
||||
await prisma.customer.deleteMany({});
|
||||
|
||||
// Stammdaten
|
||||
// Stammdaten & Kataloge
|
||||
await prisma.pdfTemplate.deleteMany({});
|
||||
await prisma.tariff.deleteMany({});
|
||||
await prisma.provider.deleteMany({});
|
||||
await prisma.rolePermission.deleteMany({});
|
||||
|
|
@ -340,6 +364,7 @@ export async function restoreBackup(backupName: string): Promise<RestoreResult>
|
|||
await prisma.contractCategory.deleteMany({});
|
||||
await prisma.emailProviderConfig.deleteMany({});
|
||||
await prisma.appSetting.deleteMany({});
|
||||
await prisma.auditRetentionPolicy.deleteMany({});
|
||||
|
||||
console.log('[Restore] Alle Daten gelöscht, starte Wiederherstellung...');
|
||||
|
||||
|
|
@ -753,6 +778,115 @@ export async function restoreBackup(backupName: string): Promise<RestoreResult>
|
|||
}
|
||||
},
|
||||
},
|
||||
// Neue Tabellen
|
||||
{
|
||||
name: 'PdfTemplate',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.pdfTemplate.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractMeter',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractMeter.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ContractDocument',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.contractDocument.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RepresentativeAuthorization',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.representativeAuthorization.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CustomerConsent',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.customerConsent.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DataDeletionRequest',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.dataDeletionRequest.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EmailLog',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.emailLog.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AuditRetentionPolicy',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.auditRetentionPolicy.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AuditLog',
|
||||
restore: async (data: any[]) => {
|
||||
for (const item of data) {
|
||||
await prisma.auditLog.upsert({
|
||||
where: { id: item.id },
|
||||
update: convertDates(item),
|
||||
create: convertDates(item),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let totalRestored = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue