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.
		
		
		
		
		
			
		
			
				
	
	
		
			156 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			156 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
| import { Database } from 'bun:sqlite';
 | |
| import { normalizeWhatsAppId } from './utils/whatsapp';
 | |
| import { mkdirSync } from 'fs';
 | |
| import { join, resolve, dirname } from 'path';
 | |
| import { Migrator } from './db/migrator';
 | |
| import { migrations } from './db/migrations';
 | |
| 
 | |
| function applyDefaultPragmas(instance: Database): void {
 | |
|   try {
 | |
|     instance.exec(`PRAGMA busy_timeout = 5000;`);
 | |
|     // Intentar activar WAL (si no es soportado, SQLite devolverá 'memory' u otro modo)
 | |
|     instance.query(`PRAGMA journal_mode = WAL`).get();
 | |
|     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('[db] No se pudieron aplicar PRAGMAs (WAL, busy_timeout...):', e);
 | |
|   }
 | |
| }
 | |
| 
 | |
|  // Function to get a database instance. Defaults to 'data/tasks.db'
 | |
| export function getDb(filename: string = 'tasks.db'): Database {
 | |
|   // Prioridad 1: DB_PATH (ruta completa al archivo). Si está definida, se usa tal cual.
 | |
|   const dbPathEnv = process?.env?.DB_PATH ? String(process.env.DB_PATH).trim() : '';
 | |
|   if (dbPathEnv) {
 | |
|     const absolutePath = resolve(dbPathEnv);
 | |
|     // Crear directorio padre si no existe
 | |
|     try {
 | |
|       mkdirSync(dirname(absolutePath), { recursive: true });
 | |
|     } catch (err) {
 | |
|       if ((err as any)?.code !== 'EEXIST') throw err; // Solo ignorar "ya existe"
 | |
|     }
 | |
|     const instance = new Database(absolutePath);
 | |
|     applyDefaultPragmas(instance);
 | |
|     return instance;
 | |
|   }
 | |
| 
 | |
|   // Prioridad 2: DATA_DIR + filename (comportamiento actual)
 | |
|   // Determine base directory for the database (env DATA_DIR or default './data'), resolve to absolute
 | |
|   const dataDir = process?.env?.DATA_DIR ? String(process.env.DATA_DIR) : 'data';
 | |
|   const dirPath = resolve(dataDir);
 | |
| 
 | |
|   // Try to create data directory if it doesn't exist (ignore if already exists)
 | |
|   try {
 | |
|     mkdirSync(dirPath, { recursive: true });
 | |
|   } catch (err) {
 | |
|     if ((err as any)?.code !== 'EEXIST') throw err; // Only ignore "already exists" errors
 | |
|   }
 | |
| 
 | |
|   const instance = new Database(join(dirPath, filename));
 | |
|   applyDefaultPragmas(instance);
 | |
|   return instance;
 | |
| }
 | |
| 
 | |
| // Default export for the main application database
 | |
| export const db = getDb();
 | |
| 
 | |
|  // Initialize function now accepts a database instance
 | |
| export function initializeDatabase(instance: Database) {
 | |
|   // Aplicar PRAGMAs por defecto (WAL, busy_timeout, FK, etc.)
 | |
|   applyDefaultPragmas(instance);
 | |
| 
 | |
|   // Ejecutar migraciones con el Migrator; si no deja el esquema listo, aplicar fallback.
 | |
|   let migratorError: unknown = null;
 | |
|   try {
 | |
|     Migrator.migrateToLatest(instance, { withBackup: false, allowBaseline: false });
 | |
|   } catch (e) {
 | |
|     migratorError = e;
 | |
|     console.error('[initializeDatabase] Error al aplicar migraciones con Migrator:', e);
 | |
|   }
 | |
| 
 | |
|   // Verificación mínima: si las tablas base no existen, aplicar fallback secuencial.
 | |
|   const tableExists = (name: string): boolean => {
 | |
|     try {
 | |
|       const row = instance
 | |
|         .query(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`)
 | |
|         .get(name) as any;
 | |
|       return Boolean(row && row.name === name);
 | |
|     } catch {
 | |
|       return false;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const needsFallback =
 | |
|     !tableExists('users') ||
 | |
|     !tableExists('tasks') ||
 | |
|     !tableExists('response_queue');
 | |
| 
 | |
|   if (needsFallback) {
 | |
|     console.warn('[initializeDatabase] Migrator no dejó el esquema listo; aplicando fallback de migraciones secuenciales');
 | |
|     try {
 | |
|       instance.transaction(() => {
 | |
|         try { instance.exec(`PRAGMA foreign_keys = ON;`); } catch {}
 | |
|         for (const m of migrations) {
 | |
|           m.up(instance);
 | |
|         }
 | |
|       })();
 | |
|     } catch (fallbackErr) {
 | |
|       console.error('[initializeDatabase] Fallback de migraciones falló:', fallbackErr);
 | |
|       throw fallbackErr;
 | |
|     }
 | |
|   } else if (migratorError) {
 | |
|     // Si el Migrator falló pero el esquema ya está correcto, sólo loggeamos.
 | |
|     console.warn('[initializeDatabase] Migrator reportó error, pero el esquema parece estar correcto. Continuando.');
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Ensures a user exists in the database based on their raw WhatsApp ID.
 | |
|  * If the user exists, updates their last_seen timestamp.
 | |
|  * If the user does not exist, creates them.
 | |
|  * Uses the normalizeWhatsAppId utility.
 | |
|  * Stores timestamps with millisecond precision.
 | |
|  * 
 | |
|  * @param rawUserId The raw WhatsApp ID (e.g., '12345@s.whatsapp.net').
 | |
|  * @param instance The database instance to use (defaults to the main db).
 | |
|  * @returns The normalized user ID if successful, otherwise null.
 | |
|  */
 | |
| export function ensureUserExists(rawUserId: string | null | undefined, instance: Database = db): string | null {
 | |
|   const normalizedId = normalizeWhatsAppId(rawUserId);
 | |
| 
 | |
|   if (!normalizedId) {
 | |
|     console.error(`[ensureUserExists] Could not normalize or invalid user ID provided: ${rawUserId}`);
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     // Use strftime for millisecond precision timestamps
 | |
|     const insertStmt = instance.prepare(`
 | |
|       INSERT INTO users (id, first_seen, last_seen) 
 | |
|       VALUES (?, strftime('%Y-%m-%d %H:%M:%f', 'now'), strftime('%Y-%m-%d %H:%M:%f', 'now'))
 | |
|       ON CONFLICT(id) DO NOTHING;
 | |
|     `);
 | |
|     
 | |
|     const updateStmt = instance.prepare(`
 | |
|       UPDATE users 
 | |
|       SET last_seen = strftime('%Y-%m-%d %H:%M:%f', 'now')
 | |
|       WHERE id = ?;
 | |
|     `);
 | |
| 
 | |
|     // Run as transaction for atomicity
 | |
|     instance.transaction(() => {
 | |
|       insertStmt.run(normalizedId);
 | |
|       // Update last_seen even if the user was just inserted or already existed
 | |
|       updateStmt.run(normalizedId); 
 | |
|     })(); // Immediately invoke the transaction
 | |
| 
 | |
|     return normalizedId;
 | |
| 
 | |
|   } catch (error) {
 | |
|     console.error(`[ensureUserExists] Database error for user ID ${normalizedId}:`, error);
 | |
|     return null;
 | |
|   }
 | |
| }
 |