import { drizzle } from 'drizzle-orm/bun-sqlite'; import { Database } from 'bun:sqlite'; import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; import * as schema from './schema'; import { config } from '../config'; import { mkdirSync, existsSync } from 'fs'; import { dirname, resolve } from 'path'; // Ensure data directory exists const dbDir = dirname(config.DATABASE_PATH); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } const sqlite = new Database(config.DATABASE_PATH, { create: true }); sqlite.exec('PRAGMA journal_mode = WAL'); sqlite.exec('PRAGMA foreign_keys = ON'); export const db = drizzle(sqlite, { schema }); export { sqlite }; /** * Run database migrations automatically on startup. * Uses CREATE TABLE IF NOT EXISTS semantics (via drizzle migrate) * so it is safe to call on every boot -- already-applied migrations * are tracked in the drizzle __drizzle_migrations journal table. * * The migrationsFolder path is resolved relative to the project root * (where package.json lives) so it works regardless of cwd. */ function autoMigrate() { try { // Resolve the drizzle folder relative to this file's location: // src/db/index.ts -> ../../drizzle const migrationsFolder = resolve(import.meta.dir, '../../drizzle'); migrate(db, { migrationsFolder }); console.info('[DB] Auto-migration completed successfully.'); } catch (err) { console.error('[DB] Auto-migration failed:', err); // Do not crash the process -- tables may already exist from a prior run. // Fallback: create core tables with raw SQL if drizzle migrations folder is missing. try { createTablesIfNotExist(); console.info('[DB] Fallback table creation completed.'); } catch (fallbackErr) { console.error('[DB] Fallback table creation also failed:', fallbackErr); } } } /** * Fallback: create all required tables using raw SQL CREATE TABLE IF NOT EXISTS. * This is only used when the drizzle migrations folder cannot be found. */ function createTablesIfNotExist() { sqlite.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY NOT NULL, plane_user_id TEXT, display_name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, git_username TEXT, role TEXT NOT NULL, password_hash TEXT NOT NULL, login_attempts INTEGER DEFAULT 0, locked_until INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY NOT NULL, plane_project_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, identifier TEXT, last_synced_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS sprint_snapshots ( id TEXT PRIMARY KEY NOT NULL, project_id TEXT REFERENCES projects(id), plane_cycle_id TEXT NOT NULL, name TEXT NOT NULL, start_date TEXT, end_date TEXT, total_points INTEGER DEFAULT 0, completed_points INTEGER DEFAULT 0, total_issues INTEGER DEFAULT 0, completed_issues INTEGER DEFAULT 0, burndown_data TEXT, status TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS task_snapshots ( id TEXT PRIMARY KEY NOT NULL, plane_issue_id TEXT NOT NULL, project_id TEXT REFERENCES projects(id), sprint_id TEXT REFERENCES sprint_snapshots(id), title TEXT NOT NULL, status TEXT, priority TEXT, assignee_id TEXT REFERENCES users(id), story_points INTEGER, created_at INTEGER, completed_at INTEGER, due_date TEXT, labels TEXT, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS milestones ( id TEXT PRIMARY KEY NOT NULL, plane_module_id TEXT NOT NULL, project_id TEXT REFERENCES projects(id), name TEXT NOT NULL, status TEXT, target_date TEXT, total_issues INTEGER DEFAULT 0, completed_issues INTEGER DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS git_commits ( id TEXT PRIMARY KEY NOT NULL, repo_name TEXT NOT NULL, sha TEXT NOT NULL UNIQUE, author_email TEXT, author_name TEXT, user_id TEXT REFERENCES users(id), message TEXT, additions INTEGER DEFAULT 0, deletions INTEGER DEFAULT 0, committed_at INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS git_prs ( id TEXT PRIMARY KEY NOT NULL, repo_name TEXT NOT NULL, external_id INTEGER NOT NULL, title TEXT, user_id TEXT REFERENCES users(id), author_username TEXT, state TEXT, additions INTEGER DEFAULT 0, deletions INTEGER DEFAULT 0, review_comments INTEGER DEFAULT 0, created_at INTEGER, merged_at INTEGER, merge_time_hours REAL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS objectives ( id TEXT PRIMARY KEY NOT NULL, title TEXT NOT NULL, owner_id TEXT REFERENCES users(id), project_id TEXT REFERENCES projects(id), period TEXT NOT NULL, progress REAL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS key_results ( id TEXT PRIMARY KEY NOT NULL, objective_id TEXT NOT NULL REFERENCES objectives(id), title TEXT NOT NULL, target_value REAL NOT NULL, current_value REAL DEFAULT 0, unit TEXT, weight REAL DEFAULT 1, linked_plane_cycle_id TEXT, linked_plane_module_id TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS author_mappings ( id TEXT PRIMARY KEY NOT NULL, git_email TEXT, git_username TEXT, user_id TEXT REFERENCES users(id), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS project_repos ( id TEXT PRIMARY KEY NOT NULL, project_id TEXT NOT NULL REFERENCES projects(id), repo_name TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS sync_logs ( id TEXT PRIMARY KEY NOT NULL, source TEXT NOT NULL, status TEXT NOT NULL, message TEXT, records_processed INTEGER DEFAULT 0, synced_at INTEGER NOT NULL ); `); } // Run auto-migration on module load (i.e. on server startup) autoMigrate();