refactor: eliminar dbInstance y añadir resetDb/clearDb, usar getDb()

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 1 month ago
parent 5c6ca072e4
commit e415a26442

@ -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.
*/

@ -140,7 +140,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
// Etapa 2: Descubrimiento seguro de grupos (modo 'discover')
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = db; } catch {}
const gatingMode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode === 'discover') {
try {
@ -149,7 +148,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
.get(remoteJid);
if (!exists) {
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
@ -168,7 +166,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
} catch {
// Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente
try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
try {
@ -189,7 +186,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
// Etapa 3: Gating en modo 'enforce' — ignorar mensajes de grupos no permitidos
if (isGroupId(remoteJid)) {
try { AllowedGroups.dbInstance = db; } catch {}
const gatingMode2 = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (gatingMode2 === 'enforce') {
try {
@ -206,8 +202,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
// Manejo de comandos de administración (/admin) antes de cualquier otra lógica de grupo
if (messageTextTrimmed.startsWith('/admin')) {
try { AdminService.dbInstance = db; } catch {}
try { AllowedGroups.dbInstance = db; } catch {}
const adminResponses = await AdminService.handle({
sender: normalizedSenderId,
groupId: remoteJid,
@ -262,8 +256,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
|| [];
// Asegurar que CommandService y TaskService usen la misma DB (tests/producción)
CommandService.dbInstance = db;
TaskService.dbInstance = db;
// Delegar el manejo del comando
const messageId = typeof data?.key?.id === 'string' ? data.key.id : null;
@ -299,7 +291,6 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
if (scope !== 'all' && !isGroup) return;
// Respetar gating 'enforce'
try { AllowedGroups.dbInstance = db; } catch {}
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (mode === 'enforce' && isGroup) {
try {

@ -189,7 +189,6 @@ export class WebhookServer {
await Migrator.migrateToLatest(this.dbInstance);
// Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort)
try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
try { AllowedGroups.seedFromEnv(); } catch {}
const PORT = process.env.PORT || '3007';

@ -1,5 +1,4 @@
import type { Database } from 'bun:sqlite';
import { db } from '../db';
import { AllowedGroups } from './allowed-groups';
import { GroupSyncService } from './group-sync';
import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
@ -17,7 +16,7 @@ type AdminContext = {
type AdminResponse = { recipient: string; message: string };
export class AdminService {
static dbInstance: Database = db;
private static admins(): Set<string> {
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;

@ -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<string, CacheEntry>();
@ -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`
)

@ -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<CommandResponse[]> {
const outcome = await this.handleWithOutcome(context);
@ -42,7 +38,7 @@ export class CommandService {
static async handleWithOutcome(context: CommandContext): Promise<CommandOutcome> {
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 {

@ -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 {

@ -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<string, string>();
@ -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

@ -67,7 +67,7 @@ export class MaintenanceService {
static async cleanupInactiveMembersOnce(instance?: Database, retentionDays: number = this.retentionDays): Promise<number> {
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<number> {
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;

@ -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;

@ -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<void> {
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 {}

@ -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 {

@ -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;
}

@ -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;
}

Loading…
Cancel
Save