/** * Database Connection Module * * Provides a unified database connection using Bun's SQL API. * Supports both SQLite (default) and PostgreSQL (via DATABASE_URL). */ import { SQL } from 'bun'; import { existsSync, mkdirSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); // Database configuration const databaseUrl = process.env.DATABASE_URL; const dataDir = process.env.DATA_DIR || './data'; // Detect database type export const isPostgres = databaseUrl && (databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://')); export const isSqlite = !isPostgres; /** * Read a SQL file from the appropriate sql directory. */ function readSql(filename: string): string { const sqlDir = isPostgres ? 'postgres' : 'sqlite'; return readFileSync(join(__dirname, sqlDir, 'sql', filename), 'utf-8'); } /** * Validate PostgreSQL connection URL format. */ function validatePostgresUrl(url: string): void { try { const parsed = new URL(url); if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') { exitWithError(`Invalid protocol "${parsed.protocol}". Expected "postgres:" or "postgresql:"`, url); } if (!parsed.hostname) { exitWithError('Missing hostname in DATABASE_URL', url); } if (!parsed.pathname || parsed.pathname === '/') { exitWithError('Missing database name in DATABASE_URL', url); } } catch { exitWithError('Invalid URL format', url); } } /** * Print connection error and exit. */ function exitWithError(error: string, url?: string): never { console.error('\n' + '='.repeat(70)); console.error('DATABASE CONNECTION ERROR'); console.error('='.repeat(70)); console.error(`\nError: ${error}`); if (url) { try { const parsed = new URL(url); if (parsed.password) parsed.password = '***'; console.error(`\nProvided URL: ${parsed.toString()}`); } catch { console.error(`\nProvided URL: ${url.replace(/:[^:@]+@/, ':***@')}`); } } console.error('\n' + '-'.repeat(70)); console.error('DATABASE_URL format:'); console.error('-'.repeat(70)); console.error('\n postgres://USER:PASSWORD@HOST:PORT/DATABASE'); console.error('\nExamples:'); console.error(' postgres://dockhand:secret@localhost:5432/dockhand'); console.error(' postgres://admin:p4ssw0rd@192.168.1.100:5432/dockhand'); console.error(' postgresql://user:pass@db.example.com/mydb?sslmode=require'); console.error('\n' + '-'.repeat(70)); console.error('To use SQLite instead, remove the DATABASE_URL environment variable.'); console.error('='.repeat(70) + '\n'); process.exit(1); } /** * Create the database connection. */ function createConnection(): SQL { if (isPostgres) { // Validate PostgreSQL URL validatePostgresUrl(databaseUrl!); console.log('Connecting to PostgreSQL database...'); try { const sql = new SQL(databaseUrl!); return sql; } catch (error) { const message = error instanceof Error ? error.message : String(error); exitWithError(`Failed to connect to PostgreSQL: ${message}`, databaseUrl); } } else { // SQLite: Ensure db directory exists const dbDir = join(dataDir, 'db'); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } const dbPath = join(dbDir, 'dockhand.db'); console.log(`Using SQLite database at: ${dbPath}`); const sql = new SQL(`sqlite://${dbPath}`); // Enable WAL mode for better performance sql.run('PRAGMA journal_mode = WAL'); return sql; } } /** * Initialize the database schema. */ async function initializeSchema(sql: SQL): Promise { try { // Create schema (tables) await sql.run(readSql('schema.sql')); // Create indexes await sql.run(readSql('indexes.sql')); // Insert seed data await sql.run(readSql('seed.sql')); // Update system roles await sql.run(readSql('system-roles.sql')); // Run maintenance await sql.run(readSql('maintenance.sql')); console.log(`Database initialized successfully (${isPostgres ? 'PostgreSQL' : 'SQLite'})`); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error('Failed to initialize database schema:', message); throw error; } } // Create and export the database connection export const sql = createConnection(); // Initialize schema (runs async but we handle it) initializeSchema(sql).catch((error) => { console.error('Database initialization failed:', error); process.exit(1); }); /** * Helper to convert SQLite integer booleans to JS booleans. * PostgreSQL returns actual booleans, SQLite returns 0/1. */ export function toBool(value: any): boolean { if (typeof value === 'boolean') return value; return Boolean(value); } /** * Helper to convert JS boolean to database value. * PostgreSQL uses boolean, SQLite uses 0/1. */ export function fromBool(value: boolean): boolean | number { return isPostgres ? value : (value ? 1 : 0); }