Refactor/socket controller (#212)

* refactor(socket): dictionary cleanup
* refactor(socket): detangle socket from EventTimer
* refactor(socket): poll object
* refactor(socket): wip, extract data responsibilities to provider
* fix: issue with onAir
* refactor: create data provider and validation utils
* refactor: remove deprecated endpoint
* refactor: detangle and validate ontime controller
* refactor: detangle and validate events controller
* refactor: validate routers
* refactor: handle post failure in modals
This commit is contained in:
Carlos Valente
2022-10-09 19:10:48 +02:00
committed by GitHub
parent cd0577bf99
commit c2ed9d7634
28 changed files with 1145 additions and 898 deletions
-6
View File
@@ -165,12 +165,6 @@ export const getInfo = async () => {
return res.data; return res.data;
}; };
/**
* @description HTTP request to mutate application info
* @return {Promise}
*/
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
/** /**
* @description HTTP request to retrieve view settings * @description HTTP request to retrieve view settings
* @return {Promise} * @return {Promise}
@@ -58,6 +58,7 @@ export default function AppSettingsModal() {
setSubmitting(true); setSubmitting(true);
// set context // set context
// TODO: add fast-equals here and check if event settings have changed
saveEventSettings(formSettings); saveEventSettings(formSettings);
const validation = { isValid: false }; const validation = { isValid: false };
@@ -83,12 +84,17 @@ export default function AppSettingsModal() {
if (!validation.isValid) { if (!validation.isValid) {
emitError(`Invalid Input: ${validation.message}`); emitError(`Invalid Input: ${validation.message}`);
} else { } else {
await postSettings(formData); try {
await refetch(); await postSettings(formData);
} catch (error) {
emitError(`Error saving settings: ${error}`)
} finally {
await refetch();
setChanged(false);
}
validation?.message && emitWarning(validation.message); validation?.message && emitWarning(validation.message);
} }
setSubmitting(false); setSubmitting(false);
setChanged(false);
}; };
/** /**
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, Input, Textarea } from '@chakra-ui/react'; import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { EVENT_TABLE } from 'common/api/apiConstants'; import { EVENT_TABLE } from 'common/api/apiConstants';
@@ -6,6 +6,7 @@ import { fetchEvent, postEvent } from 'common/api/eventApi';
import { useFetch } from 'common/hooks/useFetch'; import { useFetch } from 'common/hooks/useFetch';
import { eventPlaceholderSettings } from '../../common/api/ontimeApi'; import { eventPlaceholderSettings } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import { inputProps } from './modalHelper'; import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer'; import SubmitContainer from './SubmitContainer';
@@ -14,6 +15,7 @@ import style from './Modals.module.scss';
export default function SettingsModal() { export default function SettingsModal() {
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent); const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(eventPlaceholderSettings); const [formData, setFormData] = useState(eventPlaceholderSettings);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -42,13 +44,18 @@ export default function SettingsModal() {
event.preventDefault(); event.preventDefault();
setSubmitting(true); setSubmitting(true);
await postEvent(formData); try {
await refetch(); await postEvent(formData);
} catch (error) {
emitError(`Error saving event settings: ${error}`)
} finally {
await refetch();
setChanged(false);
}
setChanged(false);
setSubmitting(false); setSubmitting(false);
}, },
[formData, refetch] [emitError, formData, refetch]
); );
/** /**
@@ -3,7 +3,7 @@ import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react'; import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import { FiInfo } from '@react-icons/all-files/fi/FiInfo'; import { FiInfo } from '@react-icons/all-files/fi/FiInfo';
import { APP_TABLE } from 'common/api/apiConstants'; import { APP_TABLE } from 'common/api/apiConstants';
import { getInfo, httpPlaceholder, ontimeVars, postInfo } from 'common/api/ontimeApi'; import { getInfo, httpPlaceholder, ontimeVars } from 'common/api/ontimeApi';
import { useFetch } from 'common/hooks/useFetch'; import { useFetch } from 'common/hooks/useFetch';
import { LoggingContext } from '../../common/context/LoggingContext'; import { LoggingContext } from '../../common/context/LoggingContext';
@@ -50,7 +50,7 @@ export default function IntegrationSettingsModal() {
if (e.status) { if (e.status) {
emitError(`Invalid Input: ${e.message}`); emitError(`Invalid Input: ${e.message}`);
} else { } else {
await postInfo(f); // call API endpoint here with value of f
setChanged(false); setChanged(false);
setSubmitting(false); setSubmitting(false);
} }
@@ -122,10 +122,14 @@ export default function OscSettingsModal() {
if (e.status) { if (e.status) {
emitError(`Invalid Input: ${e.message}`); emitError(`Invalid Input: ${e.message}`);
} else { } else {
// Post here try {
await postOSC(formData); await postOSC(formData);
await refetch(); } catch (error){
setChanged(false); emitError(`Error setting OSC: ${error}`)
} finally {
await refetch();
setChanged(false);
}
} }
setSubmitting(false); setSubmitting(false);
}, },
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
@@ -6,6 +6,7 @@ import { USERFIELDS } from 'common/api/apiConstants';
import { useFetch } from 'common/hooks/useFetch'; import { useFetch } from 'common/hooks/useFetch';
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../common/api/ontimeApi'; import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../common/api/ontimeApi';
import { LoggingContext } from '../../common/context/LoggingContext';
import { handleLinks, host } from '../../common/utils/linkUtils'; import { handleLinks, host } from '../../common/utils/linkUtils';
import SubmitContainer from './SubmitContainer'; import SubmitContainer from './SubmitContainer';
@@ -14,6 +15,7 @@ import style from './Modals.module.scss';
export default function TableOptionsModal() { export default function TableOptionsModal() {
const { data, status, refetch } = useFetch(USERFIELDS, getUserFields); const { data, status, refetch } = useFetch(USERFIELDS, getUserFields);
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [userFields, setUserFields] = useState(userFieldsPlaceholder); const [userFields, setUserFields] = useState(userFieldsPlaceholder);
@@ -43,13 +45,17 @@ export default function TableOptionsModal() {
} }
if (!errors) { if (!errors) {
await postUserFields(validatedFields); try {
await postUserFields(validatedFields);
} catch (error) {
emitError(`Error saving table options: ${error}`)
}
await refetch(); await refetch();
setChanged(false); setChanged(false);
} }
setSubmitting(false); setSubmitting(false);
},[refetch, userFields]); },[emitError, refetch, userFields]);
/** /**
* Reverts local state equals to server state * Reverts local state equals to server state
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel } from '@chakra-ui/react'; import { FormControl, FormLabel } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
@@ -6,6 +6,7 @@ import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInforma
import { VIEW_SETTINGS } from '../../common/api/apiConstants'; import { VIEW_SETTINGS } from '../../common/api/apiConstants';
import { getView, postView, viewsPlaceholder } from '../../common/api/ontimeApi'; import { getView, postView, viewsPlaceholder } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn'; import EnableBtn from '../../common/components/buttons/EnableBtn';
import { LoggingContext } from '../../common/context/LoggingContext';
import { useFetch } from '../../common/hooks/useFetch'; import { useFetch } from '../../common/hooks/useFetch';
import { openLink } from '../../common/utils/linkUtils'; import { openLink } from '../../common/utils/linkUtils';
@@ -15,6 +16,7 @@ import style from './Modals.module.scss';
export default function ViewsSettingsModal() { export default function ViewsSettingsModal() {
const { data, status, refetch } = useFetch(VIEW_SETTINGS, getView); const { data, status, refetch } = useFetch(VIEW_SETTINGS, getView);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(viewsPlaceholder); const [formData, setFormData] = useState(viewsPlaceholder);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -35,12 +37,17 @@ export default function ViewsSettingsModal() {
async (event) => { async (event) => {
event.preventDefault(); event.preventDefault();
setSubmitting(true); setSubmitting(true);
await postView(formData); try {
await refetch(); await postView(formData);
setChanged(false); } catch (error) {
emitError(`Error view settings: ${error}`)
} finally{
await refetch();
setChanged(false);
}
setSubmitting(false); setSubmitting(false);
}, },
[formData, refetch] [emitError, formData, refetch]
); );
/** /**
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
appIni: {
mainWindowWait: 2000,
},
reactAppUrl: {
development: 'http://localhost:3000/editor',
production: 'http://localhost:4001/editor',
},
externalUrls: {
help: 'https://cpvalente.gitbook.io/ontime/',
},
};
+15 -14
View File
@@ -10,19 +10,18 @@ const {
Notification, Notification,
} = require('electron'); } = require('electron');
const path = require('path'); const path = require('path');
const electronConfig = require('./electron.config');
if (process.env.NODE_ENV === undefined) { if (process.env.NODE_ENV === undefined) {
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
} }
const env = process.env.NODE_ENV; const isProduction = process.env.NODE_ENV === 'production';
let loaded = 'Nothing loaded'; let loaded = 'Nothing loaded';
let isQuitting = false; let isQuitting = false;
const nodePath = const nodePath = isProduction
env !== 'production' ? path.join('file://', __dirname, '../', 'extraResources', 'src/app.js')
? path.join('file://', __dirname, 'src/app.js') : path.join('file://', __dirname, 'src/app.js');
: path.join('file://', __dirname, '../', 'extraResources', 'src/app.js');
(async () => { (async () => {
try { try {
@@ -144,22 +143,25 @@ app.whenReady().then(() => {
// give the nodejs server some time // give the nodejs server some time
setTimeout(() => { setTimeout(() => {
// Load page served by node // Load page served by node
const reactApp = const reactApp = isProduction
env === 'development' ? 'http://localhost:3000/editor' : 'http://localhost:4001/editor'; ? electronConfig.reactAppUrl.production
: electronConfig.reactAppUrl.development;
win.loadURL(reactApp).then(() => { win.loadURL(reactApp).then(() => {
win.webContents.setBackgroundThrottling(false); win.webContents.setBackgroundThrottling(false);
// window stuff
win.show(); win.show();
win.focus(); win.focus();
splash.destroy(); splash.destroy();
// tray stuff if (typeof loaded === 'string') {
tray.setToolTip(loaded); tray.setToolTip(loaded);
} else {
tray.setToolTip('Initialising error: please restart ontime');
}
}); });
}, 2000); }, electronConfig.appIni.mainWindowWait);
// Hide on close // Hide on close
win.on('close', function (event) { win.on('close', function (event) {
@@ -173,7 +175,6 @@ app.whenReady().then(() => {
}); });
// create tray // create tray
// TODO: Design better icon
tray = new Tray(trayIcon); tray = new Tray(trayIcon);
// Define context menu // Define context menu
@@ -269,7 +270,7 @@ ipcMain.on('send-to-link', (event, arg) => {
// send to help URL // send to help URL
if (arg === 'help') { if (arg === 'help') {
shell.openExternal('https://cpvalente.gitbook.io/ontime/'); shell.openExternal(electronConfig.externalUrls.help);
} else { } else {
shell.openExternal(arg); shell.openExternal(arg);
} }
+24 -15
View File
@@ -21,9 +21,11 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
// Global Objects // Global Objects
import { EventTimer } from './classes/timer/EventTimer.js'; import { EventTimer } from './classes/timer/EventTimer.js';
import { socketProvider } from './classes/socket/SocketController.js';
// Start OSC server // Start OSC server
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js'; import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { DataProvider } from './classes/data-provider/DataProvider.js';
// get environment // get environment
const env = process.env.NODE_ENV || 'production'; const env = process.env.NODE_ENV || 'production';
@@ -31,9 +33,11 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
export const { db, data } = await loadDb(__dirname); export const { db, data } = await loadDb(__dirname);
console.log(`Starting ontime version ${process.env.npm_package_version}`); console.log(`Starting ontime version ${process.env.npm_package_version}`);
// import socket provider
const socket = socketProvider;
// Create express APP // Create express APP
const app = express(); const app = express();
app.disable('x-powered-by'); app.disable('x-powered-by');
@@ -80,22 +84,22 @@ app.use((err, req, res, next) => {
* *
*/ */
const osc = data.osc; const { osc, settings } = DataProvider.getData();
const oscIP = osc?.targetIP || config.osc.targetIP; const oscIP = osc?.targetIP || config.osc.targetIP;
const oscOutPort = osc?.portOut || config.osc.portOut; const oscOutPort = osc?.portOut || config.osc.portOut;
const oscInPort = osc?.port || config.osc.port; const oscInPort = osc?.port || config.osc.port;
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled; const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
const serverPort = settings.serverPort || config.server.port;
const serverPort = data.settings.serverPort || config.server.port;
/** /**
* @description starts OSC server
* @description starts OSC server * @description starts OSC server
* @param overrideConfig * @param overrideConfig
* @return {Promise<void>} * @return {Promise<void>}
*/ */
export const startOSCServer = async (overrideConfig = null) => { export const startOSCServer = async (overrideConfig = null) => {
if (!oscInEnabled) { if (!oscInEnabled) {
global.timer.info('RX', 'OSC Input Disabled'); socket.info('RX', 'OSC Input Disabled');
return; return;
} }
@@ -105,7 +109,7 @@ export const startOSCServer = async (overrideConfig = null) => {
}; };
// Start OSC Server // Start OSC Server
global.timer.info('RX', `Starting OSC Server on port: ${oscInPort}`); socket.info('RX', `Starting OSC Server on port: ${oscInPort}`);
initiateOSC(oscSettings); initiateOSC(oscSettings);
}; };
@@ -119,11 +123,16 @@ const server = http.createServer(app);
*/ */
export const startServer = async (overrideConfig = null) => { export const startServer = async (overrideConfig = null) => {
const port = 4001; // port hardcoded const port = 4001; // port hardcoded
const { events, http } = DataProvider.getData();
// Start server // Start server
const returnMessage = `Ontime is listening on port ${port}`; const returnMessage = `Ontime is listening on port ${port}`;
server.listen(port, '0.0.0.0'); server.listen(port, '0.0.0.0');
// init socket controller
await socket.initServer(server);
socket.info('SERVER', 'Socket initialised');
// OSC Config // OSC Config
const oscConfig = { const oscConfig = {
ip: oscIP, ip: oscIP,
@@ -131,9 +140,11 @@ export const startServer = async (overrideConfig = null) => {
}; };
// init timer // init timer
global.timer = new EventTimer(server, config.timer, oscConfig, data.http); global.timer = new EventTimer(socket, config.timer, oscConfig, http);
global.timer.setupWithEventList(data.events); global.timer.setupWithEventList(events);
global.timer.info('SERVER', returnMessage);
socket.info('SERVER', returnMessage);
socket.startListener();
return returnMessage; return returnMessage;
}; };
@@ -146,16 +157,14 @@ export const shutdown = async () => {
// shutdown express server // shutdown express server
server.close(); server.close();
// shutdown OSC Server
shutdownOSCServer(); shutdownOSCServer();
// shutdown timer
global.timer.shutdown(); global.timer.shutdown();
socket.shutdown();
}; };
// register shutdown signals // register shutdown signals
process.once('SIGHUP', shutdown) process.once('SIGHUP', shutdown);
process.once('SIGINT', shutdown) process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown) process.once('SIGTERM', shutdown);
export { server, app }; export { server, app };
@@ -1,7 +1,168 @@
/** /**
* Class Event Provider adds functions specific for handling event data * Class Event Provider is a mediator for handling the local db
* and adds logic specific to ontime data
*/ */
import { data, db } from '../../app.js';
export class DataProvider { export class DataProvider {
static getData() {
return data;
}
static async setEventData(newData) {
data.event = { ...data.event, ...newData };
await this.persist();
return data.event;
}
static getEventData() {
return data.event;
}
static async setEvents(newData) {
data.events = [...newData];
await this.persist();
}
static getEventById(eventId) {
return data.events.find((e) => e.id === eventId);
}
static async updateEventById(eventId, newData) {
const eventIndex = data.events.findIndex((e) => e.id === eventId);
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...newData };
data.events[eventIndex].revision++;
await this.persist();
return data.events[eventIndex];
}
static async deleteEvent(eventId) {
data.events = Array.from(data.events).filter((e) => e.id !== eventId);
await this.persist();
}
static getNumEvents() {
return data.events.length;
}
static async deleteAllEvents() {
data.events = [];
await db.write();
}
/**
* Insets an event after a given index
* @param entry
* @param index
* @return {Promise<void>}
* @private
*/
static async insertEventAt(entry, index) {
// get events
const events = DataProvider.getEvents();
const count = events.length;
const order = entry.order;
// Remove order field from object
delete entry.order;
// Insert at beginning
if (order === 0) {
events.unshift(entry);
}
// insert at end
else if (order >= count) {
events.push(entry);
}
// insert in the middle
else {
events.splice(index, 0, entry);
}
// save events
await DataProvider.setEvents(events);
}
/**
* @description Inserts an entry after an element with given ID
* @param entry
* @param id
* @return {Promise<void>}
* @private
*/
static async insertEventAfterId(entry, id) {
const index = [...data.events].findIndex((event) => event.id === id);
await DataProvider.insertEventAt(entry, index + 1);
}
static getSettings() {
return data.settings;
}
static async setSettings(newData) {
data.settings = { ...newData };
await this.persist();
}
static getOsc() {
return data.osc;
}
static getAliases() {
return data.aliases;
}
static async setAliases(newData) {
data.aliases = newData;
await this.persist();
}
static getUserFields() {
return { ...data.userFields };
}
static getViews() {
return { ...data.views };
}
static async setViews(newData) {
data.views = { ...newData };
await this.persist();
}
static async setUserFields(newData) {
data.userFields = { ...newData };
await this.persist();
}
static async setOsc(newData) {
data.osc = { ...newData };
await this.persist();
}
static getEvents() {
return [...data.events];
}
static async persist() {
await db.write();
}
static async mergeIntoData(newData) {
const mergedData = DataProvider.safeMerge(data, newData);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
data.events = mergedData.events;
await this.persist();
}
/** /**
* Merges two data objects * Merges two data objects
* @param {object} existing * @param {object} existing
@@ -0,0 +1,400 @@
import { Server } from 'socket.io';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
import { Timer } from '../timer/Timer.js';
class SocketController {
constructor() {
this.numClients = 0;
this.messageStack = [];
this._MAX_MESSAGES = 100;
this._clientNames = {};
this.socket = null;
}
initServer(httpServer) {
this.socket = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
}
startListener() {
this._socketMessageHandler();
}
shutdown() {
this.info('SERVER', 'Shutting down ontime');
if (this.socket) {
this.info('TX', '... Closing socket server');
this.socket.close();
}
}
/**
* Handle socket io connections
* @private
*/
_socketMessageHandler() {
this.socket.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this.numClients++;
this._clientNames[socket.id] = getRandomName();
const message = `${this.numClients} Clients with new connection: ${
this._clientNames[socket.id]
}`;
this.info('CLIENT', message);
// send state
socket.emit('timer', global.timer.getTimeObject());
socket.emit('playstate', global.timer.state);
socket.emit('selected-id', global.timer.selectedEventId);
socket.emit('next-id', global.timer.nextEventId);
socket.emit('publicselected-id', global.timer.selectedPublicEventId);
socket.emit('publicnext-id', global.timer.nextPublicEventId);
/**
* @description handle disconnecting a user
*/
socket.on('disconnect', () => {
this.numClients--;
const message = `${this.numClients} Clients with disconnection: ${
this._clientNames[socket.id]
}`;
delete this._clientNames[socket.id];
this.info('CLIENT', message);
});
/**
* @description utility for renaming a user
*/
socket.on('rename-client', (newName) => {
if (newName) {
const previousName = this._clientNames[socket.id];
this._clientNames[socket.id] = newName;
this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
}
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ------- WEBSOCKET API ------- ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
socket.on('ontime-test', () => {
socket.emit('hello', socket.id);
});
socket.on('set-start', () => {
global.timer.trigger('start');
socket.emit('playstate', global.timer.state);
});
socket.on('set-startid', (data) => {
global.timer.trigger('startById', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-startindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
global.timer.trigger('startByIndex', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-loadid', (data) => {
global.timer.trigger('loadById', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-loadindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
global.timer.trigger('loadByIndex', data);
socket.emit('playstate', global.timer.state);
});
socket.on('set-pause', () => {
global.timer.trigger('pause');
socket.emit('playstate', global.timer.state);
});
socket.on('set-stop', () => {
global.timer.trigger('stop');
socket.emit('playstate', global.timer.state);
});
socket.on('set-reload', () => {
global.timer.trigger('reload');
socket.emit('playstate', global.timer.state);
});
socket.on('set-previous', () => {
global.timer.trigger('previous');
socket.emit('playstate', global.timer.state);
});
socket.on('set-next', () => {
global.timer.trigger('next');
socket.emit('playstate', global.timer.state);
});
socket.on('set-roll', () => {
global.timer.trigger('roll');
socket.emit('playstate', global.timer.state);
});
socket.on('set-delay', (data) => {
const delayTime = Number(data);
if (isNaN(delayTime)) {
return;
}
global.timer.increment(delayTime * 1000 * 60);
});
socket.on('set-onAir', (data) => {
try {
const d = JSON.parse(data);
d ? global.timer.trigger('onAir') : global.timer.trigger('offAir');
} catch (error) {
this.error('RX', `Failed to parse message ${data}`);
}
this.send('onAir', global.timer.onAir);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// general playback state, useful for external sync
socket.on('ontime-poll', () => {
const timerPoll = global.timer.poll();
const isDelayed = false;
const colour = '';
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
});
/*******************************************/
// ** TO BE DEPRECATED ** //
socket.on('get-timer', () => {
socket.emit('timer', global.timer.getTimeObject());
});
// ** TO BE DEPRECATED IN FAVOR OF DELAY ** //
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data, 10))) return;
if (data < -5 || data > 5) return;
global.timer.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
global.timer.trigger(data);
global.timer._broadcastFeaturePlaybackControl();
global.timer._broadcastFeatureInfo();
});
socket.on('get-playstate', () => {
socket.emit('playstate', global.timer.state);
});
socket.on('get-onAir', () => {
socket.emit('onAir', global.timer.onAir);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: global.timer.selectedEventId,
index: global.timer.selectedEventIndex,
total: global.timer._eventlist.length,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', global.timer.selectedEventId);
});
socket.on('get-next-id', () => {
socket.emit('next-id', global.timer.nextEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', global.timer.titles);
});
socket.on('get-publictitles', () => {
socket.emit('publictitles', global.timer.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Presenter message
socket.on('set-timer-message-text', (data) => {
global.timer._setTitles('set-timer-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-timer-message-visible', (data) => {
global.timer._setTitles('set-timer-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// Public message
socket.on('set-public-message-text', (data) => {
global.timer._setTitles('set-public-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-public-message-visible', (data) => {
global.timer._setTitles('set-public-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// Lower third message
socket.on('set-lower-message-text', (data) => {
global.timer._setTitles('set-lower-text', data);
global.timer._broadcastFeatureMessageControl();
});
socket.on('set-lower-message-visible', (data) => {
global.timer._setTitles('set-lower-visible', data);
global.timer._broadcastFeatureMessageControl();
});
/* MOLECULAR ENDPOINTS
* =====================
* 1. EVENT LIST
* 2. MESSAGE CONTROL
* 3. PLAYBACK CONTROL
* 4. INFO
* 5. CUE SHEET
* 6. TIMER OBJECT
* */
// 1. EVENT LIST
socket.on('get-ontime-feat-eventlist', () => {
global.timer._broadcastFeatureEventList();
});
// 2. MESSAGE CONTROL
socket.on('get-ontime-feat-messagecontrol', () => {
global.timer._broadcastFeatureMessageControl();
});
// 3. PLAYBACK CONTROL
socket.on('get-ontime-feat-playbackcontrol', () => {
global.timer._broadcastFeaturePlaybackControl();
});
// 4. INFO
socket.on('get-ontime-feat-info', () => {
global.timer._broadcastFeatureInfo();
});
// 5. CUE SHEET
socket.on('get-ontime-feat-cuesheet', () => {
global.timer._broadcastFeatureCuesheet();
});
// 6. TIMER
socket.on('get-ontime-timer', () => {
global.timer._broadcastFeatureTimer();
});
});
}
send(topic, payload) {
this.socket.emit(topic, payload);
}
/****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const logMessage = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(Timer.getCurrentTime() || 0),
};
this.messageStack.unshift(logMessage);
this.socket.emit('logger', logMessage);
if (process.env.NODE_ENV !== 'production') {
console.log(`[${logMessage.level}] \t ${logMessage.origin} \t ${logMessage.text}`);
}
if (this.messageStack.length > this._MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* Sends a message with level LOG
* @param {string} origin
* @param {string} text
*/
info(origin, text) {
this._push('INFO', origin, text);
}
/**
* Sends a message with level WARN
* @param {string} origin
* @param {string} text
*/
warning(origin, text) {
this._push('WARN', origin, text);
}
/**
* Sends a message with level ERROR
* @param {string} origin
* @param {string} text
*/
error(origin, text) {
this._push('ERROR', origin, text);
}
}
export const socketProvider = new SocketController();
+66 -443
View File
@@ -1,12 +1,8 @@
import { Timer } from './Timer.js'; import { Timer } from './Timer.js';
import { Server } from 'socket.io';
import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js'; import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
import { OSCIntegration } from './integrations/Osc.js'; import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js'; import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../../utils/url.js'; import { cleanURL } from '../../utils/url.js';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
/* /*
* Class EventTimer adds functions specific to APP * Class EventTimer adds functions specific to APP
@@ -16,12 +12,12 @@ import { stringFromMillis } from '../../utils/time.js';
export class EventTimer extends Timer { export class EventTimer extends Timer {
/** /**
* Instantiates an event timer object * Instantiates an event timer object
* @param {object} httpServer * @param {object} socket
* @param {object} timerConfig * @param {object} timerConfig
* @param {object} [oscConfig] * @param {object} [oscConfig]
* @param {object} [httpConfig] * @param {object} [httpConfig]
*/ */
constructor(httpServer, timerConfig, oscConfig, httpConfig) { constructor(socket, timerConfig, oscConfig, httpConfig) {
// call super constructor // call super constructor
super(); super();
@@ -42,13 +38,15 @@ export class EventTimer extends Timer {
this.ontimeCycle = 'idle'; this.ontimeCycle = 'idle';
this.prevCycle = null; this.prevCycle = null;
// Socket Object
this.socket = socket;
// OSC Object // OSC Object
this.osc = null; this.osc = null;
// HTTP Client Object // HTTP Client Object
this.http = null; this.http = null;
this._numClients = 0;
this._interval = null; this._interval = null;
this.presenter = { this.presenter = {
@@ -69,26 +67,9 @@ export class EventTimer extends Timer {
this._eventlist = []; this._eventlist = [];
this.onAir = false; this.onAir = false;
// initialise socketIO server
this.messageStack = [];
this.MAX_MESSAGES = 100;
this._clientNames = {};
this.io = new Server(httpServer, {
cors: {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
},
});
// set recurrent emits // set recurrent emits
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000); this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
// listen to new connections
this._listenToConnections();
if (oscConfig != null) { if (oscConfig != null) {
this._initOscClient(oscConfig); this._initOscClient(oscConfig);
} }
@@ -103,17 +84,12 @@ export class EventTimer extends Timer {
*/ */
shutdown() { shutdown() {
clearInterval(this._interval); clearInterval(this._interval);
this.info('SERVER', 'Shutting down ontime');
if (this.io != null) {
this.info('TX', '... Closing socket server');
this.io.close();
}
if (this.osc != null) { if (this.osc != null) {
this.info('TX', '... Closing OSC Client'); this.socket.info('TX', '... Closing OSC Client');
this.osc.shutdown(); this.osc.shutdown();
} }
if (this.http != null) { if (this.http != null) {
this.info('TX', '... Closing HTTP Client'); this.socket.info('TX', '... Closing HTTP Client');
this.http.shutdown(); this.http.shutdown();
} }
} }
@@ -126,7 +102,7 @@ export class EventTimer extends Timer {
_initOscClient(oscConfig) { _initOscClient(oscConfig) {
this.osc = new OSCIntegration(); this.osc = new OSCIntegration();
const r = this.osc.init(oscConfig); const r = this.osc.init(oscConfig);
r.success ? this.info('TX', r.message) : this.error('TX', r.message); r.success ? this.socket.info('TX', r.message) : this.socket.error('TX', r.message);
} }
/** /**
@@ -135,7 +111,7 @@ export class EventTimer extends Timer {
* @private * @private
*/ */
_initHTTPClient(httpConfig) { _initHTTPClient(httpConfig) {
this.info('TX', `Initialise HTTP Client on port`); this.socket.info('TX', `Initialise HTTP Client on port`);
this.http = new HTTPIntegration(); this.http = new HTTPIntegration();
this.http.init(httpConfig); this.http.init(httpConfig);
this.httpMessages = httpConfig.messages; this.httpMessages = httpConfig.messages;
@@ -146,7 +122,7 @@ export class EventTimer extends Timer {
*/ */
broadcastTimer() { broadcastTimer() {
// through websockets // through websockets
this.io.emit('timer', this.getTimeObject()); this.socket.send('timer', this.getTimeObject());
} }
/** /**
@@ -162,7 +138,7 @@ export class EventTimer extends Timer {
expectedFinish: this._getExpectedFinish(), expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt, startedAt: this._startedAt,
}; };
this.io.emit('ontime-timer', featureData); this.socket.send('ontime-timer', featureData);
} }
/** /**
@@ -174,7 +150,7 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId, selectedEventId: this.selectedEventId,
nextEventId: this.nextEventId, nextEventId: this.nextEventId,
}; };
this.io.emit('ontime-feat-eventlist', featureData); this.socket.send('ontime-feat-eventlist', featureData);
} }
/** /**
@@ -188,7 +164,7 @@ export class EventTimer extends Timer {
lower: this.lower, lower: this.lower,
onAir: this.onAir, onAir: this.onAir,
}; };
this.io.emit('ontime-feat-messagecontrol', featureData); this.socket.send('ontime-feat-messagecontrol', featureData);
} }
/** /**
@@ -201,7 +177,7 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId, selectedEventId: this.selectedEventId,
numEvents: this._eventlist.length, numEvents: this._eventlist.length,
}; };
this.io.emit('ontime-feat-playbackcontrol', featureData); this.socket.send('ontime-feat-playbackcontrol', featureData);
} }
/** /**
@@ -216,7 +192,7 @@ export class EventTimer extends Timer {
selectedEventIndex: this.selectedEventIndex, selectedEventIndex: this.selectedEventIndex,
numEvents: this._eventlist.length, numEvents: this._eventlist.length,
}; };
this.io.emit('ontime-feat-info', featureData); this.socket.send('ontime-feat-info', featureData);
} }
_broadcastFeatureCuesheet() { _broadcastFeatureCuesheet() {
@@ -227,7 +203,7 @@ export class EventTimer extends Timer {
numEvents: this._eventlist.length, numEvents: this._eventlist.length,
titleNow: this.titles.titleNow, titleNow: this.titles.titleNow,
}; };
this.io.emit('ontime-feat-cuesheet', featureData); this.socket.send('ontime-feat-cuesheet', featureData);
} }
/** /**
@@ -244,29 +220,20 @@ export class EventTimer extends Timer {
const numEvents = this._eventlist.length; const numEvents = this._eventlist.length;
this.broadcastTimer(); this.broadcastTimer();
this.io.emit('playstate', this.state); this.socket.send('playstate', this.state);
this.io.emit('selected', { this.socket.send('selected', {
id: this.selectedEventId, id: this.selectedEventId,
index: this.selectedEventIndex, index: this.selectedEventIndex,
total: numEvents, total: numEvents,
}); });
this.io.emit('selected-id', this.selectedEventId); this.socket.send('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId); this.socket.send('next-id', this.nextEventId);
this.io.emit('numevents', numEvents); this.socket.send('numevents', numEvents);
this.io.emit('publicselected-id', this.selectedPublicEventId); this.socket.send('publicselected-id', this.selectedPublicEventId);
this.io.emit('publicnext-id', this.nextPublicEventId); this.socket.send('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles); this.socket.send('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic); this.socket.send('publictitles', this.titlesPublic);
this.io.emit('onAir', this.onAir); this.socket.send('onAir', this.onAir);
}
/**
* Broadcast given message
* @param {string} address - socket io address
* @param {any} payload - message body
*/
broadcastThis(address, payload) {
this.io.emit(address, payload);
} }
/** /**
@@ -282,7 +249,7 @@ export class EventTimer extends Timer {
case 'start': { case 'start': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Start'); this.socket.info('PLAYBACK', 'Play Mode Start');
this.start(); this.start();
break; break;
} }
@@ -290,8 +257,8 @@ export class EventTimer extends Timer {
if (!numEvents) return false; if (!numEvents) return false;
const loaded = this.loadEventById(payload); const loaded = this.loadEventById(payload);
if (loaded) { if (loaded) {
this.info('PLAYBACK', `Loaded event with ID ${payload}`); this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
this.info('PLAYBACK', 'Play Mode Start'); this.socket.info('PLAYBACK', 'Play Mode Start');
this.start(); this.start();
} else { } else {
return false; return false;
@@ -302,8 +269,8 @@ export class EventTimer extends Timer {
if (!numEvents) return false; if (!numEvents) return false;
const loaded = this.loadEventByIndex(payload); const loaded = this.loadEventByIndex(payload);
if (loaded) { if (loaded) {
this.info('PLAYBACK', `Loaded event with index ${payload}`); this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
this.info('PLAYBACK', 'Play Mode Start'); this.socket.info('PLAYBACK', 'Play Mode Start');
this.start(); this.start();
} else { } else {
return false; return false;
@@ -313,35 +280,35 @@ export class EventTimer extends Timer {
case 'pause': { case 'pause': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Pause'); this.socket.info('PLAYBACK', 'Play Mode Pause');
this.pause(); this.pause();
break; break;
} }
case 'stop': { case 'stop': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Stop'); this.socket.info('PLAYBACK', 'Play Mode Stop');
this.stop(); this.stop();
break; break;
} }
case 'roll': { case 'roll': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Roll'); this.socket.info('PLAYBACK', 'Play Mode Roll');
this.roll(); this.roll();
break; break;
} }
case 'previous': { case 'previous': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Previous'); this.socket.info('PLAYBACK', 'Play Mode Previous');
this.previous(); this.previous();
break; break;
} }
case 'next': { case 'next': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Next'); this.socket.info('PLAYBACK', 'Play Mode Next');
this.next(); this.next();
break; break;
} }
@@ -349,7 +316,7 @@ export class EventTimer extends Timer {
if (!numEvents) return false; if (!numEvents) return false;
const loaded = this.loadEventById(payload); const loaded = this.loadEventById(payload);
if (loaded) { if (loaded) {
this.info('PLAYBACK', `Loaded event with ID ${payload}`); this.socket.info('PLAYBACK', `Loaded event with ID ${payload}`);
} else { } else {
return false; return false;
} }
@@ -359,7 +326,7 @@ export class EventTimer extends Timer {
if (!numEvents) return false; if (!numEvents) return false;
const loaded = this.loadEventByIndex(payload); const loaded = this.loadEventByIndex(payload);
if (loaded) { if (loaded) {
this.info('PLAYBACK', `Loaded event with index ${payload}`); this.socket.info('PLAYBACK', `Loaded event with index ${payload}`);
} else { } else {
return false; return false;
} }
@@ -368,32 +335,32 @@ export class EventTimer extends Timer {
case 'unload': { case 'unload': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Events unloaded'); this.socket.info('PLAYBACK', 'Events unloaded');
this.unload(); this.unload();
break; break;
} }
case 'reload': { case 'reload': {
if (!numEvents) return false; if (!numEvents) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Reloaded event'); this.socket.info('PLAYBACK', 'Reloaded event');
this.reload(); this.reload();
break; break;
} }
case 'onAir': { case 'onAir': {
// Call action // Call action
this.info('PLAYBACK', 'Going On Air'); this.socket.info('PLAYBACK', 'Going On Air');
this.setonAir(true); this.setonAir(true);
break; break;
} }
case 'offAir': { case 'offAir': {
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Going Off Air'); this.socket.info('PLAYBACK', 'Going Off Air');
this.setonAir(false); this.setonAir(false);
break; break;
} }
default: { default: {
// Error, disable flag // Error, disable flag
this.error('RX', `Unhandled action triggered ${action}`); this.socket.error('RX', `Unhandled action triggered ${action}`);
success = false; success = false;
break; break;
} }
@@ -518,7 +485,7 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.onUpdate; this.ontimeCycle = this.cycleState.onUpdate;
break; break;
default: default:
this.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`); this.socket.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
} }
// send http message if any // send http message if any
@@ -546,13 +513,13 @@ export class EventTimer extends Timer {
update() { update() {
// if there is nothing selected, update clock // if there is nothing selected, update clock
this.clock = this._getCurrentTime(); this.clock = Timer.getCurrentTime();
this._broadcastFeatureTimer(); this._broadcastFeatureTimer();
this.broadcastTimer(); this.broadcastTimer();
// if we are not updating, send the timers // if we are not updating, send the timers
if (this.ontimeCycle !== this.cycleState.onUpdate) { if (this.ontimeCycle !== this.cycleState.onUpdate) {
this.io.emit('timer', this.getTimeObject()); this.socket.send('timer', this.getTimeObject());
} }
// Have we skipped onStart? // Have we skipped onStart?
@@ -616,33 +583,33 @@ export class EventTimer extends Timer {
// Presenter message // Presenter message
case 'set-timer-text': case 'set-timer-text':
this.presenter.text = payload; this.presenter.text = payload;
this.broadcastThis('messages-timer', this.presenter); this.socket.send('messages-timer', this.presenter);
break; break;
case 'set-timer-visible': case 'set-timer-visible':
this.presenter.visible = payload; this.presenter.visible = payload;
this.broadcastThis('messages-timer', this.presenter); this.socket.send('messages-timer', this.presenter);
break; break;
/*******************************************/ /*******************************************/
// Public message // Public message
case 'set-public-text': case 'set-public-text':
this.public.text = payload; this.public.text = payload;
this.broadcastThis('messages-public', this.public); this.socket.send('messages-public', this.public);
break; break;
case 'set-public-visible': case 'set-public-visible':
this.public.visible = payload; this.public.visible = payload;
this.broadcastThis('messages-public', this.public); this.socket.send('messages-public', this.public);
break; break;
/*******************************************/ /*******************************************/
// Lower third message // Lower third message
case 'set-lower-text': case 'set-lower-text':
this.lower.text = payload; this.lower.text = payload;
this.broadcastThis('messages-lower', this.lower); this.socket.send('messages-lower', this.lower);
break; break;
case 'set-lower-visible': case 'set-lower-visible':
this.lower.visible = payload; this.lower.visible = payload;
this.broadcastThis('messages-lower', this.lower); this.socket.send('messages-lower', this.lower);
break; break;
default: default:
@@ -650,286 +617,6 @@ export class EventTimer extends Timer {
} }
} }
/**
* Handle socket io connections
* @private
*/
_listenToConnections() {
this.io.on('connection', (socket) => {
/*******************************/
/*** HANDLE NEW CONNECTION ***/
/*** --------------------- ***/
/*******************************/
// keep track of connections
this._numClients++;
this._clientNames[socket.id] = getRandomName();
const m = `${this._numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
this.info('CLIENT', m);
// send state
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.nextPublicEventId);
/********************************/
/*** HANDLE DISCONNECT USER ***/
/*** ---------------------- ***/
/********************************/
socket.on('disconnect', () => {
this._numClients--;
const m = `${this._numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
delete this._clientNames[socket.id];
this.info('CLIENT', m);
});
/***************************************/
/*** TIMER STATE GETTERS / SETTERS ***/
/*** ------- WEBSOCKET API ------- ***/
/*** ----------------------------- ***/
/***************************************/
/*******************************************/
socket.on('ontime-test', () => {
socket.emit('hello');
});
socket.on('set-start', () => {
this.trigger('start');
socket.emit('playstate', this.state);
});
socket.on('set-startid', (data) => {
this.trigger('startById', data);
socket.emit('playstate', this.state);
});
socket.on('set-startindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
this.trigger('startByIndex', data);
socket.emit('playstate', this.state);
});
socket.on('set-loadid', (data) => {
this.trigger('loadById', data);
socket.emit('playstate', this.state);
});
socket.on('set-loadindex', (data) => {
const eventIndex = Number(data);
if (isNaN(eventIndex)) {
return;
}
this.trigger('loadByIndex', data);
socket.emit('playstate', this.state);
});
socket.on('set-pause', () => {
this.trigger('pause');
socket.emit('playstate', this.state);
});
socket.on('set-stop', () => {
this.trigger('stop');
socket.emit('playstate', this.state);
});
socket.on('set-reload', () => {
this.trigger('reload');
socket.emit('playstate', this.state);
});
socket.on('set-previous', () => {
this.trigger('previous');
socket.emit('playstate', this.state);
});
socket.on('set-next', () => {
this.trigger('next');
socket.emit('playstate', this.state);
});
socket.on('set-roll', () => {
this.trigger('roll');
socket.emit('playstate', this.state);
});
socket.on('set-delay', (data) => {
const delayTime = Number(data);
if (isNaN(delayTime)) {
return;
}
this.increment(delayTime * 1000 * 60);
});
socket.on('set-onAir', (data) => {
try {
const d = JSON.parse(data);
this.onAir = !!d;
} catch (error) {
this.error('RX', `Failed to parse message ${data}`);
}
this.broadcastThis('onAir', this.onAir);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// general playback state
socket.on('get-state', () => {
socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId);
socket.emit('publicselected-id', this.selectedPublicEventId);
socket.emit('publicnext-id', this.this.nextPublicEventId);
});
/*******************************************/
// ** TO BE DEPRECATED ** //
socket.on('get-timer', () => {
socket.emit('timer', this.getTimeObject());
});
// ** TO BE DEPRECATED IN FAVOR OF DELAY ** //
socket.on('increment-timer', (data) => {
if (isNaN(parseInt(data, 10))) return;
if (data < -5 || data > 5) return;
this.increment(data * 1000 * 60);
});
/*******************************************/
// playstate
socket.on('set-playstate', (data) => {
this.trigger(data);
this._broadcastFeaturePlaybackControl();
this._broadcastFeatureInfo();
});
socket.on('get-playstate', () => {
socket.emit('playstate', this.state);
});
socket.on('get-onAir', () => {
socket.emit('onAir', this.onAir);
});
/*******************************************/
// selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
total: this._eventlist.length,
});
});
socket.on('get-selected-id', () => {
socket.emit('selected-id', this.selectedEventId);
});
socket.on('get-next-id', () => {
socket.emit('next-id', this.nextEventId);
});
// title data
socket.on('get-titles', () => {
socket.emit('titles', this.titles);
});
socket.on('get-publictitles', () => {
socket.emit('publictitles', this.titlesPublic);
});
/***********************************/
/*** MESSAGE GETTERS / SETTERS ***/
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// Presenter message
socket.on('set-timer-message-text', (data) => {
this._setTitles('set-timer-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-timer-message-visible', (data) => {
this._setTitles('set-timer-visible', data);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// Public message
socket.on('set-public-message-text', (data) => {
this._setTitles('set-public-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-public-message-visible', (data) => {
this._setTitles('set-public-visible', data);
this._broadcastFeatureMessageControl();
});
/*******************************************/
// Lower third message
socket.on('set-lower-message-text', (data) => {
this._setTitles('set-lower-text', data);
this._broadcastFeatureMessageControl();
});
socket.on('set-lower-message-visible', (data) => {
this._setTitles('set-lower-visible', data);
this._broadcastFeatureMessageControl();
});
/* MOLECULAR ENDPOINTS
* =====================
* 1. EVENT LIST
* 2. MESSAGE CONTROL
* 3. PLAYBACK CONTROL
* 4. INFO
* 5. CUESHEET
* 6. TIMER OBJECT
* */
// 1. EVENT LIST
socket.on('get-ontime-feat-eventlist', () => {
this._broadcastFeatureEventList();
});
// 2. MESSAGE CONTROL
socket.on('get-ontime-feat-messagecontrol', () => {
this._broadcastFeatureMessageControl();
});
// 3. PLAYBACK CONTROL
socket.on('get-ontime-feat-playbackcontrol', () => {
this._broadcastFeaturePlaybackControl();
});
// 4. INFO
socket.on('get-ontime-feat-info', () => {
this._broadcastFeatureInfo();
});
// 5. CUE SHEET
socket.on('get-ontime-feat-cuesheet', () => {
this._broadcastFeatureCuesheet();
});
// 6. TIMER
socket.on('get-ontime-timer', () => {
this._broadcastFeatureTimer();
});
});
}
/** /**
* Deletes running event list from object * Deletes running event list from object
*/ */
@@ -944,7 +631,7 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.onStop; this.ontimeCycle = this.cycleState.onStop;
// update clients // update clients
this.broadcastThis('numevents', this._eventlist.length); this.socket.send('numevents', this._eventlist.length);
} }
/** /**
@@ -1030,7 +717,7 @@ export class EventTimer extends Timer {
// find object in events // find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === id); const eventIndex = this._eventlist.findIndex((e) => e.id === id);
if (eventIndex === -1) { if (eventIndex === -1) {
throw 'Event not found'; throw new Error('Event not found');
} }
// check if event is set to be skipped // check if event is set to be skipped
@@ -1068,7 +755,7 @@ export class EventTimer extends Timer {
this._loadTitlesNow(); this._loadTitlesNow();
} }
} catch (error) { } catch (error) {
this.error('SERVER', error); this.socket.error('SERVER', error);
} }
// update clients // update clients
@@ -1117,7 +804,7 @@ export class EventTimer extends Timer {
this._loadTitlesNow(); this._loadTitlesNow();
} }
} catch (error) { } catch (error) {
this.error('SERVER', error); this.socket.error('SERVER', error);
} }
// update clients // update clients
@@ -1228,7 +915,7 @@ export class EventTimer extends Timer {
this.selectedEventIndex = eventIndex; this.selectedEventIndex = eventIndex;
this.selectedEventId = e.id; this.selectedEventId = e.id;
} else if (type === 'reload') { } else if (type === 'reload') {
const now = this._getCurrentTime(); const now = Timer.getCurrentTime();
const elapsed = this.getElapsed(); const elapsed = this.getElapsed();
this.duration = end - start; this.duration = end - start;
@@ -1441,7 +1128,7 @@ export class EventTimer extends Timer {
setonAir(onAir) { setonAir(onAir) {
this.onAir = onAir; this.onAir = onAir;
// broadcast change // broadcast change
this.broadcastThis('onAir', onAir); this.socket.send('onAir', onAir);
} }
/** /**
@@ -1509,7 +1196,7 @@ export class EventTimer extends Timer {
* @description Look for current event considering local clock * @description Look for current event considering local clock
*/ */
rollLoad() { rollLoad() {
const now = this._getCurrentTime(); const now = Timer.getCurrentTime();
const prevLoaded = this.selectedEventId; const prevLoaded = this.selectedEventId;
// maybe roll has already been loaded // maybe roll has already been loaded
@@ -1524,7 +1211,7 @@ export class EventTimer extends Timer {
// nothing to play, unload // nothing to play, unload
if (nowIndex === null && nextIndex === null) { if (nowIndex === null && nextIndex === null) {
this.unload(); this.unload();
this.warning('SERVER', 'Roll: no events found'); this.socket.warning('SERVER', 'Roll: no events found');
return; return;
} }
@@ -1551,7 +1238,7 @@ export class EventTimer extends Timer {
if (nowIndex === null) { if (nowIndex === null) {
// only warn the first time // only warn the first time
if (this.secondaryTimer === null) { if (this.secondaryTimer === null) {
this.info('SERVER', 'Roll: waiting for event start'); this.socket.info('SERVER', 'Roll: waiting for event start');
} }
// reset running timer // reset running timer
@@ -1685,70 +1372,6 @@ export class EventTimer extends Timer {
/****************************************************************************/ /****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const m = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(this._getCurrentTime()),
};
this.messageStack.unshift(m);
this.io.emit('logger', m);
if (process.env.NODE_ENV !== 'production') {
console.log(`[${m.level}] \t ${m.origin} \t ${m.text}`);
}
if (this.messageStack.length > this.MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* Sends a message with level LOG
* @param {string} origin
* @param {string} text
*/
info(origin, text) {
this._push('INFO', origin, text);
}
/**
* Sends a message with level WARN
* @param {string} origin
* @param {string} text
*/
warning(origin, text) {
this._push('WARN', origin, text);
}
/**
* Sends a message with level ERROR
* @param {string} origin
* @param {string} text
*/
error(origin, text) {
this._push('ERROR', origin, text);
}
/****************************************************************************/
/** /**
* Integrations * Integrations
* ------------- * -------------
@@ -1765,20 +1388,20 @@ export class EventTimer extends Timer {
async sendOsc(message, payload) { async sendOsc(message, payload) {
const reply = await this.osc.send(message, payload); const reply = await this.osc.send(message, payload);
if (!reply.success) { if (!reply.success) {
this.error('TX', reply.message); this.socket.error('TX', reply.message);
} }
} }
/** /**
* Builds sync object * @description Builds sync object
* @returns {{running: number, timer: (null|string|*), presenter: null, playback: string, clock: null, title: null}}
*/ */
poll() { poll() {
return { return {
clock: this.clock, currentId: this.selectedEventId,
running: Timer.toSeconds(this.current),
timer: this.timeTag, timer: this.timeTag,
clock: this.clock,
playback: this.state, playback: this.state,
currentColour: null,
title: this.titles.titleNow, title: this.titles.titleNow,
presenter: this.titles.presenterNow, presenter: this.titles.presenterNow,
}; };
+5 -42
View File
@@ -11,39 +11,12 @@ export class Timer {
this.state = 'stop'; this.state = 'stop';
} }
/**
* @description initiates a timer with given seconds
* @param seconds
* @param autoStart
*/
setupWithSeconds(seconds, autoStart = false) {
// aux
const now = this._getCurrentTime();
this.clock = now;
// populate targets
this.duration = seconds * 1000;
this._finishAt = now + seconds * 1000;
// start counting
this._startedAt = now;
if (autoStart) {
this.state = 'start';
} else {
this._pausedAt = now;
this._pausedInterval = 0;
}
this._pausedTotal = 0;
this.update();
}
/** /**
* @description updates the running timer * @description updates the running timer
*/ */
update() { update() {
// get current time // get current time
const now = this._getCurrentTime(); const now = Timer.getCurrentTime();
this.clock = now; this.clock = now;
let checkFinish = false; let checkFinish = false;
@@ -106,7 +79,7 @@ export class Timer {
* @return {number} * @return {number}
* @private * @private
*/ */
_getCurrentTime() { static getCurrentTime() {
const now = new Date(); const now = new Date();
// extract milliseconds since midnight // extract milliseconds since midnight
@@ -178,16 +151,6 @@ export class Timer {
}; };
} }
/**
* @description get current time in seconds
* @return {number|number}
*/
getCurrentInSeconds() {
// update timeStamp
this.update();
return Timer.toSeconds(this.current);
}
// playback // playback
/** /**
* @description start current time * @description start current time
@@ -197,7 +160,7 @@ export class Timer {
if (this.state === 'start') return; if (this.state === 'start') return;
else if (this._startedAt == null) { else if (this._startedAt == null) {
// it hasn't started yet // it hasn't started yet
const now = this._getCurrentTime(); const now = Timer.getCurrentTime();
// set start time as now // set start time as now
this._startedAt = now; this._startedAt = now;
// calculate expected finish time // calculate expected finish time
@@ -230,7 +193,7 @@ export class Timer {
} }
// set pause time // set pause time
this._pausedAt = this._getCurrentTime(); this._pausedAt = Timer.getCurrentTime();
// change state // change state
this.state = 'pause'; this.state = 'pause';
@@ -259,7 +222,7 @@ export class Timer {
if (amount < 0 && Math.abs(amount) > this.current) { if (amount < 0 && Math.abs(amount) > this.current) {
// if we will make the clock negative // if we will make the clock negative
if (this._finishedAt == null) this._finishedAt = this._getCurrentTime(); if (this._finishedAt == null) this._finishedAt = Timer.getCurrentTime();
} else if (this.current < 0 && this.current + amount > 0) { } else if (this.current < 0 && this.current + amount > 0) {
// clock will go from negative to positive // clock will go from negative to positive
this._finishedAt = null; this._finishedAt = null;
+15 -11
View File
@@ -1,24 +1,28 @@
// get database import { removeUndefined } from '../utils/parserUtils.js';
import { db, data } from '../app.js'; import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event' // Create controller for GET request to 'event'
// Returns ACK message
export const getEvent = async (req, res) => { export const getEvent = async (req, res) => {
res.json(data.event); res.json(DataProvider.getEventData());
}; };
// Create controller for POST request to 'event' // Create controller for POST request to 'event'
// Returns ACK message
export const postEvent = async (req, res) => { export const postEvent = async (req, res) => {
if (!req.body) { if (failEmptyObjects(req.body, res)) {
res.status(400).send('No object found in request');
return; return;
} }
// TODO: validate data
try { try {
data.event = { ...data.event, ...req.body }; const newEvent = removeUndefined({
await db.write(); title: req.body?.title,
res.sendStatus(200); url: req.body?.url,
publicInfo: req.body?.publicInfo,
backstageInfo: req.body?.backstageInfo,
endMessage: req.body?.endMessage,
});
const newData = await DataProvider.setEventData(newEvent);
res.status(200).send(newData);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -0,0 +1,14 @@
import { body, validationResult } from 'express-validator';
export const eventSanitizer = [
body('title').optional().isString().trim().escape(),
body('url').optional().isString().trim().escape(),
body('publicInfo').optional().isString().trim().escape(),
body('backstageInfo').optional().isString().trim().escape(),
body('endMessage').optional().isString().trim().escape(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+44 -128
View File
@@ -1,7 +1,3 @@
// get database
import { data, db } from '../app.js';
// utils
import { import {
block as blockDef, block as blockDef,
delay as delayDef, delay as delayDef,
@@ -10,87 +6,36 @@ import {
import { generateId } from '../utils/generate_id.js'; import { generateId } from '../utils/generate_id.js';
import { MAX_EVENTS } from '../settings.js'; import { MAX_EVENTS } from '../settings.js';
import { getPreviousPlayable } from '../utils/eventUtils.js'; import { getPreviousPlayable } from '../utils/eventUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { socketProvider } from '../classes/socket/SocketController.js';
// import socket provider
const socket = socketProvider;
async function _insertAndSync(newEvent) { async function _insertAndSync(newEvent) {
if (newEvent.order) { if (newEvent.order) {
const events = data.events; const events = DataProvider.getEvents();
await _insertAt(newEvent, newEvent.order); await DataProvider.insertEventAt(newEvent, newEvent.order);
const previousId = events?.[newEvent.order - 1]?.id; const previousId = events?.[newEvent.order - 1]?.id;
_insertEventInTimerAfterId(newEvent, previousId); _insertEventInTimerAfterId(newEvent, previousId);
} else if (newEvent.after) { } else if (newEvent.after) {
await _insertAfterId(newEvent, newEvent.after); await DataProvider.insertEventAfterId(newEvent, newEvent.after);
_insertEventInTimerAfterId(newEvent, newEvent.after); _insertEventInTimerAfterId(newEvent, newEvent.after);
} else { } else {
await _insertAt(newEvent, 0); await DataProvider.insertEventAt(newEvent, 0);
_insertEventInTimerAfterId(newEvent); _insertEventInTimerAfterId(newEvent);
} }
} }
/**
* Insets an event after a given index
* @param entry
* @param index
* @return {Promise<void>}
* @private
*/
async function _insertAt(entry, index) {
// get events
const events = data.events;
const count = events.length;
const order = entry.order;
// Remove order field from object
delete entry.order;
// Insert at beginning
if (order === 0) {
events.unshift(entry);
}
// insert at end
else if (order >= count) {
events.push(entry);
}
// insert in the middle
else {
events.splice(index, 0, entry);
}
// save events
data.events = events;
await db.write();
}
/**
* @description Inserts an entry after an element with given Id
* @param entry
* @param id
* @return {Promise<void>}
* @private
*/
async function _insertAfterId(entry, id) {
const index = [...data.events].findIndex((event) => event.id === id);
await _insertAt(entry, index + 1);
}
/**
* @description deletes an event from the db given its id
* @param eventId
* @return {Promise<void>}
*/
async function _removeById(eventId) {
data.events = Array.from(data.events).filter((e) => e.id !== eventId);
await db.write();
}
/** /**
* @description returns all events of type event * @description returns all events of type event
* @return {unknown[]} * @return {unknown[]}
*/ */
function getEventEvents() { function getEventEvents() {
// return data.events.filter((e) => e.type === 'event'); // return data.events.filter((e) => e.type === 'event');
return Array.from(data.events).filter((e) => e.type === 'event'); const events = DataProvider.getEvents();
return Array.from(events).filter((e) => e.type === 'event');
} }
// Updates timer object // Updates timer object
@@ -112,7 +57,7 @@ function _insertEventInTimerAfterId(event, previousId) {
try { try {
global.timer.insertEventAfterId(event, previousId); global.timer.insertEventAfterId(event, previousId);
} catch (error) { } catch (error) {
global.timer.error('SERVER', `Unable to update object: ${error}`); socket.error('SERVER', `Unable to update object: ${error}`);
} }
} }
} }
@@ -135,7 +80,7 @@ function _deleteTimerId(entryId) {
// Create controller for GET request to '/events' // Create controller for GET request to '/events'
// Returns - // Returns -
export const eventsGetAll = async (req, res) => { export const eventsGetAll = async (req, res) => {
res.json(data.events); res.json(DataProvider.getEvents());
}; };
// Create controller for GET request to '/events/:eventId' // Create controller for GET request to '/events/:eventId'
@@ -146,7 +91,7 @@ export const eventsGetById = async (req, res) => {
if (id == null) { if (id == null) {
res.status(400).send(`No eventId found in request`); res.status(400).send(`No eventId found in request`);
} else { } else {
const event = data.events.find((e) => e.id === id); const event = DataProvider.getEventById(id);
res.json(event); res.json(event);
} }
}; };
@@ -154,13 +99,12 @@ export const eventsGetById = async (req, res) => {
// Create controller for POST request to '/events/' // Create controller for POST request to '/events/'
// Returns - // Returns -
export const eventsPost = async (req, res) => { export const eventsPost = async (req, res) => {
// TODO: Validate event if (failEmptyObjects(req.body, res)) {
if (!req.body) {
res.status(400).send(`No object found in request`);
return; return;
} }
if (data.events.length > MAX_EVENTS) { const numEvents = DataProvider.getNumEvents();
if (numEvents > MAX_EVENTS) {
const error = `ERROR: Reached limit number of ${MAX_EVENTS} events`; const error = `ERROR: Reached limit number of ${MAX_EVENTS} events`;
res.status(400).send(error); res.status(400).send(error);
return; return;
@@ -187,7 +131,7 @@ export const eventsPost = async (req, res) => {
} }
try { try {
_insertAndSync(newEvent); await _insertAndSync(newEvent);
res.sendStatus(201); res.sendStatus(201);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
@@ -197,41 +141,33 @@ export const eventsPost = async (req, res) => {
// Create controller for PUT request to '/events/' // Create controller for PUT request to '/events/'
// Returns - // Returns -
export const eventsPut = async (req, res) => { export const eventsPut = async (req, res) => {
// no valid params if (failEmptyObjects(req.body, res)) {
if (!req.body) {
res.status(400).send(`No object found`);
return; return;
} }
const eventId = req.body.id; const eventDataFromRequest = req.body;
if (!eventId) { const eventId = eventDataFromRequest.id;
res.status(400).send(`Object malformed: id missing`); const event = DataProvider.getEventById(eventId);
return; if (typeof event === 'undefined') {
} res.status(400).send(`No event with ID found`);
const eventIndex = data.events.findIndex((e) => e.id === eventId);
if (eventIndex === -1) {
res.status(400).send(`No Id found found`);
return; return;
} }
try { try {
const e = data.events[eventIndex]; const newData = DataProvider.updateEventById(eventId, eventDataFromRequest);
data.events[eventIndex] = { ...e, ...req.body };
data.events[eventIndex].revision++;
await db.write();
if (data.events[eventIndex].skip) { if (newData.skip) {
_deleteTimerId(eventId); _deleteTimerId(eventId);
// if it is a skip, i make sure it is deleted from timer // if it is a skip, make sure it is deleted from timer
// event id might already not exist // event id might already not exist
} else { } else {
try { try {
_updateTimersSingle(eventId, req.body); _updateTimersSingle(newData.id, eventDataFromRequest);
} catch (error) { } catch (error) {
if (error === 'Event not found') { if (error === 'Event not found') {
const { id: previousId } = getPreviousPlayable(data.events, e.id); const events = DataProvider.getEvents();
_insertEventInTimerAfterId(data.events[eventIndex], previousId); const { id: previousId } = getPreviousPlayable(events, newData.id);
_insertEventInTimerAfterId(newData, previousId);
} }
} }
} }
@@ -250,16 +186,14 @@ export const eventsPatch = async (req, res) => {
}; };
export const eventsReorder = async (req, res) => { export const eventsReorder = async (req, res) => {
// TODO: Validate event if (failEmptyObjects(req.body, res)) {
if (!req.body) {
res.status(400).send(`No object found in request`);
return; return;
} }
const { index, from, to } = req.body; const { index, from, to } = req.body;
// get events // get events
const events = data.events; const events = DataProvider.getEvents();
const idx = events.findIndex((e) => e.id === index, from); const idx = events.findIndex((e) => e.id === index, from);
// Check if item is at given index // Check if item is at given index
@@ -276,8 +210,7 @@ export const eventsReorder = async (req, res) => {
events.splice(to, 0, reorderedItem); events.splice(to, 0, reorderedItem);
// save events // save events
data.events = events; await DataProvider.setEventData(events);
await db.write();
// update timer // update timer
_updateTimers(); _updateTimers();
@@ -291,15 +224,9 @@ export const eventsReorder = async (req, res) => {
// Create controller for PATCH request to '/events/applydelay/:eventId' // Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns - // Returns -
export const eventsApplyDelay = async (req, res) => { export const eventsApplyDelay = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try { try {
// get events // get events
const events = data.events; const events = DataProvider.getEvents();
// AUX // AUX
let delayIndex = null; let delayIndex = null;
@@ -340,8 +267,7 @@ export const eventsApplyDelay = async (req, res) => {
if (blockIndex) events.splice(blockIndex - 1, 1); if (blockIndex) events.splice(blockIndex - 1, 1);
// update events // update events
data.events = events; await DataProvider.setEvents(events);
await db.write();
// update timer // update timer
_updateTimers(); _updateTimers();
@@ -355,20 +281,15 @@ export const eventsApplyDelay = async (req, res) => {
// Create controller for DELETE request to '/events/:eventId' // Create controller for DELETE request to '/events/:eventId'
// Returns - // Returns -
export const eventsDelete = async (req, res) => { export const eventsDelete = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try { try {
const eventId = req.params.eventId;
// remove new event // remove new event
await _removeById(req.params.eventId); await DataProvider.deleteEvent(eventId);
// update timer // update timer
_deleteTimerId(req.params.eventId); _deleteTimerId(eventId);
res.sendStatus(201); res.sendStatus(204);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -378,14 +299,9 @@ export const eventsDelete = async (req, res) => {
// Returns - // Returns -
export const eventsDeleteAll = async (req, res) => { export const eventsDeleteAll = async (req, res) => {
try { try {
// set with nothing await DataProvider.deleteAllEvents();
data.events = [];
await db.write();
// update timer object
global.timer.clearEventList(); global.timer.clearEventList();
res.sendStatus(204);
res.sendStatus(201);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -0,0 +1,19 @@
import { body, param, validationResult } from 'express-validator';
export const eventsPutValidator = [
body('id').isString().exists(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
export const paramsMustHaveEventId = [
param('eventId').exists(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+50 -94
View File
@@ -1,10 +1,11 @@
import fs from 'fs'; import fs from 'fs';
import { data, db } from '../app.js';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js'; import { fileHandler } from '../utils/parser.js';
import { generateId } from '../utils/generate_id.js'; import { generateId } from '../utils/generate_id.js';
import { resolveDbPath } from '../modules/loadDb.js'; import { resolveDbPath } from '../modules/loadDb.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js'; import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js';
// Create controller for GET request to '/ontime/poll' // Create controller for GET request to '/ontime/poll'
// Returns data for current state // Returns data for current state
@@ -22,7 +23,8 @@ export const poll = async (req, res) => {
// Create controller for GET request to '/ontime/db' // Create controller for GET request to '/ontime/db'
// Returns - // Returns -
export const dbDownload = async (req, res) => { export const dbDownload = async (req, res) => {
const fileTitle = data?.event?.title || 'ontime events'; const { title } = DataProvider.getEventData();
const fileTitle = title || 'ontime events';
const dbInDisk = resolveDbPath(); const dbInDisk = resolveDbPath();
res.download(dbInDisk, `${fileTitle}.json`, (err) => { res.download(dbInDisk, `${fileTitle}.json`, (err) => {
@@ -48,18 +50,13 @@ const uploadAndParse = async (file, req, res, options) => {
} else if (result.message === 'success') { } else if (result.message === 'success') {
// explicitly write objects // explicitly write objects
if (typeof result !== 'undefined') { if (typeof result !== 'undefined') {
if (!options.onlyEvents) { const newEvents = result.data.events || [];
const mergedData = DataProvider.safeMerge(data, result.data); if (options.onlyEvents) {
data.event = mergedData.event; await DataProvider.setEvents(newEvents);
data.settings = mergedData.settings; } else {
data.osc = mergedData.osc; await DataProvider.mergeIntoData(result.data);
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
} }
data.events = result.data.events || []; global.timer.setupWithEventList(newEvents);
global.timer.setupWithEventList(result.data.events || []);
await db.write();
} }
res.sendStatus(200); res.sendStatus(200);
} else { } else {
@@ -71,7 +68,7 @@ const uploadAndParse = async (file, req, res, options) => {
}; };
/** /**
* @description Gets information on IPV4 non internal interfaces * @description Gets information on IPV4 non-internal interfaces
* @returns {array} - Array of objects {name: ip} * @returns {array} - Array of objects {name: ip}
*/ */
const getNetworkInterfaces = () => { const getNetworkInterfaces = () => {
@@ -96,15 +93,8 @@ const getNetworkInterfaces = () => {
// Create controller for POST request to '/ontime/info' // Create controller for POST request to '/ontime/info'
// Returns - // Returns -
export const getInfo = async (req, res) => { export const getInfo = async (req, res) => {
const version = data.settings.version; const { version, serverPort } = DataProvider.getSettings();
const serverPort = data.settings.serverPort; const osc = DataProvider.getOsc();
const osc = {
port: data.osc.port,
portOut: data.osc.portOut,
targetIP: data.osc.targetIP,
enabled: data.osc.enabled,
};
// get nif and inject localhost // get nif and inject localhost
const ni = getNetworkInterfaces(); const ni = getNetworkInterfaces();
@@ -122,18 +112,16 @@ export const getInfo = async (req, res) => {
// Create controller for POST request to '/ontime/aliases' // Create controller for POST request to '/ontime/aliases'
// Returns - // Returns -
export const getAliases = async (req, res) => { export const getAliases = async (req, res) => {
// send aliases array const aliases = DataProvider.getAliases();
res.status(200).send(data.aliases); res.status(200).send(aliases);
}; };
// Create controller for POST request to '/ontime/aliases' // Create controller for POST request to '/ontime/aliases'
// Returns ACK message // Returns ACK message
export const postAliases = async (req, res) => { export const postAliases = async (req, res) => {
if (!req.body) { if (failIsNotArray()) {
res.status(400).send('No object found in request');
return; return;
} }
// TODO: validate data
try { try {
const newAliases = []; const newAliases = [];
req.body.forEach((a) => { req.body.forEach((a) => {
@@ -144,9 +132,8 @@ export const postAliases = async (req, res) => {
pathAndParams: a.pathAndParams, pathAndParams: a.pathAndParams,
}); });
}); });
data.aliases = newAliases; await DataProvider.setAliases(newAliases);
await db.write(); res.status(200).send(newAliases);
res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -155,27 +142,21 @@ export const postAliases = async (req, res) => {
// Create controller for GET request to '/ontime/userfields' // Create controller for GET request to '/ontime/userfields'
// Returns - // Returns -
export const getUserFields = async (req, res) => { export const getUserFields = async (req, res) => {
// send userFields array const userFields = DataProvider.getUserFields();
res.status(200).send(data.userFields); res.status(200).send(userFields);
}; };
// Create controller for POST request to '/ontime/userfields' // Create controller for POST request to '/ontime/userfields'
// Returns ACK message // Returns ACK message
export const postUserFields = async (req, res) => { export const postUserFields = async (req, res) => {
if (!req.body) { if (failEmptyObjects(req.body, res)) {
res.status(400).send('No object found in request');
return; return;
} }
try { try {
const newUserFields = { ...data.userFields }; const persistedData = DataProvider.getUserFields();
for (const field in newUserFields) { const newData = mergeObject(persistedData, req.body);
if (typeof req.body[field] !== 'undefined') { await DataProvider.setUserFields(newData);
newUserFields[field] = req.body[field]; res.status(200).send(newData);
}
}
data.userFields = newUserFields;
await db.write();
res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -184,12 +165,8 @@ export const postUserFields = async (req, res) => {
// Create controller for POST request to '/ontime/settings' // Create controller for POST request to '/ontime/settings'
// Returns - // Returns -
export const getSettings = async (req, res) => { export const getSettings = async (req, res) => {
const version = data.settings.version; const { version, serverPort, pinCode, timeFormat } = DataProvider.getSettings();
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
const timeFormat = data.settings.timeFormat;
// send object with network information
res.status(200).send({ res.status(200).send({
version, version,
serverPort, serverPort,
@@ -201,12 +178,12 @@ export const getSettings = async (req, res) => {
// Create controller for POST request to '/ontime/settings' // Create controller for POST request to '/ontime/settings'
// Returns ACK message // Returns ACK message
export const postSettings = async (req, res) => { export const postSettings = async (req, res) => {
if (!req.body) { if (failEmptyObjects(req.body, res)) {
res.status(400).send('No object found in request');
return; return;
} }
try { try {
let pin = data.settings.pinCode; const settings = DataProvider.getSettings();
let pin = settings.pinCode;
if (typeof req.body?.pinCode === 'string') { if (typeof req.body?.pinCode === 'string') {
if (req.body?.pinCode.length === 0) { if (req.body?.pinCode.length === 0) {
pin = null; pin = null;
@@ -215,20 +192,20 @@ export const postSettings = async (req, res) => {
} }
} }
let timeFormat = data.settings.timeFormat; let format = settings.timeFormat;
if (typeof req.body?.timeFormat === 'string') { if (typeof req.body?.timeFormat === 'string') {
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') { if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
timeFormat = req.body.timeFormat; format = req.body.timeFormat;
} }
} }
data.settings = { const newData = {
...data.settings, ...settings,
pinCode: pin, pinCode: pin,
timeFormat: timeFormat, timeFormat: format,
}; };
await db.write(); DataProvider.setSettings(newData);
res.sendStatus(200); res.status(200).send(newData);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -239,7 +216,8 @@ export const postSettings = async (req, res) => {
* @method GET * @method GET
*/ */
export const getViewSettings = async (req, res) => { export const getViewSettings = async (req, res) => {
res.status(200).send({ ...data.views }); const views = DataProvider.getViews();
res.status(200).send(views);
}; };
/** /**
@@ -247,33 +225,14 @@ export const getViewSettings = async (req, res) => {
* @method POST * @method POST
*/ */
export const postViewSettings = async (req, res) => { export const postViewSettings = async (req, res) => {
if (!req.body) { if (failEmptyObjects(req.body, res)) {
res.status(400).send('No object found in request');
return; return;
} }
try {
data.views = {
overrideStyles: req.body?.overrideStyles ?? data.views.overrideStyles,
};
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/info'
// Returns ACK message
export const postInfo = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try { try {
data.settings = { ...data.settings, ...req.body }; const newData = { overrideStyles: req.body.overrideStyles };
await db.write(); DataProvider.setViews(newData);
res.sendStatus(200); res.status(200).send(newData);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
} }
@@ -282,25 +241,22 @@ export const postInfo = async (req, res) => {
// Create controller for POST request to '/ontime/osc' // Create controller for POST request to '/ontime/osc'
// Returns - // Returns -
export const getOSC = async (req, res) => { export const getOSC = async (req, res) => {
// send object with network information const osc = DataProvider.getOsc();
res.status(200).send(data.osc); res.status(200).send(osc);
}; };
// Create controller for POST request to '/ontime/osc' // Create controller for POST request to '/ontime/osc'
// Returns ACK message // Returns ACK message
export const postOSC = async (req, res) => { export const postOSC = async (req, res) => {
if (!req.body) { if (failEmptyObjects(req.body, res)) {
res.status(400).send('No object found in request');
return; return;
} }
// TODO: validate data
try { try {
data.osc = { ...data.osc, ...req.body }; await DataProvider.setOsc(req.body);
await db.write(); res.send(req.body).status(200);
res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
console.log(error);
} }
}; };
@@ -313,7 +269,7 @@ export const dbUpload = async (req, res) => {
} }
const options = req.query; const options = req.query;
const file = req.file.path; const file = req.file.path;
uploadAndParse(file, req, res, options); await uploadAndParse(file, req, res, options);
}; };
// Create controller for POST request to '/ontime/dbpath' // Create controller for POST request to '/ontime/dbpath'
@@ -323,5 +279,5 @@ export const dbPathToUpload = async (req, res) => {
res.status(400).send({ message: 'Path to file not found' }); res.status(400).send({ message: 'Path to file not found' });
return; return;
} }
uploadAndParse(req.body.path, req, res); await uploadAndParse(req.body.path, req, res);
}; };
@@ -0,0 +1,77 @@
import { body, check, validationResult } from 'express-validator';
/**
* @description Validates object for POST /ontime/views
*/
export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/aliases
*/
export const validateAliases = [
body().isArray(),
body('*.enabled').isBoolean(),
body('*.alias').isString().trim(),
body('*.pathAndParams').isString().trim(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/userfields
*/
export const validateUserFields = [
body('user0').exists().isString().trim(),
body('user1').exists().isString().trim(),
body('user2').exists().isString().trim(),
body('user3').exists().isString().trim(),
body('user4').exists().isString().trim(),
body('user5').exists().isString().trim(),
body('user6').exists().isString().trim(),
body('user7').exists().isString().trim(),
body('user8').exists().isString().trim(),
body('user9').exists().isString().trim(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/settings
*/
export const validateSettings = [
body('pinCode').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
body('timeFormat').isString().isIn(['12', '24']),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
/**
* @description Validates object for POST /ontime/osc
*/
export const validateOSC = [
body('port').exists().isInt({ min: 0, max: 65353 }),
body('portOut').exists().isInt({ min: 0, max: 65353 }),
body('targetIP').exists().isIP(),
body('enabled').exists().isBoolean(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+2 -1
View File
@@ -3,9 +3,10 @@ export const router = express.Router();
// import event controller // import event controller
import { getEvent, postEvent } from '../controllers/eventController.js'; import { getEvent, postEvent } from '../controllers/eventController.js';
import { eventSanitizer } from '../controllers/eventController.validate.js';
// create route between controller and 'GET /event' endpoint // create route between controller and 'GET /event' endpoint
router.get('/', getEvent); router.get('/', getEvent);
// create route between controller and 'POST /event' endpoint // create route between controller and 'POST /event' endpoint
router.post('/', postEvent); router.post('/', eventSanitizer, postEvent);
+7 -3
View File
@@ -13,6 +13,10 @@ import {
eventsDeleteAll, eventsDeleteAll,
eventsDelete, eventsDelete,
} from '../controllers/eventsController.js'; } from '../controllers/eventsController.js';
import {
eventsPutValidator,
paramsMustHaveEventId,
} from '../controllers/eventsController.validate.js';
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
router.get('/', eventsGetAll); router.get('/', eventsGetAll);
@@ -24,7 +28,7 @@ router.get('/:eventId', eventsGetById);
router.post('/', eventsPost); router.post('/', eventsPost);
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
router.put('/', eventsPut); router.put('/', eventsPutValidator, eventsPut);
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
// DEPRECATED // DEPRECATED
@@ -34,10 +38,10 @@ router.patch('/', eventsPatch);
router.patch('/reorder/', eventsReorder); router.patch('/reorder/', eventsReorder);
// create route between controller and '/events/applydelay/:eventId' endpoint // create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', eventsApplyDelay); router.patch('/applydelay/:eventId', paramsMustHaveEventId, eventsApplyDelay);
// create route between controller and '/events/all' endpoint // create route between controller and '/events/all' endpoint
router.delete('/all', eventsDeleteAll); router.delete('/all', eventsDeleteAll);
// create route between controller and '/events/:eventId' endpoint // create route between controller and '/events/:eventId' endpoint
router.delete('/:eventId', eventsDelete); router.delete('/:eventId', paramsMustHaveEventId, eventsDelete);
+11 -9
View File
@@ -12,14 +12,19 @@ import {
getViewSettings, getViewSettings,
poll, poll,
postAliases, postAliases,
postInfo,
postOSC, postOSC,
postSettings, postSettings,
postUserFields, postUserFields,
postViewSettings, postViewSettings,
} from '../controllers/ontimeController.js'; } from '../controllers/ontimeController.js';
import { viewValidator } from '../validation/ontimeValidator.js'; import {
viewValidator,
validateAliases,
validateUserFields,
validateSettings,
validateOSC,
} from '../controllers/ontimeController.validate.js';
export const router = express.Router(); export const router = express.Router();
@@ -36,7 +41,7 @@ router.post('/db', uploadFile, dbUpload);
router.get('/settings', getSettings); router.get('/settings', getSettings);
// create route between controller and '/ontime/settings' endpoint // create route between controller and '/ontime/settings' endpoint
router.post('/settings', postSettings); router.post('/settings', validateSettings, postSettings);
// create route between controller and '/ontime/views' endpoint // create route between controller and '/ontime/views' endpoint
router.get('/views', getViewSettings); router.get('/views', getViewSettings);
@@ -48,25 +53,22 @@ router.post('/views', viewValidator, postViewSettings);
router.get('/aliases', getAliases); router.get('/aliases', getAliases);
// create route between controller and '/ontime/aliases' endpoint // create route between controller and '/ontime/aliases' endpoint
router.post('/aliases', postAliases); router.post('/aliases', validateAliases, postAliases);
// create route between controller and '/ontime/aliases' endpoint // create route between controller and '/ontime/aliases' endpoint
router.get('/userfields', getUserFields); router.get('/userfields', getUserFields);
// create route between controller and '/ontime/aliases' endpoint // create route between controller and '/ontime/aliases' endpoint
router.post('/userfields', postUserFields); router.post('/userfields', validateUserFields, postUserFields);
// create route between controller and '/ontime/info' endpoint // create route between controller and '/ontime/info' endpoint
router.get('/info', getInfo); router.get('/info', getInfo);
// create route between controller and '/ontime/info' endpoint
router.post('/info', postInfo);
// create route between controller and '/ontime/osc' endpoint // create route between controller and '/ontime/osc' endpoint
router.get('/osc', getOSC); router.get('/osc', getOSC);
// create route between controller and '/ontime/osc' endpoint // create route between controller and '/ontime/osc' endpoint
router.post('/osc', postOSC); router.post('/osc', validateOSC, postOSC);
// create route between controller and '/ontime/dbpath' endpoint // create route between controller and '/ontime/dbpath' endpoint
router.post('/dbpath', dbPathToUpload); router.post('/dbpath', dbPathToUpload);
@@ -0,0 +1,92 @@
import { isEmptyObject, mergeObject, removeUndefined } from '../parserUtils.js';
describe('isEmptyObject()', () => {
test('finds an empty object', () => {
const isEmpty = isEmptyObject({});
expect(isEmpty).toBe(true);
});
test('throws on other types', () => {
expect(() => isEmptyObject(12)).toThrow();
});
test('resolves an object with methods', () => {
const isEmpty = isEmptyObject({ test: 'yes' });
expect(isEmpty).toBe(false);
});
});
describe('mergeObject()', () => {
test('it suppresses undefined keys', () => {
const a = {
first: 'yes',
second: 'yes',
};
const b = {
first: undefined,
second: 'no',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 'yes',
second: 'no',
});
});
test('it handles falsy values', () => {
const a = {
first: 'yes',
second: 'yes',
third: 'yes',
};
const b = {
first: 0,
second: null,
third: '',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 0,
second: null,
third: '',
});
});
test.skip('it only merges fields of the first object', () => {
const a = {
first: 'yes',
second: 'yes',
third: 'yes',
};
const b = {
first: 0,
second: null,
third: '',
forth: 'not-this',
};
const merged = mergeObject(a, b);
expect(merged).toStrictEqual({
first: 0,
second: null,
third: '',
});
});
});
describe('removeUndefined()', () => {
test('it removes undefined keys from object', () => {
const obj = {
first: 'yes',
second: undefined,
third: 'yes',
};
expect(removeUndefined(obj)).toStrictEqual({
first: 'yes',
third: 'yes',
});
});
test('it handles falsy values object', () => {
const obj = {
first: '',
second: 0,
third: 'null',
};
expect(removeUndefined(obj)).toStrictEqual(obj);
});
});
+3 -96
View File
@@ -1,19 +1,13 @@
const adjective = [ const adjective = [
'abandoned',
'able',
'absolute',
'adorable',
'adventurous',
'academic', 'academic',
'acceptable',
'acclaimed', 'acclaimed',
'accomplished', 'accomplished',
'accurate', 'accurate',
'aching',
'acidic',
'acrobatic', 'acrobatic',
'active', 'active',
'actual', 'adaptable',
'adorable',
'adventurous',
'adept', 'adept',
'admirable', 'admirable',
'admired', 'admired',
@@ -23,16 +17,11 @@ const adjective = [
'advanced', 'advanced',
'afraid', 'afraid',
'affectionate', 'affectionate',
'aged',
'aggravating',
'aggressive',
'agile', 'agile',
'agitated', 'agitated',
'agonizing',
'agreeable', 'agreeable',
'ajar', 'ajar',
'alarmed', 'alarmed',
'alarming',
'alert', 'alert',
'alienated', 'alienated',
'alive', 'alive',
@@ -52,13 +41,11 @@ const adjective = [
'annual', 'annual',
'another', 'another',
'antique', 'antique',
'anxious',
'any', 'any',
'apprehensive', 'apprehensive',
'appropriate', 'appropriate',
'apt', 'apt',
'arctic', 'arctic',
'arid',
'aromatic', 'aromatic',
'artistic', 'artistic',
'ashamed', 'ashamed',
@@ -73,18 +60,12 @@ const adjective = [
'authorized', 'authorized',
'automatic', 'automatic',
'avaricious', 'avaricious',
'average',
'aware', 'aware',
'awesome', 'awesome',
'awful',
'awkward',
'babyish',
'bad', 'bad',
'back', 'back',
'baggy',
'bare', 'bare',
'barren', 'barren',
'basic',
'beautiful', 'beautiful',
'belated', 'belated',
'beloved', 'beloved',
@@ -112,7 +93,6 @@ const adjective = [
'boiling', 'boiling',
'bold', 'bold',
'bony', 'bony',
'boring',
'bossy', 'bossy',
'both', 'both',
'bouncy', 'bouncy',
@@ -127,12 +107,10 @@ const adjective = [
'broken', 'broken',
'bronze', 'bronze',
'brown', 'brown',
'bruised',
'bubbly', 'bubbly',
'bulky', 'bulky',
'bumpy', 'bumpy',
'buoyant', 'buoyant',
'burdensome',
'burly', 'burly',
'bustling', 'bustling',
'busy', 'busy',
@@ -195,7 +173,6 @@ const adjective = [
'cooperative', 'cooperative',
'coordinated', 'coordinated',
'corny', 'corny',
'corrupt',
'costly', 'costly',
'courageous', 'courageous',
'courteous', 'courteous',
@@ -203,8 +180,6 @@ const adjective = [
'crazy', 'crazy',
'creamy', 'creamy',
'creative', 'creative',
'creepy',
'criminal',
'crisp', 'crisp',
'critical', 'critical',
'crooked', 'crooked',
@@ -219,7 +194,6 @@ const adjective = [
'curvy', 'curvy',
'cute', 'cute',
'cylindrical', 'cylindrical',
'damaged',
'damp', 'damp',
'dangerous', 'dangerous',
'dapper', 'dapper',
@@ -236,10 +210,8 @@ const adjective = [
'decimal', 'decimal',
'decisive', 'decisive',
'deep', 'deep',
'defenseless',
'defensive', 'defensive',
'defiant', 'defiant',
'deficient',
'definite', 'definite',
'definitive', 'definitive',
'delayed', 'delayed',
@@ -249,7 +221,6 @@ const adjective = [
'delirious', 'delirious',
'demanding', 'demanding',
'dense', 'dense',
'dental',
'dependable', 'dependable',
'dependent', 'dependent',
'descriptive', 'descriptive',
@@ -258,29 +229,19 @@ const adjective = [
'determined', 'determined',
'devoted', 'devoted',
'different', 'different',
'difficult',
'digital', 'digital',
'diligent', 'diligent',
'dim', 'dim',
'dimpled', 'dimpled',
'dimwitted',
'direct',
'disastrous',
'discrete', 'discrete',
'disfigured',
'disgusting',
'disloyal',
'dismal', 'dismal',
'distant', 'distant',
'downright', 'downright',
'dreary', 'dreary',
'dirty',
'disguised', 'disguised',
'dishonest',
'dismal', 'dismal',
'distant', 'distant',
'distinct', 'distinct',
'distorted',
'dizzy', 'dizzy',
'dopey', 'dopey',
'doting', 'doting',
@@ -293,7 +254,6 @@ const adjective = [
'droopy', 'droopy',
'dry', 'dry',
'dual', 'dual',
'dull',
'dutiful', 'dutiful',
'each', 'each',
'eager', 'eager',
@@ -474,20 +434,16 @@ const adjective = [
'great', 'great',
'greedy', 'greedy',
'green', 'green',
'gregarious',
'grim', 'grim',
'grimy', 'grimy',
'gripping', 'gripping',
'grizzled', 'grizzled',
'gross',
'grotesque',
'grouchy', 'grouchy',
'grounded', 'grounded',
'growing', 'growing',
'growling', 'growling',
'grown', 'grown',
'grubby', 'grubby',
'gruesome',
'grumpy', 'grumpy',
'guilty', 'guilty',
'gullible', 'gullible',
@@ -506,7 +462,6 @@ const adjective = [
'harmonious', 'harmonious',
'harsh', 'harsh',
'hasty', 'hasty',
'hateful',
'haunting', 'haunting',
'healthy', 'healthy',
'heartfelt', 'heartfelt',
@@ -528,16 +483,13 @@ const adjective = [
'honorable', 'honorable',
'honored', 'honored',
'hopeful', 'hopeful',
'horrible',
'hospitable', 'hospitable',
'hot', 'hot',
'huge', 'huge',
'humble', 'humble',
'humiliating',
'humming', 'humming',
'humongous', 'humongous',
'hungry', 'hungry',
'hurtful',
'husky', 'husky',
'icky', 'icky',
'icy', 'icy',
@@ -548,10 +500,7 @@ const adjective = [
'idiotic', 'idiotic',
'idolized', 'idolized',
'ignorant', 'ignorant',
'ill',
'illegal', 'illegal',
'ill-fated',
'ill-informed',
'illiterate', 'illiterate',
'illustrious', 'illustrious',
'imaginary', 'imaginary',
@@ -566,15 +515,11 @@ const adjective = [
'imperfect', 'imperfect',
'imperturbable', 'imperturbable',
'impish', 'impish',
'impolite',
'important', 'important',
'impossible', 'impossible',
'impractical',
'impressionable', 'impressionable',
'impressive', 'impressive',
'improbable', 'improbable',
'impure',
'inborn',
'incomparable', 'incomparable',
'incompatible', 'incompatible',
'incomplete', 'incomplete',
@@ -592,7 +537,6 @@ const adjective = [
'innocent', 'innocent',
'insecure', 'insecure',
'insidious', 'insidious',
'insignificant',
'insistent', 'insistent',
'instructive', 'instructive',
'insubstantial', 'insubstantial',
@@ -639,7 +583,6 @@ const adjective = [
'known', 'known',
'kooky', 'kooky',
'kosher', 'kosher',
'lame',
'lanky', 'lanky',
'large', 'large',
'last', 'last',
@@ -722,9 +665,7 @@ const adjective = [
'miniature', 'miniature',
'minor', 'minor',
'minty', 'minty',
'miserable',
'miserly', 'miserly',
'misguided',
'misty', 'misty',
'mixed', 'mixed',
'modern', 'modern',
@@ -777,18 +718,15 @@ const adjective = [
'noteworthy', 'noteworthy',
'novel', 'novel',
'noxious', 'noxious',
'numb',
'nutritious', 'nutritious',
'nutty', 'nutty',
'obedient', 'obedient',
'obese',
'oblong', 'oblong',
'oily', 'oily',
'oblong', 'oblong',
'obvious', 'obvious',
'occasional', 'occasional',
'odd', 'odd',
'oddball',
'offbeat', 'offbeat',
'offensive', 'offensive',
'official', 'official',
@@ -836,7 +774,6 @@ const adjective = [
'personal', 'personal',
'pertinent', 'pertinent',
'pesky', 'pesky',
'pessimistic',
'petty', 'petty',
'phony', 'phony',
'physical', 'physical',
@@ -858,7 +795,6 @@ const adjective = [
'pointed', 'pointed',
'pointless', 'pointless',
'poised', 'poised',
'poor',
'popular', 'popular',
'portly', 'portly',
'posh', 'posh',
@@ -903,7 +839,6 @@ const adjective = [
'quarterly', 'quarterly',
'queasy', 'queasy',
'querulous', 'querulous',
'questionable',
'quick', 'quick',
'quick-witted', 'quick-witted',
'quiet', 'quiet',
@@ -931,7 +866,6 @@ const adjective = [
'reliable', 'reliable',
'relieved', 'relieved',
'remarkable', 'remarkable',
'remorseful',
'remote', 'remote',
'repentant', 'repentant',
'required', 'required',
@@ -949,7 +883,6 @@ const adjective = [
'robust', 'robust',
'rosy', 'rosy',
'rotating', 'rotating',
'rotten',
'rough', 'rough',
'round', 'round',
'rowdy', 'rowdy',
@@ -963,7 +896,6 @@ const adjective = [
'rusty', 'rusty',
'sad', 'sad',
'safe', 'safe',
'salty',
'same', 'same',
'sandy', 'sandy',
'sane', 'sane',
@@ -998,7 +930,6 @@ const adjective = [
'shadowy', 'shadowy',
'shady', 'shady',
'shallow', 'shallow',
'shameful',
'shameless', 'shameless',
'sharp', 'sharp',
'shimmering', 'shimmering',
@@ -1011,7 +942,6 @@ const adjective = [
'showy', 'showy',
'shrill', 'shrill',
'shy', 'shy',
'sick',
'silent', 'silent',
'silky', 'silky',
'silly', 'silly',
@@ -1027,7 +957,6 @@ const adjective = [
'sleepy', 'sleepy',
'slight', 'slight',
'slim', 'slim',
'slimy',
'slippery', 'slippery',
'slow', 'slow',
'slushy', 'slushy',
@@ -1136,7 +1065,6 @@ const adjective = [
'tender', 'tender',
'tense', 'tense',
'tepid', 'tepid',
'terrible',
'terrific', 'terrific',
'testy', 'testy',
'thankful', 'thankful',
@@ -1183,7 +1111,6 @@ const adjective = [
'tubby', 'tubby',
'turbulent', 'turbulent',
'twin', 'twin',
'ugly',
'ultimate', 'ultimate',
'unacceptable', 'unacceptable',
'unaware', 'unaware',
@@ -1198,14 +1125,12 @@ const adjective = [
'unfolded', 'unfolded',
'unfortunate', 'unfortunate',
'unhappy', 'unhappy',
'unhealthy',
'uniform', 'uniform',
'unimportant', 'unimportant',
'unique', 'unique',
'united', 'united',
'unkempt', 'unkempt',
'unknown', 'unknown',
'unlawful',
'unlined', 'unlined',
'unlucky', 'unlucky',
'unnatural', 'unnatural',
@@ -1235,7 +1160,6 @@ const adjective = [
'usable', 'usable',
'used', 'used',
'useful', 'useful',
'useless',
'utilized', 'utilized',
'utter', 'utter',
'vacant', 'vacant',
@@ -1271,7 +1195,6 @@ const adjective = [
'warmhearted', 'warmhearted',
'warped', 'warped',
'wary', 'wary',
'wasteful',
'watchful', 'watchful',
'waterlogged', 'waterlogged',
'watery', 'watery',
@@ -1611,7 +1534,6 @@ const object = [
'classroom', 'classroom',
'delivery', 'delivery',
'device', 'device',
'difficulty',
'drama', 'drama',
'election', 'election',
'engine', 'engine',
@@ -1625,7 +1547,6 @@ const object = [
'suggestion', 'suggestion',
'tension', 'tension',
'variation', 'variation',
'anxiety',
'atmosphere', 'atmosphere',
'awareness', 'awareness',
'bread', 'bread',
@@ -1761,7 +1682,6 @@ const object = [
'drawer', 'drawer',
'establishment', 'establishment',
'examination', 'examination',
'garbage',
'grocery', 'grocery',
'honey', 'honey',
'impression', 'impression',
@@ -1886,7 +1806,6 @@ const object = [
'group', 'group',
'risk', 'risk',
'word', 'word',
'fat',
'force', 'force',
'key', 'key',
'light', 'light',
@@ -1934,7 +1853,6 @@ const object = [
'coast', 'coast',
'action', 'action',
'age', 'age',
'bad',
'boat', 'boat',
'record', 'record',
'result', 'result',
@@ -2223,7 +2141,6 @@ const object = [
'pleasure', 'pleasure',
'score', 'score',
'screw', 'screw',
'sex',
'shop', 'shop',
'shower', 'shower',
'suit', 'suit',
@@ -2416,7 +2333,6 @@ const object = [
'counter', 'counter',
'flower', 'flower',
'grandfather', 'grandfather',
'harm',
'knee', 'knee',
'lawyer', 'lawyer',
'leather', 'leather',
@@ -2747,7 +2663,6 @@ const object = [
'crazy', 'crazy',
'escape', 'escape',
'gather', 'gather',
'hate',
'prior', 'prior',
'repair', 'repair',
'rough', 'rough',
@@ -2757,19 +2672,15 @@ const object = [
'strike', 'strike',
'employ', 'employ',
'external', 'external',
'hurt',
'illegal',
'laugh', 'laugh',
'lay', 'lay',
'mobile', 'mobile',
'nasty',
'ordinary', 'ordinary',
'respond', 'respond',
'royal', 'royal',
'senior', 'senior',
'split', 'split',
'strain', 'strain',
'struggle',
'swim', 'swim',
'train', 'train',
'upper', 'upper',
@@ -2808,7 +2719,6 @@ const object = [
'prompt', 'prompt',
'quiet', 'quiet',
'refuse', 'refuse',
'regret',
'reveal', 'reveal',
'rush', 'rush',
'shake', 'shake',
@@ -2848,9 +2758,6 @@ const object = [
'wake', 'wake',
'wrap', 'wrap',
'yesterday', 'yesterday',
'Thomas',
'Tom',
'Lieuwe',
]; ];
/** /**
+36
View File
@@ -49,3 +49,39 @@ export const validateFile = (file) => {
return false; return false;
} }
}; };
/**
* @description Verifies if object is empty
* @param {object} obj
*/
export const isEmptyObject = (obj) => {
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
return Object.keys(obj).length === 0;
}
throw new Error('Variable is not an object');
};
/**
* @description Merges two objects, suppressing undefined keys
* @param {object} a
* @param {object} b
*/
export const mergeObject = (a, b) => {
const merged = {};
Object.keys({ ...a, ...b }).map((key) => {
merged[key] = typeof b[key] === 'undefined' ? a[key] : b[key];
});
return merged;
};
/**
* @description Removes undefined
* @param {object} obj
*/
export const removeUndefined = (obj) => {
const patched = {};
Object.keys({ ...obj })
.filter((key) => typeof obj[key] !== 'undefined')
.map((key) => (patched[key] = obj[key]));
return patched;
};
+39
View File
@@ -0,0 +1,39 @@
import { isEmptyObject } from './parserUtils.js';
/**
* @description initial checks for an empty of malformed request object
* @param obj
* @param res
*/
export const failEmptyObjects = (obj, res) => {
let failed = false;
try {
if (isEmptyObject(obj)) {
res.status(400).send('No object found in request');
failed = true;
}
} catch (error) {
res.status(400).send(error);
failed = true;
}
return failed;
};
/**
* @description initial checks for an empty of malformed request object
* @param obj
* @param res
*/
export const failIsNotArray = (obj, res) => {
let failed = false;
try {
if (!Array.isArray(obj)) {
res.status(400).send('No array found in request');
failed = true;
}
} catch (error) {
res.status(400).send(error);
failed = true;
}
return failed;
};
-13
View File
@@ -1,13 +0,0 @@
import { check, validationResult } from 'express-validator';
/**
* @description Validates object for POST /ontime/views
*/
export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];