import type { Request, Response, Router } from 'express'; import express from 'express'; import { matchedData } from 'express-validator'; import { ErrorResponse, OntimeEntry, ProjectRundowns, ProjectRundownsList, RenumberCues, Rundown, RundownImportPayload, } from 'ontime-types'; import { getErrorMessage } from 'ontime-utils'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { paramsWithId } from '../validation-utils/validationFunction.js'; import { getCurrentRundown, getProcessedRundown } from './rundown.dao.js'; import { addEntry, applyDelay, batchEditEntries, cloneEntry, applyImportToRundown, createNewRundown, createRundownFromImport, deleteAllEntries, deleteEntries, deleteRundown, duplicateExistingRundown, editEntry, groupEntries, loadRundown, renameRundown, renumberEntries, reorderEntry, swapEvents, ungroupEntries, } from './rundown.service.js'; import { normalisedToRundownArray } from './rundown.utils.js'; import { clonePostValidator, entryBatchPutValidator, entryPostValidator, entryPutValidator, entryRenumberValidator, entryReorderValidator, entrySwapValidator, rundownArrayOfIds, rundownImportValidator, rundownPatchValidator, rundownPostValidator, } from './rundown.validation.js'; export const router: Router = express.Router(); // #region operations on project rundowns ========================= /** * Returns all rundowns in the project */ router.get('/', (_req: Request, res: Response) => { const projectRundowns = getDataProvider().getProjectRundowns(); res.json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); }); /** * Returns the current rundown */ router.get('/current', (_req: Request, res: Response) => { const rundown = getCurrentRundown(); res.json(rundown); }); /** * Returns a given rundown in its normalised client shape */ router.get('/:id', paramsWithId, (req: Request, res: Response) => { try { const rundown = getProcessedRundown(req.params.id); res.json(rundown); } catch (error) { const message = getErrorMessage(error); res.status(404).send({ message }); } }); /** * Loads a given rundown */ router.post('/:id/load', paramsWithId, async (req: Request, res: Response) => { try { const projectRundowns = await loadRundown(req.params.id); 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 projectRundowns = await createNewRundown(req.body.title); 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 projectRundowns = await duplicateExistingRundown(req.params.id); 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', rundownPatchValidator, async (req: Request, res: Response) => { try { const projectRundowns = await renameRundown(req.params.id, req.body.title); res.status(200).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 { const newProjectRundowns = await deleteRundown(req.params.id); res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(newProjectRundowns) }); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }); /** * Applies an imported rundown: override or merge into an existing rundown, or create a new one. */ router.post( '/import', rundownImportValidator, async (req: Request, res: Response) => { try { const { mode, targetRundownId, rundown, customFields, providedFields } = matchedData(req); let projectRundowns: ProjectRundowns; if (mode === 'new') { projectRundowns = await createRundownFromImport(rundown, customFields); } else { // the validator guarantees this for override/merge, the guard narrows the type and adds defence in depth if (!targetRundownId) { throw new Error('targetRundownId is required when mode is override or merge'); } projectRundowns = await applyImportToRundown( mode, targetRundownId, rundown, customFields, providedFields ?? { event: [], custom: [] }, ); } res.status(200).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) }); } 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, async (req: Request, res: Response) => { try { const newEvent = await addEntry(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const event = await editEntry(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const rundown = await batchEditEntries(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const { entryId, destinationId, order } = req.body; const rundown = await reorderEntry(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const rundown = await applyDelay(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const rundown = await swapEvents(req.params.rundownId, 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, clonePostValidator, async (req: Request, res: Response) => { try { const rundown = await cloneEntry(req.params.rundownId, req.params.id, { before: req.body?.before, after: req.body?.after, }); 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, async (req: Request, res: Response) => { try { const rundown = await groupEntries(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const rundown = await ungroupEntries(req.params.rundownId, 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, async (req: Request, res: Response) => { try { const rundown = await deleteEntries(req.params.rundownId, 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', async (req: Request, res: Response) => { try { const rundown = await deleteAllEntries(req.params.rundownId); 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/renumber', entryRenumberValidator, async (req: Request, res: Response) => { try { const { ids, prefix, start, increment } = matchedData(req); const rundown = await renumberEntries(req.params.rundownId, ids, prefix, start, increment); res.status(200).send(rundown); } catch (error) { const message = getErrorMessage(error); res.status(400).send({ message }); } }, ); // #endregion operations on rundown entries =======================