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