Files
Neon-Desk/scripts/bootstrap-production.ts

122 lines
3.5 KiB
TypeScript

import { spawnSync } from 'node:child_process';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
import {
ensureFinancialIngestionSchemaHealthy,
resolveFinancialSchemaRepairMode
} from '../lib/server/db/financial-ingestion-schema';
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;
}
function runWorkflowSetup() {
const startedAt = performance.now();
const result = spawnSync('./node_modules/.bin/workflow-postgres-setup', [], {
env: process.env,
stdio: 'inherit'
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`workflow-postgres-setup failed with exit code ${result.status ?? 'unknown'}`);
}
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;');
migrate(drizzle(client), { migrationsFolder: './drizzle' });
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) {
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);
}