import Database from "better-sqlite3" import path from "path" import fs from "fs" const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), "data", "worst-scan.db") let db: Database.Database | null = null function getDb(): Database.Database { if (!db) { fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }) db = new Database(DB_PATH) db.pragma("journal_mode = WAL") db.pragma("foreign_keys = ON") migrate(db) } return db } function migrate(db: Database.Database) { db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, gid TEXT NOT NULL UNIQUE, title TEXT NOT NULL, title_jpn TEXT, artist TEXT, parody TEXT, tags TEXT, num_pages INTEGER DEFAULT 0, source TEXT, cover_url TEXT, summary TEXT, slug TEXT UNIQUE NOT NULL, published INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), published_at TEXT ); CREATE INDEX IF NOT EXISTS idx_posts_gid ON posts(gid); CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug); CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published); CREATE INDEX IF NOT EXISTS idx_posts_created ON posts(created_at DESC); `) } export interface Post { id: number gid: string title: string title_jpn: string | null artist: string | null parody: string | null tags: string[] num_pages: number source: string | null cover_url: string | null summary: string | null slug: string published: number created_at: string updated_at: string published_at: string | null } export interface PostInput { gid: string title: string title_jpn?: string artist?: string parody?: string tags?: string[] num_pages?: number source?: string cover_url?: string summary?: string slug: string published?: number } function rowToPost(row: Record): Post { return { ...row, tags: typeof row.tags === "string" ? JSON.parse(row.tags as string) : [], } as unknown as Post } export function getAllPosts(publishedOnly = false): Post[] { const d = getDb() const q = publishedOnly ? "SELECT * FROM posts WHERE published = 1 ORDER BY published_at DESC" : "SELECT * FROM posts ORDER BY created_at DESC" return (d.prepare(q).all() as Record[]).map(rowToPost) } export function getPostById(id: number): Post | null { const d = getDb() const row = d.prepare("SELECT * FROM posts WHERE id = ?").get(id) as Record | undefined return row ? rowToPost(row) : null } export function getPostByGid(gid: string): Post | null { const d = getDb() const row = d.prepare("SELECT * FROM posts WHERE gid = ?").get(gid) as Record | undefined return row ? rowToPost(row) : null } export function getPostBySlug(slug: string): Post | null { const d = getDb() const row = d.prepare("SELECT * FROM posts WHERE slug = ?").get(slug) as Record | undefined return row ? rowToPost(row) : null } export function createPost(input: PostInput): Post { const d = getDb() const published = input.published ?? 1 const publishedAt = published ? "datetime('now')" : null const stmt = d.prepare(` INSERT INTO posts (gid, title, title_jpn, artist, parody, tags, num_pages, source, cover_url, summary, slug, published, published_at) VALUES (@gid, @title, @title_jpn, @artist, @parody, @tags, @num_pages, @source, @cover_url, @summary, @slug, @published, ${publishedAt}) `) const result = stmt.run({ gid: input.gid, title: input.title, title_jpn: input.title_jpn || null, artist: input.artist || null, parody: input.parody || null, tags: JSON.stringify(input.tags || []), num_pages: input.num_pages || 0, source: input.source || null, cover_url: input.cover_url || null, summary: input.summary || null, slug: input.slug, published, }) return getPostById(result.lastInsertRowid as number)! } export function updatePost(id: number, updates: Partial): Post | null { const d = getDb() const fields: string[] = [] const values: Record = { id } for (const [k, v] of Object.entries(updates)) { if (v !== undefined) { if (k === "tags") { fields.push("tags = @tags") values.tags = JSON.stringify(v) } else { fields.push(`${k} = @${k}`) values[k] = v } } } if (fields.length === 0) return getPostById(id) fields.push("updated_at = datetime('now')") d.prepare(`UPDATE posts SET ${fields.join(", ")} WHERE id = ?`).run(id) return getPostById(id) } export function publishPost(id: number, publish: boolean): Post | null { const d = getDb() if (publish) { d.prepare("UPDATE posts SET published = 1, published_at = datetime('now'), updated_at = datetime('now') WHERE id = ?").run(id) } else { d.prepare("UPDATE posts SET published = 0, published_at = NULL, updated_at = datetime('now') WHERE id = ?").run(id) } return getPostById(id) } export function deletePost(id: number): boolean { const d = getDb() const result = d.prepare("DELETE FROM posts WHERE id = ?").run(id) return result.changes > 0 } export function getPostsByTag(tag: string, publishedOnly = true): Post[] { const d = getDb() const q = publishedOnly ? "SELECT * FROM posts WHERE published = 1 AND EXISTS (SELECT 1 FROM json_each(posts.tags) WHERE json_each.value = ?) ORDER BY published_at DESC" : "SELECT * FROM posts WHERE EXISTS (SELECT 1 FROM json_each(posts.tags) WHERE json_each.value = ?) ORDER BY created_at DESC" return (d.prepare(q).all(tag) as Record[]).map(rowToPost) } export function getPostCount(): number { const d = getDb() const row = d.prepare("SELECT COUNT(*) as count FROM posts").get() as { count: number } return row.count } export function getGidsNotInPosts(): string[] { return [] }