From 6a5020424b3f422a37a8edfca4c05c654ef08da2 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Tue, 30 Jan 2024 21:12:19 +0100 Subject: [PATCH] refactor: improve typing in backend (#748) --- apps/server/package.json | 3 +- apps/server/src/adapters/WebsocketAdapter.ts | 20 +- apps/server/src/classes/Logger.ts | 2 +- .../src/classes/event-loader/EventLoader.ts | 8 +- .../controllers/ontimeController.validate.ts | 42 +- .../controllers/projectController.validate.ts | 4 +- .../controllers/rundownController.validate.ts | 19 +- apps/server/src/modules/loadDb.ts | 2 +- apps/server/src/services/RestoreService.ts | 18 +- .../services/__tests__/RestoreService.test.ts | 22 +- apps/server/src/services/delayUtils.ts | 8 +- .../extra-timer-service/ExtraTimerService.ts | 6 +- .../integration-service/integrationUtils.ts | 2 +- .../rundown-service/RundownService.ts | 5 +- .../runtime-service/RuntimeService.ts | 8 +- apps/server/src/services/timerUtils.ts | 6 +- apps/server/src/stores/cachingStore.ts | 6 +- .../server/src/utils/__tests__/parser.test.ts | 153 ++++-- apps/server/src/utils/fileManagement.ts | 2 +- apps/server/src/utils/parser.ts | 2 +- .../src/utils/sanitizeProjectFilename.ts | 3 +- apps/server/tsconfig.json | 2 +- apps/test-db/test-db.json | 481 ++++++++++++++++++ packages/types/src/index.ts | 2 +- packages/types/src/utils/utils.type.ts | 1 + pnpm-lock.yaml | 23 +- 26 files changed, 703 insertions(+), 147 deletions(-) create mode 100644 apps/test-db/test-db.json diff --git a/apps/server/package.json b/apps/server/package.json index 9b9cb18db..70b298cb5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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", diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts index b35eb6ecc..f0cea0cae 100644 --- a/apps/server/src/adapters/WebsocketAdapter.ts +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -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 */ } }); } diff --git a/apps/server/src/classes/Logger.ts b/apps/server/src/classes/Logger.ts index 5437d23ad..9541c09bf 100644 --- a/apps/server/src/classes/Logger.ts +++ b/apps/server/src/classes/Logger.ts @@ -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, diff --git a/apps/server/src/classes/event-loader/EventLoader.ts b/apps/server/src/classes/event-loader/EventLoader.ts index 1f1108504..037b12ef4 100644 --- a/apps/server/src/classes/event-loader/EventLoader.ts +++ b/apps/server/src/classes/event-loader/EventLoader.ts @@ -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; } } diff --git a/apps/server/src/controllers/ontimeController.validate.ts b/apps/server/src/controllers/ontimeController.validate.ts index 531b2bd46..7e34d0136 100644 --- a/apps/server/src/controllers/ontimeController.validate.ts +++ b/apps/server/src/controllers/ontimeController.validate.ts @@ -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(); diff --git a/apps/server/src/controllers/projectController.validate.ts b/apps/server/src/controllers/projectController.validate.ts index 9f2460de9..ceae196ef 100644 --- a/apps/server/src/controllers/projectController.validate.ts +++ b/apps/server/src/controllers/projectController.validate.ts @@ -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(); diff --git a/apps/server/src/controllers/rundownController.validate.ts b/apps/server/src/controllers/rundownController.validate.ts index aba619090..8c36159df 100644 --- a/apps/server/src/controllers/rundownController.validate.ts +++ b/apps/server/src/controllers/rundownController.validate.ts @@ -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(); diff --git a/apps/server/src/modules/loadDb.ts b/apps/server/src/modules/loadDb.ts index dde8f05ed..b0d1da656 100644 --- a/apps/server/src/modules/loadDb.ts +++ b/apps/server/src/modules/loadDb.ts @@ -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; } diff --git a/apps/server/src/services/RestoreService.ts b/apps/server/src/services/RestoreService.ts index 8af01a6ff..823330d67 100644 --- a/apps/server/src/services/RestoreService.ts +++ b/apps/server/src/services/RestoreService.ts @@ -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; - 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 } diff --git a/apps/server/src/services/__tests__/RestoreService.test.ts b/apps/server/src/services/__tests__/RestoreService.test.ts index afd62acec..e7866e978 100644 --- a/apps/server/src/services/__tests__/RestoreService.test.ts +++ b/apps/server/src/services/__tests__/RestoreService.test.ts @@ -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, }; diff --git a/apps/server/src/services/delayUtils.ts b/apps/server/src/services/delayUtils.ts index 59a62c736..992d917dc 100644 --- a/apps/server/src/services/delayUtils.ts +++ b/apps/server/src/services/delayUtils.ts @@ -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'); } diff --git a/apps/server/src/services/extra-timer-service/ExtraTimerService.ts b/apps/server/src/services/extra-timer-service/ExtraTimerService.ts index e61af19c2..688be114c 100644 --- a/apps/server/src/services/extra-timer-service/ExtraTimerService.ts +++ b/apps/server/src/services/extra-timer-service/ExtraTimerService.ts @@ -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 diff --git a/apps/server/src/services/integration-service/integrationUtils.ts b/apps/server/src/services/integration-service/integrationUtils.ts index 94f142c13..b69d5a207 100644 --- a/apps/server/src/services/integration-service/integrationUtils.ts +++ b/apps/server/src/services/integration-service/integrationUtils.ts @@ -19,7 +19,7 @@ function formatDisplayFromString(value: string, hideZero = false): string { return formatted; } -type AliasesDefinition = Record string }>; +type AliasesDefinition = Record string }>; const quickAliases: AliasesDefinition = { clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) }, duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) }, diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index ae41c8e87..85c41ca08 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -88,6 +88,9 @@ export async function addEvent(eventData: Partial | Partial | Partial | Partial) { + 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) * @param eventId * @returns {Promise} */ -export async function deleteEvent(eventId) { +export async function deleteEvent(eventId: string) { await cachedDelete(eventId); notifyChanges({ timer: [eventId], external: true }); diff --git a/apps/server/src/services/runtime-service/RuntimeService.ts b/apps/server/src/services/runtime-service/RuntimeService.ts index 29e19a86f..35d575c5f 100644 --- a/apps/server/src/services/runtime-service/RuntimeService.ts +++ b/apps/server/src/services/runtime-service/RuntimeService.ts @@ -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(); + } } /** diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 2c9c1b5f0..f9c801c3c 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -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) { diff --git a/apps/server/src/stores/cachingStore.ts b/apps/server/src/stores/cachingStore.ts index a5ac8d05d..c9ac5b34d 100644 --- a/apps/server/src/stores/cachingStore.ts +++ b/apps/server/src/stores/cachingStore.ts @@ -14,7 +14,7 @@ export function getCached(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(key: string, callback: () => T): T { export function setCached(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); } diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts index d9610f4ec..57e5fdadd 100644 --- a/apps/server/src/utils/__tests__/parser.test.ts +++ b/apps/server/src/utils/__tests__/parser.test.ts @@ -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 = { 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(); }); diff --git a/apps/server/src/utils/fileManagement.ts b/apps/server/src/utils/fileManagement.ts index 7c74236b1..40e8c9f86 100644 --- a/apps/server/src/utils/fileManagement.ts +++ b/apps/server/src/utils/fileManagement.ts @@ -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 }); diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 2ce73ee35..c0c811170 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -316,7 +316,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial => { +export const parseJson = async (jsonData: Partial): Promise => { if (!jsonData || typeof jsonData !== 'object') { return null; } diff --git a/apps/server/src/utils/sanitizeProjectFilename.ts b/apps/server/src/utils/sanitizeProjectFilename.ts index 8f322d6c0..0c4b4ab68 100644 --- a/apps/server/src/utils/sanitizeProjectFilename.ts +++ b/apps/server/src/utils/sanitizeProjectFilename.ts @@ -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; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index ddc5f578f..f768b1a07 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -9,7 +9,7 @@ "skipLibCheck": true, "types": ["vitest/globals"], "outDir": "dist", - "experimentalDecorators": true + "experimentalDecorators": true, }, "include": [ "src/**/*" diff --git a/apps/test-db/test-db.json b/apps/test-db/test-db.json new file mode 100644 index 000000000..bd2a11201 --- /dev/null +++ b/apps/test-db/test-db.json @@ -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": [] + } + } +} \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 301ca85ba..b13c30938 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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'; diff --git a/packages/types/src/utils/utils.type.ts b/packages/types/src/utils/utils.type.ts index c67ec8c2d..c7841f40b 100644 --- a/packages/types/src/utils/utils.type.ts +++ b/packages/types/src/utils/utils.type.ts @@ -1,4 +1,5 @@ export type MaybeNumber = number | null; +export type MaybeString = string | null; export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be5b17fda..624c0f387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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