Skip to content

Commit

Permalink
feat: granular endpoints for rundown
Browse files Browse the repository at this point in the history
  • Loading branch information
cpvalente committed Jul 10, 2024
1 parent 6c410c2 commit 295f5ef
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 18 deletions.
61 changes: 53 additions & 8 deletions apps/server/src/api-data/rundown/rundown.controller.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<OntimeRundown>) {
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<RundownCached>) {
const cachedRundown = getNormalisedRundown();
res.json(cachedRundown);
}

export async function rundownGetById(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
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<RundownPaginated | ErrorResponse>) {
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<OntimeRundownEntry | ErrorResponse>) {
if (failEmptyObjects(req.body, res)) {
return;
Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/api-data/rundown/rundown.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
rundownApplyDelay,
rundownBatchPut,
rundownDelete,
rundownGetAll,
rundownGetById,
rundownGetNormalised,
rundownGetPaginated,
rundownPost,
rundownPut,
rundownReorder,
Expand All @@ -16,6 +17,7 @@ import {
paramsMustHaveEventId,
rundownArrayOfIds,
rundownBatchPutValidator,
rundownGetPaginatedQueryParams,
rundownPostValidator,
rundownPutValidator,
rundownReorderValidator,
Expand All @@ -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);

Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/api-data/rundown/rundown.validation.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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();
},
];
Original file line number Diff line number Diff line change
@@ -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);
});
});
24 changes: 20 additions & 4 deletions apps/server/src/services/rundown-service/rundownUtils.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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,
};
}
10 changes: 7 additions & 3 deletions apps/server/src/services/runtime-service/RuntimeService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
EndAction,
isOntimeEvent,
LogOrigin,
MaybeNumber,
OntimeEvent,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -523,7 +527,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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
};
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down

0 comments on commit 295f5ef

Please sign in to comment.