/** * SQLite Adapter – implements DatabaseInterface with better-sqlite3 */ import Database from 'better-sqlite3'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import type { DatabaseInterface, DBProject, DBDrawing, DBLayer, DBElement, DBBlock, DBSetting, DBUser, } from './DatabaseInterface.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); export class SqliteAdapter implements DatabaseInterface { private db: Database.Database; constructor(dbPath: string = ':memory:') { this.db = new Database(dbPath); this.db.pragma('journal_mode = WAL'); this.db.pragma('foreign_keys = ON'); } async init(): Promise { const schemaPath = join(__dirname, 'schema.sql'); const schema = readFileSync(schemaPath, 'utf-8'); this.db.exec(schema); } close(): void { this.db.close(); } // ─── Users ────────────────────────────────────────── getUser(id: string): DBUser | null { return (this.db.prepare('SELECT * FROM users WHERE id = ?').get(id) as DBUser) ?? null; } getUserByEmail(email: string): DBUser | null { return (this.db.prepare('SELECT * FROM users WHERE email = ?').get(email) as DBUser) ?? null; } createUser(data: Partial): DBUser { const id = data.id ?? `user-${Date.now()}`; this.db.prepare( 'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)', ).run(id, data.email!, data.password_hash!, data.name!, data.role ?? 'planer'); return this.getUser(id)!; } updateUser(id: string, data: Partial): DBUser | null { const sets: string[] = []; const vals: any[] = []; if (data.email !== undefined) { sets.push('email = ?'); vals.push(data.email); } if (data.password_hash !== undefined) { sets.push('password_hash = ?'); vals.push(data.password_hash); } if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); } if (data.role !== undefined) { sets.push('role = ?'); vals.push(data.role); } sets.push("updated_at = datetime('now')"); if (sets.length > 1) { this.db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.getUser(id); } deleteUser(id: string): boolean { return this.db.prepare('DELETE FROM users WHERE id = ?').run(id).changes > 0; } listUsers(): DBUser[] { return this.db.prepare('SELECT * FROM users ORDER BY created_at DESC').all() as DBUser[]; } // ─── Projects ────────────────────────────────────── listProjects(): DBProject[] { return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as DBProject[]; } getProject(id: string): DBProject | null { return (this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as DBProject) ?? null; } createProject(data: Partial): DBProject { const id = data.id ?? `proj-${Date.now()}`; this.db.prepare( 'INSERT INTO projects (id, name, description, owner_id) VALUES (?, ?, ?, ?)', ).run(id, data.name ?? 'Unbenannt', data.description ?? null, data.owner_id ?? 'user-default'); return this.getProject(id)!; } updateProject(id: string, data: Partial): DBProject | null { const sets: string[] = []; const vals: any[] = []; if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); } if (data.description !== undefined) { sets.push('description = ?'); vals.push(data.description); } sets.push("updated_at = datetime('now')"); if (sets.length > 1) { this.db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.getProject(id); } deleteProject(id: string): boolean { return this.db.prepare('DELETE FROM projects WHERE id = ?').run(id).changes > 0; } // ─── Drawings ────────────────────────────────────── listDrawings(projectId: string): DBDrawing[] { return this.db.prepare('SELECT * FROM drawings WHERE project_id = ? ORDER BY updated_at DESC').all(projectId) as DBDrawing[]; } getDrawing(id: string): DBDrawing | null { return (this.db.prepare('SELECT * FROM drawings WHERE id = ?').get(id) as DBDrawing) ?? null; } createDrawing(data: Partial): DBDrawing { const id = data.id ?? `draw-${Date.now()}`; this.db.prepare( 'INSERT INTO drawings (id, project_id, name, data_json) VALUES (?, ?, ?, ?)', ).run(id, data.project_id!, data.name ?? 'Unbenannt', data.data_json ?? '{}'); return this.getDrawing(id)!; } updateDrawing(id: string, data: Partial): DBDrawing | null { const sets: string[] = []; const vals: any[] = []; if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); } if (data.data_json !== undefined) { sets.push('data_json = ?'); vals.push(data.data_json); } sets.push("updated_at = datetime('now')"); if (sets.length > 1) { this.db.prepare(`UPDATE drawings SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.getDrawing(id); } deleteDrawing(id: string): boolean { return this.db.prepare('DELETE FROM drawings WHERE id = ?').run(id).changes > 0; } // ─── Layers ──────────────────────────────────────── listLayers(drawingId: string): DBLayer[] { return this.db.prepare('SELECT * FROM layers WHERE drawing_id = ? ORDER BY sort_order').all(drawingId) as DBLayer[]; } createLayer(data: Partial): DBLayer { const id = data.id ?? `layer-${Date.now()}`; this.db.prepare( 'INSERT INTO layers (id, drawing_id, name, visible, locked, color, line_type, transparency, sort_order, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ).run(id, data.drawing_id!, data.name ?? 'Layer', data.visible ?? 1, data.locked ?? 0, data.color ?? '#ffffff', data.line_type ?? 'solid', data.transparency ?? 0, data.sort_order ?? 0, data.parent_id ?? null); return this.db.prepare('SELECT * FROM layers WHERE id = ?').get(id) as DBLayer; } updateLayer(id: string, data: Partial): DBLayer | null { const sets: string[] = []; const vals: any[] = []; for (const [k, v] of Object.entries(data)) { if (['name','visible','locked','color','line_type','transparency','sort_order','parent_id'].includes(k)) { sets.push(`${k} = ?`); vals.push(v); } } if (sets.length > 0) { this.db.prepare(`UPDATE layers SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.db.prepare('SELECT * FROM layers WHERE id = ?').get(id) as DBLayer ?? null; } deleteLayer(id: string): boolean { return this.db.prepare('DELETE FROM layers WHERE id = ?').run(id).changes > 0; } // ─── Elements ────────────────────────────────────── listElements(drawingId: string): DBElement[] { return this.db.prepare('SELECT * FROM elements WHERE drawing_id = ?').all(drawingId) as DBElement[]; } createElement(data: Partial): DBElement { const id = data.id ?? `elem-${Date.now()}`; this.db.prepare( 'INSERT INTO elements (id, drawing_id, layer_id, type, x, y, width, height, properties_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', ).run(id, data.drawing_id!, data.layer_id!, data.type!, data.x ?? 0, data.y ?? 0, data.width ?? 0, data.height ?? 0, data.properties_json ?? '{}'); return this.db.prepare('SELECT * FROM elements WHERE id = ?').get(id) as DBElement; } updateElement(id: string, data: Partial): DBElement | null { const sets: string[] = []; const vals: any[] = []; for (const [k, v] of Object.entries(data)) { if (['layer_id','type','x','y','width','height','properties_json'].includes(k)) { sets.push(`${k} = ?`); vals.push(v); } } if (sets.length > 0) { this.db.prepare(`UPDATE elements SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.db.prepare('SELECT * FROM elements WHERE id = ?').get(id) as DBElement ?? null; } deleteElement(id: string): boolean { return this.db.prepare('DELETE FROM elements WHERE id = ?').run(id).changes > 0; } // ─── Blocks ──────────────────────────────────────── listBlocks(drawingId: string): DBBlock[] { return this.db.prepare('SELECT * FROM blocks WHERE drawing_id = ?').all(drawingId) as DBBlock[]; } createBlock(data: Partial): DBBlock { const id = data.id ?? `block-${Date.now()}`; this.db.prepare( 'INSERT INTO blocks (id, drawing_id, name, description, category, elements_json, thumbnail) VALUES (?, ?, ?, ?, ?, ?, ?)', ).run(id, data.drawing_id!, data.name ?? 'Block', data.description ?? null, data.category ?? 'Allgemein', data.elements_json ?? '[]', data.thumbnail ?? null); return this.db.prepare('SELECT * FROM blocks WHERE id = ?').get(id) as DBBlock; } updateBlock(id: string, data: Partial): DBBlock | null { const sets: string[] = []; const vals: any[] = []; for (const [k, v] of Object.entries(data)) { if (['name','description','category','elements_json','thumbnail'].includes(k)) { sets.push(`${k} = ?`); vals.push(v); } } if (sets.length > 0) { this.db.prepare(`UPDATE blocks SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id); } return this.db.prepare('SELECT * FROM blocks WHERE id = ?').get(id) as DBBlock ?? null; } deleteBlock(id: string): boolean { return this.db.prepare('DELETE FROM blocks WHERE id = ?').run(id).changes > 0; } // ─── Settings ────────────────────────────────────── getSetting(key: string): DBSetting | null { return (this.db.prepare('SELECT * FROM settings WHERE key = ?').get(key) as DBSetting) ?? null; } setSetting(key: string, value: string): void { this.db.prepare( "INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')", ).run(key, value); } }