mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 11:23:50 +00:00
refactor: improve typing in backend (#748)
This commit is contained in:
@@ -45,7 +45,7 @@ export class SocketServer implements IAdapter {
|
||||
}
|
||||
|
||||
init(server: Server) {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server });
|
||||
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
let clientId = getRandomName();
|
||||
@@ -75,11 +75,8 @@ export class SocketServer implements IAdapter {
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
if (data.length > this.MAX_PAYLOAD) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error -- ??
|
||||
const message = JSON.parse(data);
|
||||
const { type, payload } = message;
|
||||
|
||||
@@ -110,11 +107,6 @@ export class SocketServer implements IAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'hello') {
|
||||
ws.send('hi');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ontime-log') {
|
||||
if (payload.level && payload.origin && payload.text) {
|
||||
logger.emit(payload.level, payload.origin, payload.text);
|
||||
@@ -148,8 +140,12 @@ export class SocketServer implements IAdapter {
|
||||
// message is any serializable value
|
||||
sendAsJson(message: unknown) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
try {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class Logger {
|
||||
* @param origin
|
||||
* @param text
|
||||
*/
|
||||
emit(level, origin: string, text: string) {
|
||||
emit(level: LogLevel, origin: string, text: string) {
|
||||
const log = {
|
||||
id: generateId(),
|
||||
level,
|
||||
|
||||
@@ -74,12 +74,12 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0);
|
||||
return timedEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = Math.max(currentIndex - 1, 0);
|
||||
const previousEvent = timedEvents.at(newIndex);
|
||||
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||
return previousEvent;
|
||||
}
|
||||
|
||||
@@ -95,12 +95,12 @@ export class EventLoader {
|
||||
|
||||
// if there is no event running, go to first
|
||||
if (!currentEventId) {
|
||||
return timedEvents.at(0);
|
||||
return timedEvents.at(0) ?? null;
|
||||
}
|
||||
|
||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||
const newIndex = (currentIndex + 1) % timedEvents.length;
|
||||
const nextEvent = timedEvents.at(newIndex);
|
||||
return nextEvent;
|
||||
return nextEvent ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { body, check, validationResult } from 'express-validator';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import {
|
||||
validateHttpSubscriptionObject,
|
||||
@@ -20,7 +21,7 @@ export const viewValidator = [
|
||||
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
||||
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
||||
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -35,7 +36,8 @@ export const validateAliases = [
|
||||
body('*.enabled').isBoolean(),
|
||||
body('*.alias').isString().trim(),
|
||||
body('*.pathAndParams').isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -56,7 +58,8 @@ export const validateUserFields = [
|
||||
body('user7').exists().isString().trim(),
|
||||
body('user8').exists().isString().trim(),
|
||||
body('user9').exists().isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -72,7 +75,8 @@ export const validateSettings = [
|
||||
body('timeFormat').isString().isIn(['12', '24']),
|
||||
body('language').isString(),
|
||||
body('serverPort').isPort().optional(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -91,7 +95,7 @@ export const validateOSC = [
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateOscSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -106,7 +110,8 @@ export const validateHTTP = [
|
||||
body('subscriptions')
|
||||
.isObject()
|
||||
.custom((value) => validateHttpSubscriptionObject(value)),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -135,7 +140,8 @@ export const validateOscSubscription = [
|
||||
body('onFinish')
|
||||
.isArray()
|
||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -150,7 +156,8 @@ export const validatePatchProjectFile = [
|
||||
body('aliases').isArray().optional({ nullable: false }),
|
||||
body('userFields').isObject().optional({ nullable: false }),
|
||||
body('osc').isObject().optional({ nullable: false }),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -162,7 +169,8 @@ export const validatePatchProjectFile = [
|
||||
*/
|
||||
export const validateLoadProjectFile = [
|
||||
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -183,7 +191,7 @@ export const validateProjectDuplicate = [
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -205,7 +213,7 @@ export const validateProjectRename = [
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||
|
||||
(req, res, next) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -226,7 +234,8 @@ export const validateProjectCreate = [
|
||||
.withMessage('Filename must be a string')
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Filename must be between 1 and 255 characters'),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
@@ -269,7 +278,8 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
|
||||
|
||||
export const validateSheetid = [
|
||||
body('id').exists().isString(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -279,7 +289,8 @@ export const validateSheetid = [
|
||||
export const validateWorksheet = [
|
||||
body('id').exists().isString(),
|
||||
body('worksheet').exists().isString(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -289,7 +300,8 @@ export const validateWorksheet = [
|
||||
export const validateSheetOptions = [
|
||||
body('id').exists().isString(),
|
||||
// body('options').exists().isObject(), TODO:
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const projectSanitiser = [
|
||||
body('title').optional().isString().trim(),
|
||||
@@ -8,7 +9,8 @@ export const projectSanitiser = [
|
||||
body('backstageUrl').optional().isString().trim(),
|
||||
body('backstageInfo').optional().isString().trim(),
|
||||
body('endMessage').optional().isString().trim(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { body, param, validationResult } from 'express-validator';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const rundownPostValidator = [
|
||||
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -11,7 +13,8 @@ export const rundownPostValidator = [
|
||||
|
||||
export const rundownPutValidator = [
|
||||
body('id').isString().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -21,7 +24,8 @@ export const rundownPutValidator = [
|
||||
export const rundownBatchPutValidator = [
|
||||
body('data').isObject().exists(),
|
||||
body('ids').isArray().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -32,7 +36,8 @@ export const rundownReorderValidator = [
|
||||
body('eventId').isString().exists(),
|
||||
body('from').isNumeric().exists(),
|
||||
body('to').isNumeric().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -42,7 +47,8 @@ export const rundownReorderValidator = [
|
||||
export const rundownSwapValidator = [
|
||||
body('from').isString().exists(),
|
||||
body('to').isString().exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
@@ -51,7 +57,8 @@ export const rundownSwapValidator = [
|
||||
|
||||
export const paramsMustHaveEventId = [
|
||||
param('eventId').exists(),
|
||||
(req, res, next) => {
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
|
||||
@@ -57,7 +57,7 @@ async function loadDb() {
|
||||
|
||||
const data = await parseDb(dbInDisk, db);
|
||||
if (data === null) {
|
||||
console.log('ERROR: Invalid JSON format');
|
||||
console.error('ERROR: Invalid JSON format');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
import { MaybeNumber, MaybeString, Playback } from 'ontime-types';
|
||||
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { resolveRestoreFile } from '../setup.js';
|
||||
|
||||
export type RestorePoint = {
|
||||
playback: Playback;
|
||||
selectedEventId: string | null;
|
||||
startedAt: number | null;
|
||||
addedTime: number | null;
|
||||
pausedAt: number | null;
|
||||
selectedEventId: MaybeString;
|
||||
startedAt: MaybeNumber;
|
||||
addedTime: number;
|
||||
pausedAt: MaybeNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof restorePoint.addedTime !== 'number' && restorePoint.addedTime !== null) {
|
||||
if (typeof restorePoint.addedTime !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
||||
* that can then be restored when reopening
|
||||
*/
|
||||
export class RestoreService {
|
||||
private readonly filePath: string | null;
|
||||
private readonly filePath: MaybeString;
|
||||
private readonly file: JSONFile<RestorePoint | null>;
|
||||
private lastStore: string | null;
|
||||
private lastStore: MaybeString;
|
||||
private failedCreateAttempts: number;
|
||||
|
||||
constructor(filePath: string) {
|
||||
@@ -128,7 +128,7 @@ export class RestoreService {
|
||||
*/
|
||||
async clear() {
|
||||
try {
|
||||
await this.write(null);
|
||||
await this.file.write(null);
|
||||
} catch (_error) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { isRestorePoint, RestorePoint, RestoreService } from '../RestoreService.
|
||||
|
||||
describe('isRestorePoint()', () => {
|
||||
it('validates a well defined object', () => {
|
||||
let restorePoint = {
|
||||
playback: 'play',
|
||||
let restorePoint: RestorePoint = {
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: 1,
|
||||
addedTime: 2,
|
||||
@@ -17,10 +17,10 @@ describe('isRestorePoint()', () => {
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
|
||||
restorePoint = {
|
||||
playback: 'roll',
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||
@@ -32,7 +32,7 @@ describe('isRestorePoint()', () => {
|
||||
playback: 'unknown',
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
@@ -41,17 +41,17 @@ describe('isRestorePoint()', () => {
|
||||
const restorePoint = {
|
||||
selectedEventId: '123',
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
});
|
||||
it('with incorrect value', () => {
|
||||
const restorePoint = {
|
||||
playback: 'roll',
|
||||
playback: Playback.Roll,
|
||||
selectedEventId: '123',
|
||||
startedAt: 'testing',
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||
@@ -62,7 +62,7 @@ describe('isRestorePoint()', () => {
|
||||
describe('RestoreService()', () => {
|
||||
describe('load()', () => {
|
||||
it('loads working file with times', async () => {
|
||||
const expected = {
|
||||
const expected: RestorePoint = {
|
||||
playback: Playback.Play,
|
||||
selectedEventId: 'da5b4',
|
||||
startedAt: 1234,
|
||||
@@ -78,11 +78,11 @@ describe('RestoreService()', () => {
|
||||
});
|
||||
|
||||
it('loads working file without times', async () => {
|
||||
const expected = {
|
||||
const expected: RestorePoint = {
|
||||
playback: Playback.Stop,
|
||||
selectedEventId: null,
|
||||
startedAt: null,
|
||||
addedTime: null,
|
||||
addedTime: 0,
|
||||
pausedAt: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isOntimeBlock, isOntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
|
||||
import { deleteAtIndex } from '../utils/arrayUtils.js';
|
||||
|
||||
@@ -6,7 +6,11 @@ export function _applyDelay(eventId: string, rundown: OntimeRundown): OntimeRund
|
||||
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||
const delayEvent = rundown.at(delayIndex);
|
||||
|
||||
if (delayEvent.type !== SupportedEvent.Delay) {
|
||||
if (!delayEvent) {
|
||||
throw new Error('Given event ID not found');
|
||||
}
|
||||
|
||||
if (!isOntimeDelay(delayEvent)) {
|
||||
throw new Error('Given event ID is not a delay');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export type GetTimeFn = () => number;
|
||||
|
||||
export class ExtraTimerService {
|
||||
private timer: SimpleTimer;
|
||||
private interval: NodeJS.Timer;
|
||||
private interval: NodeJS.Timer | null = null;
|
||||
private emit: EmitFn;
|
||||
private getTime: GetTimeFn;
|
||||
|
||||
@@ -23,7 +23,9 @@ export class ExtraTimerService {
|
||||
}
|
||||
|
||||
private stopInterval() {
|
||||
clearInterval(this.interval);
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
@broadcastReturn
|
||||
|
||||
@@ -19,7 +19,7 @@ function formatDisplayFromString(value: string, hideZero = false): string {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: unknown) => string }>;
|
||||
type AliasesDefinition = Record<string, { key: string; cb: (value: string) => string }>;
|
||||
const quickAliases: AliasesDefinition = {
|
||||
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
|
||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
||||
|
||||
@@ -88,6 +88,9 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
|
||||
}
|
||||
|
||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||
if (!eventData?.id) {
|
||||
throw new Error('Event misses ID');
|
||||
}
|
||||
if (isOntimeEvent(eventData) && eventData?.cue === '') {
|
||||
throw new Error('Cue value invalid');
|
||||
}
|
||||
@@ -114,7 +117,7 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
||||
* @param eventId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteEvent(eventId) {
|
||||
export async function deleteEvent(eventId: string) {
|
||||
await cachedDelete(eventId);
|
||||
|
||||
notifyChanges({ timer: [eventId], external: true });
|
||||
|
||||
@@ -12,7 +12,7 @@ import { state, stateMutations } from '../../state.js';
|
||||
* Coordinating with necessary services
|
||||
*/
|
||||
class RuntimeService {
|
||||
private eventTimer: TimerService;
|
||||
private eventTimer: TimerService | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -28,8 +28,10 @@ class RuntimeService {
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
||||
this.eventTimer.shutdown();
|
||||
if (this.eventTimer) {
|
||||
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
||||
this.eventTimer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,9 +15,8 @@ export const normaliseEndTime = (start: number, end: number) => (end < start ? e
|
||||
*/
|
||||
export function getExpectedFinish(state: TState): MaybeNumber {
|
||||
const { startedAt, finishedAt, duration, addedTime } = state.timer;
|
||||
const { timerType } = state.eventNow;
|
||||
const { timerType, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { timeEnd } = state.eventNow;
|
||||
const { clock } = state;
|
||||
|
||||
if (startedAt === null) {
|
||||
@@ -51,9 +50,8 @@ export function getExpectedFinish(state: TState): MaybeNumber {
|
||||
*/
|
||||
export function getCurrent(state: TState): number {
|
||||
const { startedAt, duration, addedTime } = state.timer;
|
||||
const { timerType } = state.eventNow;
|
||||
const { timerType, timeEnd } = state.eventNow;
|
||||
const { pausedAt } = state._timer;
|
||||
const { timeEnd } = state.eventNow;
|
||||
const { clock } = state;
|
||||
|
||||
if (timerType === TimerType.TimeToEnd) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export function getCached<T>(key: string, callback: () => T): T {
|
||||
const data = callback();
|
||||
runtimeCache.set(key, { data });
|
||||
} catch (error) {
|
||||
console.log(`Failed retrieving data from callback: ${error}`);
|
||||
console.error(`Failed retrieving data from callback: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ export function getCached<T>(key: string, callback: () => T): T {
|
||||
|
||||
export function setCached<T>(key: string, value: T): T {
|
||||
runtimeCache.set(key, { data: value });
|
||||
return runtimeCache.get(key).data as T;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function invalidate(key) {
|
||||
export function invalidate(key: string) {
|
||||
runtimeCache.delete(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/* eslint-disable no-console -- we are mocking the console */
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||
import {
|
||||
DatabaseModel,
|
||||
EndAction,
|
||||
OntimeEvent,
|
||||
ProjectData,
|
||||
Settings,
|
||||
SupportedEvent,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { dbModel } from '../../models/dataModel.js';
|
||||
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
||||
@@ -9,24 +18,24 @@ import { makeString } from '../parserUtils.js';
|
||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
||||
|
||||
describe('test json parser with valid def', () => {
|
||||
const testData = {
|
||||
const testData: Partial<DatabaseModel> = {
|
||||
rundown: [
|
||||
{
|
||||
id: '4b31',
|
||||
cue: 'Guest Welcoming',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Guest Welcoming',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 31500000,
|
||||
timeEnd: 32400000,
|
||||
timeType: 'start-end',
|
||||
duration: 32400000 - 31500000,
|
||||
isPublic: false,
|
||||
endAction: 'play-next',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '4b31',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -37,24 +46,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: 'f24d',
|
||||
cue: 'Good Morning',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Good Morning',
|
||||
subtitle: 'Days schedule',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.PlayNext,
|
||||
timerType: TimerType.CountUp,
|
||||
timeStart: 32400000,
|
||||
timeEnd: 36000000,
|
||||
timeType: 'start-end',
|
||||
duration: 36000000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'play-next',
|
||||
timerType: 'count-up',
|
||||
skip: true,
|
||||
colour: 'red',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'f24d',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -65,23 +76,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: 'bbc5',
|
||||
cue: 'Stage 2 setup',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Stage 2 setup',
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: 'wrong action' as EndAction, // testing
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 32400000,
|
||||
timeEnd: 37200000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: false,
|
||||
endAction: 'wrong action',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'bbc5',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -92,41 +106,47 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
// testing incomplete dataset
|
||||
id: '5b3e',
|
||||
cue: 'Working Procedures',
|
||||
type: SupportedEvent.Event,
|
||||
title: 'Working Procedures',
|
||||
subtitle: '',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 37200000,
|
||||
timeEnd: 39000000,
|
||||
timeType: 'start-end',
|
||||
duration: 39000000 - 37200000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '5b3e',
|
||||
},
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
id: '8e2c',
|
||||
cue: 'Lunch',
|
||||
title: 'Lunch',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '',
|
||||
presenter: '',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 39600000,
|
||||
timeEnd: 45000000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: false,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: false,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '8e2c',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -137,23 +157,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
id: '08e9',
|
||||
cue: 'A day being carlos',
|
||||
title: 'A day being carlos',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: 'My life in a song',
|
||||
presenter: 'Carlos Valente',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 46800000,
|
||||
timeEnd: 50400000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
skip: true,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: '08e9',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -164,23 +187,26 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
},
|
||||
{
|
||||
// testing incomplete dataset
|
||||
id: 'e25a',
|
||||
cue: 'Hamburgers and Cheese',
|
||||
title: 'Hamburgers and Cheese',
|
||||
type: SupportedEvent.Event,
|
||||
subtitle: '... and other life questions',
|
||||
presenter: 'Filip Johansen',
|
||||
note: '',
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.Clock,
|
||||
timeStart: 54000000,
|
||||
timeEnd: 57600000,
|
||||
timeType: 'start-end',
|
||||
duration: 37200000 - 32400000,
|
||||
isPublic: true,
|
||||
endAction: 'none',
|
||||
timerType: 'clock',
|
||||
colour: '',
|
||||
type: 'event',
|
||||
revision: 0,
|
||||
id: 'e25a',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
@@ -191,20 +217,23 @@ describe('test json parser with valid def', () => {
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
},
|
||||
revision: 0,
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
} as OntimeEvent,
|
||||
],
|
||||
project: {
|
||||
title: 'This is a test definition',
|
||||
url: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
},
|
||||
} as ProjectData,
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
timeFormat: '24',
|
||||
},
|
||||
viewSettings: {},
|
||||
} as Settings,
|
||||
viewSettings: {} as ViewSettings,
|
||||
};
|
||||
|
||||
let parseResponse;
|
||||
@@ -283,6 +312,8 @@ describe('test parser edge cases', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(typeof (parseResponse.rundown[0] as OntimeEvent).cue).toBe('string');
|
||||
expect(typeof (parseResponse.rundown[1] as OntimeEvent).cue).toBe('string');
|
||||
@@ -298,6 +329,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(parseResponse.rundown[0].id).toBeDefined();
|
||||
});
|
||||
@@ -319,6 +351,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
//@ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: ID collision on import, skipping');
|
||||
expect(parseResponse?.rundown.length).toBe(1);
|
||||
@@ -339,6 +372,7 @@ describe('test parser edge cases', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parseResponse = await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
|
||||
expect(parseResponse?.rundown.length).toBe(0);
|
||||
@@ -352,6 +386,7 @@ describe('test parser edge cases', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
await parseJson(testData);
|
||||
expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping');
|
||||
});
|
||||
@@ -378,9 +413,11 @@ describe('test corrupt data', () => {
|
||||
{},
|
||||
{},
|
||||
],
|
||||
event: {
|
||||
project: {
|
||||
title: 'All about Carlos demo event',
|
||||
url: 'www.carlosvalente.com',
|
||||
description: 'description',
|
||||
publicUrl: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
endMessage: '',
|
||||
@@ -389,11 +426,11 @@ describe('test corrupt data', () => {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyEvents);
|
||||
expect(parsedDef.rundown.length).toBe(2);
|
||||
});
|
||||
@@ -401,9 +438,11 @@ describe('test corrupt data', () => {
|
||||
it('handles all empty events', async () => {
|
||||
const emptyEvents = {
|
||||
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
||||
event: {
|
||||
project: {
|
||||
title: 'All about Carlos demo event',
|
||||
url: 'www.carlosvalente.com',
|
||||
description: 'description',
|
||||
publicUrl: 'www.carlosvalente.com',
|
||||
backstageUrl: 'www.carlosvalente.com',
|
||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||
endMessage: '',
|
||||
@@ -412,11 +451,11 @@ describe('test corrupt data', () => {
|
||||
app: 'ontime',
|
||||
version: '2.0.0',
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyEvents);
|
||||
expect(parsedDef.rundown.length).toBe(0);
|
||||
});
|
||||
@@ -434,6 +473,7 @@ describe('test corrupt data', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(emptyProjectData);
|
||||
expect(parsedDef.project).toStrictEqual(dbModel.project);
|
||||
});
|
||||
@@ -448,12 +488,15 @@ describe('test corrupt data', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(missingSettings);
|
||||
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
|
||||
});
|
||||
|
||||
it('fails with invalid JSON', async () => {
|
||||
const invalidJSON = 'some random dataset';
|
||||
|
||||
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||
const parsedDef = await parseJson(invalidJSON);
|
||||
expect(parsedDef).toBeNull();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
* @description Creates a directory if it doesn't exist
|
||||
* @param {string} directory - directory that should exist or will be created
|
||||
*/
|
||||
export function ensureDirectory(directory) {
|
||||
export function ensureDirectory(directory: string) {
|
||||
if (!existsSync(directory)) {
|
||||
try {
|
||||
mkdirSync(directory, { recursive: true });
|
||||
|
||||
@@ -316,7 +316,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
* @param {object} jsonData - project file to be parsed
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<DatabaseModel | null> => {
|
||||
if (!jsonData || typeof jsonData !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ensureJsonExtension } from './ensureJsonExtension.js';
|
||||
|
||||
export const sanitizeProjectFilename = (req, _res, next) => {
|
||||
export const sanitizeProjectFilename = (req: Request, _res: Response, next: NextFunction) => {
|
||||
const { filename, newFilename } = req.body;
|
||||
const { filename: projectName } = req.params;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user