add sqlite cache schema, thread/message storage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
backend/src/db/index.test.ts
Normal file
51
backend/src/db/index.test.ts
Normal 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
90
backend/src/db/index.ts
Normal 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
32
backend/src/db/schema.ts
Normal 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);
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user