/** * Feature/Symbol-Repair Skript * * Problem: Features (Zeichnungen) speichern Symbol-Referenzen als iconId in * properties JSONB. Nach dem Icon-Verlust zeigen diese auf gelöschte UUIDs. * * Dieses Skript versucht: * 1. Alle symbol-Features zu scannen * 2. Zu prüfen, ob die referenzierte iconId noch existiert * 3. Falls nicht: via migratedFromIconId oder Dateinamen-Matching zu reparieren * * Ausführung: * Dry-Run (nur Analyse, keine Änderungen): * node prisma/repair-features.js --dry-run * * Apply (mit Backup und Reparatur): * node prisma/repair-features.js --apply * * SAFETY: * - In Production (NODE_ENV=production) nur mit --apply möglich * - Vor Apply wird automatisch ein JSON-Backup erstellt * - Keine Features werden gelöscht * - Keine Projekte oder Tenant-Daten werden verändert */ const { PrismaClient } = require('@prisma/client') const fs = require('fs') const path = require('path') const prisma = new PrismaClient() const IS_DRY_RUN = process.argv.includes('--dry-run') const IS_APPLY = process.argv.includes('--apply') const IS_PROD = process.env.NODE_ENV === 'production' // ─── Safety Guards ───────────────────────────────────────────────────────── if (!IS_DRY_RUN && !IS_APPLY) { console.error('❌ Fehler: Bitte --dry-run oder --apply angeben') console.error('') console.error(' Analyse-Modus (keine Änderungen):') console.error(' node prisma/repair-features.js --dry-run') console.error('') console.error(' Reparatur-Modus (mit Backup):') console.error(' node prisma/repair-features.js --apply') process.exit(1) } if (IS_DRY_RUN && IS_APPLY) { console.error('❌ Fehler: --dry-run und --apply können nicht zusammen verwendet werden') process.exit(1) } if (IS_APPLY) { console.log('⚠️ REPAIR-MODUS (mit Änderungen)') if (IS_PROD) { console.log(' Production-Umgebung erkannt. Fortfahren in 3 Sekunden...') await new Promise(r => setTimeout(r, 3000)) } } else { console.log('🔍 DRY-RUN MODUS (keine Änderungen)') } // ─── Statistics ──────────────────────────────────────────────────────────── const stats = { totalFeatures: 0, validRefs: 0, brokenRefs: 0, repaired: 0, notRepairable: 0, details: [], } // ─── Main ────────────────────────────────────────────────────────────────── async function repair() { console.log('\n🔧 Feature-Repair gestartet') console.log('────────────────────────────────────────') // ─── 1. Lade alle gültigen Icons für schnelle Lookup ─── console.log('\n[1/5] Lade Icon-Lookup-Tabellen...') const allIcons = await prisma.iconAsset.findMany({ select: { id: true, name: true, fileKey: true }, }) const iconById = new Map(allIcons.map(i => [i.id, i])) const iconByFileKey = new Map(allIcons.map(i => [i.fileKey, i])) // tenant_symbols mit migratedFromIconId const tenantSymbols = await prisma.tenantSymbol.findMany({ select: { id: true, iconId: true, migratedFromIconId: true, customName: true }, }) const tsByMigratedId = new Map() for (const ts of tenantSymbols) { if (ts.migratedFromIconId) { tsByMigratedId.set(ts.migratedFromIconId, ts) } } console.log(` ✅ ${allIcons.length} IconAssets geladen`) console.log(` ✅ ${tenantSymbols.length} TenantSymbols geladen`) // ─── 2. Lade alle symbol-Features ─── console.log('\n[2/5] Scanne Features...') const features = await prisma.feature.findMany({ where: { type: 'symbol' }, include: { project: { select: { id: true, title: true, tenantId: true } } }, orderBy: { createdAt: 'asc' }, }) stats.totalFeatures = features.length console.log(` 📊 ${features.length} symbol-Features gefunden`) // ─── 3. Analysiere jede Feature ─── console.log('\n[3/5] Analysiere Symbol-Referenzen...') const toRepair = [] for (const feature of features) { const props = feature.properties || {} const iconId = props.iconId const imageUrl = props.imageUrl || '' // Keine iconId → nicht reparierbar, aber auch nicht unbedingt kaputt if (!iconId) { stats.validRefs++ // Oder neutral continue } // Prüfe ob iconId noch existiert const icon = iconById.get(iconId) if (icon) { stats.validRefs++ continue } // ❌ Broken reference stats.brokenRefs++ // Versuche Reparatur-Strategien let repairedId = null let repairMethod = null // Strategie 1: via migratedFromIconId const ts = tsByMigratedId.get(iconId) if (ts && ts.iconId && iconById.has(ts.iconId)) { repairedId = ts.iconId repairMethod = 'migratedFromIconId' } // Strategie 2: via imageUrl (extrahiere Dateinamen) if (!repairedId && imageUrl) { const match = imageUrl.match(/\/api\/icons\/([^\/]+)\/image/) if (match) { // Die alte iconId war in der URL, aber die ist ja gelöscht // Wir können hier nicht viel machen } } // Strategie 3: via Name-Matching (wenn iconId ein bekannter Name war) // Nicht anwendbar, da iconId eine UUID ist if (repairedId) { stats.repaired++ toRepair.push({ featureId: feature.id, projectId: feature.projectId, projectTitle: feature.project?.title || '(unbekannt)', oldIconId: iconId, newIconId: repairedId, repairMethod, properties: props, }) } else { stats.notRepairable++ stats.details.push({ featureId: feature.id, projectId: feature.projectId, projectTitle: feature.project?.title || '(unbekannt)', iconId, reason: 'Keine Zuordnung möglich (iconId nicht in migratedFromIconId)', properties: JSON.stringify(props).substring(0, 200), }) } } // ─── 4. Ergebnis-Anzeige ─── console.log('\n[4/5] Ergebnis-Zusammenfassung') console.log('────────────────────────────────────────') console.log(` Gesamt geprüfte Features: ${stats.totalFeatures}`) console.log(` ✅ Gültige Referenzen: ${stats.validRefs}`) console.log(` ❌ Kaputte Referenzen: ${stats.brokenRefs}`) console.log(` 🔧 Automatisch reparierbar: ${stats.repaired}`) console.log(` ⚠️ Nicht reparierbar: ${stats.notRepairable}`) if (stats.details.length > 0) { console.log('\n Nicht reparierbare Features (erste 10):') for (const d of stats.details.slice(0, 10)) { console.log(` - Feature ${d.featureId} in "${d.projectTitle}"`) console.log(` iconId: ${d.iconId}`) console.log(` Grund: ${d.reason}`) } if (stats.details.length > 10) { console.log(` ... und ${stats.details.length - 10} weitere`) } } // ─── 5. Apply (nur wenn --apply) ─── if (IS_APPLY && toRepair.length > 0) { console.log('\n[5/5] Wende Reparaturen an...') // Backup erstellen const backupPath = path.join(__dirname, `feature-backup-${Date.now()}.json`) const backup = toRepair.map(r => ({ featureId: r.featureId, oldIconId: r.oldIconId, newIconId: r.newIconId, repairMethod: r.repairMethod, oldProperties: r.properties, })) fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2)) console.log(` 💾 Backup erstellt: ${backupPath}`) // Reparaturen durchführen let applied = 0 for (const r of toRepair) { try { const newProps = { ...r.properties, iconId: r.newIconId, } await prisma.feature.update({ where: { id: r.featureId }, data: { properties: newProps }, }) applied++ console.log(` ✅ Feature ${r.featureId}: ${r.oldIconId} → ${r.newIconId} (${r.repairMethod})`) } catch (e) { console.error(` ❌ Feature ${r.featureId}: Update fehlgeschlagen - ${e.message}`) } } console.log(`\n ✅ ${applied}/${toRepair.length} Reparaturen erfolgreich angewendet`) } else if (IS_APPLY) { console.log('\n[5/5] Keine Reparaturen notwendig.') } else { console.log('\n[5/5] DRY-RUN abgeschlossen (keine Änderungen).') if (toRepair.length > 0) { console.log(` Zum Anwenden: node prisma/repair-features.js --apply`) } } console.log('\n✅ Feature-Repair abgeschlossen') } repair() .then(async () => { await prisma.$disconnect() }) .catch(async (e) => { console.error('❌ Fehler:', e) await prisma.$disconnect() process.exit(1) })