132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import { mkdirSync, readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { Database } from 'bun:sqlite';
|
|
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
import { schema } from './schema';
|
|
|
|
type AppDrizzleDb = ReturnType<typeof createDb>;
|
|
|
|
declare global {
|
|
// eslint-disable-next-line no-var
|
|
var __fiscalSqliteClient: Database | undefined;
|
|
// eslint-disable-next-line no-var
|
|
var __fiscalDrizzleDb: AppDrizzleDb | undefined;
|
|
}
|
|
|
|
function getDatabasePath() {
|
|
const raw = process.env.DATABASE_URL?.trim() || '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.');
|
|
}
|
|
|
|
return databasePath;
|
|
}
|
|
|
|
function hasTable(client: Database, tableName: string) {
|
|
const row = client
|
|
.query('SELECT name FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1')
|
|
.get('table', tableName) as { name: string } | null;
|
|
|
|
return row !== null;
|
|
}
|
|
|
|
function hasColumn(client: Database, tableName: string, columnName: string) {
|
|
if (!hasTable(client, tableName)) {
|
|
return false;
|
|
}
|
|
|
|
const rows = client.query(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }>;
|
|
return rows.some((row) => row.name === columnName);
|
|
}
|
|
|
|
function applySqlFile(client: Database, fileName: string) {
|
|
const sql = readFileSync(join(process.cwd(), 'drizzle', fileName), 'utf8');
|
|
client.exec(sql);
|
|
}
|
|
|
|
function ensureLocalSqliteSchema(client: Database) {
|
|
if (!hasTable(client, 'filing_statement_snapshot')) {
|
|
applySqlFile(client, '0001_glossy_statement_snapshots.sql');
|
|
}
|
|
|
|
if (hasTable(client, 'task_run')) {
|
|
const missingTaskColumns: Array<{ name: string; sql: string }> = [
|
|
{ name: 'stage', sql: "ALTER TABLE `task_run` ADD `stage` text NOT NULL DEFAULT 'queued';" },
|
|
{ name: 'stage_detail', sql: 'ALTER TABLE `task_run` ADD `stage_detail` text;' },
|
|
{ name: 'resource_key', sql: 'ALTER TABLE `task_run` ADD `resource_key` text;' },
|
|
{ name: 'notification_read_at', sql: 'ALTER TABLE `task_run` ADD `notification_read_at` text;' },
|
|
{ name: 'notification_silenced_at', sql: 'ALTER TABLE `task_run` ADD `notification_silenced_at` text;' }
|
|
];
|
|
|
|
for (const column of missingTaskColumns) {
|
|
if (!hasColumn(client, 'task_run', column.name)) {
|
|
client.exec(column.sql);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasTable(client, 'task_stage_event')) {
|
|
applySqlFile(client, '0003_task_stage_event_timeline.sql');
|
|
}
|
|
|
|
if (hasTable(client, 'watchlist_item')) {
|
|
const missingWatchlistColumns: Array<{ name: string; sql: string }> = [
|
|
{ name: 'category', sql: 'ALTER TABLE `watchlist_item` ADD `category` text;' },
|
|
{ name: 'tags', sql: 'ALTER TABLE `watchlist_item` ADD `tags` text;' }
|
|
];
|
|
|
|
for (const column of missingWatchlistColumns) {
|
|
if (!hasColumn(client, 'watchlist_item', column.name)) {
|
|
client.exec(column.sql);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasTable(client, 'filing_taxonomy_snapshot')) {
|
|
applySqlFile(client, '0005_financial_taxonomy_v3.sql');
|
|
}
|
|
}
|
|
|
|
export function getSqliteClient() {
|
|
if (!globalThis.__fiscalSqliteClient) {
|
|
const databasePath = getDatabasePath();
|
|
|
|
if (databasePath !== ':memory:') {
|
|
mkdirSync(dirname(databasePath), { recursive: true });
|
|
}
|
|
|
|
const client = new Database(databasePath, { create: true });
|
|
client.exec('PRAGMA foreign_keys = ON;');
|
|
client.exec('PRAGMA journal_mode = WAL;');
|
|
client.exec('PRAGMA busy_timeout = 5000;');
|
|
ensureLocalSqliteSchema(client);
|
|
|
|
globalThis.__fiscalSqliteClient = client;
|
|
}
|
|
|
|
return globalThis.__fiscalSqliteClient;
|
|
}
|
|
|
|
function createDb() {
|
|
return drizzle(getSqliteClient(), { schema });
|
|
}
|
|
|
|
export const db = globalThis.__fiscalDrizzleDb ?? createDb();
|
|
|
|
if (!globalThis.__fiscalDrizzleDb) {
|
|
globalThis.__fiscalDrizzleDb = db;
|
|
}
|
|
|
|
export const __dbInternals = {
|
|
ensureLocalSqliteSchema,
|
|
getDatabasePath,
|
|
hasColumn,
|
|
hasTable
|
|
};
|