import postgres from "postgres"; import { randomUUID } from "node:crypto"; type Sql = ReturnType; export interface ListRow { id: string; name: string; } export interface TaskRow { id: string; list_id: string; title: string; description: string | null; source: string; consumed: boolean; created_at: Date; updated_at: Date; } export async function getLists(sql: Sql): Promise { return sql`select id, name from lists order by name`; } /** Full-replace catalog: upsert all supplied, delete any list not present (cascades tasks). */ export async function replaceLists( sql: Sql, lists: { id: string; name: string }[], ): Promise { await sql.begin(async (tx) => { for (const l of lists) { await tx` insert into lists (id, name, updated_at) values (${l.id}, ${l.name}, now()) on conflict (id) do update set name = excluded.name, updated_at = now()`; } if (lists.length) { const ids = lists.map((l) => l.id); await tx`delete from lists where id <> all(${ids})`; } else { await tx`delete from lists`; } }); } export async function listExists(sql: Sql, id: string): Promise { const rows = await sql`select 1 from lists where id = ${id}`; return rows.length > 0; } export async function getTasksForList(sql: Sql, listId: string): Promise { return sql`select * from tasks where list_id = ${listId} order by created_at`; } export async function createWebTask( sql: Sql, t: { listId: string; title: string; description: string | null }, ): Promise { const id = randomUUID(); const [row] = await sql` insert into tasks (id, list_id, title, description, source, consumed) values (${id}, ${t.listId}, ${t.title}, ${t.description}, 'web', false) returning *`; return row; } /** Idempotent upsert of a desktop Idle task by id. Returns whether a row was inserted. */ export async function upsertDesktopTask( sql: Sql, id: string, t: { listId: string; title: string; description?: string | null }, ): Promise<{ created: boolean }> { const [row] = await sql<{ created: boolean }[]>` insert into tasks (id, list_id, title, description, source) values (${id}, ${t.listId}, ${t.title}, ${t.description ?? null}, 'desktop') on conflict (id) do update set list_id = excluded.list_id, title = excluded.title, description = excluded.description, updated_at = now() returning (xmax = 0) as created`; return { created: row.created }; } /** Web-created tasks the desktop has not yet imported. */ export async function getUnconsumed(sql: Sql): Promise { return sql` select * from tasks where source = 'web' and consumed = false order by created_at`; } export async function consume(sql: Sql, id: string): Promise { const res = await sql`update tasks set consumed = true, updated_at = now() where id = ${id}`; return res.count > 0; } export async function deleteTask(sql: Sql, id: string): Promise { await sql`delete from tasks where id = ${id}`; }