refactor(rundown): extract rundown rename/delete/duplicate into service layer

Moves the logic previously inlined in the router handlers into
rundown.service.ts so other consumers (such as the MCP server) can reuse
it without duplication. Status codes and error messages are unchanged.

https://claude.ai/code/session_01V27pYyjw2PGSWy7wNtiWKj
This commit is contained in:
Claude
2026-06-11 05:06:37 +00:00
committed by Carlos Valente
parent 5be596b83e
commit e2a2ea1d24
2 changed files with 48 additions and 48 deletions
@@ -16,7 +16,7 @@ import {
deleteAllEntries, deleteAllEntries,
deleteEntries, deleteEntries,
deleteRundown, deleteRundown,
duplicateRundown, duplicateExistingRundown,
editEntry, editEntry,
groupEntries, groupEntries,
loadRundown, loadRundown,
@@ -107,7 +107,7 @@ router.post(
paramsWithId, paramsWithId,
async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => { async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
try { try {
const projectRundowns = await duplicateRundown(req.params.id); const projectRundowns = await duplicateExistingRundown(req.params.id);
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
} catch (error) { } catch (error) {
const message = getErrorMessage(error); const message = getErrorMessage(error);
@@ -16,7 +16,7 @@ import {
isOntimeEvent, isOntimeEvent,
isOntimeGroup, isOntimeGroup,
} from 'ontime-types'; } from 'ontime-types';
import { customFieldLabelToKey, generateId, getInsertAfterId, resolveInsertParent } from 'ontime-utils'; import { customFieldLabelToKey, getInsertAfterId, resolveInsertParent } from 'ontime-utils';
import { sendRefetch } from '../../adapters/WebsocketAdapter.js'; import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
@@ -34,7 +34,7 @@ import {
updateBackgroundRundown, updateBackgroundRundown,
} from './rundown.dao.js'; } from './rundown.dao.js';
import type { RundownMetadata } from './rundown.types.js'; import type { RundownMetadata } from './rundown.types.js';
import { generateEvent, getIntegerAndFraction, hasChanges } from './rundown.utils.js'; import { duplicateRundown, generateEvent, getIntegerAndFraction, hasChanges } from './rundown.utils.js';
/** /**
* creates a new entry with given data * creates a new entry with given data
@@ -643,7 +643,7 @@ export function isCurrentRundown(id: string) {
/** /**
* @throws if the provided id does not exist * @throws if the provided id does not exist
*/ */
export function loadRundown(id: string) { export async function loadRundown(id: string) {
const dataProvider = getDataProvider(); const dataProvider = getDataProvider();
if (isCurrentRundown(id)) { if (isCurrentRundown(id)) {
return dataProvider.getProjectRundowns(); return dataProvider.getProjectRundowns();
@@ -651,7 +651,7 @@ export function loadRundown(id: string) {
const rundown = dataProvider.getRundown(id); const rundown = dataProvider.getRundown(id);
const customField = dataProvider.getCustomFields(); const customField = dataProvider.getCustomFields();
initRundown(rundown, customField); await initRundown(rundown, customField);
return dataProvider.getProjectRundowns(); return dataProvider.getProjectRundowns();
} }
@@ -659,7 +659,11 @@ export function loadRundown(id: string) {
* Sets a new rundown in the cache * Sets a new rundown in the cache
* and marks it as the currently loaded one * and marks it as the currently loaded one
*/ */
export function initRundown(rundown: Readonly<Rundown>, customFields: Readonly<CustomFields>, reload: boolean = false) { export async function initRundown(
rundown: Readonly<Rundown>,
customFields: Readonly<CustomFields>,
reload: boolean = false,
) {
runtimeService.stop(); runtimeService.stop();
const { rundownMetadata, revision } = rundownCache.init(rundown, customFields); const { rundownMetadata, revision } = rundownCache.init(rundown, customFields);
logger.info(LogOrigin.Server, `Switch to rundown: ${rundown.id}`); logger.info(LogOrigin.Server, `Switch to rundown: ${rundown.id}`);
@@ -690,71 +694,67 @@ export async function createNewRundown(title: string) {
} }
/** /**
* duplicate a rundown * Renames an existing rundown
* @throws * @throws if the provided id does not exist
*/
export async function duplicateRundown(id: string) {
const dataProvider = getDataProvider();
const rundown = dataProvider.getRundown(id);
const newRundownId = generateId();
const newRundown: Rundown = structuredClone(rundown);
newRundown.id = newRundownId;
newRundown.title = `Copy of ${rundown.title}`;
newRundown.revision = 0;
const newProjectRundowns = await dataProvider.setRundown(newRundownId, newRundown);
setImmediate(() => {
sendRefetch(RefetchKey.ProjectRundowns);
});
return newProjectRundowns;
}
/**
* rename a rundown
* @throws
*/ */
export async function renameRundown(id: string, title: string) { export async function renameRundown(id: string, title: string) {
const dataProvider = getDataProvider(); const dataProvider = getDataProvider();
const rundown = dataProvider.getRundown(id); const rundown = dataProvider.getRundown(id);
const newProjectRundowns = await dataProvider.setRundown(rundown.id, { ...rundown, title });
await dataProvider.setRundown(id, { ...rundown, title });
/** /**
* If we are modifying the loaded rundown we re-init it * If loaded we re-init the rundown
* This is likely over-kill but the simplest way to ensure state consistency * This is likely over-kill but the simplest way to ensure state consistency
*/ */
if (isCurrentRundown(id)) { if (isCurrentRundown(id)) {
const rundown = dataProvider.getRundown(id); await initRundown(dataProvider.getRundown(id), dataProvider.getCustomFields());
const customField = dataProvider.getCustomFields();
initRundown(rundown, customField);
} else { } else {
setImmediate(() => { setImmediate(() => {
sendRefetch(RefetchKey.ProjectRundowns); sendRefetch(RefetchKey.ProjectRundowns);
}); });
} }
return newProjectRundowns; return dataProvider.getProjectRundowns();
} }
/** /**
* delete a rundown * Duplicates an existing rundown without making it the loaded one
* @throws * @throws if the provided id does not exist
*/ */
export async function deleteRundown(id: string) { export async function duplicateExistingRundown(id: string) {
if (isCurrentRundown(id)) throw new Error('Cannot delete loaded rundown');
const dataProvider = getDataProvider(); const dataProvider = getDataProvider();
const projectRundowns = dataProvider.getProjectRundowns(); const rundown = dataProvider.getRundown(id);
// might never hit this as it is likely covered by the case of trying to delete the loaded rundown const duplicatedRundown = duplicateRundown(rundown, `Copy of ${rundown.title}`);
if (Object.keys(projectRundowns).length <= 1) throw new Error('Cannot delete the last rundown'); await dataProvider.setRundown(duplicatedRundown.id, duplicatedRundown);
const newProjectRundowns = await dataProvider.deleteRundown(id);
setImmediate(() => { setImmediate(() => {
sendRefetch(RefetchKey.ProjectRundowns); sendRefetch(RefetchKey.ProjectRundowns);
}); });
return newProjectRundowns; return dataProvider.getProjectRundowns();
}
/**
* Deletes a rundown
* @throws if attempting to delete the loaded rundown or the last rundown in the project
*/
export async function deleteRundown(id: string) {
if (isCurrentRundown(id)) {
throw new Error('Cannot delete loaded rundown');
}
const dataProvider = getDataProvider();
if (Object.keys(dataProvider.getProjectRundowns()).length <= 1) {
throw new Error('Cannot delete the last rundown');
}
const projectRundowns = await dataProvider.deleteRundown(id);
setImmediate(() => {
sendRefetch(RefetchKey.ProjectRundowns);
});
return projectRundowns;
} }