import { ErrorResponse, OntimeEntry, ProjectRundownsList, Rundown } from 'ontime-types'; import { generateId, getErrorMessage } from 'ontime-utils'; import type { Request, Response } from 'express'; import express from 'express'; import { getCurrentRundown } from './rundown.dao.js'; import { addEntry, applyDelay, batchEditEntries, cloneEntry, deleteAllEntries, deleteEntries, editEntry, groupEntries, initRundown, reorderEntry, swapEvents, ungroupEntries, } from './rundown.service.js'; import { rundownArrayOfIds, entryBatchPutValidator, entryPostValidator, rundownPostValidator, entryPutValidator, entryReorderValidator, entrySwapValidator, validateRundownMutation, } from './rundown.validation.js'; import { paramsWithId } from '../validation-utils/validationFunction.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { defaultRundown } from '../../models/dataModel.js'; import { duplicateRundown, normalisedToRundownArray } from './rundown.utils.js'; export const router = express.Router(); // #region operations on project rundowns ========================= /** * Returns all rundowns in the project */ router.get('/', async (_req: Request, res: Response) => { const projectRundowns = getDataProvider().getProjectRundowns(); res.json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); }); /** * Returns the current rundown */ router.get('/current', async (_req: Request, res: Response) => { const rundown = getCurrentRundown(); res.json(rundown); }); /** * Loads a given rundown */ router.post('/:id/load', paramsWithId, async (req: Request, res: Response) => { try { // maybe the rundown is already loaded if (req.params.id === getCurrentRundown().id) { const projectRundowns = getDataProvider().getProjectRundowns(); res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); return; } const dataProvider = getDataProvider(); const rundown = dataProvider.getRundown(req.params.id); const customField = dataProvider.getCustomFields(); await initRundown(rundown, customField); const projectRundowns = getDataProvider().getProjectRundowns(); res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }); /** * Creates a new rundown */ router.post('/', rundownPostValidator, async (req: Request, res: Response) => { try { const id = generateId(); await getDataProvider().setRundown(id, { ...defaultRundown, id, title: req.body.title }); const projectRundowns = getDataProvider().getProjectRundowns(); res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }); /** * Duplicates an existing rundown */ router.post( '/:id/duplicate', paramsWithId, async (req: Request, res: Response) => { try { const dataProvider = getDataProvider(); const rundown = dataProvider.getRundown(req.params.id); const duplicatedRundown: Rundown = duplicateRundown(rundown, `Copy of ${rundown.title}`); await dataProvider.setRundown(duplicatedRundown.id, duplicatedRundown); const projectRundowns = getDataProvider().getProjectRundowns(); res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Patches the data of an existing rundown * Currently only the title can be changed */ router.patch('/:id', paramsWithId, async (req: Request, res: Response) => { try { const dataProvider = getDataProvider(); const rundown = dataProvider.getRundown(req.params.id); if (!rundown) throw new Error(`Rundown with ID ${req.params.id} not found`); if (!req.body.title) throw new Error('No title provided'); await dataProvider.setRundown(rundown.id, { ...rundown, title: req.body.title }); /** * If loaded we re-init the rundown * This is likely over-kill but the simplest way to ensure state consistency */ if (req.params.id === getCurrentRundown().id) { const rundown = dataProvider.getRundown(req.params.id); const customField = dataProvider.getCustomFields(); await initRundown(rundown, customField); } const projectRundowns = getDataProvider().getProjectRundowns(); res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }); /** * Deletes a rundown if not loaded */ router.delete('/:id', paramsWithId, async (req: Request, res: Response) => { try { if (req.params.id === getCurrentRundown().id) { res.status(400).send({ message: 'Cannot delete loaded rundown' }); return; } const dataProvider = getDataProvider(); const projectRundowns = dataProvider.getProjectRundowns(); if (Object.keys(projectRundowns).length <= 1) { // might never hit this as it is likely covered by the case of trying to delete the loaded rundown res.status(400).send({ message: 'Cannot delete the last rundown' }); return; } await dataProvider.deleteRundown(req.params.id); const newProjectRundowns = getDataProvider().getProjectRundowns(); res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(newProjectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }); // #endregion operations on project rundowns ====================== // #region operations on rundown entries ========================== /** * Creates a new entry in a given rundown */ router.post( '/:rundownId/entry', entryPostValidator, validateRundownMutation, async (req: Request, res: Response) => { try { const newEvent = await addEntry(req.body); res.status(201).send(newEvent); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Edits an entry in a given rundown */ router.put( '/:rundownId/entry', entryPutValidator, validateRundownMutation, async (req: Request, res: Response) => { try { const event = await editEntry(req.body); res.status(200).send(event); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Edits an entry in a given rundown */ router.put( '/:rundownId/batch', entryBatchPutValidator, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await batchEditEntries(req.body.ids, req.body.data); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Reorders two entries in a rundown */ router.patch( '/:rundownId/reorder', entryReorderValidator, validateRundownMutation, async (req: Request, res: Response) => { try { const { entryId, destinationId, order } = req.body; const rundown = await reorderEntry(entryId, destinationId, order); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Applies a delay into the schedule */ router.patch( '/:rundownId/applydelay/:id', paramsWithId, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await applyDelay(req.params.id); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Swaps data between two Ontime events */ router.patch( '/:rundownId/swap', entrySwapValidator, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await swapEvents(req.body.from, req.body.to); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Clones the contents of an entry into a new one */ router.post( '/:rundownId/clone/:id', paramsWithId, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await cloneEntry(req.params.id); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Creates a group out of a list of entries */ router.post( '/:rundownId/group', rundownArrayOfIds, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await groupEntries(req.body.ids); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Dissolves a group by moving its children to the main rundown */ router.post( '/:rundownId/ungroup/:id', paramsWithId, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await ungroupEntries(req.params.id); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Deletes a list of entries by their ID */ router.delete( '/:rundownId/entries', rundownArrayOfIds, validateRundownMutation, async (req: Request, res: Response) => { try { const rundown = await deleteEntries(req.body.ids); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); /** * Deletes all entries in a given rundown */ router.delete( '/:rundownId/all', validateRundownMutation, async (_req: Request, res: Response) => { try { const rundown = await deleteAllEntries(); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); // #endregion operations on rundown entries =======================