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 { // 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 { 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 { if (_db) return _db; _db = await openDb(filename); return _db; }