// SQL for the reserved-name review queue and the §2.9 approval queue. const { query } = require('../../utils/db') // ── The hide/display state on `teams` ────────────────────────────────────── async function setHidden(teamId, { hidden, reason, term }) { await query( 'UPDATE teams SET hidden = ?, hidden_reason = ?, hidden_term = ? WHERE id = ?', [hidden ? 1 : 0, hidden ? reason : null, hidden ? term || null : null, teamId], ) } /** * Record that a human has decided about this name. * * What makes a staff decision STICKY (§2.8.3). Re-screening runs on every sync, * and without this stamp an operator adding a reserved term — or simply renaming * the deployment — would re-hide a Team staff had already allowed, every fifteen * minutes, forever. */ async function markNameReviewed(teamId) { await query('UPDATE teams SET name_reviewed_at = NOW() WHERE id = ?', [teamId]) } async function setDisplayNameOverride(teamId, displayName) { await query('UPDATE teams SET display_name_override = ? WHERE id = ?', [displayName, teamId]) } /** Active teams whose name has never been screened by a human. */ async function unreviewedActive(moduleId) { return query( `SELECT id, name, hidden, hidden_reason FROM teams WHERE module_id = ? AND status = 'active' AND name_reviewed_at IS NULL`, [moduleId], ) } /** The reserved-name review queue (§2.8.3). */ async function reviewQueue() { return query( `SELECT id, name, slug, hidden_term, display_name_override, member_count, created_at FROM teams WHERE status = 'active' AND hidden = 1 AND hidden_reason = 'reserved_name' AND name_reviewed_at IS NULL ORDER BY created_at DESC`, ) } // ── team_moderation_requests (§2.9) ──────────────────────────────────────── const REQUEST_COLUMNS = ` id, team_id, action, payload, reason, requested_by, requested_username, requested_at, status, decided_by, decided_username, decided_at, decision_note` async function insertRequest({ teamId, action, payload, reason, requestedBy, requestedUsername }) { const res = await query( `INSERT INTO team_moderation_requests (team_id, action, payload, reason, requested_by, requested_username) VALUES (?, ?, ?, ?, ?, ?)`, [teamId, action, payload == null ? null : JSON.stringify(payload), reason, requestedBy, requestedUsername], ) return res.insertId } async function findRequest(id) { const rows = await query(`SELECT ${REQUEST_COLUMNS} FROM team_moderation_requests WHERE id = ?`, [id]) return rows[0] } /** The approval queue. Decided rows are kept — see §2.9 — so `status` is a filter. */ async function listRequests({ status = 'pending', limit = 100 } = {}) { const params = [] let sql = `SELECT r.${REQUEST_COLUMNS.trim().split(/,\s*/).join(', r.')}, t.name AS team_name, t.slug AS team_slug FROM team_moderation_requests r JOIN teams t ON t.id = r.team_id` if (status !== 'all') { sql += ' WHERE r.status = ?' params.push(status) } sql += ' ORDER BY r.requested_at DESC, r.id DESC LIMIT ?' params.push(limit) return query(sql, params) } /** * Decide a request, but only if it is still pending. * * The `status = 'pending'` guard is the concurrency control: two admins opening * the same queue and both clicking approve would otherwise each apply the action, * and the second would overwrite the first's record of who decided it. The caller * applies the effect only when this reports a row was actually moved. */ async function decideRequest(id, { status, decidedBy, decidedUsername, note }) { const res = await query( `UPDATE team_moderation_requests SET status = ?, decided_by = ?, decided_username = ?, decided_at = NOW(), decision_note = ? WHERE id = ? AND status = 'pending'`, [status, decidedBy, decidedUsername, note, id], ) return res.affectedRows > 0 } /** Pending requests for one team — shown on its admin page so a second is not filed. */ async function pendingForTeam(teamId) { return query( `SELECT ${REQUEST_COLUMNS} FROM team_moderation_requests WHERE team_id = ? AND status = 'pending' ORDER BY requested_at`, [teamId], ) } module.exports = { setHidden, markNameReviewed, setDisplayNameOverride, unreviewedActive, reviewQueue, insertRequest, findRequest, listRequests, decideRequest, pendingForTeam, }