feat: aplicar fallback de DB: parámetro → .dbInstance → getDb()

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

@ -6,6 +6,7 @@ import { normalizeWhatsAppId, isGroupId } from '../utils/whatsapp';
import { Metrics } from './metrics'; import { Metrics } from './metrics';
import { TaskService } from '../tasks/service'; import { TaskService } from '../tasks/service';
import { codeId, formatDDMM } from '../utils/formatting'; import { codeId, formatDDMM } from '../utils/formatting';
import { getDb } from '../db/locator';
type AdminContext = { type AdminContext = {
sender: string; // normalized user id (digits only) 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.' }]; 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 // 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 raw = String(ctx.message || '').trim();
const lower = raw.toLowerCase(); const lower = raw.toLowerCase();
@ -109,18 +111,18 @@ export class AdminService {
if (!isGroupId(ctx.groupId)) { if (!isGroupId(ctx.groupId)) {
return [{ recipient: sender, message: ' Este comando se debe usar dentro de un grupo.' }]; return [{ recipient: sender, message: ' Este comando se debe usar dentro de un grupo.' }];
} }
this.dbInstance.transaction(() => { instanceDb.transaction(() => {
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE groups UPDATE groups
SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now') SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE id = ? WHERE id = ?
`).run(ctx.groupId); `).run(ctx.groupId);
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE calendar_tokens UPDATE calendar_tokens
SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now') SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE group_id = ? AND revoked_at IS NULL WHERE group_id = ? AND revoked_at IS NULL
`).run(ctx.groupId); `).run(ctx.groupId);
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE group_members UPDATE group_members
SET is_active = 0 SET is_active = 0
WHERE group_id = ? AND is_active = 1 WHERE group_id = ? AND is_active = 1
@ -136,18 +138,18 @@ export class AdminService {
if (!isGroupId(arg)) { if (!isGroupId(arg)) {
return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }];
} }
this.dbInstance.transaction(() => { instanceDb.transaction(() => {
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE groups UPDATE groups
SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now') SET active = 0, archived = 1, last_verified = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE id = ? WHERE id = ?
`).run(arg); `).run(arg);
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE calendar_tokens UPDATE calendar_tokens
SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now') SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE group_id = ? AND revoked_at IS NULL WHERE group_id = ? AND revoked_at IS NULL
`).run(arg); `).run(arg);
this.dbInstance.prepare(` instanceDb.prepare(`
UPDATE group_members UPDATE group_members
SET is_active = 0 SET is_active = 0
WHERE group_id = ? AND is_active = 1 WHERE group_id = ? AND is_active = 1
@ -162,10 +164,10 @@ export class AdminService {
if (!isGroupId(ctx.groupId)) { if (!isGroupId(ctx.groupId)) {
return [{ recipient: sender, message: ' Este comando se debe usar dentro de un grupo.' }]; return [{ recipient: sender, message: ' Este comando se debe usar dentro de un grupo.' }];
} }
this.dbInstance.transaction(() => { instanceDb.transaction(() => {
this.dbInstance.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(ctx.groupId); instanceDb.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(ctx.groupId);
this.dbInstance.prepare(`DELETE FROM groups WHERE id = ?`).run(ctx.groupId); instanceDb.prepare(`DELETE FROM groups WHERE id = ?`).run(ctx.groupId);
try { this.dbInstance.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(ctx.groupId); } catch {} 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}` }]; return [{ recipient: sender, message: `🗑️ Grupo borrado y datos asociados eliminados: ${ctx.groupId}` }];
} }
@ -176,10 +178,10 @@ export class AdminService {
if (!isGroupId(arg)) { if (!isGroupId(arg)) {
return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }]; return [{ recipient: sender, message: '⚠️ Debes indicar un group_id válido terminado en @g.us' }];
} }
this.dbInstance.transaction(() => { instanceDb.transaction(() => {
this.dbInstance.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(arg); instanceDb.prepare(`DELETE FROM tasks WHERE group_id = ?`).run(arg);
this.dbInstance.prepare(`DELETE FROM groups WHERE id = ?`).run(arg); instanceDb.prepare(`DELETE FROM groups WHERE id = ?`).run(arg);
try { this.dbInstance.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(arg); } catch {} try { instanceDb.prepare(`DELETE FROM allowed_groups WHERE group_id = ?`).run(arg); } catch {}
})(); })();
return [{ recipient: sender, message: `🗑️ Grupo borrado y datos asociados eliminados: ${arg}` }]; return [{ recipient: sender, message: `🗑️ Grupo borrado y datos asociados eliminados: ${arg}` }];
} }
@ -229,7 +231,7 @@ export class AdminService {
// /admin sync-grupos // /admin sync-grupos
if (rest === 'sync-grupos' || rest === 'group-sync' || rest === 'syncgroups') { 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 { try {
const r = await GroupSyncService.syncGroups(true); const r = await GroupSyncService.syncGroups(true);
return [{ recipient: sender, message: `✅ Sync de grupos ejecutado: ${r.added} añadidos, ${r.updated} actualizados.` }]; 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 ') rest.startsWith('list-all ')
) { ) {
// Asegurar acceso a la misma DB para TaskService // Asegurar acceso a la misma DB para TaskService
try { TaskService.dbInstance = this.dbInstance; } catch {} try { TaskService.dbInstance = instanceDb; } catch {}
const DEFAULT_LIMIT = 50; const DEFAULT_LIMIT = 50;
let limit = DEFAULT_LIMIT; let limit = DEFAULT_LIMIT;

@ -5,6 +5,7 @@ import { AllowedGroups } from './allowed-groups';
import { Metrics } from './metrics'; import { Metrics } from './metrics';
import { route as routeCommand } from './commands'; import { route as routeCommand } from './commands';
import { ACTION_ALIASES } from './commands/shared'; import { ACTION_ALIASES } from './commands/shared';
import { getDb } from '../db/locator';
type CommandContext = { type CommandContext = {
sender: string; // normalized user id (digits only), but accept raw too sender: string; // normalized user id (digits only), but accept raw too
@ -41,6 +42,7 @@ export class CommandService {
static async handleWithOutcome(context: CommandContext): Promise<CommandOutcome> { static async handleWithOutcome(context: CommandContext): Promise<CommandOutcome> {
const msg = (context.message || '').trim(); const msg = (context.message || '').trim();
const instanceDb = ((this as any).dbInstance ?? getDb()) as Database;
if (!/^\/(tarea|t)\b/i.test(msg)) { if (!/^\/(tarea|t)\b/i.test(msg)) {
return { responses: [], ok: true }; return { responses: [], ok: true };
} }
@ -49,14 +51,14 @@ export class CommandService {
try { try {
let usersTableExists = false; let usersTableExists = false;
try { 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; usersTableExists = !!row;
} catch {} } catch {}
if (usersTableExists) { if (usersTableExists) {
const ensured = ensureUserExists(context.sender, this.dbInstance); const ensured = ensureUserExists(context.sender, instanceDb);
if (ensured) { if (ensured) {
try { 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 {} } catch {}
} }
} }
@ -64,7 +66,7 @@ export class CommandService {
// Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente) // Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente)
if (isGroupId(context.groupId)) { 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(); const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (mode === 'enforce') { if (mode === 'enforce') {
try { try {
@ -79,7 +81,7 @@ export class CommandService {
} }
try { try {
const routed = await routeCommand(context, { db: this.dbInstance }); const routed = await routeCommand(context, { db: instanceDb });
const responses = routed ?? []; const responses = routed ?? [];
// Clasificación explícita del outcome (evita lógica en server) // Clasificación explícita del outcome (evita lógica en server)

@ -1,5 +1,6 @@
import type { Database } from 'bun:sqlite'; 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 { normalizeWhatsAppId } from '../utils/whatsapp';
import { Metrics } from './metrics'; import { Metrics } from './metrics';
import { IdentityService } from './identity'; import { IdentityService } from './identity';
@ -43,8 +44,14 @@ type EvolutionGroup = {
}; };
export class GroupSyncService { export class GroupSyncService {
// Static property for DB instance injection (defaults to global db) // Static property for DB instance injection (with fallback to global locator)
static dbInstance: Database = db; 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) // In-memory cache for active groups (made public for tests)
public static readonly activeGroupsCache = new Map<string, string>(); // groupId -> groupName public static readonly activeGroupsCache = new Map<string, string>(); // groupId -> groupName

@ -1,5 +1,5 @@
import type { Database } from 'bun:sqlite'; import type { Database } from 'bun:sqlite';
import { db } from '../db'; import { getDb } from '../db/locator';
import { toIsoSqlUTC } from '../utils/datetime'; import { toIsoSqlUTC } from '../utils/datetime';
export class MaintenanceService { export class MaintenanceService {
@ -64,9 +64,10 @@ export class MaintenanceService {
} }
} }
static async cleanupInactiveMembersOnce(instance: Database = db, retentionDays: number = this.retentionDays): Promise<number> { static async cleanupInactiveMembersOnce(instance?: Database, retentionDays: number = this.retentionDays): Promise<number> {
if (retentionDays <= 0) return 0; if (retentionDays <= 0) return 0;
const threshold = toIsoSqlUTC(new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)); 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(` const res = instance.prepare(`
DELETE FROM group_members DELETE FROM group_members
WHERE is_active = 0 WHERE is_active = 0
@ -81,8 +82,9 @@ export class MaintenanceService {
* en todas las tablas relevantes, basándose en user_aliases. * en todas las tablas relevantes, basándose en user_aliases.
* Devuelve el número de alias procesados. * Devuelve el número de alias procesados.
*/ */
static async reconcileAliasUsersOnce(instance: Database = db): Promise<number> { static async reconcileAliasUsersOnce(instance?: Database): Promise<number> {
try { 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[]; const rows = instance.prepare(`SELECT alias, user_id FROM user_aliases WHERE alias != user_id`).all() as any[];
let merged = 0; let merged = 0;

@ -8,6 +8,7 @@ import { ICONS } from '../utils/icons';
import { codeId, formatDDMM, bold, italic } from '../utils/formatting'; import { codeId, formatDDMM, bold, italic } from '../utils/formatting';
import { AllowedGroups } from './allowed-groups'; import { AllowedGroups } from './allowed-groups';
import { Metrics } from './metrics'; import { Metrics } from './metrics';
import { getDb } from '../db/locator';
type UserPreference = { type UserPreference = {
user_id: string; user_id: string;
@ -80,13 +81,14 @@ export class RemindersService {
} }
static async runOnce(now: Date = new Date()): Promise<void> { static async runOnce(now: Date = new Date()): Promise<void> {
const instanceDb = ((this as any).dbInstance ?? getDb()) as Database;
const todayYMD = this.ymdInTZ(now); const todayYMD = this.ymdInTZ(now);
const nowHM = this.hmInTZ(now); const nowHM = this.hmInTZ(now);
const weekday = this.weekdayShortInTZ(now); // 'Mon'..'Sun' const weekday = this.weekdayShortInTZ(now); // 'Mon'..'Sun'
const graceRaw = Number(process.env.REMINDERS_GRACE_MINUTES); const graceRaw = Number(process.env.REMINDERS_GRACE_MINUTES);
const GRACE_MIN = Number.isFinite(graceRaw) && graceRaw >= 0 ? Math.min(Math.floor(graceRaw), 180) : 60; 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 SELECT user_id, reminder_freq, reminder_time, last_reminded_on
FROM user_preferences FROM user_preferences
WHERE reminder_freq IN ('daily', 'weekly', 'weekdays') 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'; const enforce = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase() === 'enforce';
if (enforce) { if (enforce) {
try { try {
(AllowedGroups as any).dbInstance = this.dbInstance; (AllowedGroups as any).dbInstance = instanceDb;
// Evitar falsos positivos por caché obsoleta entre operaciones previas del test // Evitar falsos positivos por caché obsoleta entre operaciones previas del test
AllowedGroups.clearCache?.(); AllowedGroups.clearCache?.();
} catch {} } catch {}
@ -228,7 +230,7 @@ export class RemindersService {
}]); }]);
// Marcar como enviado hoy // Marcar como enviado hoy
this.dbInstance.prepare(` instanceDb.prepare(`
INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) 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'), VALUES (?, COALESCE((SELECT reminder_freq FROM user_preferences WHERE user_id = ?), 'daily'),
COALESCE((SELECT reminder_time FROM user_preferences WHERE user_id = ?), '08:30'), COALESCE((SELECT reminder_time FROM user_preferences WHERE user_id = ?), '08:30'),

Loading…
Cancel
Save