From 917b182e39ad3e11815d689f20ab4d730fa81ed6 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 10 Jul 2024 13:23:37 +0200 Subject: [PATCH] feat: granular endpoints for rundown --- .../api-data/rundown/rundown.controller.ts | 61 ++++++++++++++++--- .../src/api-data/rundown/rundown.router.ts | 7 ++- .../api-data/rundown/rundown.validation.ts | 13 +++- .../__tests__/rundownUtils.test.ts | 39 ++++++++++++ .../services/rundown-service/rundownUtils.ts | 24 ++++++-- .../runtime-service/RuntimeService.ts | 10 ++- .../ontime-controller/BackendResponse.type.ts | 6 ++ packages/types/src/index.ts | 1 + 8 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 apps/server/src/services/rundown-service/__tests__/rundownUtils.test.ts diff --git a/apps/server/src/api-data/rundown/rundown.controller.ts b/apps/server/src/api-data/rundown/rundown.controller.ts index 1eb5db5fd8..eae0bd4b41 100644 --- a/apps/server/src/api-data/rundown/rundown.controller.ts +++ b/apps/server/src/api-data/rundown/rundown.controller.ts @@ -1,4 +1,5 @@ -import { ErrorResponse, MessageResponse, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types'; +import { ErrorResponse, MessageResponse, OntimeRundownEntry, RundownCached, RundownPaginated } from 'ontime-types'; +import { getErrorMessage } from 'ontime-utils'; import { Request, Response } from 'express'; @@ -13,19 +14,63 @@ import { reorderEvent, swapEvents, } from '../../services/rundown-service/RundownService.js'; -import { getNormalisedRundown, getRundown } from '../../services/rundown-service/rundownUtils.js'; -import { getErrorMessage } from 'ontime-utils'; - -export async function rundownGetAll(_req: Request, res: Response) { - const rundown = getRundown(); - res.json(rundown); -} +import { + getEventWithId, + getNormalisedRundown, + getPaginated, + getRundown, +} from '../../services/rundown-service/rundownUtils.js'; export async function rundownGetNormalised(_req: Request, res: Response) { const cachedRundown = getNormalisedRundown(); res.json(cachedRundown); } +export async function rundownGetById(req: Request, res: Response) { + const { eventId } = req.params; + + try { + const event = getEventWithId(eventId); + + if (!event) { + res.status(404).send({ message: 'Event not found' }); + return; + } + res.status(200).json(event); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).json({ message }); + } +} + +export async function rundownGetPaginated(req: Request, res: Response) { + const { limit, offset } = req.query; + + if (limit == null && offset == null) { + return res.json({ + rundown: getRundown(), + total: getRundown().length, + }); + } + + try { + let parsedOffset = Number(offset); + if (Number.isNaN(parsedOffset)) { + parsedOffset = 0; + } + let parsedLimit = Number(limit); + if (Number.isNaN(parsedLimit)) { + parsedLimit = Infinity; + } + const paginatedRundown = getPaginated(parsedOffset, parsedLimit); + + res.status(200).json(paginatedRundown); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).json({ message }); + } +} + export async function rundownPost(req: Request, res: Response) { if (failEmptyObjects(req.body, res)) { return; diff --git a/apps/server/src/api-data/rundown/rundown.router.ts b/apps/server/src/api-data/rundown/rundown.router.ts index 024be86a28..e8f1f59707 100644 --- a/apps/server/src/api-data/rundown/rundown.router.ts +++ b/apps/server/src/api-data/rundown/rundown.router.ts @@ -5,8 +5,9 @@ import { rundownApplyDelay, rundownBatchPut, rundownDelete, - rundownGetAll, + rundownGetById, rundownGetNormalised, + rundownGetPaginated, rundownPost, rundownPut, rundownReorder, @@ -16,6 +17,7 @@ import { paramsMustHaveEventId, rundownArrayOfIds, rundownBatchPutValidator, + rundownGetPaginatedQueryParams, rundownPostValidator, rundownPutValidator, rundownReorderValidator, @@ -24,8 +26,9 @@ import { export const router = express.Router(); -router.get('/', rundownGetAll); // not used in Ontime frontend +router.get('/', rundownGetPaginatedQueryParams, rundownGetPaginated); // not used in Ontime frontend router.get('/normalised', rundownGetNormalised); +router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend router.post('/', rundownPostValidator, rundownPost); diff --git a/apps/server/src/api-data/rundown/rundown.validation.ts b/apps/server/src/api-data/rundown/rundown.validation.ts index ab539bd9b8..2de741ff95 100644 --- a/apps/server/src/api-data/rundown/rundown.validation.ts +++ b/apps/server/src/api-data/rundown/rundown.validation.ts @@ -1,4 +1,4 @@ -import { body, param, validationResult } from 'express-validator'; +import { body, param, query, validationResult } from 'express-validator'; import { Request, Response, NextFunction } from 'express'; export const rundownPostValidator = [ @@ -75,3 +75,14 @@ export const rundownArrayOfIds = [ next(); }, ]; + +export const rundownGetPaginatedQueryParams = [ + query('offset').isNumeric().optional(), + query('limit').isNumeric().optional(), + + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + next(); + }, +]; diff --git a/apps/server/src/services/rundown-service/__tests__/rundownUtils.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownUtils.test.ts new file mode 100644 index 0000000000..e917d8f6a2 --- /dev/null +++ b/apps/server/src/services/rundown-service/__tests__/rundownUtils.test.ts @@ -0,0 +1,39 @@ +import { OntimeRundown } from 'ontime-types'; +import { getPaginated } from '../rundownUtils.js'; + +describe('getPaginated', () => { + // mock cache so we dont run data functions + beforeAll(() => { + vi.mock('../rundownCache.js', () => ({})); + }); + + // @ts-expect-error -- we know this is not correct, but good enough for the test + const getData = () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as OntimeRundown; + + it('should return the correct paginated rundown', () => { + const offset = 0; + const limit = 1; + const result = getPaginated(offset, limit, getData); + + expect(result.rundown).toHaveLength(1); + expect(result.total).toBe(10); + }); + + it('should handle overflows', () => { + const offset = 0; + const limit = 20; + const result = getPaginated(offset, limit, getData); + + expect(result.rundown).toHaveLength(10); + expect(result.total).toBe(10); + }); + + it('should handle out of range', () => { + const offset = 11; + const limit = Infinity; + const result = getPaginated(offset, limit, getData); + + expect(result.rundown).toHaveLength(0); + expect(result.total).toBe(10); + }); +}); diff --git a/apps/server/src/services/rundown-service/rundownUtils.ts b/apps/server/src/services/rundown-service/rundownUtils.ts index b060af91f4..c1f79f4a75 100644 --- a/apps/server/src/services/rundown-service/rundownUtils.ts +++ b/apps/server/src/services/rundown-service/rundownUtils.ts @@ -1,4 +1,4 @@ -import { OntimeEvent, OntimeRundown, isOntimeEvent, RundownCached } from 'ontime-types'; +import { OntimeEvent, OntimeRundown, isOntimeEvent, RundownCached, OntimeRundownEntry } from 'ontime-types'; import * as cache from './rundownCache.js'; @@ -53,9 +53,9 @@ export function getEventAtIndex(eventIndex: number): OntimeEvent | undefined { * @param {string} eventId * @return {object | undefined} */ -export function getEventWithId(eventId: string): OntimeEvent | undefined { - const timedEvents = getTimedEvents(); - return timedEvents.find((event) => event.id === eventId); +export function getEventWithId(eventId: string): OntimeRundownEntry | undefined { + const rundown = getRundown(); + return rundown.find((event) => event.id === eventId); } /** @@ -117,3 +117,19 @@ export function findNext(currentEventId?: string): OntimeEvent | null { const nextEvent = timedEvents.at(newIndex); return nextEvent ?? null; } + +/** + * Returns a paginated rundown + * Exposes a getter function for the rundown for testing + */ +export function getPaginated( + offset: number, + limit: number, + source = getRundown, +): { rundown: OntimeRundownEntry[]; total: number } { + const rundown = source(); + return { + rundown: rundown.slice(Math.min(offset, rundown.length), Math.min(offset + limit, rundown.length)), + total: rundown.length, + }; +} diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 643ed3eb12..43a45a55ba 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -1,5 +1,6 @@ import { EndAction, + isOntimeEvent, LogOrigin, MaybeNumber, OntimeEvent, @@ -225,6 +226,9 @@ class RuntimeService { } // load stuff again, but keep running if our events still exist const eventNow = getEventWithId(state.eventNow.id); + if (!isOntimeEvent(eventNow)) { + return; + } const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id; if (onlyChangedNow) { runtimeState.reload(eventNow); @@ -272,7 +276,7 @@ class RuntimeService { */ startById(eventId: string): boolean { const event = getEventWithId(eventId); - if (!event) { + if (!event || !isOntimeEvent(event)) { return false; } const loaded = this.loadEvent(event); @@ -323,7 +327,7 @@ class RuntimeService { */ loadById(eventId: string): boolean { const event = getEventWithId(eventId); - if (!event) { + if (!event || !isOntimeEvent(event)) { return false; } return this.loadEvent(event); @@ -529,7 +533,7 @@ class RuntimeService { // the db would have to change for the event not to exist // we do not kow the reason for the crash, so we check anyway const event = getEventWithId(selectedEventId); - if (!event) { + if (!event || !isOntimeEvent(event)) { return; } diff --git a/packages/types/src/api/ontime-controller/BackendResponse.type.ts b/packages/types/src/api/ontime-controller/BackendResponse.type.ts index 7d805bf9bc..0a4db74802 100644 --- a/packages/types/src/api/ontime-controller/BackendResponse.type.ts +++ b/packages/types/src/api/ontime-controller/BackendResponse.type.ts @@ -1,4 +1,5 @@ import type { OSCSettings } from '../../definitions/core/OscSettings.type.js'; +import type { OntimeRundown } from '../../definitions/core/Rundown.type.js'; export type NetworkInterface = { name: string; @@ -32,3 +33,8 @@ export type MessageResponse = { export type ErrorResponse = MessageResponse; export type AuthenticationStatus = 'authenticated' | 'not_authenticated' | 'pending'; + +export type RundownPaginated = { + rundown: OntimeRundown; + total: number; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 68a2dbe5af..a280d73d20 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -49,6 +49,7 @@ export type { ErrorResponse, ProjectFileListResponse, MessageResponse, + RundownPaginated, } from './api/ontime-controller/BackendResponse.type.js'; export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';