const { all, one } = require('../db'); // Read-only access to the system-wide audit trail captured by db.audit(). Powers the admin // Activity & Audit Trail viewer: filtered + paginated queries, filter facets, and CSV export. const MAX_PAGE_SIZE = 200; // Build a parameterized WHERE clause from the supported filters. function buildWhere(filters = {}) { const clauses = []; const params = []; if (filters.actor) { clauses.push('al.actor_id = ?'); params.push(Number(filters.actor)); } if (filters.action) { clauses.push('al.action = ?'); params.push(filters.action); } if (filters.entity_type) { clauses.push('al.entity_type = ?'); params.push(filters.entity_type); } if (filters.from) { clauses.push('al.created_at >= ?'); params.push(String(filters.from)); } if (filters.to) { clauses.push('al.created_at <= ?'); params.push(`${String(filters.to)}￿`); } if (filters.q) { clauses.push('(al.entity_type LIKE ? OR al.entity_id LIKE ? OR al.action LIKE ? OR u.name LIKE ?)'); const like = `%${filters.q}%`; params.push(like, like, like, like); } return { where: clauses.length ? `WHERE ${clauses.join(' AND ')}` : '', params }; } const BASE = ` FROM audit_logs al LEFT JOIN users u ON u.id = al.actor_id `; // Paginated query. Returns { rows, total, page, pageSize }. function query(filters = {}, { page = 1, pageSize = 25 } = {}) { const size = Math.min(MAX_PAGE_SIZE, Math.max(1, Number(pageSize) || 25)); const current = Math.max(1, Number(page) || 1); const { where, params } = buildWhere(filters); const total = one(`SELECT COUNT(*) AS c ${BASE} ${where}`, params).c; const rows = all( `SELECT al.id, al.actor_id, u.name AS actor_name, al.action, al.entity_type, al.entity_id, al.before_json, al.after_json, al.created_at ${BASE} ${where} ORDER BY al.id DESC LIMIT ? OFFSET ?`, [...params, size, (current - 1) * size] ); return { rows, total, page: current, pageSize: size }; } // All matching rows (no pagination) — used by CSV export. Capped to avoid unbounded memory. function queryAll(filters = {}, limit = 50000) { const { where, params } = buildWhere(filters); return all( `SELECT al.id, al.actor_id, u.name AS actor_name, al.action, al.entity_type, al.entity_id, al.before_json, al.after_json, al.created_at ${BASE} ${where} ORDER BY al.id DESC LIMIT ?`, [...params, limit] ); } // Distinct values for the filter dropdowns. function facets() { return { actions: all('SELECT DISTINCT action FROM audit_logs ORDER BY action').map((r) => r.action), entity_types: all('SELECT DISTINCT entity_type FROM audit_logs ORDER BY entity_type').map((r) => r.entity_type), actors: all( `SELECT DISTINCT al.actor_id AS id, u.name AS name FROM audit_logs al LEFT JOIN users u ON u.id = al.actor_id WHERE al.actor_id IS NOT NULL ORDER BY u.name` ) }; } function csvCell(value) { const s = value === null || value === undefined ? '' : String(value); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } // Serialize rows to CSV text. function toCsv(rows) { const header = ['id', 'timestamp', 'actor', 'action', 'entity_type', 'entity_id', 'before', 'after']; const lines = [header.join(',')]; for (const r of rows) { lines.push([ r.id, r.created_at, r.actor_name || `#${r.actor_id || ''}`, r.action, r.entity_type, r.entity_id, r.before_json, r.after_json ].map(csvCell).join(',')); } return lines.join('\n'); } module.exports = { facets, query, queryAll, toCsv };