247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
/**
|
||
* 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<void> {
|
||
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>): 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>): 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>): 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>): 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>): 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>): 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>): 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>): 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>): 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>): 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>): 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>): 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);
|
||
}
|
||
}
|