fix(event-types): renaming a type's slug cascades to events, quotes + reminder template

Renaming an event type's slug_prefix is editable in the UI but previously
orphaned everything keyed on the old slug: existing events/quotes (their
event_type) detached, and the authored per-type pre-event reminder template
(event_reminder_<slug>) was left behind → reminders fell back to default.

updateEventType now cascades atomically when the slug changes: re-points
events.event_type + quotes.event_type old→new and renames the
event_reminder_<old> template to <new> (guarded so it never clobbers an existing
target). So a photographer can rename a type to e.g. "concert" and the edited
subject/body follow. Column check resolved before the transaction (avoids the
SQLite global-read-in-trx deadlock). Tests cover the cascade + no-clobber.
This commit is contained in:
Luca
2026-06-24 13:52:42 +02:00
parent 10d091b55e
commit 415c93a512
2 changed files with 107 additions and 1 deletions
+43 -1
View File
@@ -7,6 +7,8 @@
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { hasColumnCached } = require('../utils/schemaCache');
const logger = require('../utils/logger');
/**
* Get all event types
@@ -201,7 +203,47 @@ const updateEventType = async (id, updates) => {
updateData.updated_at = new Date();
await db('event_types').where('id', id).update(updateData);
// A slug_prefix rename must CASCADE, or it silently orphans everything keyed on
// the old slug: existing events/quotes (their event_type), and the per-type
// pre-event reminder template (event_reminder_<slug>). Re-point them so a
// rename behaves like a rename, not a detach. Atomic.
const oldSlug = eventType.slug_prefix;
const newSlug = updateData.slug_prefix;
const slugChanged = newSlug !== undefined && newSlug !== oldSlug;
if (!slugChanged) {
await db('event_types').where('id', id).update(updateData);
return getEventTypeById(id);
}
// Resolve schema lookups BEFORE opening the transaction — hasColumnCached
// reads via the global db, and a global read inside a SQLite transaction
// (single connection) deadlocks.
const quotesHasEventType = await hasColumnCached('quotes', 'event_type');
await db.transaction(async (trx) => {
await trx('event_types').where('id', id).update(updateData);
// Re-point existing documents from the old slug to the new one.
const evCount = await trx('events').where('event_type', oldSlug).update({ event_type: newSlug });
let qCount = 0;
if (quotesHasEventType) {
qCount = await trx('quotes').where('event_type', oldSlug).update({ event_type: newSlug });
}
// Carry the authored per-type reminder template along (subject/body follow the
// rename). Guard: never clobber an existing target template for the new slug.
const oldKey = `event_reminder_${oldSlug}`;
const newKey = `event_reminder_${newSlug}`;
let tplMoved = false;
const src = await trx('email_templates').where({ template_key: oldKey }).first('id');
const dst = await trx('email_templates').where({ template_key: newKey }).first('id');
if (src && !dst) {
await trx('email_templates').where({ template_key: oldKey }).update({ template_key: newKey });
tplMoved = true;
}
logger.info('Event type slug renamed — cascaded references', {
id, oldSlug, newSlug, events: evCount, quotes: qCount, reminderTemplateMoved: tplMoved,
});
});
return getEventTypeById(id);
};