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
+15 -11
View File
@@ -1,24 +1,28 @@
// get database
import { db, data } from '../app.js';
import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event'
// Returns ACK message
export const getEvent = async (req, res) => {
res.json(data.event);
res.json(DataProvider.getEventData());
};
// Create controller for POST request to 'event'
// Returns ACK message
export const postEvent = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
// TODO: validate data
try {
data.event = { ...data.event, ...req.body };
await db.write();
res.sendStatus(200);
const newEvent = removeUndefined({
title: req.body?.title,
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) {
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 {
block as blockDef,
delay as delayDef,
@@ -10,87 +6,36 @@ import {
import { generateId } from '../utils/generate_id.js';
import { MAX_EVENTS } from '../settings.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) {
if (newEvent.order) {
const events = data.events;
await _insertAt(newEvent, newEvent.order);
const events = DataProvider.getEvents();
await DataProvider.insertEventAt(newEvent, newEvent.order);
const previousId = events?.[newEvent.order - 1]?.id;
_insertEventInTimerAfterId(newEvent, previousId);
} else if (newEvent.after) {
await _insertAfterId(newEvent, newEvent.after);
await DataProvider.insertEventAfterId(newEvent, newEvent.after);
_insertEventInTimerAfterId(newEvent, newEvent.after);
} else {
await _insertAt(newEvent, 0);
await DataProvider.insertEventAt(newEvent, 0);
_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
* @return {unknown[]}
*/
function getEventEvents() {
// 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
@@ -112,7 +57,7 @@ function _insertEventInTimerAfterId(event, previousId) {
try {
global.timer.insertEventAfterId(event, previousId);
} 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'
// Returns -
export const eventsGetAll = async (req, res) => {
res.json(data.events);
res.json(DataProvider.getEvents());
};
// Create controller for GET request to '/events/:eventId'
@@ -146,7 +91,7 @@ export const eventsGetById = async (req, res) => {
if (id == null) {
res.status(400).send(`No eventId found in request`);
} else {
const event = data.events.find((e) => e.id === id);
const event = DataProvider.getEventById(id);
res.json(event);
}
};
@@ -154,13 +99,12 @@ export const eventsGetById = async (req, res) => {
// Create controller for POST request to '/events/'
// Returns -
export const eventsPost = async (req, res) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
if (failEmptyObjects(req.body, res)) {
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`;
res.status(400).send(error);
return;
@@ -187,7 +131,7 @@ export const eventsPost = async (req, res) => {
}
try {
_insertAndSync(newEvent);
await _insertAndSync(newEvent);
res.sendStatus(201);
} catch (error) {
res.status(400).send(error);
@@ -197,41 +141,33 @@ export const eventsPost = async (req, res) => {
// Create controller for PUT request to '/events/'
// Returns -
export const eventsPut = async (req, res) => {
// no valid params
if (!req.body) {
res.status(400).send(`No object found`);
if (failEmptyObjects(req.body, res)) {
return;
}
const eventId = req.body.id;
if (!eventId) {
res.status(400).send(`Object malformed: id missing`);
return;
}
const eventIndex = data.events.findIndex((e) => e.id === eventId);
if (eventIndex === -1) {
res.status(400).send(`No Id found found`);
const eventDataFromRequest = req.body;
const eventId = eventDataFromRequest.id;
const event = DataProvider.getEventById(eventId);
if (typeof event === 'undefined') {
res.status(400).send(`No event with ID found`);
return;
}
try {
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...req.body };
data.events[eventIndex].revision++;
await db.write();
const newData = DataProvider.updateEventById(eventId, eventDataFromRequest);
if (data.events[eventIndex].skip) {
if (newData.skip) {
_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
} else {
try {
_updateTimersSingle(eventId, req.body);
_updateTimersSingle(newData.id, eventDataFromRequest);
} catch (error) {
if (error === 'Event not found') {
const { id: previousId } = getPreviousPlayable(data.events, e.id);
_insertEventInTimerAfterId(data.events[eventIndex], previousId);
const events = DataProvider.getEvents();
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) => {
// TODO: Validate event
if (!req.body) {
res.status(400).send(`No object found in request`);
if (failEmptyObjects(req.body, res)) {
return;
}
const { index, from, to } = req.body;
// get events
const events = data.events;
const events = DataProvider.getEvents();
const idx = events.findIndex((e) => e.id === index, from);
// Check if item is at given index
@@ -276,8 +210,7 @@ export const eventsReorder = async (req, res) => {
events.splice(to, 0, reorderedItem);
// save events
data.events = events;
await db.write();
await DataProvider.setEventData(events);
// update timer
_updateTimers();
@@ -291,15 +224,9 @@ export const eventsReorder = async (req, res) => {
// Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns -
export const eventsApplyDelay = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
// get events
const events = data.events;
const events = DataProvider.getEvents();
// AUX
let delayIndex = null;
@@ -340,8 +267,7 @@ export const eventsApplyDelay = async (req, res) => {
if (blockIndex) events.splice(blockIndex - 1, 1);
// update events
data.events = events;
await db.write();
await DataProvider.setEvents(events);
// update timer
_updateTimers();
@@ -355,20 +281,15 @@ export const eventsApplyDelay = async (req, res) => {
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const eventsDelete = async (req, res) => {
// no valid params
if (!req.params.eventId) {
res.status(400).send(`No id found in request`);
return;
}
try {
const eventId = req.params.eventId;
// remove new event
await _removeById(req.params.eventId);
await DataProvider.deleteEvent(eventId);
// update timer
_deleteTimerId(req.params.eventId);
_deleteTimerId(eventId);
res.sendStatus(201);
res.sendStatus(204);
} catch (error) {
res.status(400).send(error);
}
@@ -378,14 +299,9 @@ export const eventsDelete = async (req, res) => {
// Returns -
export const eventsDeleteAll = async (req, res) => {
try {
// set with nothing
data.events = [];
await db.write();
// update timer object
await DataProvider.deleteAllEvents();
global.timer.clearEventList();
res.sendStatus(201);
res.sendStatus(204);
} catch (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 { data, db } from '../app.js';
import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from '../utils/generate_id.js';
import { resolveDbPath } from '../modules/loadDb.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'
// Returns data for current state
@@ -22,7 +23,8 @@ export const poll = async (req, res) => {
// Create controller for GET request to '/ontime/db'
// Returns -
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();
res.download(dbInDisk, `${fileTitle}.json`, (err) => {
@@ -48,18 +50,13 @@ const uploadAndParse = async (file, req, res, options) => {
} else if (result.message === 'success') {
// explicitly write objects
if (typeof result !== 'undefined') {
if (!options.onlyEvents) {
const mergedData = DataProvider.safeMerge(data, result.data);
data.event = mergedData.event;
data.settings = mergedData.settings;
data.osc = mergedData.osc;
data.http = mergedData.http;
data.aliases = mergedData.aliases;
data.userFields = mergedData.userFields;
const newEvents = result.data.events || [];
if (options.onlyEvents) {
await DataProvider.setEvents(newEvents);
} else {
await DataProvider.mergeIntoData(result.data);
}
data.events = result.data.events || [];
global.timer.setupWithEventList(result.data.events || []);
await db.write();
global.timer.setupWithEventList(newEvents);
}
res.sendStatus(200);
} 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}
*/
const getNetworkInterfaces = () => {
@@ -96,15 +93,8 @@ const getNetworkInterfaces = () => {
// Create controller for POST request to '/ontime/info'
// Returns -
export const getInfo = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const osc = {
port: data.osc.port,
portOut: data.osc.portOut,
targetIP: data.osc.targetIP,
enabled: data.osc.enabled,
};
const { version, serverPort } = DataProvider.getSettings();
const osc = DataProvider.getOsc();
// get nif and inject localhost
const ni = getNetworkInterfaces();
@@ -122,18 +112,16 @@ export const getInfo = async (req, res) => {
// Create controller for POST request to '/ontime/aliases'
// Returns -
export const getAliases = async (req, res) => {
// send aliases array
res.status(200).send(data.aliases);
const aliases = DataProvider.getAliases();
res.status(200).send(aliases);
};
// Create controller for POST request to '/ontime/aliases'
// Returns ACK message
export const postAliases = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failIsNotArray()) {
return;
}
// TODO: validate data
try {
const newAliases = [];
req.body.forEach((a) => {
@@ -144,9 +132,8 @@ export const postAliases = async (req, res) => {
pathAndParams: a.pathAndParams,
});
});
data.aliases = newAliases;
await db.write();
res.sendStatus(200);
await DataProvider.setAliases(newAliases);
res.status(200).send(newAliases);
} catch (error) {
res.status(400).send(error);
}
@@ -155,27 +142,21 @@ export const postAliases = async (req, res) => {
// Create controller for GET request to '/ontime/userfields'
// Returns -
export const getUserFields = async (req, res) => {
// send userFields array
res.status(200).send(data.userFields);
const userFields = DataProvider.getUserFields();
res.status(200).send(userFields);
};
// Create controller for POST request to '/ontime/userfields'
// Returns ACK message
export const postUserFields = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
try {
const newUserFields = { ...data.userFields };
for (const field in newUserFields) {
if (typeof req.body[field] !== 'undefined') {
newUserFields[field] = req.body[field];
}
}
data.userFields = newUserFields;
await db.write();
res.sendStatus(200);
const persistedData = DataProvider.getUserFields();
const newData = mergeObject(persistedData, req.body);
await DataProvider.setUserFields(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -184,12 +165,8 @@ export const postUserFields = async (req, res) => {
// Create controller for POST request to '/ontime/settings'
// Returns -
export const getSettings = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
const timeFormat = data.settings.timeFormat;
const { version, serverPort, pinCode, timeFormat } = DataProvider.getSettings();
// send object with network information
res.status(200).send({
version,
serverPort,
@@ -201,12 +178,12 @@ export const getSettings = async (req, res) => {
// Create controller for POST request to '/ontime/settings'
// Returns ACK message
export const postSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
try {
let pin = data.settings.pinCode;
const settings = DataProvider.getSettings();
let pin = settings.pinCode;
if (typeof req.body?.pinCode === 'string') {
if (req.body?.pinCode.length === 0) {
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 (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
timeFormat = req.body.timeFormat;
format = req.body.timeFormat;
}
}
data.settings = {
...data.settings,
const newData = {
...settings,
pinCode: pin,
timeFormat: timeFormat,
timeFormat: format,
};
await db.write();
res.sendStatus(200);
DataProvider.setSettings(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -239,7 +216,8 @@ export const postSettings = async (req, res) => {
* @method GET
*/
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
*/
export const postViewSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
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 {
data.settings = { ...data.settings, ...req.body };
await db.write();
res.sendStatus(200);
const newData = { overrideStyles: req.body.overrideStyles };
DataProvider.setViews(newData);
res.status(200).send(newData);
} catch (error) {
res.status(400).send(error);
}
@@ -282,25 +241,22 @@ export const postInfo = async (req, res) => {
// Create controller for POST request to '/ontime/osc'
// Returns -
export const getOSC = async (req, res) => {
// send object with network information
res.status(200).send(data.osc);
const osc = DataProvider.getOsc();
res.status(200).send(osc);
};
// Create controller for POST request to '/ontime/osc'
// Returns ACK message
export const postOSC = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
if (failEmptyObjects(req.body, res)) {
return;
}
// TODO: validate data
try {
data.osc = { ...data.osc, ...req.body };
await db.write();
res.sendStatus(200);
await DataProvider.setOsc(req.body);
res.send(req.body).status(200);
} catch (error) {
res.status(400).send(error);
console.log(error);
}
};
@@ -313,7 +269,7 @@ export const dbUpload = async (req, res) => {
}
const options = req.query;
const file = req.file.path;
uploadAndParse(file, req, res, options);
await uploadAndParse(file, req, res, options);
};
// 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' });
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();
},
];