Files
web-cad/backend/src/database/SqliteAdapter.ts
T

320 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { randomUUID } from 'crypto';
import type {
DatabaseInterface, DBProject, DBDrawing, DBLayer,
DBElement, DBBlock, DBSetting, DBUser, DBSession,
DBNotification, DBProjectShare,
} 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(ownerId?: string): DBProject[] {
if (ownerId) {
return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? ORDER BY updated_at DESC').all(ownerId) as 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);
}
// ─── Sessions ───────────────────────────────────────
createSession(data: Partial<DBSession>): DBSession {
const token = data.token ?? randomUUID();
this.db.prepare(
'INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)',
).run(token, data.user_id!, data.expires_at!);
return this.getSession(token)!;
}
getSession(token: string): DBSession | null {
return (this.db.prepare('SELECT * FROM sessions WHERE token = ?').get(token) as DBSession) ?? null;
}
deleteSession(token: string): boolean {
return this.db.prepare('DELETE FROM sessions WHERE token = ?').run(token).changes > 0;
}
deleteExpiredSessions(): void {
this.db.prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now());
}
// ─── Notifications ──────────────────────────────────
listNotifications(userId: string): DBNotification[] {
return this.db.prepare('SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC').all(userId) as DBNotification[];
}
getNotification(id: string): DBNotification | null {
return this.db.prepare('SELECT * FROM notifications WHERE id = ?').get(id) as DBNotification | null;
}
createNotification(data: Partial<DBNotification>): DBNotification {
const id = data.id ?? `notif-${Date.now()}`;
this.db.prepare(
'INSERT INTO notifications (id, user_id, type, title, message, read) VALUES (?, ?, ?, ?, ?, ?)',
).run(id, data.user_id!, data.type ?? 'info', data.title!, data.message!, data.read ?? 0);
return this.db.prepare('SELECT * FROM notifications WHERE id = ?').get(id) as DBNotification;
}
markNotificationRead(id: string): boolean {
return this.db.prepare('UPDATE notifications SET read = 1 WHERE id = ?').run(id).changes > 0;
}
deleteNotification(id: string): boolean {
return this.db.prepare('DELETE FROM notifications WHERE id = ?').run(id).changes > 0;
}
// ─── Project Shares ─────────────────────────────────
listProjectShares(projectId: string): DBProjectShare[] {
return this.db.prepare('SELECT * FROM project_shares WHERE project_id = ? ORDER BY created_at DESC').all(projectId) as DBProjectShare[];
}
getProjectShare(id: string): DBProjectShare | null {
return this.db.prepare('SELECT * FROM project_shares WHERE id = ?').get(id) as DBProjectShare | null;
}
createProjectShare(data: Partial<DBProjectShare>): DBProjectShare {
const id = data.id ?? `share-${Date.now()}`;
const shareToken = data.share_token ?? randomUUID();
this.db.prepare(
'INSERT INTO project_shares (id, project_id, shared_with_email, shared_by, permission, share_token) VALUES (?, ?, ?, ?, ?, ?)',
).run(id, data.project_id!, data.shared_with_email!, data.shared_by!, data.permission ?? 'view', shareToken);
return this.db.prepare('SELECT * FROM project_shares WHERE id = ?').get(id) as DBProjectShare;
}
deleteProjectShare(id: string): boolean {
return this.db.prepare('DELETE FROM project_shares WHERE id = ?').run(id).changes > 0;
}
}