You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
5.3 KiB
TypeScript

import { mkdirSync, existsSync } from 'fs';
import { dirname } from 'path';
import { resolveDbAbsolutePath, isDev, DEV_AUTOSEED_DB, DEV_DEFAULT_USER } from './env';
function applyDefaultPragmas(instance: any): void {
try {
instance.exec(`PRAGMA busy_timeout = 5000;`);
// Intentar activar WAL (si no es soportado, SQLite devolverá 'memory' u otro modo)
try {
if (typeof instance.query === 'function') {
instance.query(`PRAGMA journal_mode = WAL`)?.get?.();
} else {
instance.prepare?.(`PRAGMA journal_mode = WAL`)?.get?.();
}
} catch {}
instance.exec(`PRAGMA synchronous = NORMAL;`);
instance.exec(`PRAGMA wal_autocheckpoint = 1000;`);
// Asegurar claves foráneas siempre activas
instance.exec(`PRAGMA foreign_keys = ON;`);
} catch (e) {
console.warn('[web/db] No se pudieron aplicar PRAGMAs (WAL, busy_timeout...):', e);
}
}
/**
* Intenta cargar un constructor de Database compatible:
* - En Bun (SSR nativo): bun:sqlite
* - En Node (Vite dev SSR): better-sqlite3
*/
async function importSqliteDatabase(): Promise<any> {
// En desarrollo (Vite SSR), cargar better-sqlite3 vía require de Node para mantener el contexto CJS
if (import.meta.env.DEV) {
const modModule: any = await import('node:module');
const require = modModule.createRequire(import.meta.url);
const mod = require('better-sqlite3');
return (mod as any).default || (mod as any).Database || mod;
}
// En producción (Bun en runtime), usar bun:sqlite nativo
const mod: any = await import('bun:sqlite');
return (mod as any).Database || (mod as any).default || mod;
}
/**
* Abre la BD compartida. En desarrollo, si el archivo no existe y DEV_AUTOSEED_DB=true,
* inicializa el esquema (migraciones) y siembra datos de demo.
* Nota: usa bun:sqlite si está disponible; en SSR Node usa better-sqlite3.
*/
async function openDb(filename: string = 'tasks.db'): Promise<any> {
const absolutePath = resolveDbAbsolutePath(filename);
const firstCreate = !existsSync(absolutePath);
// Crear directorio padre si no existe
try {
mkdirSync(dirname(absolutePath), { recursive: true });
} catch (err: any) {
if (err?.code !== 'EEXIST') throw err;
}
const DatabaseCtor = await importSqliteDatabase();
const instance = new DatabaseCtor(absolutePath);
applyDefaultPragmas(instance);
// Auto-inicialización de esquema en desarrollo si falta y seed opcional
if (isDev()) {
// ¿Existe la tabla principal?
let hasTasksTable = false;
try {
instance.prepare(`SELECT 1 FROM tasks LIMIT 1`).get();
hasTasksTable = true;
} catch {}
// Si no existe el esquema, aplicar inicialización/migraciones
if (!hasTasksTable) {
const isBun = typeof (globalThis as any).Bun !== 'undefined';
if (isBun) {
// En Bun podemos reutilizar initializeDatabase del repo principal
try {
const dbModule = await import('../../../../../src/db');
if (typeof (dbModule as any).initializeDatabase === 'function') {
(dbModule as any).initializeDatabase(instance);
hasTasksTable = true;
}
} catch (e) {
console.warn('[web/db] No se pudo ejecutar initializeDatabase en dev (Bun):', e);
}
} else {
// En SSR Node: aplicar migraciones directamente con compat para .query
try {
const mod = await import('../../../../../src/db/migrations/index.ts');
const list = (mod as any).migrations as any[];
const compat: any = instance;
if (typeof compat.query !== 'function') {
compat.query = (sql: string) => ({
all: () => compat.prepare(sql).all(),
get: () => compat.prepare(sql).get()
});
}
try { compat.exec?.(`PRAGMA foreign_keys = ON;`); } catch {}
for (const m of list) {
try {
await (m.up as any)(compat);
} catch (e) {
console.warn('[web/db] Error aplicando migración en dev (Node):', (m as any)?.name ?? '(sin nombre)', e);
}
}
// Verificar de nuevo
try {
compat.prepare(`SELECT 1 FROM tasks LIMIT 1`).get();
hasTasksTable = true;
} catch {}
} catch (e) {
console.warn('[web/db] No se pudieron aplicar migraciones en dev (Node):', e);
}
}
}
// Seed de datos de demo si está habilitado y la tabla está vacía
if (DEV_AUTOSEED_DB) {
try {
let count = 0;
try {
const row = instance.prepare(`SELECT COUNT(1) AS c FROM tasks`).get() as any;
count = Number(row?.c ?? 0);
} catch {
// Si aún no existe la tabla, no seedear
count = 0;
}
if (count === 0) {
const seed = await import('./dev-seed');
if (typeof (seed as any).seedDev === 'function') {
await (seed as any).seedDev(instance, DEV_DEFAULT_USER);
}
}
} catch (e) {
console.warn('[web/db] No se pudo realizar el seed de datos de demo:', e);
}
}
}
return instance;
}
let _db: any | null = null;
/**
* Devuelve una única instancia compartida (lazy) de la BD.
*/
export async function getDb(filename: string = 'tasks.db'): Promise<any> {
if (_db) return _db;
_db = await openDb(filename);
return _db;
}