Files
Lageplan/prisma/repair-features.js
Pepe Ziberi a53f77c97c
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
fix(db): comprehensive symbol recovery + safety fixes
2026-05-20 15:05:44 +02:00

264 lines
8.9 KiB
JavaScript

/**
* 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)
})