From 24b37821dcad34aeb63a9c0ec228a85d3a5241fe Mon Sep 17 00:00:00 2001 From: Stefan ATZLESBERGER Date: Thu, 16 Apr 2026 16:44:06 +0200 Subject: [PATCH] snapshot --- src/features/notes/NotesApp.tsx | 73 +++++++++++++++++++++++++------- src/server/db.ts | 50 +--------------------- src/server/db/migrate.ts | 75 +++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/server/db/migrate.ts diff --git a/src/features/notes/NotesApp.tsx b/src/features/notes/NotesApp.tsx index 3f24a21..2b3db8b 100644 --- a/src/features/notes/NotesApp.tsx +++ b/src/features/notes/NotesApp.tsx @@ -85,6 +85,16 @@ export function NotesApp() { setSnippets(prev => prev.filter(x => x.id !== id)); }, []); + const { snippetColumnLeft, snippetColumnRight } = useMemo(() => { + const left: Snippet[] = []; + const right: Snippet[] = []; + snippets.forEach((s, i) => { + if (i % 2 === 0) left.push(s); + else right.push(s); + }); + return { snippetColumnLeft: left, snippetColumnRight: right }; + }, [snippets]); + const createNew = async () => { setError(null); try { @@ -105,7 +115,7 @@ export function NotesApp() { }; return ( -
+
{error}

: null} {loading ?

Lade Snippets …

: null} -
- {!loading && snippets.length === 0 ? ( -

Keine Snippets. Mit „Neu“ starten.

- ) : null} - {snippets.map(s => ( - - ))} -
+ {!loading && snippets.length === 0 ? ( +

Keine Snippets. Mit „Neu“ starten.

+ ) : null} + + {snippets.length > 0 ? ( + <> +
+ {snippets.map(s => ( + + ))} +
+
+
+ {snippetColumnLeft.map(s => ( + + ))} +
+
+ {snippetColumnRight.map(s => ( + + ))} +
+
+ + ) : null}
); } diff --git a/src/server/db.ts b/src/server/db.ts index 31a7a3f..085e841 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -2,55 +2,9 @@ import { Database } from "bun:sqlite"; import { mkdirSync } from "fs"; import path from "path"; -let dbInstance: Database | null = null; +import { migrate } from "./db/migrate"; -function migrate(database: Database) { - database.run(` - CREATE TABLE IF NOT EXISTS snippets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT, - body TEXT NOT NULL DEFAULT '', - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - `); - database.run(` - CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE COLLATE NOCASE - ); - `); - database.run(` - CREATE TABLE IF NOT EXISTS snippet_tags ( - snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, - PRIMARY KEY (snippet_id, tag_id) - ); - `); - database.run(` - CREATE TABLE IF NOT EXISTS tag_tree_nodes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE COLLATE NOCASE, - parent_id INTEGER REFERENCES tag_tree_nodes(id) ON DELETE CASCADE, - color TEXT, - label_prefix TEXT - ); - `); - try { - database.run(`ALTER TABLE tag_tree_nodes ADD COLUMN label_prefix TEXT`); - } catch { - /* Spalte existiert bereits */ - } - database.run(`CREATE INDEX IF NOT EXISTS idx_tag_tree_parent ON tag_tree_nodes(parent_id);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_updated ON snippets(updated_at);`); - try { - database.run(`ALTER TABLE snippets ADD COLUMN deadline_at TEXT`); - } catch { - /* Spalte existiert bereits */ - } - database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_deadline ON snippets(deadline_at);`); -} +let dbInstance: Database | null = null; export function getDb(): Database { if (dbInstance) return dbInstance; diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts new file mode 100644 index 0000000..65f6638 --- /dev/null +++ b/src/server/db/migrate.ts @@ -0,0 +1,75 @@ +import type { Database } from "bun:sqlite"; + +/** + * Einmaliges Kernschema — so, als würde eine leere DB zum ersten Mal angelegt. + * `CREATE TABLE IF NOT EXISTS` ändert bestehende Tabellen nicht; fehlende Spalten + * werden danach idempotent per `ensureColumn` nachgezogen (bestehende Zeilen bleiben). + */ + +function assertSqlIdent(name: string, label: string): void { + if (!/^[a-z][a-z0-9_]*$/i.test(name)) { + throw new Error(`${label} ist kein gültiger SQL-Identifier: ${name}`); + } +} + +function tableHasColumn(database: Database, table: string, column: string): boolean { + assertSqlIdent(table, "Tabellenname"); + assertSqlIdent(column, "Spaltenname"); + const cols = database.query(`PRAGMA table_info(${table})`).all() as { name: string }[]; + return cols.some(c => c.name === column); +} + +/** Fügt eine Spalte nur hinzu, wenn sie noch fehlt (keine Exceptions als Steuerung). */ +function ensureColumn(database: Database, table: string, column: string, sqlType: string): void { + if (tableHasColumn(database, table, column)) return; + assertSqlIdent(table, "Tabellenname"); + assertSqlIdent(column, "Spaltenname"); + database.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${sqlType}`); +} + +export function migrate(database: Database): void { + database.run(` + CREATE TABLE IF NOT EXISTS snippets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + body TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + deadline_at TEXT + ); + `); + + database.run(` + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE COLLATE NOCASE + ); + `); + + database.run(` + CREATE TABLE IF NOT EXISTS snippet_tags ( + snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (snippet_id, tag_id) + ); + `); + + database.run(` + CREATE TABLE IF NOT EXISTS tag_tree_nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE COLLATE NOCASE, + parent_id INTEGER REFERENCES tag_tree_nodes(id) ON DELETE CASCADE, + color TEXT, + label_prefix TEXT + ); + `); + + /* Bestehende Installationen: Tabellen schon da, einzelne Spalten fehlen → ALTER nur bei Bedarf */ + // ensureColumn(database, "snippets", "deadline_at", "TEXT"); + // ensureColumn(database, "tag_tree_nodes", "label_prefix", "TEXT"); + + database.run(`CREATE INDEX IF NOT EXISTS idx_tag_tree_parent ON tag_tree_nodes(parent_id);`); + database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_updated ON snippets(updated_at);`); + database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_deadline ON snippets(deadline_at);`); +}