@ -2,6 +2,7 @@ import { Database } from 'bun:sqlite';
import { normalizeWhatsAppId } from './utils/whatsapp' ;
import { mkdirSync } from 'fs' ;
import { join } from 'path' ;
import { Migrator } from './db/migrator' ;
function applyDefaultPragmas ( instance : Database ) : void {
try {
@ -10,6 +11,8 @@ function applyDefaultPragmas(instance: Database): void {
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 ) ;
}
@ -31,172 +34,17 @@ export function getDb(filename: string = 'tasks.db'): Database {
// Default export for the main application database
export const db = getDb ( ) ;
// Initialize function now accepts a database instance
// Initialize function now accepts a database instance
export function initializeDatabase ( instance : Database ) {
// Aplicar PRAGMAs por defecto (WAL, busy_timeout, etc.)
// Aplicar PRAGMAs por defecto (WAL, busy_timeout, FK, etc.)
applyDefaultPragmas ( instance ) ;
// Enable foreign key constraints
instance . exec ( ` PRAGMA foreign_keys = ON; ` ) ;
// Create users table first as others depend on it
// Use TEXT for timestamps to store higher precision ISO8601 format easily
instance . exec ( `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY , -- WhatsApp user ID ( normalized )
first_seen TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
last_seen TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) )
) ;
` );
// Create groups table
instance . exec ( `
CREATE TABLE IF NOT EXISTS groups (
id TEXT PRIMARY KEY , -- Group ID ( normalized )
community_id TEXT NOT NULL ,
name TEXT ,
last_verified TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
active BOOLEAN DEFAULT TRUE
) ;
` );
// Create group_members table
instance . exec ( `
CREATE TABLE IF NOT EXISTS group_members (
group_id TEXT NOT NULL ,
user_id TEXT NOT NULL ,
is_admin BOOLEAN NOT NULL DEFAULT 0 ,
is_active BOOLEAN NOT NULL DEFAULT 1 ,
first_seen_at TEXT NOT NULL DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
last_seen_at TEXT NOT NULL DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
last_role_change_at TEXT NULL ,
PRIMARY KEY ( group_id , user_id ) ,
FOREIGN KEY ( group_id ) REFERENCES groups ( id ) ON DELETE CASCADE ,
FOREIGN KEY ( user_id ) REFERENCES users ( id ) ON DELETE CASCADE
) ;
` );
// Indexes for membership lookups
instance . exec ( `
CREATE INDEX IF NOT EXISTS idx_group_members_group_active
ON group_members ( group_id , is_active ) ;
` );
instance . exec ( `
CREATE INDEX IF NOT EXISTS idx_group_members_user_active
ON group_members ( user_id , is_active ) ;
` );
// Create tasks table
instance . exec ( `
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
description TEXT NOT NULL ,
created_at TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
due_date TEXT NULL , -- Store dates as ISO8601 strings or YYYY - MM - DD
completed BOOLEAN DEFAULT FALSE ,
completed_at TEXT NULL ,
group_id TEXT NULL , -- Normalized group ID
created_by TEXT NOT NULL , -- Normalized user ID
completed_by TEXT NULL , -- Normalized user ID who completed the task
FOREIGN KEY ( created_by ) REFERENCES users ( id ) ON DELETE CASCADE ,
FOREIGN KEY ( completed_by ) REFERENCES users ( id ) ON DELETE SET NULL ,
FOREIGN KEY ( group_id ) REFERENCES groups ( id ) ON DELETE SET NULL -- Optional : Link task to group
) ;
` );
// Create task_assignments table
instance . exec ( `
CREATE TABLE IF NOT EXISTS task_assignments (
task_id INTEGER NOT NULL ,
user_id TEXT NOT NULL , -- Normalized user ID
assigned_by TEXT NOT NULL , -- Normalized user ID
assigned_at TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
PRIMARY KEY ( task_id , user_id ) ,
FOREIGN KEY ( task_id ) REFERENCES tasks ( id ) ON DELETE CASCADE ,
FOREIGN KEY ( user_id ) REFERENCES users ( id ) ON DELETE CASCADE ,
FOREIGN KEY ( assigned_by ) REFERENCES users ( id ) ON DELETE CASCADE
) ;
` );
// Create response_queue table (persistent outbox for replies)
instance . exec ( `
CREATE TABLE IF NOT EXISTS response_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
recipient TEXT NOT NULL ,
message TEXT NOT NULL ,
status TEXT NOT NULL DEFAULT 'queued' CHECK ( status IN ( 'queued' , 'processing' , 'sent' , 'failed' ) ) ,
attempts INTEGER NOT NULL DEFAULT 0 ,
last_error TEXT NULL ,
metadata TEXT NULL ,
created_at TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) ) ,
updated_at TEXT DEFAULT ( strftime ( '%Y-%m-%d %H:%M:%f' , 'now' ) )
) ;
` );
// Index to fetch pending items efficiently
instance . exec ( `
CREATE INDEX IF NOT EXISTS idx_response_queue_status_created_at
ON response_queue ( status , created_at ) ;
` );
// Migration: ensure 'metadata' column exists on response_queue for message options (e.g., mentions)
// Ejecutar migraciones up-only (sin baseline por defecto). Evitar backup duplicado aquí.
try {
const cols = instance . query ( ` PRAGMA table_info('response_queue') ` ) . all ( ) as any [ ] ;
const hasMetadata = Array . isArray ( cols ) && cols . some ( ( c : any ) = > c . name === 'metadata' ) ;
if ( ! hasMetadata ) {
instance . exec ( ` ALTER TABLE response_queue ADD COLUMN metadata TEXT NULL; ` ) ;
}
} catch ( e ) {
console . warn ( '[initializeDatabase] Skipped adding response_queue.metadata column:' , e ) ;
}
// Migration: ensure 'completed_by' column exists on tasks (to record who completed)
try {
const cols = instance . query ( ` PRAGMA table_info('tasks') ` ) . all ( ) as any [ ] ;
const hasCompletedBy = Array . isArray ( cols ) && cols . some ( ( c : any ) = > c . name === 'completed_by' ) ;
if ( ! hasCompletedBy ) {
instance . exec ( ` ALTER TABLE tasks ADD COLUMN completed_by TEXT NULL; ` ) ;
}
} catch ( e ) {
console . warn ( '[initializeDatabase] Skipped adding tasks.completed_by column:' , e ) ;
}
// Migration: ensure reliability columns exist on response_queue (next_attempt_at, lease_until, last_status_code)
try {
const cols = instance . query ( ` PRAGMA table_info('response_queue') ` ) . all ( ) as any [ ] ;
const hasNextAttempt = Array . isArray ( cols ) && cols . some ( ( c : any ) = > c . name === 'next_attempt_at' ) ;
if ( ! hasNextAttempt ) {
instance . exec ( ` ALTER TABLE response_queue ADD COLUMN next_attempt_at TEXT NULL; ` ) ;
}
const hasLeaseUntil = Array . isArray ( cols ) && cols . some ( ( c : any ) = > c . name === 'lease_until' ) ;
if ( ! hasLeaseUntil ) {
instance . exec ( ` ALTER TABLE response_queue ADD COLUMN lease_until TEXT NULL; ` ) ;
}
const hasLastStatus = Array . isArray ( cols ) && cols . some ( ( c : any ) = > c . name === 'last_status_code' ) ;
if ( ! hasLastStatus ) {
instance . exec ( ` ALTER TABLE response_queue ADD COLUMN last_status_code INTEGER NULL; ` ) ;
}
} catch ( e ) {
console . warn ( '[initializeDatabase] Skipped ensuring response_queue reliability columns:' , e ) ;
}
// Ensure supporting indexes exist
try {
instance . exec ( `
CREATE INDEX IF NOT EXISTS idx_response_queue_status_next_attempt
ON response_queue ( status , next_attempt_at ) ;
` );
} catch ( e ) {
console . warn ( '[initializeDatabase] Skipped creating idx_response_queue_status_next_attempt:' , e ) ;
}
try {
instance . exec ( `
CREATE INDEX IF NOT EXISTS idx_response_queue_status_lease_until
ON response_queue ( status , lease_until ) ;
` );
Migrator . migrateToLatest ( instance , { withBackup : false , allowBaseline : false } ) ;
} catch ( e ) {
console . warn ( '[initializeDatabase] Skipped creating idx_response_queue_status_lease_until:' , e ) ;
console . error ( '[initializeDatabase] Error al aplicar migraciones:' , e ) ;
throw e ;
}
}