mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
refactor: improve typing in backend (#748)
This commit is contained in:
@@ -13,8 +13,8 @@
|
|||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"express-static-gzip": "^2.1.7",
|
"express-static-gzip": "^2.1.7",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"got": "^14.0.0",
|
|
||||||
"google-auth-library": "^9.4.2",
|
"google-auth-library": "^9.4.2",
|
||||||
|
"got": "^14.0.0",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-osc": "^9.0.2",
|
"node-osc": "^9.0.2",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/node-osc": "^6.0.2",
|
"@types/node-osc": "^6.0.2",
|
||||||
"@types/websocket": "^1.0.5",
|
"@types/websocket": "^1.0.5",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
"esbuild": "^0.19.10",
|
"esbuild": "^0.19.10",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class SocketServer implements IAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(server: Server) {
|
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) => {
|
this.wss.on('connection', (ws) => {
|
||||||
let clientId = getRandomName();
|
let clientId = getRandomName();
|
||||||
@@ -75,11 +75,8 @@ export class SocketServer implements IAdapter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
if (data.length > this.MAX_PAYLOAD) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// @ts-expect-error -- ??
|
||||||
const message = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
|
|
||||||
@@ -110,11 +107,6 @@ export class SocketServer implements IAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'hello') {
|
|
||||||
ws.send('hi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'ontime-log') {
|
if (type === 'ontime-log') {
|
||||||
if (payload.level && payload.origin && payload.text) {
|
if (payload.level && payload.origin && payload.text) {
|
||||||
logger.emit(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
|
// message is any serializable value
|
||||||
sendAsJson(message: unknown) {
|
sendAsJson(message: unknown) {
|
||||||
this.wss?.clients.forEach((client) => {
|
this.wss?.clients.forEach((client) => {
|
||||||
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
try {
|
||||||
client.send(JSON.stringify(message));
|
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 origin
|
||||||
* @param text
|
* @param text
|
||||||
*/
|
*/
|
||||||
emit(level, origin: string, text: string) {
|
emit(level: LogLevel, origin: string, text: string) {
|
||||||
const log = {
|
const log = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
level,
|
level,
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ export class EventLoader {
|
|||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (!currentEventId) {
|
if (!currentEventId) {
|
||||||
return timedEvents.at(0);
|
return timedEvents.at(0) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
const newIndex = Math.max(currentIndex - 1, 0);
|
const newIndex = Math.max(currentIndex - 1, 0);
|
||||||
const previousEvent = timedEvents.at(newIndex);
|
const previousEvent = timedEvents.at(newIndex) ?? null;
|
||||||
return previousEvent;
|
return previousEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,12 +95,12 @@ export class EventLoader {
|
|||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (!currentEventId) {
|
if (!currentEventId) {
|
||||||
return timedEvents.at(0);
|
return timedEvents.at(0) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
const currentIndex = timedEvents.findIndex((event) => event.id === currentEventId);
|
||||||
const newIndex = (currentIndex + 1) % timedEvents.length;
|
const newIndex = (currentIndex + 1) % timedEvents.length;
|
||||||
const nextEvent = timedEvents.at(newIndex);
|
const nextEvent = timedEvents.at(newIndex);
|
||||||
return nextEvent;
|
return nextEvent ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { body, check, validationResult } from 'express-validator';
|
import { body, check, validationResult } from 'express-validator';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateHttpSubscriptionObject,
|
validateHttpSubscriptionObject,
|
||||||
@@ -20,7 +21,7 @@ export const viewValidator = [
|
|||||||
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
||||||
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
||||||
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
||||||
(req, res, next) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -35,7 +36,8 @@ export const validateAliases = [
|
|||||||
body('*.enabled').isBoolean(),
|
body('*.enabled').isBoolean(),
|
||||||
body('*.alias').isString().trim(),
|
body('*.alias').isString().trim(),
|
||||||
body('*.pathAndParams').isString().trim(),
|
body('*.pathAndParams').isString().trim(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -56,7 +58,8 @@ export const validateUserFields = [
|
|||||||
body('user7').exists().isString().trim(),
|
body('user7').exists().isString().trim(),
|
||||||
body('user8').exists().isString().trim(),
|
body('user8').exists().isString().trim(),
|
||||||
body('user9').exists().isString().trim(),
|
body('user9').exists().isString().trim(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -72,7 +75,8 @@ export const validateSettings = [
|
|||||||
body('timeFormat').isString().isIn(['12', '24']),
|
body('timeFormat').isString().isIn(['12', '24']),
|
||||||
body('language').isString(),
|
body('language').isString(),
|
||||||
body('serverPort').isPort().optional(),
|
body('serverPort').isPort().optional(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -91,7 +95,7 @@ export const validateOSC = [
|
|||||||
body('subscriptions')
|
body('subscriptions')
|
||||||
.isObject()
|
.isObject()
|
||||||
.custom((value) => validateOscSubscriptionObject(value)),
|
.custom((value) => validateOscSubscriptionObject(value)),
|
||||||
(req, res, next) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -106,7 +110,8 @@ export const validateHTTP = [
|
|||||||
body('subscriptions')
|
body('subscriptions')
|
||||||
.isObject()
|
.isObject()
|
||||||
.custom((value) => validateHttpSubscriptionObject(value)),
|
.custom((value) => validateHttpSubscriptionObject(value)),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -135,7 +140,8 @@ export const validateOscSubscription = [
|
|||||||
body('onFinish')
|
body('onFinish')
|
||||||
.isArray()
|
.isArray()
|
||||||
.custom((value) => validateOscSubscriptionCycle(value)),
|
.custom((value) => validateOscSubscriptionCycle(value)),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -150,7 +156,8 @@ export const validatePatchProjectFile = [
|
|||||||
body('aliases').isArray().optional({ nullable: false }),
|
body('aliases').isArray().optional({ nullable: false }),
|
||||||
body('userFields').isObject().optional({ nullable: false }),
|
body('userFields').isObject().optional({ nullable: false }),
|
||||||
body('osc').isObject().optional({ nullable: false }),
|
body('osc').isObject().optional({ nullable: false }),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -162,7 +169,8 @@ export const validatePatchProjectFile = [
|
|||||||
*/
|
*/
|
||||||
export const validateLoadProjectFile = [
|
export const validateLoadProjectFile = [
|
||||||
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
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);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.status(422).json({ errors: errors.array() });
|
||||||
@@ -183,7 +191,7 @@ export const validateProjectDuplicate = [
|
|||||||
.isLength({ min: 1, max: 255 })
|
.isLength({ min: 1, max: 255 })
|
||||||
.withMessage('New project filename must be between 1 and 255 characters'),
|
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
(req, res, next) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.status(422).json({ errors: errors.array() });
|
||||||
@@ -205,7 +213,7 @@ export const validateProjectRename = [
|
|||||||
.isLength({ min: 1, max: 255 })
|
.isLength({ min: 1, max: 255 })
|
||||||
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
(req, res, next) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.status(422).json({ errors: errors.array() });
|
||||||
@@ -226,7 +234,8 @@ export const validateProjectCreate = [
|
|||||||
.withMessage('Filename must be a string')
|
.withMessage('Filename must be a string')
|
||||||
.isLength({ min: 1, max: 255 })
|
.isLength({ min: 1, max: 255 })
|
||||||
.withMessage('Filename must be between 1 and 255 characters'),
|
.withMessage('Filename must be between 1 and 255 characters'),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(422).json({ errors: errors.array() });
|
return res.status(422).json({ errors: errors.array() });
|
||||||
@@ -269,7 +278,8 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
|
|||||||
|
|
||||||
export const validateSheetid = [
|
export const validateSheetid = [
|
||||||
body('id').exists().isString(),
|
body('id').exists().isString(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -279,7 +289,8 @@ export const validateSheetid = [
|
|||||||
export const validateWorksheet = [
|
export const validateWorksheet = [
|
||||||
body('id').exists().isString(),
|
body('id').exists().isString(),
|
||||||
body('worksheet').exists().isString(),
|
body('worksheet').exists().isString(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -289,7 +300,8 @@ export const validateWorksheet = [
|
|||||||
export const validateSheetOptions = [
|
export const validateSheetOptions = [
|
||||||
body('id').exists().isString(),
|
body('id').exists().isString(),
|
||||||
// body('options').exists().isObject(), TODO:
|
// body('options').exists().isObject(), TODO:
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { body, validationResult } from 'express-validator';
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
export const projectSanitiser = [
|
export const projectSanitiser = [
|
||||||
body('title').optional().isString().trim(),
|
body('title').optional().isString().trim(),
|
||||||
@@ -8,7 +9,8 @@ export const projectSanitiser = [
|
|||||||
body('backstageUrl').optional().isString().trim(),
|
body('backstageUrl').optional().isString().trim(),
|
||||||
body('backstageInfo').optional().isString().trim(),
|
body('backstageInfo').optional().isString().trim(),
|
||||||
body('endMessage').optional().isString().trim(),
|
body('endMessage').optional().isString().trim(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { body, param, validationResult } from 'express-validator';
|
import { body, param, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
export const rundownPostValidator = [
|
export const rundownPostValidator = [
|
||||||
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -11,7 +13,8 @@ export const rundownPostValidator = [
|
|||||||
|
|
||||||
export const rundownPutValidator = [
|
export const rundownPutValidator = [
|
||||||
body('id').isString().exists(),
|
body('id').isString().exists(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -21,7 +24,8 @@ export const rundownPutValidator = [
|
|||||||
export const rundownBatchPutValidator = [
|
export const rundownBatchPutValidator = [
|
||||||
body('data').isObject().exists(),
|
body('data').isObject().exists(),
|
||||||
body('ids').isArray().exists(),
|
body('ids').isArray().exists(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -32,7 +36,8 @@ export const rundownReorderValidator = [
|
|||||||
body('eventId').isString().exists(),
|
body('eventId').isString().exists(),
|
||||||
body('from').isNumeric().exists(),
|
body('from').isNumeric().exists(),
|
||||||
body('to').isNumeric().exists(),
|
body('to').isNumeric().exists(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -42,7 +47,8 @@ export const rundownReorderValidator = [
|
|||||||
export const rundownSwapValidator = [
|
export const rundownSwapValidator = [
|
||||||
body('from').isString().exists(),
|
body('from').isString().exists(),
|
||||||
body('to').isString().exists(),
|
body('to').isString().exists(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
@@ -51,7 +57,8 @@ export const rundownSwapValidator = [
|
|||||||
|
|
||||||
export const paramsMustHaveEventId = [
|
export const paramsMustHaveEventId = [
|
||||||
param('eventId').exists(),
|
param('eventId').exists(),
|
||||||
(req, res, next) => {
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async function loadDb() {
|
|||||||
|
|
||||||
const data = await parseDb(dbInDisk, db);
|
const data = await parseDb(dbInDisk, db);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
console.log('ERROR: Invalid JSON format');
|
console.error('ERROR: Invalid JSON format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Playback } from 'ontime-types';
|
import { MaybeNumber, MaybeString, Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { JSONFile } from 'lowdb/node';
|
import { JSONFile } from 'lowdb/node';
|
||||||
import { resolveRestoreFile } from '../setup.js';
|
import { resolveRestoreFile } from '../setup.js';
|
||||||
|
|
||||||
export type RestorePoint = {
|
export type RestorePoint = {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
selectedEventId: string | null;
|
selectedEventId: MaybeString;
|
||||||
startedAt: number | null;
|
startedAt: MaybeNumber;
|
||||||
addedTime: number | null;
|
addedTime: number;
|
||||||
pausedAt: number | null;
|
pausedAt: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +35,7 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof restorePoint.addedTime !== 'number' && restorePoint.addedTime !== null) {
|
if (typeof restorePoint.addedTime !== 'number') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +55,9 @@ export function isRestorePoint(obj: unknown): obj is RestorePoint {
|
|||||||
* that can then be restored when reopening
|
* that can then be restored when reopening
|
||||||
*/
|
*/
|
||||||
export class RestoreService {
|
export class RestoreService {
|
||||||
private readonly filePath: string | null;
|
private readonly filePath: MaybeString;
|
||||||
private readonly file: JSONFile<RestorePoint | null>;
|
private readonly file: JSONFile<RestorePoint | null>;
|
||||||
private lastStore: string | null;
|
private lastStore: MaybeString;
|
||||||
private failedCreateAttempts: number;
|
private failedCreateAttempts: number;
|
||||||
|
|
||||||
constructor(filePath: string) {
|
constructor(filePath: string) {
|
||||||
@@ -128,7 +128,7 @@ export class RestoreService {
|
|||||||
*/
|
*/
|
||||||
async clear() {
|
async clear() {
|
||||||
try {
|
try {
|
||||||
await this.write(null);
|
await this.file.write(null);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { isRestorePoint, RestorePoint, RestoreService } from '../RestoreService.
|
|||||||
|
|
||||||
describe('isRestorePoint()', () => {
|
describe('isRestorePoint()', () => {
|
||||||
it('validates a well defined object', () => {
|
it('validates a well defined object', () => {
|
||||||
let restorePoint = {
|
let restorePoint: RestorePoint = {
|
||||||
playback: 'play',
|
playback: Playback.Roll,
|
||||||
selectedEventId: '123',
|
selectedEventId: '123',
|
||||||
startedAt: 1,
|
startedAt: 1,
|
||||||
addedTime: 2,
|
addedTime: 2,
|
||||||
@@ -17,10 +17,10 @@ describe('isRestorePoint()', () => {
|
|||||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||||
|
|
||||||
restorePoint = {
|
restorePoint = {
|
||||||
playback: 'roll',
|
playback: Playback.Roll,
|
||||||
selectedEventId: '123',
|
selectedEventId: '123',
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
addedTime: null,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(true);
|
expect(isRestorePoint(restorePoint)).toBe(true);
|
||||||
@@ -32,7 +32,7 @@ describe('isRestorePoint()', () => {
|
|||||||
playback: 'unknown',
|
playback: 'unknown',
|
||||||
selectedEventId: '123',
|
selectedEventId: '123',
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
addedTime: null,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||||
@@ -41,17 +41,17 @@ describe('isRestorePoint()', () => {
|
|||||||
const restorePoint = {
|
const restorePoint = {
|
||||||
selectedEventId: '123',
|
selectedEventId: '123',
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
addedTime: null,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||||
});
|
});
|
||||||
it('with incorrect value', () => {
|
it('with incorrect value', () => {
|
||||||
const restorePoint = {
|
const restorePoint = {
|
||||||
playback: 'roll',
|
playback: Playback.Roll,
|
||||||
selectedEventId: '123',
|
selectedEventId: '123',
|
||||||
startedAt: 'testing',
|
startedAt: 'testing',
|
||||||
addedTime: null,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
};
|
};
|
||||||
expect(isRestorePoint(restorePoint)).toBe(false);
|
expect(isRestorePoint(restorePoint)).toBe(false);
|
||||||
@@ -62,7 +62,7 @@ describe('isRestorePoint()', () => {
|
|||||||
describe('RestoreService()', () => {
|
describe('RestoreService()', () => {
|
||||||
describe('load()', () => {
|
describe('load()', () => {
|
||||||
it('loads working file with times', async () => {
|
it('loads working file with times', async () => {
|
||||||
const expected = {
|
const expected: RestorePoint = {
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
selectedEventId: 'da5b4',
|
selectedEventId: 'da5b4',
|
||||||
startedAt: 1234,
|
startedAt: 1234,
|
||||||
@@ -78,11 +78,11 @@ describe('RestoreService()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads working file without times', async () => {
|
it('loads working file without times', async () => {
|
||||||
const expected = {
|
const expected: RestorePoint = {
|
||||||
playback: Playback.Stop,
|
playback: Playback.Stop,
|
||||||
selectedEventId: null,
|
selectedEventId: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
addedTime: null,
|
addedTime: 0,
|
||||||
pausedAt: null,
|
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';
|
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 delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||||
const delayEvent = rundown.at(delayIndex);
|
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');
|
throw new Error('Given event ID is not a delay');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type GetTimeFn = () => number;
|
|||||||
|
|
||||||
export class ExtraTimerService {
|
export class ExtraTimerService {
|
||||||
private timer: SimpleTimer;
|
private timer: SimpleTimer;
|
||||||
private interval: NodeJS.Timer;
|
private interval: NodeJS.Timer | null = null;
|
||||||
private emit: EmitFn;
|
private emit: EmitFn;
|
||||||
private getTime: GetTimeFn;
|
private getTime: GetTimeFn;
|
||||||
|
|
||||||
@@ -23,7 +23,9 @@ export class ExtraTimerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private stopInterval() {
|
private stopInterval() {
|
||||||
clearInterval(this.interval);
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@broadcastReturn
|
@broadcastReturn
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function formatDisplayFromString(value: string, hideZero = false): string {
|
|||||||
return formatted;
|
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 = {
|
const quickAliases: AliasesDefinition = {
|
||||||
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
|
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
|
||||||
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
|
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>) {
|
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 === '') {
|
if (isOntimeEvent(eventData) && eventData?.cue === '') {
|
||||||
throw new Error('Cue value invalid');
|
throw new Error('Cue value invalid');
|
||||||
}
|
}
|
||||||
@@ -114,7 +117,7 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
|||||||
* @param eventId
|
* @param eventId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function deleteEvent(eventId) {
|
export async function deleteEvent(eventId: string) {
|
||||||
await cachedDelete(eventId);
|
await cachedDelete(eventId);
|
||||||
|
|
||||||
notifyChanges({ timer: [eventId], external: true });
|
notifyChanges({ timer: [eventId], external: true });
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { state, stateMutations } from '../../state.js';
|
|||||||
* Coordinating with necessary services
|
* Coordinating with necessary services
|
||||||
*/
|
*/
|
||||||
class RuntimeService {
|
class RuntimeService {
|
||||||
private eventTimer: TimerService;
|
private eventTimer: TimerService | null = null;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@@ -28,8 +28,10 @@ class RuntimeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
logger.info(LogOrigin.Server, 'Runtime service shutting down');
|
if (this.eventTimer) {
|
||||||
this.eventTimer.shutdown();
|
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 {
|
export function getExpectedFinish(state: TState): MaybeNumber {
|
||||||
const { startedAt, finishedAt, duration, addedTime } = state.timer;
|
const { startedAt, finishedAt, duration, addedTime } = state.timer;
|
||||||
const { timerType } = state.eventNow;
|
const { timerType, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { timeEnd } = state.eventNow;
|
|
||||||
const { clock } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
@@ -51,9 +50,8 @@ export function getExpectedFinish(state: TState): MaybeNumber {
|
|||||||
*/
|
*/
|
||||||
export function getCurrent(state: TState): number {
|
export function getCurrent(state: TState): number {
|
||||||
const { startedAt, duration, addedTime } = state.timer;
|
const { startedAt, duration, addedTime } = state.timer;
|
||||||
const { timerType } = state.eventNow;
|
const { timerType, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { timeEnd } = state.eventNow;
|
|
||||||
const { clock } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (timerType === TimerType.TimeToEnd) {
|
if (timerType === TimerType.TimeToEnd) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function getCached<T>(key: string, callback: () => T): T {
|
|||||||
const data = callback();
|
const data = callback();
|
||||||
runtimeCache.set(key, { data });
|
runtimeCache.set(key, { data });
|
||||||
} catch (error) {
|
} 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 {
|
export function setCached<T>(key: string, value: T): T {
|
||||||
runtimeCache.set(key, { data: value });
|
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);
|
runtimeCache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
/* eslint-disable no-console -- we are mocking the console */
|
/* eslint-disable no-console -- we are mocking the console */
|
||||||
import { vi } from 'vitest';
|
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 { dbModel } from '../../models/dataModel.js';
|
||||||
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
||||||
@@ -9,24 +18,24 @@ import { makeString } from '../parserUtils.js';
|
|||||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
||||||
|
|
||||||
describe('test json parser with valid def', () => {
|
describe('test json parser with valid def', () => {
|
||||||
const testData = {
|
const testData: Partial<DatabaseModel> = {
|
||||||
rundown: [
|
rundown: [
|
||||||
{
|
{
|
||||||
|
id: '4b31',
|
||||||
|
cue: 'Guest Welcoming',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
title: 'Guest Welcoming',
|
title: 'Guest Welcoming',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
presenter: '',
|
presenter: '',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.PlayNext,
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 31500000,
|
timeStart: 31500000,
|
||||||
timeEnd: 32400000,
|
timeEnd: 32400000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 32400000 - 31500000,
|
duration: 32400000 - 31500000,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
endAction: 'play-next',
|
skip: false,
|
||||||
timerType: 'clock',
|
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '4b31',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -37,24 +46,26 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'f24d',
|
||||||
|
cue: 'Good Morning',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
title: 'Good Morning',
|
title: 'Good Morning',
|
||||||
subtitle: 'Days schedule',
|
subtitle: 'Days schedule',
|
||||||
presenter: 'Carlos Valente',
|
presenter: 'Carlos Valente',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.PlayNext,
|
||||||
|
timerType: TimerType.CountUp,
|
||||||
timeStart: 32400000,
|
timeStart: 32400000,
|
||||||
timeEnd: 36000000,
|
timeEnd: 36000000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 36000000 - 32400000,
|
duration: 36000000 - 32400000,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
endAction: 'play-next',
|
|
||||||
timerType: 'count-up',
|
|
||||||
skip: true,
|
skip: true,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: 'f24d',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -65,23 +76,26 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'bbc5',
|
||||||
|
cue: 'Stage 2 setup',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
title: 'Stage 2 setup',
|
title: 'Stage 2 setup',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
presenter: '',
|
presenter: '',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: 'wrong action' as EndAction, // testing
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 32400000,
|
timeStart: 32400000,
|
||||||
timeEnd: 37200000,
|
timeEnd: 37200000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 37200000 - 32400000,
|
duration: 37200000 - 32400000,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
endAction: 'wrong action',
|
skip: false,
|
||||||
timerType: 'clock',
|
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: 'bbc5',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -92,41 +106,47 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// testing incomplete dataset
|
||||||
|
id: '5b3e',
|
||||||
|
cue: 'Working Procedures',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
title: 'Working Procedures',
|
title: 'Working Procedures',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
presenter: 'Filip Johansen',
|
presenter: 'Filip Johansen',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.None,
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 37200000,
|
timeStart: 37200000,
|
||||||
timeEnd: 39000000,
|
timeEnd: 39000000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 39000000 - 37200000,
|
duration: 39000000 - 37200000,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
endAction: 'none',
|
|
||||||
timerType: 'clock',
|
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '5b3e',
|
timeWarning: 0,
|
||||||
},
|
timeDanger: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
{
|
{
|
||||||
|
id: '8e2c',
|
||||||
|
cue: 'Lunch',
|
||||||
title: 'Lunch',
|
title: 'Lunch',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
presenter: '',
|
presenter: '',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.None,
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 39600000,
|
timeStart: 39600000,
|
||||||
timeEnd: 45000000,
|
timeEnd: 45000000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 37200000 - 32400000,
|
duration: 37200000 - 32400000,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
endAction: 'none',
|
skip: false,
|
||||||
timerType: 'clock',
|
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '8e2c',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -137,23 +157,26 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: '08e9',
|
||||||
|
cue: 'A day being carlos',
|
||||||
title: 'A day being carlos',
|
title: 'A day being carlos',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
subtitle: 'My life in a song',
|
subtitle: 'My life in a song',
|
||||||
presenter: 'Carlos Valente',
|
presenter: 'Carlos Valente',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.None,
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 46800000,
|
timeStart: 46800000,
|
||||||
timeEnd: 50400000,
|
timeEnd: 50400000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 37200000 - 32400000,
|
duration: 37200000 - 32400000,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
endAction: 'none',
|
skip: true,
|
||||||
timerType: 'clock',
|
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '08e9',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -164,23 +187,26 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// testing incomplete dataset
|
||||||
|
id: 'e25a',
|
||||||
|
cue: 'Hamburgers and Cheese',
|
||||||
title: 'Hamburgers and Cheese',
|
title: 'Hamburgers and Cheese',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
subtitle: '... and other life questions',
|
subtitle: '... and other life questions',
|
||||||
presenter: 'Filip Johansen',
|
presenter: 'Filip Johansen',
|
||||||
note: '',
|
note: '',
|
||||||
|
endAction: EndAction.None,
|
||||||
|
timerType: TimerType.Clock,
|
||||||
timeStart: 54000000,
|
timeStart: 54000000,
|
||||||
timeEnd: 57600000,
|
timeEnd: 57600000,
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 37200000 - 32400000,
|
duration: 37200000 - 32400000,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
endAction: 'none',
|
|
||||||
timerType: 'clock',
|
|
||||||
colour: '',
|
colour: '',
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: 'e25a',
|
|
||||||
user0: '',
|
user0: '',
|
||||||
user1: '',
|
user1: '',
|
||||||
user2: '',
|
user2: '',
|
||||||
@@ -191,20 +217,23 @@ describe('test json parser with valid def', () => {
|
|||||||
user7: '',
|
user7: '',
|
||||||
user8: '',
|
user8: '',
|
||||||
user9: '',
|
user9: '',
|
||||||
},
|
revision: 0,
|
||||||
|
timeWarning: 0,
|
||||||
|
timeDanger: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
],
|
],
|
||||||
project: {
|
project: {
|
||||||
title: 'This is a test definition',
|
title: 'This is a test definition',
|
||||||
url: 'www.carlosvalente.com',
|
backstageUrl: 'www.carlosvalente.com',
|
||||||
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||||
},
|
} as ProjectData,
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
},
|
} as Settings,
|
||||||
viewSettings: {},
|
viewSettings: {} as ViewSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
let parseResponse;
|
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);
|
const parseResponse = await parseJson(testData);
|
||||||
expect(typeof (parseResponse.rundown[0] as OntimeEvent).cue).toBe('string');
|
expect(typeof (parseResponse.rundown[0] as OntimeEvent).cue).toBe('string');
|
||||||
expect(typeof (parseResponse.rundown[1] 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);
|
const parseResponse = await parseJson(testData);
|
||||||
expect(parseResponse.rundown[0].id).toBeDefined();
|
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);
|
const parseResponse = await parseJson(testData);
|
||||||
expect(console.log).toHaveBeenCalledWith('ERROR: ID collision on import, skipping');
|
expect(console.log).toHaveBeenCalledWith('ERROR: ID collision on import, skipping');
|
||||||
expect(parseResponse?.rundown.length).toBe(1);
|
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);
|
const parseResponse = await parseJson(testData);
|
||||||
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
|
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
|
||||||
expect(parseResponse?.rundown.length).toBe(0);
|
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);
|
await parseJson(testData);
|
||||||
expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping');
|
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',
|
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',
|
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||||
endMessage: '',
|
endMessage: '',
|
||||||
@@ -389,11 +426,11 @@ describe('test corrupt data', () => {
|
|||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
lock: null,
|
|
||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const parsedDef = await parseJson(emptyEvents);
|
const parsedDef = await parseJson(emptyEvents);
|
||||||
expect(parsedDef.rundown.length).toBe(2);
|
expect(parsedDef.rundown.length).toBe(2);
|
||||||
});
|
});
|
||||||
@@ -401,9 +438,11 @@ describe('test corrupt data', () => {
|
|||||||
it('handles all empty events', async () => {
|
it('handles all empty events', async () => {
|
||||||
const emptyEvents = {
|
const emptyEvents = {
|
||||||
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
||||||
event: {
|
project: {
|
||||||
title: 'All about Carlos demo event',
|
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',
|
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
|
||||||
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
|
||||||
endMessage: '',
|
endMessage: '',
|
||||||
@@ -412,11 +451,11 @@ describe('test corrupt data', () => {
|
|||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
lock: null,
|
|
||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const parsedDef = await parseJson(emptyEvents);
|
const parsedDef = await parseJson(emptyEvents);
|
||||||
expect(parsedDef.rundown.length).toBe(0);
|
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);
|
const parsedDef = await parseJson(emptyProjectData);
|
||||||
expect(parsedDef.project).toStrictEqual(dbModel.project);
|
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);
|
const parsedDef = await parseJson(missingSettings);
|
||||||
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
|
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails with invalid JSON', async () => {
|
it('fails with invalid JSON', async () => {
|
||||||
const invalidJSON = 'some random dataset';
|
const invalidJSON = 'some random dataset';
|
||||||
|
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
const parsedDef = await parseJson(invalidJSON);
|
const parsedDef = await parseJson(invalidJSON);
|
||||||
expect(parsedDef).toBeNull();
|
expect(parsedDef).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|||||||
* @description Creates a directory if it doesn't exist
|
* @description Creates a directory if it doesn't exist
|
||||||
* @param {string} directory - directory that should exist or will be created
|
* @param {string} directory - directory that should exist or will be created
|
||||||
*/
|
*/
|
||||||
export function ensureDirectory(directory) {
|
export function ensureDirectory(directory: string) {
|
||||||
if (!existsSync(directory)) {
|
if (!existsSync(directory)) {
|
||||||
try {
|
try {
|
||||||
mkdirSync(directory, { recursive: true });
|
mkdirSync(directory, { recursive: true });
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
* @param {object} jsonData - project file to be parsed
|
* @param {object} jsonData - project file to be parsed
|
||||||
* @returns {object} - parsed object
|
* @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') {
|
if (!jsonData || typeof jsonData !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import { ensureJsonExtension } from './ensureJsonExtension.js';
|
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, newFilename } = req.body;
|
||||||
const { filename: projectName } = req.params;
|
const { filename: projectName } = req.params;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["vitest/globals"],
|
"types": ["vitest/globals"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,4 +67,4 @@ export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './defini
|
|||||||
|
|
||||||
// TYPE UTILITIES
|
// TYPE UTILITIES
|
||||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js';
|
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,4 +1,5 @@
|
|||||||
export type MaybeNumber = number | null;
|
export type MaybeNumber = number | null;
|
||||||
|
export type MaybeString = string | null;
|
||||||
|
|
||||||
export type DeepPartial<T> = {
|
export type DeepPartial<T> = {
|
||||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||||
|
|||||||
Generated
+13
-10
@@ -316,6 +316,9 @@ importers:
|
|||||||
'@types/websocket':
|
'@types/websocket':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
|
'@types/ws':
|
||||||
|
specifier: ^8.5.10
|
||||||
|
version: 8.5.10
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^6.10.0
|
specifier: ^6.10.0
|
||||||
version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)
|
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
|
'@jest/schemas': 29.6.3
|
||||||
'@types/istanbul-lib-coverage': 2.0.4
|
'@types/istanbul-lib-coverage': 2.0.4
|
||||||
'@types/istanbul-reports': 3.0.1
|
'@types/istanbul-reports': 3.0.1
|
||||||
'@types/node': 20.10.5
|
'@types/node': 18.19.3
|
||||||
'@types/yargs': 17.0.19
|
'@types/yargs': 17.0.19
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3155,7 +3158,7 @@ packages:
|
|||||||
/@types/connect@3.4.35:
|
/@types/connect@3.4.35:
|
||||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.19.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/debug@4.1.12:
|
/@types/debug@4.1.12:
|
||||||
@@ -3188,7 +3191,7 @@ packages:
|
|||||||
/@types/fs-extra@9.0.13:
|
/@types/fs-extra@9.0.13:
|
||||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.10.5
|
'@types/node': 18.19.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/http-cache-semantics@4.0.4:
|
/@types/http-cache-semantics@4.0.4:
|
||||||
@@ -3265,12 +3268,6 @@ packages:
|
|||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
dev: true
|
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:
|
/@types/parse-json@4.0.0:
|
||||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3350,6 +3347,12 @@ packages:
|
|||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
dev: true
|
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:
|
/@types/yargs-parser@21.0.0:
|
||||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -6590,7 +6593,7 @@ packages:
|
|||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.3.1
|
'@jest/types': 29.3.1
|
||||||
'@types/node': 20.10.5
|
'@types/node': 18.19.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.7.1
|
ci-info: 3.7.1
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|||||||
Reference in New Issue
Block a user