import Database from 'better-sqlite3'; import { mkdirSync } from 'fs'; mkdirSync('data', { recursive: true }); const db = new Database('data/fiddles.db'); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS fiddles ( id TEXT PRIMARY KEY, title TEXT NOT NULL DEFAULT 'Untitled', html TEXT NOT NULL DEFAULT '', css TEXT NOT NULL DEFAULT '', css_type TEXT NOT NULL DEFAULT 'css', js TEXT NOT NULL DEFAULT '', js_type TEXT NOT NULL DEFAULT 'javascript', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) `); // Migration: add js_type column for existing databases try { db.exec(`ALTER TABLE fiddles ADD COLUMN js_type TEXT NOT NULL DEFAULT 'javascript'`); } catch (_) { /* column already exists */ } // Migration: add listed column try { db.exec(`ALTER TABLE fiddles ADD COLUMN listed INTEGER NOT NULL DEFAULT 1`); } catch (_) { /* column already exists */ } // Migration: add options column (JSON string for per-fiddle settings like tailwind) try { db.exec(`ALTER TABLE fiddles ADD COLUMN options TEXT NOT NULL DEFAULT '{}'`); } catch (_) { /* column already exists */ } // Tags tables db.exec(` CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE COLLATE NOCASE ) `); db.exec(` CREATE TABLE IF NOT EXISTS fiddle_tags ( fiddle_id TEXT NOT NULL REFERENCES fiddles(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (fiddle_id, tag_id) ) `); db.exec(`CREATE INDEX IF NOT EXISTS idx_fiddles_listed_updated ON fiddles(listed, updated_at DESC)`); export const stmts = { insert: db.prepare(` INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed, options) VALUES (@id, @title, @html, @css, @css_type, @js, @js_type, @listed, @options) `), get: db.prepare('SELECT * FROM fiddles WHERE id = ?'), update: db.prepare(` UPDATE fiddles SET title = @title, html = @html, css = @css, css_type = @css_type, js = @js, js_type = @js_type, listed = @listed, options = @options, updated_at = datetime('now') WHERE id = @id `), list: db.prepare('SELECT id, title, css_type, js_type, created_at, updated_at FROM fiddles ORDER BY updated_at DESC LIMIT 50'), // Tags getTagsForFiddle: db.prepare(` SELECT t.id, t.name FROM tags t JOIN fiddle_tags ft ON ft.tag_id = t.id WHERE ft.fiddle_id = ? `), insertTag: db.prepare('INSERT OR IGNORE INTO tags (name) VALUES (?)'), getTagByName: db.prepare('SELECT id, name FROM tags WHERE name = ? COLLATE NOCASE'), insertFiddleTag: db.prepare('INSERT OR IGNORE INTO fiddle_tags (fiddle_id, tag_id) VALUES (?, ?)'), deleteFiddleTags: db.prepare('DELETE FROM fiddle_tags WHERE fiddle_id = ?'), listTags: db.prepare(` SELECT t.id, t.name, COUNT(ft.fiddle_id) as count FROM tags t LEFT JOIN fiddle_tags ft ON ft.tag_id = t.id GROUP BY t.id HAVING count > 0 ORDER BY count DESC `), }; /** * Upsert tags for a fiddle. Accepts an array of tag name strings. */ export function setFiddleTags(fiddleId, tagNames) { stmts.deleteFiddleTags.run(fiddleId); for (const name of tagNames) { const trimmed = name.trim(); if (!trimmed) continue; stmts.insertTag.run(trimmed); const tag = stmts.getTagByName.get(trimmed); if (tag) stmts.insertFiddleTag.run(fiddleId, tag.id); } } export default db;