fix(db): comprehensive symbol recovery + safety fixes
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
This commit is contained in:
263
prisma/repair-features.js
Normal file
263
prisma/repair-features.js
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
Reference in New Issue
Block a user