Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import type { Config } from '../config/index.js';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
export function createDatabase(config: Config): Database.Database {
|
|
const dbPath = path.resolve(process.cwd(), config.database.path);
|
|
const dbDir = path.dirname(dbPath);
|
|
|
|
// Ensure directory exists
|
|
if (!fs.existsSync(dbDir)) {
|
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
}
|
|
|
|
const db = new Database(dbPath);
|
|
|
|
// Enable WAL mode for better concurrency
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
|
|
return db;
|
|
}
|
|
|
|
export function runMigrations(db: Database.Database): void {
|
|
// Create tables if they don't exist
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS processed_posts (
|
|
id TEXT PRIMARY KEY,
|
|
subreddit TEXT NOT NULL,
|
|
author TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
image_url TEXT,
|
|
image_analysis TEXT,
|
|
generated_message TEXT,
|
|
processed_at INTEGER NOT NULL,
|
|
message_sent INTEGER DEFAULT 0,
|
|
error TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sent_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
post_id TEXT NOT NULL,
|
|
recipient TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
sent_at INTEGER NOT NULL,
|
|
FOREIGN KEY (post_id) REFERENCES processed_posts(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS successful_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sent_message_id INTEGER,
|
|
original_message TEXT NOT NULL,
|
|
image_analysis TEXT,
|
|
post_title TEXT,
|
|
response_snippet TEXT,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS activity_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp INTEGER NOT NULL,
|
|
type TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
recipient TEXT,
|
|
image_url TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_posts_subreddit
|
|
ON processed_posts(subreddit);
|
|
CREATE INDEX IF NOT EXISTS idx_posts_processed_at
|
|
ON processed_posts(processed_at);
|
|
CREATE INDEX IF NOT EXISTS idx_posts_message_sent
|
|
ON processed_posts(message_sent);
|
|
CREATE INDEX IF NOT EXISTS idx_activity_log_timestamp
|
|
ON activity_log(timestamp);
|
|
`);
|
|
|
|
// Add new columns to existing tables (safe to run multiple times)
|
|
const addColumnIfNotExists = (table: string, column: string, definition: string) => {
|
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
const exists = columns.some((c) => c.name === column);
|
|
if (!exists) {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
}
|
|
};
|
|
|
|
addColumnIfNotExists('sent_messages', 'got_response', 'INTEGER DEFAULT 0');
|
|
addColumnIfNotExists('sent_messages', 'response_checked_at', 'INTEGER');
|
|
|
|
// Create index for response checking
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sent_messages_response ON sent_messages(got_response)`);
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sent_messages_recipient ON sent_messages(recipient)`);
|
|
|
|
// Add strategy_id column for A/B testing
|
|
addColumnIfNotExists('sent_messages', 'strategy_id', 'TEXT');
|
|
|
|
// Add subreddit column to successful_messages for smarter example selection
|
|
addColumnIfNotExists('successful_messages', 'subreddit', 'TEXT');
|
|
|
|
// Migration: Make sent_message_id nullable in successful_messages
|
|
// SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
|
const successfulMsgColumns = db.prepare(`PRAGMA table_info(successful_messages)`).all() as { name: string; notnull: number }[];
|
|
const sentMsgIdCol = successfulMsgColumns.find((c) => c.name === 'sent_message_id');
|
|
|
|
if (sentMsgIdCol && sentMsgIdCol.notnull === 1) {
|
|
// Column is currently NOT NULL, we need to migrate
|
|
db.exec(`
|
|
-- Create new table with nullable sent_message_id
|
|
CREATE TABLE successful_messages_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sent_message_id INTEGER,
|
|
original_message TEXT NOT NULL,
|
|
image_analysis TEXT,
|
|
post_title TEXT,
|
|
response_snippet TEXT,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
-- Copy existing data
|
|
INSERT INTO successful_messages_new
|
|
SELECT id, sent_message_id, original_message, image_analysis, post_title, response_snippet, created_at
|
|
FROM successful_messages;
|
|
|
|
-- Drop old table
|
|
DROP TABLE successful_messages;
|
|
|
|
-- Rename new table
|
|
ALTER TABLE successful_messages_new RENAME TO successful_messages;
|
|
`);
|
|
}
|
|
}
|