From cd834552ccd91b8039337c45b8f815650a803c1b Mon Sep 17 00:00:00 2001 From: brobert Date: Mon, 10 Nov 2025 16:20:08 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20aplicar=20fallback=20de=20DB:=20par?= =?UTF-8?q?=C3=A1metro=20=E2=86=92=20.dbInstance=20=E2=86=92=20getDb()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- src/services/admin.ts | 40 +++++++++++++++++++------------------ src/services/command.ts | 12 ++++++----- src/services/group-sync.ts | 13 +++++++++--- src/services/maintenance.ts | 8 +++++--- src/services/reminders.ts | 8 +++++--- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/services/admin.ts b/src/services/admin.ts index eba6e40..0bae411 100644 --- a/src/services/admin.ts +++ b/src/services/admin.ts @@ -6,6 +6,7 @@ import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp'; import { Metrics } from './metrics'; import { TaskService } from '../tasks/service'; import { codeId, formatDDMM } from '../utils/formatting'; +import { getDb } from '../db/locator'; type AdminContext = { sender: string; // normalized user id (digits only) @@ -60,8 +61,9 @@ export class AdminService { return [{ recipient: sender, message: '🚫 No estás autorizado para usar /admin.' }]; } + const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; // Asegurar acceso a la misma DB para AllowedGroups - try { AllowedGroups.dbInstance = this.dbInstance; } catch {} + try { AllowedGroups.dbInstance = instanceDb; } catch {} const raw = String(ctx.message || '').trim(); const lower = raw.toLowerCase(); @@ -109,18 +111,18 @@ export class AdminService { if (!isGroupId(ctx.groupId)) { return [{ recipient: sender, message: 'ℹ️ Este comando se debe usar dentro de un grupo.' }]; } - this.dbInstance.transaction(() => { - this.dbInstance.prepare(` + instanceDb.transaction(() => { + instanceDb.prepare(` UPDATE groups SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ? `).run(ctx.groupId); - this.dbInstance.prepare(` + instanceDb.prepare(` UPDATE calendar_tokens SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now') WHERE group_id = ? AND revoked_at IS NULL `).run(ctx.groupId); - this.dbInstance.prepare(` + instanceDb.prepare(` UPDATE group_members SET is_active = 0 WHERE group_id = ? AND is_active = 1 @@ -136,18 +138,18 @@ export class AdminService { if (!isGroupId(arg)) { return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; } - this.dbInstance.transaction(() => { - this.dbInstance.prepare(` + instanceDb.transaction(() => { + instanceDb.prepare(` UPDATE groups SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ? `).run(arg); - this.dbInstance.prepare(` + instanceDb.prepare(` UPDATE calendar_tokens SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now') WHERE group_id = ? AND revoked_at IS NULL `).run(arg); - this.dbInstance.prepare(` + instanceDb.prepare(` UPDATE group_members SET is_active = 0 WHERE group_id = ? AND is_active = 1 @@ -162,10 +164,10 @@ export class AdminService { if (!isGroupId(ctx.groupId)) { return [{ recipient: sender, message: 'ℹ️ Este comando se debe usar dentro de un grupo.' }]; } - this.dbInstance.transaction(() => { - this.dbInstance.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(ctx.groupId); - this.dbInstance.prepare(`DELETE FROM groups WHERE id = ?`).run(ctx.groupId); - try { this.dbInstance.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(ctx.groupId); } catch {} + instanceDb.transaction(() => { + instanceDb.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(ctx.groupId); + instanceDb.prepare(`DELETE FROM groups WHERE id = ?`).run(ctx.groupId); + try { instanceDb.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(ctx.groupId); } catch {} })(); return [{ recipient: sender, message: `🗑️ Grupo borrado y datos asociados eliminados: ${ctx.groupId}` }]; } @@ -176,10 +178,10 @@ export class AdminService { if (!isGroupId(arg)) { return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; } - this.dbInstance.transaction(() => { - this.dbInstance.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(arg); - this.dbInstance.prepare(`DELETE FROM groups WHERE id = ?`).run(arg); - try { this.dbInstance.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(arg); } catch {} + instanceDb.transaction(() => { + instanceDb.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(arg); + instanceDb.prepare(`DELETE FROM groups WHERE id = ?`).run(arg); + try { instanceDb.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(arg); } catch {} })(); return [{ recipient: sender, message: `🗑️ Grupo borrado y datos asociados eliminados: ${arg}` }]; } @@ -229,7 +231,7 @@ export class AdminService { // /admin sync-grupos if (rest === 'sync-grupos' || rest === 'group-sync' || rest === 'syncgroups') { - try { (GroupSyncService as any).dbInstance = this.dbInstance; } catch {} + try { (GroupSyncService as any).dbInstance = instanceDb; } catch {} try { const r = await GroupSyncService.syncGroups(true); return [{ recipient: sender, message: `✅ Sync de grupos ejecutado: ${r.added} añadidos, ${r.updated} actualizados.` }]; @@ -251,7 +253,7 @@ export class AdminService { rest.startsWith('list-all ') ) { // Asegurar acceso a la misma DB para TaskService - try { TaskService.dbInstance = this.dbInstance; } catch {} + try { TaskService.dbInstance = instanceDb; } catch {} const DEFAULT_LIMIT = 50; let limit = DEFAULT_LIMIT; diff --git a/src/services/command.ts b/src/services/command.ts index b583c5f..7496317 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -5,6 +5,7 @@ import { AllowedGroups } from './allowed-groups'; import { Metrics } from './metrics'; import { route as routeCommand } from './commands'; import { ACTION_ALIASES } from './commands/shared'; +import { getDb } from '../db/locator'; type CommandContext = { sender: string; // normalized user id (digits only), but accept raw too @@ -41,6 +42,7 @@ export class CommandService { static async handleWithOutcome(context: CommandContext): Promise { const msg = (context.message || '').trim(); + const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; if (!/^\/(tarea|t)\b/i.test(msg)) { return { responses: [], ok: true }; } @@ -49,14 +51,14 @@ export class CommandService { try { let usersTableExists = false; try { - const row = this.dbInstance.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`).get() as { name?: string } | undefined; + const row = instanceDb.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`).get() as { name?: string } | undefined; usersTableExists = !!row; } catch {} if (usersTableExists) { - const ensured = ensureUserExists(context.sender, this.dbInstance); + const ensured = ensureUserExists(context.sender, instanceDb); if (ensured) { try { - this.dbInstance.prepare(`UPDATE users SET last_command_at = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ?`).run(ensured); + instanceDb.prepare(`UPDATE users SET last_command_at = strftime('%Y-%m-%d %H:%M:%f','now') WHERE id = ?`).run(ensured); } catch {} } } @@ -64,7 +66,7 @@ export class CommandService { // Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente) if (isGroupId(context.groupId)) { - try { AllowedGroups.dbInstance = this.dbInstance; } catch { } + try { AllowedGroups.dbInstance = instanceDb; } catch { } const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { try { @@ -79,7 +81,7 @@ export class CommandService { } try { - const routed = await routeCommand(context, { db: this.dbInstance }); + const routed = await routeCommand(context, { db: instanceDb }); const responses = routed ?? []; // Clasificación explícita del outcome (evita lógica en server) diff --git a/src/services/group-sync.ts b/src/services/group-sync.ts index 02539f8..78c7aca 100644 --- a/src/services/group-sync.ts +++ b/src/services/group-sync.ts @@ -1,5 +1,6 @@ import type { Database } from 'bun:sqlite'; -import { db, ensureUserExists } from '../db'; +import { ensureUserExists } from '../db'; +import { getDb as getGlobalDb } from '../db/locator'; import { normalizeWhatsAppId } from '../utils/whatsapp'; import { Metrics } from './metrics'; import { IdentityService } from './identity'; @@ -43,8 +44,14 @@ type EvolutionGroup = { }; export class GroupSyncService { - // Static property for DB instance injection (defaults to global db) - static dbInstance: Database = db; + // Static property for DB instance injection (with fallback to global locator) + private static _dbInstance: Database | null = null; + static get dbInstance(): Database { + return (this._dbInstance as Database) ?? getGlobalDb(); + } + static set dbInstance(value: Database) { + this._dbInstance = value; + } // In-memory cache for active groups (made public for tests) public static readonly activeGroupsCache = new Map(); // groupId -> groupName diff --git a/src/services/maintenance.ts b/src/services/maintenance.ts index eb882e2..8d74472 100644 --- a/src/services/maintenance.ts +++ b/src/services/maintenance.ts @@ -1,5 +1,5 @@ import type { Database } from 'bun:sqlite'; -import { db } from '../db'; +import { getDb } from '../db/locator'; import { toIsoSqlUTC } from '../utils/datetime'; export class MaintenanceService { @@ -64,9 +64,10 @@ export class MaintenanceService { } } - static async cleanupInactiveMembersOnce(instance: Database = db, retentionDays: number = this.retentionDays): Promise { + static async cleanupInactiveMembersOnce(instance?: Database, retentionDays: number = this.retentionDays): Promise { if (retentionDays <= 0) return 0; const threshold = toIsoSqlUTC(new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)); + instance = (instance ?? (this as any).dbInstance ?? getDb()) as Database; const res = instance.prepare(` DELETE FROM group_members WHERE is_active = 0 @@ -81,8 +82,9 @@ export class MaintenanceService { * en todas las tablas relevantes, basándose en user_aliases. * Devuelve el número de alias procesados. */ - static async reconcileAliasUsersOnce(instance: Database = db): Promise { + static async reconcileAliasUsersOnce(instance?: Database): Promise { try { + instance = (instance ?? (this as any).dbInstance ?? getDb()) as Database; const rows = instance.prepare(`SELECT alias, user_id FROM user_aliases WHERE alias != user_id`).all() as any[]; let merged = 0; diff --git a/src/services/reminders.ts b/src/services/reminders.ts index ba1d06a..81c6cce 100644 --- a/src/services/reminders.ts +++ b/src/services/reminders.ts @@ -8,6 +8,7 @@ import { ICONS } from '../utils/icons'; import { codeId, formatDDMM, bold, italic } from '../utils/formatting'; import { AllowedGroups } from './allowed-groups'; import { Metrics } from './metrics'; +import { getDb } from '../db/locator'; type UserPreference = { user_id: string; @@ -80,13 +81,14 @@ export class RemindersService { } static async runOnce(now: Date = new Date()): Promise { + const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; const todayYMD = this.ymdInTZ(now); const nowHM = this.hmInTZ(now); const weekday = this.weekdayShortInTZ(now); // 'Mon'..'Sun' const graceRaw = Number(process.env.REMINDERS_GRACE_MINUTES); const GRACE_MIN = Number.isFinite(graceRaw) && graceRaw >= 0 ? Math.min(Math.floor(graceRaw), 180) : 60; - const rows = this.dbInstance.prepare(` + const rows = instanceDb.prepare(` SELECT user_id, reminder_freq, reminder_time, last_reminded_on FROM user_preferences WHERE reminder_freq IN ('daily', 'weekly', 'weekdays') @@ -96,7 +98,7 @@ export class RemindersService { const enforce = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase() === 'enforce'; if (enforce) { try { - (AllowedGroups as any).dbInstance = this.dbInstance; + (AllowedGroups as any).dbInstance = instanceDb; // Evitar falsos positivos por caché obsoleta entre operaciones previas del test AllowedGroups.clearCache?.(); } catch {} @@ -228,7 +230,7 @@ export class RemindersService { }]); // Marcar como enviado hoy - this.dbInstance.prepare(` + instanceDb.prepare(` INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) VALUES (?, COALESCE((SELECT reminder_freq FROM user_preferences WHERE user_id = ?), 'daily'), COALESCE((SELECT reminder_time FROM user_preferences WHERE user_id = ?), '08:30'),