131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
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);
|
|
}
|