const { query } = require('../../utils/db') async function getAll() { return query('SELECT `key`, value, updated_at FROM settings ORDER BY `key`') } async function get(key) { const rows = await query('SELECT value FROM settings WHERE `key` = ? LIMIT 1', [key]) return rows[0] ? rows[0].value : null } async function set(key, value, updatedBy = null) { await query( 'INSERT INTO settings (`key`, value, updated_by) VALUES (?, ?, ?) ' + 'ON DUPLICATE KEY UPDATE value = VALUES(value), updated_by = VALUES(updated_by)', [key, value, updatedBy], ) } // One row WITH its provenance. `updated_by`/`updated_at` are already stored for // every key; this is the only reader that needs them, because TEAMS.md §5.5.5 // makes the uploads acknowledgement a RECORDED consent rather than a displayed // one, and "which admin accepted it, and when" is the question that has to be // answerable afterwards. async function getRow(key) { const rows = await query( 'SELECT s.`key`, s.value, s.updated_by, s.updated_at, u.username AS updated_by_username ' + 'FROM settings s LEFT JOIN users u ON u.id = s.updated_by WHERE s.`key` = ? LIMIT 1', [key], ) return rows[0] || null } // Insert a default only if the key does not already exist. async function seedDefault(key, value) { await query('INSERT IGNORE INTO settings (`key`, value) VALUES (?, ?)', [key, value]) } /** * Take a one-shot guard, atomically. `true` means THIS caller wrote the row. * * The same `INSERT IGNORE` as `seedDefault`, and the difference is the whole * point: this one reports whether it won. A guard read with `get()` and written * later with `set()` is not a guard at all under concurrency — two processes * both read "absent" and both proceed — and this is used where proceeding twice * means seeding a rule group twice, i.e. two mails per event. * * The atomicity is the PRIMARY KEY's: exactly one INSERT can create a given * `key`, so exactly one caller sees `affectedRows === 1`. No transaction and no * lock, the same bargain `engagementWorker`'s claim makes. */ async function claim(key, value) { const res = await query('INSERT IGNORE INTO settings (`key`, value) VALUES (?, ?)', [key, value]) return Number(res && res.affectedRows) === 1 } // Delete a settings row. "Reset to defaults" for the theming/nav keys is the // *absence* of a row, not a stored copy of the defaults — see // docs/website/THEMING_AND_NAV.md §2. Deleting a key that was never set is a // no-op, so reset is idempotent. async function remove(key) { await query('DELETE FROM settings WHERE `key` = ?', [key]) } module.exports = { getAll, get, getRow, set, seedDefault, claim, remove }