// Pure reshaping/annotation helpers for the moderation dashboard, deliberately // free of any DB (or other side-effecting) imports so they can be unit-tested // without opening a database pool. moderation.model re-exports these. function zeroCounts() { return { ban: 0, kick: 0, mute: 0, warn: 0 } } // Tag each action as automated (staff is the bot) and fold the joined // user_identities columns into a linked_account object. The string coercion // matters — snowflakes can arrive as number or string from different columns. function annotate(rows, appId) { return rows.map((r) => { const isAutomated = appId != null && String(r.staff_user_id) === String(appId) return { ...r, is_automated: isAutomated, linked_account: r.target_site_user_id ? { id: r.target_site_user_id, username: r.target_site_username } : null, } }) } // Fold the per-type window rows into the { windows: { '24h', '7d', '30d' } } // shape the dashboard tiles consume, zero-filling any type with no rows. function reshapeWindows(rows) { const windows = { '24h': zeroCounts(), '7d': zeroCounts(), '30d': zeroCounts() } for (const row of rows) { const t = row.action_type if (windows['24h'][t] === undefined) continue windows['24h'][t] = Number(row.d1) || 0 windows['7d'][t] = Number(row.d7) || 0 windows['30d'][t] = Number(row.d30) || 0 } return { windows } } // Pull the count for one window key ('24h'|'7d'|'30d') out of a // { d1, d7, d30 } sum row, coercing to a number and tolerating a null row. function windowValue(row, key) { if (!row) return 0 const col = key === '24h' ? row.d1 : key === '7d' ? row.d7 : row.d30 return Number(col) || 0 } module.exports = { zeroCounts, annotate, reshapeWindows, windowValue }