import { mkdirSync } from 'node:fs'; import { dirname } from 'node:path'; import { Database } from 'bun:sqlite'; import { drizzle as drizzlePostgres } from 'drizzle-orm/postgres-js'; import { migrate as migratePostgres } from 'drizzle-orm/postgres-js/migrator'; import postgres from 'postgres'; import { ensureFinancialIngestionSchemaHealthy, resolveFinancialSchemaRepairMode } from '../lib/server/db/financial-ingestion-schema'; import { ensureLocalSqliteSchema } from '../lib/server/db/sqlite-schema-compat'; import { resolveSqlitePath } from './dev-env'; function trim(value: string | undefined) { const candidate = value?.trim(); return candidate ? candidate : undefined; } function shouldRun(value: string | undefined) { return trim(value) !== 'false'; } function log(message: string) { console.info(`[bootstrap ${new Date().toISOString()}] ${message}`); } function formatDuration(startedAt: number) { return `${(performance.now() - startedAt).toFixed(1)}ms`; } function getDatabasePath() { const raw = trim(process.env.DATABASE_URL) || 'file:data/fiscal.sqlite'; let databasePath = raw.startsWith('file:') ? raw.slice(5) : raw; if (databasePath.startsWith('///')) { databasePath = databasePath.slice(2); } if (!databasePath) { throw new Error('DATABASE_URL must point to a SQLite file path.'); } if (databasePath.includes('://')) { throw new Error(`DATABASE_URL must resolve to a SQLite file path. Received: ${raw}`); } return databasePath; } async function runWorkflowSetup() { const startedAt = performance.now(); const connectionString = trim(process.env.WORKFLOW_POSTGRES_URL) || trim(process.env.DATABASE_URL) || 'postgres://world:world@localhost:5432/world'; const migrationsFolder = trim(process.env.WORKFLOW_MIGRATIONS_DIR) || '/app/workflow-migrations'; const pgClient = postgres(connectionString, { max: 1 }); console.info('🔧 Setting up database schema...'); console.info(`📍 Connection: ${connectionString.replace(/^(\w+:\/\/)([^@]+)@/, '$1[redacted]@')}`); console.info(`📂 Running migrations from: ${migrationsFolder}`); try { const db = drizzlePostgres(pgClient); await migratePostgres(db, { migrationsFolder, migrationsTable: 'workflow_migrations', migrationsSchema: 'workflow_drizzle' }); } finally { await pgClient.end({ timeout: 5 }); } log(`workflow-postgres-setup completed in ${formatDuration(startedAt)}`); } function runDatabaseMigrations() { const startedAt = performance.now(); const databasePath = getDatabasePath(); if (databasePath !== ':memory:') { const normalizedPath = resolveSqlitePath(databasePath); mkdirSync(dirname(normalizedPath), { recursive: true }); } const client = new Database(databasePath, { create: true }); try { client.exec('PRAGMA foreign_keys = ON;'); ensureLocalSqliteSchema(client); const repairResult = ensureFinancialIngestionSchemaHealthy(client, { mode: resolveFinancialSchemaRepairMode(process.env.FINANCIAL_SCHEMA_REPAIR_MODE) }); if (!repairResult.ok) { throw new Error(repairResult.error ?? `financial ingestion schema is ${repairResult.mode}`); } } finally { client.close(); } log(`database migrations completed in ${formatDuration(startedAt)} (${databasePath})`); } const totalStartedAt = performance.now(); try { const shouldRunWorkflowSetup = shouldRun(process.env.RUN_WORKFLOW_SETUP_ON_START) && trim(process.env.WORKFLOW_TARGET_WORLD) === '@workflow/world-postgres'; const shouldRunMigrations = shouldRun(process.env.RUN_DB_MIGRATIONS_ON_START); log('starting production bootstrap'); if (shouldRunWorkflowSetup) { await runWorkflowSetup(); } else { log('workflow-postgres-setup skipped'); } if (shouldRunMigrations) { runDatabaseMigrations(); } else { log('database migrations skipped'); } log(`production bootstrap completed in ${formatDuration(totalStartedAt)}`); } catch (error) { const reason = error instanceof Error ? error.message : String(error); log(`production bootstrap failed after ${formatDuration(totalStartedAt)}: ${reason}`); process.exit(1); }