// ── event_versions — SQL only ────────────────────────────────────────────── // // EVENTS.md §D. Immutable: there is an insert and there are reads, and there is // deliberately no update and no delete. A run pins a version, and that pin is // what makes the run reproducible and an audit answerable after the definition // has been edited underneath it. const { query } = require('../../utils/db') const { parseJson } = require('./eventJson') const hydrate = (row) => row && { ...row, spec: parseJson(row.spec, null) } const listForDefinition = async (definitionId) => ( await query( `SELECT v.id, v.definition_id, v.version, v.published_at, v.published_by, u.username AS published_by_username FROM event_versions v LEFT JOIN users u ON u.id = v.published_by WHERE v.definition_id = ? ORDER BY v.version DESC`, [definitionId], ) ).map((row) => row) const getById = async (id) => { const [row] = await query('SELECT * FROM event_versions WHERE id = ?', [id]) return hydrate(row) } /** * The next version number for a definition. * * Read separately and then INSERTed, which is a read-then-write — and it is safe * only because `UNIQUE (definition_id, version)` is behind it. Two publishes * racing for version 4 is one 1062 the caller reports, not two rows called 4. * The unique index is the mechanism; this query is the ergonomics. */ const nextVersion = async (definitionId) => { const [row] = await query( 'SELECT COALESCE(MAX(version), 0) + 1 AS next FROM event_versions WHERE definition_id = ?', [definitionId], ) return Number(row?.next || 1) } const insert = async (definitionId, version, spec, userId) => { const result = await query( 'INSERT INTO event_versions (definition_id, version, spec, published_by) VALUES (?, ?, ?, ?)', [definitionId, version, JSON.stringify(spec), userId], ) return result.insertId } /** * Record that a dry run passed against this version (Phase 6). * * A version is immutable in every respect that describes the EVENT — its spec, * its number, who published it. These two columns describe something that * happened to it afterwards, which is why they can be written at all: a pass is * a fact about a review, not a change to the plan reviewed. * * Deliberately not idempotent-checked: verifying twice stamps the second one, and * the later reviewer is the more useful answer to "who last looked at this * before it ran unattended". */ const markVerified = async (id, userId, at = new Date()) => { const result = await query( 'UPDATE event_versions SET verified_at = ?, verified_by = ? WHERE id = ?', [at, userId, id], ) return (result.affectedRows || 0) > 0 } module.exports = { listForDefinition, getById, nextVersion, insert, markVerified }