import { DatabaseSync } from 'node:sqlite' import { mkdirSync } from 'node:fs' import { dirname, join, resolve, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { TABLE_DEFINITIONS } from './db/tableRegistry' const __dirname = dirname(fileURLToPath(import.meta.url)) const projectRoot = resolve(join(__dirname, '..')) /** V2-Defaults weichen bewusst von Produktiv (assignment-monitor.sqlite3) ab. */ const sqliteFilename = process.env.SQLITE_FILENAME?.trim() || 'assignment-monitor-v2-dev.sqlite3' if (sqliteFilename.includes('/') || sqliteFilename.includes('\\') || sqliteFilename.includes('..')) { throw new Error( `[db] SAFETY: SQLITE_FILENAME darf nur ein Dateiname sein (kein Pfad): "${sqliteFilename}"`, ) } const dbPath = resolve(join(__dirname, 'data', sqliteFilename)) /** * Harte Absicherung: DB darf nur unter diesem V2-Projekt liegen und niemals unter * dem Produktiv-Ordner c:\dev\AssigmentMonitor (ohne V2-Suffix). */ function assertDevDbPath(resolvedDbPath: string): void { const dbNorm = resolvedDbPath.toLowerCase() const rootNorm = projectRoot.toLowerCase() const rootPrefix = rootNorm.endsWith(sep) ? rootNorm : rootNorm + sep if (!dbNorm.startsWith(rootPrefix) && dbNorm !== rootNorm) { throw new Error( `[db] SAFETY: DB-Pfad liegt außerhalb von AssignmentMonitorV2.\n` + ` DB: ${resolvedDbPath}\n` + ` Root: ${projectRoot}`, ) } // Produktiv-Sibling: ...\AssigmentMonitor\server\data\... — V2-Pfad enthält "assigmentmonitorv2" const looksLikeProdData = /[/\\]assigmentmonitor[/\\]server[/\\]data[/\\]/.test(dbNorm) && !/[/\\]assigmentmonitorv2[/\\]/.test(dbNorm) if (looksLikeProdData) { throw new Error( `[db] SAFETY: Versuch, die Produktiv-Datenbank zu öffnen wurde blockiert:\n ${resolvedDbPath}`, ) } } assertDevDbPath(dbPath) mkdirSync(dirname(dbPath), { recursive: true }) export const db = new DatabaseSync(dbPath) /** * `CREATE TABLE IF NOT EXISTS` legt eine fehlende Tabelle vollständig an, migriert aber niemals eine * bereits bestehende — neue Spalten, die tableRegistry.ts für eine vorhandene Tabelle ergänzt (z.B. * suggestedAchievements auf feedbackDimensionTexts, 2026-07-07), fehlen dort sonst dauerhaft und * führen erst zur Laufzeit zu "no column named ..."-Fehlern. Deshalb zusätzlich per PRAGMA table_info * abgleichen und fehlende Spalten per ALTER TABLE nachziehen. */ for (const table of TABLE_DEFINITIONS) { const columnsSql = table.columns.map(c => `"${c.name}" ${c.sqlType}`).join(', ') db.exec(`CREATE TABLE IF NOT EXISTS "${table.name}" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${columnsSql})`) const existingColumns = new Set( (db.prepare(`PRAGMA table_info("${table.name}")`).all() as { name: string }[]).map(c => c.name), ) for (const col of table.columns) { if (!existingColumns.has(col.name)) { db.exec(`ALTER TABLE "${table.name}" ADD COLUMN "${col.name}" ${col.sqlType}`) } } } console.log(`[db] DEV-SQLite bereit unter ${dbPath}`)