import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; const DATA_DIR = process.env.DATA_DIR ?? '/data'; const DB_DIR = path.join(DATA_DIR, 'db'); const DB_PATH = path.join(DB_DIR, 'memer.db'); fs.mkdirSync(DB_DIR, { recursive: true }); const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); // Core tables db.exec(` CREATE TABLE IF NOT EXISTS collections ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, is_default INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS memes ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, file_path TEXT NOT NULL, file_name TEXT NOT NULL, file_size INTEGER NOT NULL, mime_type TEXT NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, parent_id TEXT REFERENCES memes(id) ON DELETE CASCADE, collection_id INTEGER REFERENCES collections(id) ON DELETE SET NULL, ocr_text TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL COLLATE NOCASE ); CREATE TABLE IF NOT EXISTS meme_tags ( meme_id TEXT NOT NULL REFERENCES memes(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (meme_id, tag_id) ); CREATE INDEX IF NOT EXISTS idx_memes_parent_id ON memes(parent_id); CREATE INDEX IF NOT EXISTS idx_memes_created_at ON memes(created_at DESC); CREATE INDEX IF NOT EXISTS idx_meme_tags_meme_id ON meme_tags(meme_id); CREATE INDEX IF NOT EXISTS idx_meme_tags_tag_id ON meme_tags(tag_id); `); // Migrations — run after CREATE TABLE IF NOT EXISTS so they only apply to existing DBs const memesCols = db.prepare('PRAGMA table_info(memes)').all() as { name: string }[]; if (!memesCols.find((c) => c.name === 'collection_id')) { db.exec('ALTER TABLE memes ADD COLUMN collection_id INTEGER REFERENCES collections(id) ON DELETE SET NULL'); } if (!memesCols.find((c) => c.name === 'ocr_text')) { db.exec('ALTER TABLE memes ADD COLUMN ocr_text TEXT'); } if (!memesCols.find((c) => c.name === 'share_count')) { db.exec('ALTER TABLE memes ADD COLUMN share_count INTEGER NOT NULL DEFAULT 0'); } // Indexes that depend on migrated columns — created after columns are guaranteed to exist db.exec(` CREATE INDEX IF NOT EXISTS idx_memes_collection_id ON memes(collection_id); CREATE INDEX IF NOT EXISTS idx_memes_ocr ON memes(ocr_text) WHERE ocr_text IS NOT NULL; `); // Seed the default UNSORTED collection const defaultCollection = db .prepare('SELECT id FROM collections WHERE is_default = 1') .get() as { id: number } | undefined; if (!defaultCollection) { db.prepare("INSERT INTO collections (name, is_default) VALUES ('Unsorted', 1)").run(); } const unsorted = db .prepare('SELECT id FROM collections WHERE is_default = 1') .get() as { id: number }; // Assign any existing memes with no collection to UNSORTED db.prepare('UPDATE memes SET collection_id = ? WHERE collection_id IS NULL').run(unsorted.id); export const UNSORTED_ID = unsorted.id; export default db;