From 8b1cb7e07f3ad341cefcac4f743ac57987616d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Tue, 10 Mar 2026 11:31:04 +0100 Subject: [PATCH] add sqlite cache schema, thread/message storage Co-Authored-By: Claude Opus 4.6 --- backend/src/db/index.test.ts | 51 ++++++++++++++++++++ backend/src/db/index.ts | 90 ++++++++++++++++++++++++++++++++++++ backend/src/db/schema.ts | 32 +++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 backend/src/db/index.test.ts create mode 100644 backend/src/db/index.ts create mode 100644 backend/src/db/schema.ts diff --git a/backend/src/db/index.test.ts b/backend/src/db/index.test.ts new file mode 100644 index 0000000..54670fb --- /dev/null +++ b/backend/src/db/index.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test, beforeEach } from "bun:test"; +import { createDatabase, insertThread, insertMessage, getThreads, getMessagesForThread } from "./index"; +import type { Database } from "bun:sqlite"; + +let db: Database; + +beforeEach(() => { + db = createDatabase(":memory:"); +}); + +describe("threads", () => { + test("insert and retrieve threads", () => { + insertThread(db, { + threadId: "t001", + subject: "Q1 Planning", + authors: "Alice, Bob", + totalMessages: 3, + tags: "inbox,unread", + timestamp: 1709884532, + accountId: "personal", + }); + + const threads = getThreads(db, "personal"); + expect(threads).toHaveLength(1); + expect(threads[0].threadId).toBe("t001"); + expect(threads[0].subject).toBe("Q1 Planning"); + }); +}); + +describe("messages", () => { + test("insert and retrieve messages for a thread", () => { + insertMessage(db, { + messageId: "msg001@example.com", + threadId: "t001", + fromHeader: "Alice ", + toHeader: "user@example.com", + subject: "Q1 Planning", + date: "2024-03-08T10:15:32+01:00", + inReplyTo: "", + body: "Hey, let's plan Q1.", + tags: "inbox,unread", + timestamp: 1709884532, + accountId: "personal", + }); + + const messages = getMessagesForThread(db, "t001"); + expect(messages).toHaveLength(1); + expect(messages[0].messageId).toBe("msg001@example.com"); + expect(messages[0].body).toBe("Hey, let's plan Q1."); + }); +}); diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts new file mode 100644 index 0000000..9b02455 --- /dev/null +++ b/backend/src/db/index.ts @@ -0,0 +1,90 @@ +import { Database } from "bun:sqlite"; +import { SCHEMA } from "./schema"; + +export function createDatabase(path: string): Database { + const db = new Database(path, { create: true }); + db.run("PRAGMA journal_mode = WAL"); + db.exec(SCHEMA); + return db; +} + +export interface ThreadRow { + threadId: string; + subject: string; + authors: string; + totalMessages: number; + tags: string; + timestamp: number; + accountId: string; +} + +export interface MessageRow { + messageId: string; + threadId: string; + fromHeader: string; + toHeader: string; + subject: string; + date: string; + inReplyTo: string; + body: string; + tags: string; + timestamp: number; + accountId: string; +} + +export function insertThread(db: Database, thread: ThreadRow): void { + db.prepare(` + INSERT OR REPLACE INTO threads + (thread_id, account_id, subject, authors, total_messages, tags, timestamp) + VALUES ($threadId, $accountId, $subject, $authors, $totalMessages, $tags, $timestamp) + `).run({ + $threadId: thread.threadId, + $accountId: thread.accountId, + $subject: thread.subject, + $authors: thread.authors, + $totalMessages: thread.totalMessages, + $tags: thread.tags, + $timestamp: thread.timestamp, + }); +} + +export function insertMessage(db: Database, msg: MessageRow): void { + db.prepare(` + INSERT OR REPLACE INTO messages + (message_id, thread_id, account_id, from_header, to_header, subject, date, in_reply_to, body, tags, timestamp) + VALUES ($messageId, $threadId, $accountId, $fromHeader, $toHeader, $subject, $date, $inReplyTo, $body, $tags, $timestamp) + `).run({ + $messageId: msg.messageId, + $threadId: msg.threadId, + $accountId: msg.accountId, + $fromHeader: msg.fromHeader, + $toHeader: msg.toHeader, + $subject: msg.subject, + $date: msg.date, + $inReplyTo: msg.inReplyTo, + $body: msg.body, + $tags: msg.tags, + $timestamp: msg.timestamp, + }); +} + +export function getThreads(db: Database, accountId: string): ThreadRow[] { + return db.prepare(` + SELECT thread_id as threadId, subject, authors, total_messages as totalMessages, + tags, timestamp, account_id as accountId + FROM threads + WHERE account_id = $accountId + ORDER BY timestamp DESC + `).all({ $accountId: accountId }) as ThreadRow[]; +} + +export function getMessagesForThread(db: Database, threadId: string): MessageRow[] { + return db.prepare(` + SELECT message_id as messageId, thread_id as threadId, from_header as fromHeader, + to_header as toHeader, subject, date, in_reply_to as inReplyTo, body, tags, + timestamp, account_id as accountId + FROM messages + WHERE thread_id = $threadId + ORDER BY timestamp ASC + `).all({ $threadId: threadId }) as MessageRow[]; +} diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts new file mode 100644 index 0000000..14589a0 --- /dev/null +++ b/backend/src/db/schema.ts @@ -0,0 +1,32 @@ +export const SCHEMA = ` + CREATE TABLE IF NOT EXISTS threads ( + thread_id TEXT NOT NULL, + account_id TEXT NOT NULL, + subject TEXT NOT NULL, + authors TEXT NOT NULL, + total_messages INTEGER NOT NULL, + tags TEXT NOT NULL DEFAULT '', + timestamp INTEGER NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (thread_id, account_id) + ); + + CREATE TABLE IF NOT EXISTS messages ( + message_id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + account_id TEXT NOT NULL, + from_header TEXT NOT NULL, + to_header TEXT NOT NULL, + subject TEXT NOT NULL, + date TEXT NOT NULL, + in_reply_to TEXT NOT NULL DEFAULT '', + body TEXT NOT NULL DEFAULT '', + tags TEXT NOT NULL DEFAULT '', + timestamp INTEGER NOT NULL, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); + CREATE INDEX IF NOT EXISTS idx_threads_account ON threads(account_id); + CREATE INDEX IF NOT EXISTS idx_threads_timestamp ON threads(timestamp DESC); +`;