refactor: improve typing in backend (#748)

This commit is contained in:
Carlos Valente
2024-01-30 21:12:19 +01:00
committed by GitHub
parent 39711c1edd
commit 6a5020424b
26 changed files with 703 additions and 147 deletions
+2 -1
View File
@@ -13,8 +13,8 @@
"express-session": "^1.17.3",
"express-static-gzip": "^2.1.7",
"express-validator": "^6.14.2",
"got": "^14.0.0",
"google-auth-library": "^9.4.2",
"got": "^14.0.0",
"lowdb": "^7.0.1",
"multer": "^1.4.5-lts.1",
"node-osc": "^9.0.2",
@@ -31,6 +31,7 @@
"@types/node": "^18.11.18",
"@types/node-osc": "^6.0.2",
"@types/websocket": "^1.0.5",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"esbuild": "^0.19.10",
+8 -12
View File
@@ -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 */
}
});
}
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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;
}
+9 -9
View File
@@ -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,
};
+6 -2
View File
@@ -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();
}
}
/**
+2 -4
View File
@@ -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) {
+3 -3
View File
@@ -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);
}
+98 -55
View File
@@ -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();
});
+1 -1
View File
@@ -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 });
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -9,7 +9,7 @@
"skipLibCheck": true,
"types": ["vitest/globals"],
"outDir": "dist",
"experimentalDecorators": true
"experimentalDecorators": true,
},
"include": [
"src/**/*"
+481
View File
@@ -0,0 +1,481 @@
{
"rundown": [
{
"id": "32d31",
"type": "event",
"title": "Albania",
"subtitle": "Sekret",
"presenter": "Ronela Hajati",
"timeStart": 36000000,
"timeEnd": 37200000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.01",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.01",
"revision": 0
},
{
"id": "21cd2",
"type": "event",
"title": "Latvia",
"subtitle": "Eat Your Salad",
"presenter": "Citi Zeni",
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.02",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.02",
"revision": 0
},
{
"id": "0b371",
"type": "event",
"title": "Lithuania",
"subtitle": "Sentimentai",
"presenter": "Monika Liu",
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.03",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.03",
"revision": 0
},
{
"id": "3cd28",
"type": "event",
"title": "Switzerland",
"subtitle": "Boys Do Cry",
"presenter": "Marius Bear",
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.04",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.04",
"revision": 0
},
{
"id": "e457f",
"type": "event",
"title": "Slovenia",
"subtitle": "Disko",
"presenter": "LPS",
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.05",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.05",
"revision": 0
},
{
"title": "Lunch break",
"type": "block",
"id": "01e85"
},
{
"id": "1c420",
"type": "event",
"title": "Ukraine",
"subtitle": "Stefania",
"presenter": "Kalush Orchestra",
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.06",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.06",
"revision": 0
},
{
"id": "b7737",
"type": "event",
"title": "Bulgaria",
"subtitle": "Intention",
"presenter": "Intelligent Music Project",
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.07",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.07",
"revision": 0
},
{
"id": "d3a80",
"type": "event",
"title": "Netherlands",
"subtitle": "De Diepte",
"presenter": "S10",
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.08",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.08",
"revision": 0
},
{
"id": "8276c",
"type": "event",
"title": "Moldova",
"subtitle": "Trenuletul",
"presenter": "Zdob si Zdub",
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.09",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.09",
"revision": 0
},
{
"id": "2340b",
"type": "event",
"title": "Portugal",
"subtitle": "Saudade Saudade",
"presenter": "Maro",
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.10",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.10",
"revision": 0
},
{
"title": "Afternoon break",
"type": "block",
"id": "cb90b"
},
{
"id": "503c4",
"type": "event",
"title": "Croatia",
"subtitle": "Guilty Pleasure",
"presenter": "Mia Dimsic",
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.11",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.11",
"revision": 0
},
{
"id": "5e965",
"type": "event",
"title": "Denmark",
"subtitle": "The Show",
"presenter": "Reddi",
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.12",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.12",
"revision": 0
},
{
"id": "bab4a",
"type": "event",
"title": "Austria",
"subtitle": "Halo",
"presenter": "LUM!X & Pia Maria",
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.13",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.13",
"revision": 0
},
{
"id": "d3eb1",
"type": "event",
"title": "Greece",
"subtitle": "Die Together",
"presenter": "Amanda Tenfjord",
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"endAction": "none",
"timerType": "count-down",
"isPublic": true,
"skip": false,
"note": "SF1.14",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"colour": "",
"cue": "SF1.14",
"revision": 0
}
],
"project": {
"title": "Eurovision Song Contest",
"description": "Turin 2022",
"publicUrl": "www.getontime.no",
"publicInfo": "Rehearsal Schedule - Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
},
"settings": {
"app": "ontime",
"version": "3.0.0-alpha",
"serverPort": 4001,
"editorKey": null,
"operatorKey": null,
"timeFormat": "24",
"language": "en"
},
"viewSettings": {
"overrideStyles": false,
"normalColor": "#ffffffcc",
"warningColor": "#FFAB33",
"dangerColor": "#ED3333",
"endMessage": ""
},
"aliases": [
{
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
}
],
"userFields": {
"user0": "user0",
"user1": "user1",
"user2": "user2",
"user3": "user3",
"user4": "user4",
"user5": "user5",
"user6": "user6",
"user7": "user7",
"user8": "user8",
"user9": "user9"
},
"osc": {
"portIn": 8888,
"portOut": 9999,
"targetIP": "127.0.0.1",
"enabledIn": true,
"enabledOut": true,
"subscriptions": {
"onLoad": [],
"onStart": [],
"onPause": [],
"onStop": [],
"onUpdate": [
{
"id": "10eea",
"enabled": true,
"message": "/ontime/update/{{timer.current}}"
}
],
"onFinish": []
}
},
"http": {
"enabledOut": true,
"subscriptions": {
"onLoad": [],
"onStart": [],
"onPause": [],
"onStop": [],
"onUpdate": [],
"onFinish": []
}
}
}
+1 -1
View File
@@ -67,4 +67,4 @@ export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './defini
// TYPE UTILITIES
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js';
export type { MaybeNumber, DeepPartial } from './utils/utils.type.js';
export type { DeepPartial, MaybeNumber, MaybeString } from './utils/utils.type.js';
+1
View File
@@ -1,4 +1,5 @@
export type MaybeNumber = number | null;
export type MaybeString = string | null;
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
+13 -10
View File
@@ -316,6 +316,9 @@ importers:
'@types/websocket':
specifier: ^1.0.5
version: 1.0.5
'@types/ws':
specifier: ^8.5.10
version: 8.5.10
'@typescript-eslint/eslint-plugin':
specifier: ^6.10.0
version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)
@@ -2317,7 +2320,7 @@ packages:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 20.10.5
'@types/node': 18.19.3
'@types/yargs': 17.0.19
chalk: 4.1.2
dev: true
@@ -3155,7 +3158,7 @@ packages:
/@types/connect@3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 18.11.18
'@types/node': 18.19.3
dev: true
/@types/debug@4.1.12:
@@ -3188,7 +3191,7 @@ packages:
/@types/fs-extra@9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies:
'@types/node': 20.10.5
'@types/node': 18.19.3
dev: true
/@types/http-cache-semantics@4.0.4:
@@ -3265,12 +3268,6 @@ packages:
undici-types: 5.26.5
dev: true
/@types/node@20.10.5:
resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==}
dependencies:
undici-types: 5.26.5
dev: true
/@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: false
@@ -3350,6 +3347,12 @@ packages:
'@types/node': 18.11.18
dev: true
/@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies:
'@types/node': 18.19.3
dev: true
/@types/yargs-parser@21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: true
@@ -6590,7 +6593,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.3.1
'@types/node': 20.10.5
'@types/node': 18.19.3
chalk: 4.1.2
ci-info: 3.7.1
graceful-fs: 4.2.11