// ── event_series — SQL only ──────────────────────────────────────────────── // // EVENTS.md §D. The arc a definition may belong to. Phase 1 needed only the // reads — `event_definitions.series_id` is a foreign key and the definition save // path has to check it resolves — and Phase 4 adds the writes, because the // calendar is what makes an arc visible and a form cannot offer a value nobody // can create. // // `ordering` here places a SERIES among the others on the calendar. A // definition's place WITHIN its arc is `event_definitions.series_order`, which // is the column an editor drags; the two are deliberately different columns on // different tables and the schema comment says so. const { query } = require('../../utils/db') // `definition_count` is a correlated subquery rather than a join with a GROUP BY: // the list is a handful of rows, and the delete path needs the same number to // tell an operator what they are about to detach. const SELECT_LIST = ` SELECT s.*, (SELECT COUNT(*) FROM event_definitions d WHERE d.series_id = s.id) AS definition_count FROM event_series s ` const list = async () => query(`${SELECT_LIST} ORDER BY s.ordering, s.name, s.id`) const getById = async (id) => { const [row] = await query(`${SELECT_LIST} WHERE s.id = ?`, [id]) return row || null } const getBySlug = async (slug) => { const [row] = await query(`${SELECT_LIST} WHERE s.slug = ?`, [slug]) return row || null } const exists = async (id) => { const [row] = await query('SELECT id FROM event_series WHERE id = ?', [id]) return Boolean(row) } /** Does any OTHER series hold this slug? The uniqueness pre-check. */ const slugTaken = async (slug, exceptId = null) => { const rows = exceptId ? await query('SELECT id FROM event_series WHERE slug = ? AND id <> ?', [slug, exceptId]) : await query('SELECT id FROM event_series WHERE slug = ?', [slug]) return rows.length > 0 } const insert = async (s) => { const result = await query( `INSERT INTO event_series (name, slug, description, ordering, created_by) VALUES (?, ?, ?, ?, ?)`, [s.name, s.slug, s.description, s.ordering, s.created_by], ) return Number(result.insertId) } const update = (id, s) => query( `UPDATE event_series SET name = ?, slug = ?, description = ?, ordering = ? WHERE id = ?`, [s.name, s.slug, s.description, s.ordering, id], ) /** * A hard delete, and the one place in this feature that is one. * * A series is a label rather than authored content: nothing pins one, no run * references one, and `event_definitions.series_id` is `ON DELETE SET NULL`, so * removing a series detaches its definitions and destroys nothing. That is why * it is not archived the way a definition is — an archived label would be a * state every calendar query has to remember for no benefit. The model answers * with how many definitions were detached, so the operator learns what happened * rather than discovering it on the calendar. */ const remove = (id) => query('DELETE FROM event_series WHERE id = ?', [id]) module.exports = { list, getById, getBySlug, exists, slugTaken, insert, update, remove }