diff --git a/src/lib/db.ts b/src/lib/db.ts index 637d141..7718da6 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -6,6 +6,17 @@ export type Text = { created_at: number; updated_at: number; last_saved_version_id: string | null; + folder_id: string | null; + sort_order: number | null; +}; + +export type Folder = { + id: string; + name: string; + parent_id: string | null; + created_at: number; + updated_at: number; + sort_order: number | null; }; export type TextVersion = { @@ -26,6 +37,15 @@ export type TextDraft = { const MIGRATIONS = [ "PRAGMA foreign_keys = ON;", + `CREATE TABLE IF NOT EXISTS folders( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent_id TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + sort_order INTEGER, + FOREIGN KEY(parent_id) REFERENCES folders(id) + );`, `CREATE TABLE IF NOT EXISTS prompts( id TEXT PRIMARY KEY, title TEXT NOT NULL, @@ -50,7 +70,10 @@ const MIGRATIONS = [ FOREIGN KEY(prompt_id) REFERENCES prompts(id) ON DELETE CASCADE );`, "CREATE INDEX IF NOT EXISTS idx_prompt_versions_prompt_time ON prompt_versions(prompt_id, created_at DESC);", - "CREATE INDEX IF NOT EXISTS idx_prompts_updated ON prompts(updated_at DESC);" + "CREATE INDEX IF NOT EXISTS idx_prompts_updated ON prompts(updated_at DESC);", + "CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id);", + "CREATE INDEX IF NOT EXISTS idx_folders_updated ON folders(updated_at DESC);", + "CREATE INDEX IF NOT EXISTS idx_prompts_folder ON prompts(folder_id);" ]; let dbPromise: Promise | null = null; @@ -59,6 +82,21 @@ async function migrate(db: Database) { for (const statement of MIGRATIONS) { await db.execute(statement); } + await ensureColumn(db, "prompts", "folder_id", "TEXT REFERENCES folders(id) ON DELETE SET NULL"); + await ensureColumn(db, "prompts", "sort_order", "INTEGER"); +} + +async function ensureColumn( + db: Database, + table: string, + column: string, + definition: string +) { + const columns = await db.select<{ name: string }[]>( + `PRAGMA table_info(${table})` + ); + if (columns.some((col) => col.name === column)) return; + await db.execute(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`); } export async function getDb(): Promise { @@ -79,7 +117,11 @@ export async function initDb() { export async function listTexts(): Promise { const db = await getDb(); const rows = await db.select( - "SELECT * FROM prompts ORDER BY updated_at DESC" + `SELECT * FROM prompts + ORDER BY + CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END, + sort_order ASC, + updated_at DESC` ); return rows; } @@ -97,7 +139,10 @@ export async function searchTexts(term: string): Promise { WHERE v.prompt_id = p.id AND LOWER(v.body) LIKE $1 ) - ORDER BY p.updated_at DESC`, + ORDER BY + CASE WHEN p.sort_order IS NULL THEN 1 ELSE 0 END, + p.sort_order ASC, + p.updated_at DESC`, [normalized] ); return rows; @@ -144,7 +189,9 @@ export async function getDraft(promptId: string): Promise { export async function createText( title: string, - body: string + body: string, + folderId: string | null = null, + sortOrder: number | null = null ): Promise<{ textId: string; versionId: string; createdAt: number }> { const db = await getDb(); const now = Date.now(); @@ -152,8 +199,8 @@ export async function createText( const versionId = crypto.randomUUID(); await db.execute( - "INSERT INTO prompts(id, title, created_at, updated_at, last_saved_version_id) VALUES($1, $2, $3, $4, $5)", - [textId, title, now, now, versionId] + "INSERT INTO prompts(id, title, created_at, updated_at, last_saved_version_id, folder_id, sort_order) VALUES($1, $2, $3, $4, $5, $6, $7)", + [textId, title, now, now, versionId, folderId, sortOrder] ); await db.execute( @@ -166,6 +213,118 @@ export async function createText( return { textId, versionId, createdAt: now }; } +export async function listFolders(): Promise { + const db = await getDb(); + return db.select( + `SELECT * FROM folders + ORDER BY + CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END, + sort_order ASC, + updated_at DESC` + ); +} + +export async function createFolder( + name: string, + parentId: string | null = null, + sortOrder: number | null = null +): Promise<{ folderId: string; createdAt: number }> { + const db = await getDb(); + const now = Date.now(); + const folderId = crypto.randomUUID(); + await db.execute( + "INSERT INTO folders(id, name, parent_id, created_at, updated_at, sort_order) VALUES($1, $2, $3, $4, $5, $6)", + [folderId, name, parentId, now, now, sortOrder] + ); + return { folderId, createdAt: now }; +} + +export async function updateFolderName(folderId: string, name: string) { + const db = await getDb(); + const now = Date.now(); + await db.execute( + "UPDATE folders SET name = $1, updated_at = $2 WHERE id = $3", + [name, now, folderId] + ); +} + +export async function moveFolder( + folderId: string, + parentId: string | null, + sortOrder: number | null = null +) { + const db = await getDb(); + const now = Date.now(); + await db.execute( + "UPDATE folders SET parent_id = $1, sort_order = $2, updated_at = $3 WHERE id = $4", + [parentId, sortOrder, now, folderId] + ); +} + +export async function setFolderOrder(folderIds: string[]) { + const db = await getDb(); + await db.execute("BEGIN TRANSACTION"); + try { + for (let i = 0; i < folderIds.length; i += 1) { + await db.execute("UPDATE folders SET sort_order = $1 WHERE id = $2", [ + i, + folderIds[i] + ]); + } + await db.execute("COMMIT"); + } catch (error) { + await db.execute("ROLLBACK"); + throw error; + } +} + +export async function deleteFolder(folderId: string) { + const db = await getDb(); + const rows = await db.select<{ parent_id: string | null }[]>( + "SELECT parent_id FROM folders WHERE id = $1 LIMIT 1", + [folderId] + ); + const parentId = rows[0]?.parent_id ?? null; + + await db.execute("UPDATE folders SET parent_id = $1 WHERE parent_id = $2", [ + parentId, + folderId + ]); + await db.execute("UPDATE prompts SET folder_id = $1 WHERE folder_id = $2", [ + parentId, + folderId + ]); + await db.execute("DELETE FROM folders WHERE id = $1", [folderId]); +} + +export async function moveTextToFolder( + textId: string, + folderId: string | null, + sortOrder: number | null = null +) { + const db = await getDb(); + await db.execute( + "UPDATE prompts SET folder_id = $1, sort_order = $2 WHERE id = $3", + [folderId, sortOrder, textId] + ); +} + +export async function setTextOrder(textIds: string[]) { + const db = await getDb(); + await db.execute("BEGIN TRANSACTION"); + try { + for (let i = 0; i < textIds.length; i += 1) { + await db.execute("UPDATE prompts SET sort_order = $1 WHERE id = $2", [ + i, + textIds[i] + ]); + } + await db.execute("COMMIT"); + } catch (error) { + await db.execute("ROLLBACK"); + throw error; + } +} export async function saveManualVersion( promptId: string, title: string,