add sqlite cache schema, thread/message storage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:31:04 +01:00
parent 64e35177f3
commit 8b1cb7e07f
3 changed files with 173 additions and 0 deletions

View File

@@ -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 <alice@example.com>",
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.");
});
});

90
backend/src/db/index.ts Normal file
View File

@@ -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[];
}

32
backend/src/db/schema.ts Normal file
View File

@@ -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);
`;