From e415a264422a9bd69d1387b1501f88748c34be62 Mon Sep 17 00:00:00 2001 From: brobert Date: Mon, 10 Nov 2025 16:50:51 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20eliminar=20dbInstance=20y=20a=C3=B1?= =?UTF-8?q?adir=20resetDb/clearDb,=20usar=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/db/locator.ts | 14 ++++++++++++++ src/http/webhook-handler.ts | 9 --------- src/server.ts | 1 - src/services/admin.ts | 8 ++------ src/services/allowed-groups.ts | 14 ++++++-------- src/services/command.ts | 9 ++------- src/services/group-sync/repo.ts | 1 - src/services/identity.ts | 9 ++++----- src/services/maintenance.ts | 4 ++-- src/services/onboarding.ts | 2 -- src/services/reminders.ts | 6 ++---- src/services/response-queue.ts | 11 ++--------- src/tasks/complete-reaction.ts | 1 - src/tasks/service.ts | 6 ++---- 14 files changed, 36 insertions(+), 59 deletions(-) diff --git a/src/db/locator.ts b/src/db/locator.ts index 9dcac53..9a1be67 100644 --- a/src/db/locator.ts +++ b/src/db/locator.ts @@ -28,6 +28,20 @@ export function getDb(): Database { throw new DbNotConfiguredError('Database has not been configured. Call setDb(db) before using getDb().'); } +/** + * Resetea la instancia global de DB. Útil en tests para detectar fugas entre suites. + */ +export function resetDb(): void { + currentDb = null; +} + +/** + * Alias de resetDb() por ergonomía en tests. + */ +export function clearDb(): void { + currentDb = null; +} + /** * Ejecuta una función con la DB actual (sync o async) y devuelve su resultado. */ diff --git a/src/http/webhook-handler.ts b/src/http/webhook-handler.ts index 07842a2..2feb641 100644 --- a/src/http/webhook-handler.ts +++ b/src/http/webhook-handler.ts @@ -140,7 +140,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise { const raw = String(process.env.ADMIN_USERS || ''); @@ -61,9 +60,8 @@ export class AdminService { return [{ recipient: sender, message: '🚫 No estás autorizado para usar /admin.' }]; } - const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; + const instanceDb = getDb() as Database; // Asegurar acceso a la misma DB para AllowedGroups - try { AllowedGroups.dbInstance = instanceDb; } catch {} const raw = String(ctx.message || '').trim(); const lower = raw.toLowerCase(); @@ -231,7 +229,6 @@ export class AdminService { // /admin sync-grupos if (rest === 'sync-grupos' || rest === 'group-sync' || rest === 'syncgroups') { - 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.` }]; @@ -253,7 +250,6 @@ export class AdminService { rest.startsWith('list-all ') ) { // Asegurar acceso a la misma DB para TaskService - try { TaskService.dbInstance = instanceDb; } catch {} const DEFAULT_LIMIT = 50; let limit = DEFAULT_LIMIT; diff --git a/src/services/allowed-groups.ts b/src/services/allowed-groups.ts index 153c0a9..8a26d51 100644 --- a/src/services/allowed-groups.ts +++ b/src/services/allowed-groups.ts @@ -1,5 +1,4 @@ -import type { Database } from 'bun:sqlite'; -import { db } from '../db'; +import { getDb } from '../db/locator'; type GroupStatus = 'pending' | 'allowed' | 'blocked'; @@ -9,7 +8,6 @@ type CacheEntry = { }; export class AllowedGroups { - static dbInstance: Database = db; // Caché en memoria: group_id (JID completo) -> { status, label } private static cache = new Map(); @@ -26,7 +24,7 @@ export class AllowedGroups { private static getRow(groupId: string): { group_id: string; label: string | null; status: GroupStatus } | null { try { - const row = this.dbInstance + const row = getDb() .prepare(`SELECT group_id, label, status FROM allowed_groups WHERE group_id = ?`) .get(groupId) as { group_id?: string; label?: string | null; status?: string } | undefined; if (!row) return null; @@ -66,7 +64,7 @@ export class AllowedGroups { const row = this.getRow(gid); if (!row) { // Insertar como pending - this.dbInstance + getDb() .prepare(` INSERT INTO allowed_groups (group_id, label, status, discovered_at, updated_at, discovered_by) VALUES (?, ?, 'pending', ${this.nowExpr}, ${this.nowExpr}, ?) @@ -79,7 +77,7 @@ export class AllowedGroups { // No cambiar status existente. Solo actualizar label si se aporta y cambió. const newLabel = label ?? row.label; if (label != null && String(row.label ?? '') !== String(label)) { - this.dbInstance + getDb() .prepare(` UPDATE allowed_groups SET label = ?, updated_at = ${this.nowExpr} @@ -98,7 +96,7 @@ export class AllowedGroups { if (!gid) return false; const before = this.getRow(gid); - this.dbInstance + getDb() .prepare(` INSERT INTO allowed_groups (group_id, label, status, discovered_at, updated_at) VALUES (?, ?, ?, ${this.nowExpr}, ${this.nowExpr}) @@ -120,7 +118,7 @@ export class AllowedGroups { } static listByStatus(status: GroupStatus): Array<{ group_id: string; label: string | null }> { - const rows = this.dbInstance + const rows = getDb() .prepare( `SELECT group_id, label FROM allowed_groups WHERE status = ? ORDER BY group_id` ) diff --git a/src/services/command.ts b/src/services/command.ts index 7496317..c074eeb 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -1,5 +1,5 @@ import type { Database } from 'bun:sqlite'; -import { db, ensureUserExists } from '../db'; +import { ensureUserExists } from '../db'; import { isGroupId } from '../utils/whatsapp'; import { AllowedGroups } from './allowed-groups'; import { Metrics } from './metrics'; @@ -30,10 +30,6 @@ export type CommandOutcome = { }; export class CommandService { - static dbInstance: Database = db; - - - static async handle(context: CommandContext): Promise { const outcome = await this.handleWithOutcome(context); @@ -42,7 +38,7 @@ export class CommandService { static async handleWithOutcome(context: CommandContext): Promise { const msg = (context.message || '').trim(); - const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; + const instanceDb = getDb() as Database; if (!/^\/(tarea|t)\b/i.test(msg)) { return { responses: [], ok: true }; } @@ -66,7 +62,6 @@ export class CommandService { // Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente) if (isGroupId(context.groupId)) { - try { AllowedGroups.dbInstance = instanceDb; } catch { } const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { try { diff --git a/src/services/group-sync/repo.ts b/src/services/group-sync/repo.ts index 6d856a1..a564499 100644 --- a/src/services/group-sync/repo.ts +++ b/src/services/group-sync/repo.ts @@ -48,7 +48,6 @@ export async function upsertGroups( // Propagar sujeto a allowed_groups try { - (AllowedGroups as any).dbInstance = db; if (isCommunityFlag) { AllowedGroups.setStatus(group.id, 'blocked', group.subject); } else { diff --git a/src/services/identity.ts b/src/services/identity.ts index 671b341..93c2aee 100644 --- a/src/services/identity.ts +++ b/src/services/identity.ts @@ -1,10 +1,9 @@ -import type { Database } from 'bun:sqlite'; -import { db, ensureUserExists } from '../db'; +import { ensureUserExists } from '../db'; +import { getDb } from '../db/locator'; import { normalizeWhatsAppId } from '../utils/whatsapp'; import { Metrics } from './metrics'; export class IdentityService { - static dbInstance: Database = db; // Caché en memoria como respaldo si la tabla user_aliases no está disponible (tests o migraciones incompletas) private static readonly inMemoryAliases = new Map(); @@ -19,7 +18,7 @@ export class IdentityService { // Asegurar que el user_id numérico exista para no violar la FK (user_aliases.user_id -> users.id) try { ensureUserExists(u, this.dbInstance); } catch {} try { - this.dbInstance.prepare(` + getDb().prepare(` INSERT INTO user_aliases (alias, user_id, source, created_at, updated_at) VALUES (?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', 'now'), strftime('%Y-%m-%d %H:%M:%f', 'now')) ON CONFLICT(alias) DO UPDATE SET @@ -58,7 +57,7 @@ export class IdentityService { // Después, intentar en la base de datos try { - const row = this.dbInstance.prepare(`SELECT user_id FROM user_aliases WHERE alias = ?`).get(n) as { user_id?: string } | undefined; + const row = getDb().prepare(`SELECT user_id FROM user_aliases WHERE alias = ?`).get(n) as { user_id?: string } | undefined; if (row?.user_id) { const v = String(row.user_id); // Mantener caché en memoria para futuras resoluciones rápidas diff --git a/src/services/maintenance.ts b/src/services/maintenance.ts index 90e7c34..81bda69 100644 --- a/src/services/maintenance.ts +++ b/src/services/maintenance.ts @@ -67,7 +67,7 @@ export class MaintenanceService { 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)); - const dbi = ((instance ?? (this as any).dbInstance ?? getDb()) as Database); + const dbi = ((instance ?? getDb()) as Database); const res = dbi.prepare(` DELETE FROM group_members WHERE is_active = 0 @@ -84,7 +84,7 @@ export class MaintenanceService { */ static async reconcileAliasUsersOnce(instance?: Database): Promise { try { - const dbi = ((instance ?? (this as any).dbInstance ?? getDb()) as Database); + const dbi = ((instance ?? getDb()) as Database); const rows = dbi.prepare(`SELECT alias, user_id FROM user_aliases WHERE alias != user_id`).all() as any[]; let merged = 0; diff --git a/src/services/onboarding.ts b/src/services/onboarding.ts index 9f19e23..1f7646d 100644 --- a/src/services/onboarding.ts +++ b/src/services/onboarding.ts @@ -85,7 +85,6 @@ export function maybeEnqueueOnboardingBundle(db: Database, params: { try { const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { - try { (AllowedGroups as any).dbInstance = db; } catch {} allowed = AllowedGroups.isAllowed(gid); } } catch {} @@ -235,7 +234,6 @@ export function publishGroupCoveragePrompt(db: Database, groupId: string, ratio: try { const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { - try { (AllowedGroups as any).dbInstance = db; } catch {} if (!AllowedGroups.isAllowed(groupId)) { try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'not_allowed' }); } catch {} return; diff --git a/src/services/reminders.ts b/src/services/reminders.ts index 81c6cce..f95d465 100644 --- a/src/services/reminders.ts +++ b/src/services/reminders.ts @@ -1,5 +1,4 @@ import type { Database } from 'bun:sqlite'; -import { db } from '../db'; import { TaskService } from '../tasks/service'; import { ResponseQueue } from './response-queue'; import { ContactsService } from './contacts'; @@ -18,7 +17,7 @@ type UserPreference = { }; export class RemindersService { - static dbInstance: Database = db; + private static _running = false; private static _timer: any = null; @@ -81,7 +80,7 @@ export class RemindersService { } static async runOnce(now: Date = new Date()): Promise { - const instanceDb = ((this as any).dbInstance ?? getDb()) as Database; + const instanceDb = getDb() as Database; const todayYMD = this.ymdInTZ(now); const nowHM = this.hmInTZ(now); const weekday = this.weekdayShortInTZ(now); // 'Mon'..'Sun' @@ -98,7 +97,6 @@ export class RemindersService { const enforce = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase() === 'enforce'; if (enforce) { try { - (AllowedGroups as any).dbInstance = instanceDb; // Evitar falsos positivos por caché obsoleta entre operaciones previas del test AllowedGroups.clearCache?.(); } catch {} diff --git a/src/services/response-queue.ts b/src/services/response-queue.ts index 8563a5a..839c434 100644 --- a/src/services/response-queue.ts +++ b/src/services/response-queue.ts @@ -1,5 +1,4 @@ import type { Database } from 'bun:sqlite'; -import { db } from '../db'; import { getDb } from '../db/locator'; import { IdentityService } from './identity'; import { normalizeWhatsAppId } from '../utils/whatsapp'; @@ -32,8 +31,7 @@ type ClaimedItem = { }; export const ResponseQueue = { - // Permite inyectar una DB distinta en tests si se necesita - dbInstance: db as Database, + // Conservamos la cola en memoria por compatibilidad, aunque no se usa para persistencia queue: [] as QueuedResponse[], @@ -65,12 +63,7 @@ export const ResponseQueue = { _cleanupRunCount: 0, getDbInstance(): Database { - const anyThis = this as any; - try { - return (anyThis.dbInstance as Database) ?? getDb(); - } catch { - return anyThis.dbInstance as Database; - } + return getDb(); }, nowIso(): string { diff --git a/src/tasks/complete-reaction.ts b/src/tasks/complete-reaction.ts index 7d49bad..73a7f8b 100644 --- a/src/tasks/complete-reaction.ts +++ b/src/tasks/complete-reaction.ts @@ -55,7 +55,6 @@ export function enqueueCompletionReactionIfEligible(db: Database, taskId: number const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode === 'enforce') { let allowed = true; - try { (AllowedGroups as any).dbInstance = db; } catch {} try { allowed = AllowedGroups.isAllowed(chatId); } catch { allowed = true; } if (!allowed) return; } diff --git a/src/tasks/service.ts b/src/tasks/service.ts index ac18e7e..abdf784 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -1,5 +1,5 @@ import type { Database } from 'bun:sqlite'; -import { db, ensureUserExists } from '../db'; +import { ensureUserExists } from '../db'; import { getDb as getGlobalDb } from '../db/locator'; import { AllowedGroups } from '../services/allowed-groups'; import { isGroupId } from '../utils/whatsapp'; @@ -20,10 +20,9 @@ type CreateAssignmentInput = { }; export class TaskService { - static dbInstance: Database = db; private static getDb(): Database { - return ((this as any).dbInstance as Database) ?? getGlobalDb(); + return getGlobalDb(); } static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number { @@ -46,7 +45,6 @@ export class TaskService { try { const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (groupIdToInsert && isGroupId(groupIdToInsert) && mode === 'enforce') { - try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {} if (!AllowedGroups.isAllowed(groupIdToInsert)) { groupIdToInsert = null; }