All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
264 lines
8.9 KiB
JavaScript
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)
|
|
})
|