(null);
@@ -102,7 +100,6 @@ export default function ProjectListItem({
) : (
<>
| {filename} |
- {new Date(createdAt).toLocaleString()} |
{new Date(updatedAt).toLocaleString()} |
>} A promise that resolves to an array of ProjectFile objects,
* each representing a file in the 'uploads' folder with its metadata.
- * The metadata includes the filename, creation time (createdAt),
- * and last modification time (updatedAt) of each file.
+ * The metadata includes the filename, creation or overwriting time (updatedAt)
*
* @throws {Error} Throws an error if there is an issue in reading the directory or fetching file statistics.
*/
@@ -83,7 +82,6 @@ export async function getProjectFiles(): Promise {
projectFiles.push({
filename: removeFileExtension(file),
- createdAt: stats.birthtime.toISOString(),
updatedAt: stats.mtime.toISOString(),
});
}
diff --git a/apps/server/src/services/project-service/__tests__/ProjectService.test.ts b/apps/server/src/services/project-service/__tests__/ProjectService.test.ts
index 877a2288f..dc0ab6fb9 100644
--- a/apps/server/src/services/project-service/__tests__/ProjectService.test.ts
+++ b/apps/server/src/services/project-service/__tests__/ProjectService.test.ts
@@ -23,7 +23,6 @@ describe('getProjectFiles test', () => {
const expectedFiles = ['file1', 'file2', 'file3'].map((file) => ({
filename: file,
- createdAt: new Date('2020-01-01').toISOString(),
updatedAt: new Date('2021-01-01').toISOString(),
}));
diff --git a/packages/types/src/api/ontime-controller/BackendResponse.type.ts b/packages/types/src/api/ontime-controller/BackendResponse.type.ts
index 699c34281..4cd3f09ce 100644
--- a/packages/types/src/api/ontime-controller/BackendResponse.type.ts
+++ b/packages/types/src/api/ontime-controller/BackendResponse.type.ts
@@ -15,7 +15,6 @@ export interface GetInfo {
export type ProjectFile = {
filename: string;
- createdAt: string;
updatedAt: string;
};
From d432f1e3ff9d0ef76105eb135d9c0e46b767d8e2 Mon Sep 17 00:00:00 2001
From: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Date: Fri, 5 Apr 2024 12:07:26 +0200
Subject: [PATCH 7/9] Typescript null (#869)
* refactor: improve null checking
* fix: return result of mutation
---
apps/server/src/adapters/WebsocketAdapter.ts | 16 +-
.../src/api-data/excel/excel.service.ts | 18 +-
.../src/classes/simple-timer/SimpleTimer.ts | 1 +
apps/server/src/services/RestoreService.ts | 2 +-
.../project-service/ProjectService.ts | 2 +-
.../rundown-service/RundownService.ts | 47 ++-
.../services/rundown-service/rundownCache.ts | 34 +-
apps/server/src/setup/loadDb.ts | 8 +-
.../server/src/utils/__tests__/parser.test.ts | 8 +-
apps/server/src/utils/parser.ts | 21 +-
apps/server/src/utils/parserFunctions.ts | 322 +++++++++---------
apps/server/src/utils/parserUtils.ts | 10 +-
12 files changed, 260 insertions(+), 229 deletions(-)
diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts
index 079f8aa54..f953af9a1 100644
--- a/apps/server/src/adapters/WebsocketAdapter.ts
+++ b/apps/server/src/adapters/WebsocketAdapter.ts
@@ -116,16 +116,14 @@ export class SocketServer implements IAdapter {
// Protocol specific stuff handled above
try {
- const reply = dispatchFromAdapter(
- type,
- {
- payload,
- },
- 'ws',
- );
+ const reply = dispatchFromAdapter(type, { payload }, 'ws');
if (reply) {
- const { payload } = reply;
- ws.send(type, payload);
+ ws.send(
+ JSON.stringify({
+ type: 'ontime-change',
+ payload: reply.payload,
+ }),
+ );
}
} catch (error) {
logger.error(LogOrigin.Rx, `WS IN: ${error}`);
diff --git a/apps/server/src/api-data/excel/excel.service.ts b/apps/server/src/api-data/excel/excel.service.ts
index 32930e31c..929ea6c90 100644
--- a/apps/server/src/api-data/excel/excel.service.ts
+++ b/apps/server/src/api-data/excel/excel.service.ts
@@ -3,10 +3,13 @@
* Google Sheets
*/
+import { CustomFields, OntimeRundown } from 'ontime-types';
+import { ImportMap } from 'ontime-utils';
+
import { extname } from 'path';
import { existsSync } from 'fs';
-import { ImportMap } from 'ontime-utils';
import xlsx from 'node-xlsx';
+
import { parseExcel } from '../../utils/parser.js';
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
import { deleteFile } from '../../utils/parserUtils.js';
@@ -29,7 +32,7 @@ export function listWorksheets() {
return excelData.map((value) => value.name);
}
-export function generateRundownPreview(options: ImportMap) {
+export function generateRundownPreview(options: ImportMap): { rundown: OntimeRundown; customFields: CustomFields } {
const data = excelData.find(({ name }) => name.toLowerCase() === options.worksheet.toLowerCase())?.data;
if (!data) {
@@ -39,15 +42,14 @@ export function generateRundownPreview(options: ImportMap) {
const dataFromExcel = parseExcel(data, options);
// we run the parsed data through an extra step to ensure the objects shape
- const result = { rundown: [], customFields: {} };
- result.rundown = parseRundown(dataFromExcel);
- if (result.rundown.length < 1) {
+ const rundown = parseRundown(dataFromExcel);
+ if (rundown.length === 0) {
throw new Error(`Could not find data to import in the worksheet: ${options.worksheet}`);
}
- result.customFields = parseCustomFields(dataFromExcel);
+ const customFields = parseCustomFields(dataFromExcel);
- //clear the data
+ // clear the data
excelData = [];
- return result;
+ return { rundown, customFields };
}
diff --git a/apps/server/src/classes/simple-timer/SimpleTimer.ts b/apps/server/src/classes/simple-timer/SimpleTimer.ts
index 31e31ca97..7465be16d 100644
--- a/apps/server/src/classes/simple-timer/SimpleTimer.ts
+++ b/apps/server/src/classes/simple-timer/SimpleTimer.ts
@@ -64,6 +64,7 @@ export class SimpleTimer {
public update(timeNow: number): SimpleTimerState {
if (this.state.playback === SimplePlayback.Start) {
+ // we know startedAt is not null since we are in play mode
const elapsed = timeNow - this.startedAt;
if (this.state.direction === SimpleDirection.CountDown) {
this.state.current = this.state.duration - elapsed;
diff --git a/apps/server/src/services/RestoreService.ts b/apps/server/src/services/RestoreService.ts
index 46bfad9cd..01358eb22 100644
--- a/apps/server/src/services/RestoreService.ts
+++ b/apps/server/src/services/RestoreService.ts
@@ -64,7 +64,7 @@ export class RestoreService {
private readonly filePath: MaybeString;
private readonly file: JSONFile;
private failedCreateAttempts: number;
- private savedState: RestorePoint;
+ private savedState: RestorePoint | null;
constructor(filePath: string) {
this.filePath = filePath;
diff --git a/apps/server/src/services/project-service/ProjectService.ts b/apps/server/src/services/project-service/ProjectService.ts
index 55ab86b9d..191528330 100644
--- a/apps/server/src/services/project-service/ProjectService.ts
+++ b/apps/server/src/services/project-service/ProjectService.ts
@@ -75,7 +75,7 @@ export async function getProjectFiles(): Promise {
const allFiles = await getFilesFromFolder(resolveProjectsDirectory);
const filteredFiles = filterProjectFiles(allFiles);
- const projectFiles = [];
+ const projectFiles: ProjectFile[] = [];
for (const file of filteredFiles) {
const filePath = join(resolveProjectsDirectory, file);
const stats = await stat(filePath);
diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts
index e3c11f2a5..fd245cbd6 100644
--- a/apps/server/src/services/rundown-service/RundownService.ts
+++ b/apps/server/src/services/rundown-service/RundownService.ts
@@ -22,20 +22,32 @@ import { runtimeService } from '../runtime-service/RuntimeService.js';
import * as cache from './rundownCache.js';
import { getPlayableEvents } from './rundownUtils.js';
-function generateEvent(eventData: Partial | Partial | Partial) {
- // we discard any UI provided events and add our own
+type PatchWithId = (Partial | Partial | Partial) & { id: string };
+
+type CompleteEntry = T extends Partial
+ ? OntimeEvent
+ : T extends Partial
+ ? OntimeDelay
+ : T extends Partial
+ ? OntimeBlock
+ : never;
+
+function generateEvent | Partial | Partial>(
+ eventData: T,
+): CompleteEntry {
+ // we discard any UI provided IDs and add our own
const id = cache.getUniqueId();
if (isOntimeEvent(eventData)) {
- return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as OntimeEvent;
+ return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as CompleteEntry;
}
if (isOntimeDelay(eventData)) {
- return { ...delayDef, duration: eventData.duration ?? 0, id } as OntimeDelay;
+ return { ...delayDef, duration: eventData.duration ?? 0, id } as CompleteEntry;
}
if (isOntimeBlock(eventData)) {
- return { ...blockDef, title: eventData?.title ?? '', id } as OntimeBlock;
+ return { ...blockDef, title: eventData?.title ?? '', id } as CompleteEntry;
}
throw new Error('Invalid event type');
@@ -46,9 +58,7 @@ function generateEvent(eventData: Partial | Partial |
* @param {object} eventData
* @return {OntimeRundownEntry}
*/
-export async function addEvent(
- eventData: Partial | Partial | Partial,
-): Promise {
+export async function addEvent(eventData: PatchWithId & { after?: string }): Promise {
// if the user didnt provide an index, we add the event to start
let atIndex = 0;
if (eventData?.after !== undefined) {
@@ -62,15 +72,16 @@ export async function addEvent(
// generate a fully formed event from the patch
const eventToAdd = generateEvent(eventData);
+
// modify rundown
const scopedMutation = cache.mutateCache(cache.add);
- const { newEvent } = await scopedMutation({ atIndex, event: eventToAdd as OntimeRundownEntry });
+ const { newEvent } = await scopedMutation({ atIndex, event: eventToAdd });
// notify runtime that rundown has changed
updateRuntimeOnChange();
// notify timer and external services of change
- notifyChanges({ timer: [newEvent.id], external: true });
+ notifyChanges({ timer: [eventData.id], external: true });
return newEvent;
}
@@ -81,7 +92,11 @@ export async function addEvent(
*/
export async function deleteEvent(eventId: string) {
const scopedMutation = cache.mutateCache(cache.remove);
- await scopedMutation({ eventId });
+ const { didMutate } = await scopedMutation({ eventId });
+
+ if (didMutate === false) {
+ return;
+ }
// notify runtime that rundown has changed
updateRuntimeOnChange();
@@ -108,14 +123,18 @@ export async function deleteAllEvents() {
* Apply patch to an element in rundown
* @param patch
*/
-export async function editEvent(patch: Partial | Partial | Partial) {
+export async function editEvent(patch: PatchWithId) {
if (isOntimeEvent(patch) && patch?.cue === '') {
throw new Error('Cue value invalid');
}
const scopedMutation = cache.mutateCache(cache.edit);
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know patch has an id
- const { newEvent } = await scopedMutation({ patch, eventId: patch.id! });
+ const { newEvent, didMutate } = await scopedMutation({ patch, eventId: patch.id });
+
+ // short circuit if nothing changed
+ if (didMutate === false) {
+ return newEvent;
+ }
// notify runtime that rundown has changed
updateRuntimeOnChange();
diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts
index 254cc0d91..e2851b96d 100644
--- a/apps/server/src/services/rundown-service/rundownCache.ts
+++ b/apps/server/src/services/rundown-service/rundownCache.ts
@@ -83,7 +83,7 @@ export function generate(
let accumulatedDelay = 0;
let daySpan = 0;
- let previousEnd: number;
+ let previousEnd: MaybeNumber = null;
for (let i = 0; i < initialRundown.length; i++) {
const currentEvent = initialRundown[i];
@@ -106,7 +106,7 @@ export function generate(
lastEnd = updatedEvent.timeEnd;
// check if we go over midnight, account for eventual gaps
- const gapOverMidnight = previousEnd > updatedEvent.timeStart;
+ const gapOverMidnight = previousEnd !== null && previousEnd > updatedEvent.timeStart;
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
if (gapOverMidnight || durationOverMidnight) {
daySpan++;
@@ -135,7 +135,9 @@ export function generate(
isStale = false;
totalDelay = accumulatedDelay;
- totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
+ if (lastEnd !== null && firstStart !== null) {
+ totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
+ }
return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
}
@@ -210,6 +212,7 @@ type MutationParams = T & CommonParams;
type MutatingReturn = {
newRundown: OntimeRundown;
newEvent?: OntimeRundownEntry;
+ didMutate: boolean;
};
type MutatingFn = (params: MutationParams) => MutatingReturn;
@@ -227,7 +230,7 @@ export function mutateCache(mutation: MutatingFn) {
*/
isStale = true;
- const { newEvent, newRundown } = mutation({ ...params, persistedRundown });
+ const { newEvent, newRundown, didMutate } = mutation({ ...params, persistedRundown });
revision = revision + 1;
persistedRundown = newRundown;
@@ -244,7 +247,7 @@ export function mutateCache(mutation: MutatingFn) {
DataProvider.setRundown(persistedRundown);
});
- return { newEvent };
+ return { newEvent, newRundown, didMutate };
}
return scopedMutation;
@@ -256,7 +259,7 @@ export function add({ persistedRundown, atIndex, event }: AddArgs): Required;
@@ -265,11 +268,11 @@ export function remove({ persistedRundown, eventId }: RemoveArgs): MutatingRetur
const atIndex = persistedRundown.findIndex((event) => event.id === eventId);
const newRundown = deleteAtIndex(atIndex, persistedRundown);
- return { newRundown };
+ return { newRundown, didMutate: atIndex !== -1 };
}
-export function removeAll(): { newRundown: OntimeRundown } {
- return { newRundown: [] };
+export function removeAll(): MutatingReturn {
+ return { newRundown: [], didMutate: true };
}
/**
@@ -304,7 +307,7 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required }>;
@@ -339,7 +343,7 @@ export function batchEdit({ persistedRundown, eventIds, patch }: BatchEditArgs):
newRundown.push(persistedRundown[i]);
}
}
- return { newRundown };
+ return { newRundown, didMutate: true };
}
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>;
@@ -357,14 +361,14 @@ export function reorder({ persistedRundown, eventId, from, to }: ReorderArgs): R
event.revision += 1;
}
}
- return { newRundown, newEvent: newRundown.at(from) };
+ return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true };
}
type ApplyDelayArgs = MutationParams<{ eventId: string }>;
export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn {
const newRundown = apply(eventId, persistedRundown);
- return { newRundown };
+ return { newRundown, didMutate: true };
}
type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
@@ -388,7 +392,7 @@ export function swap({ persistedRundown, fromId, toId }: SwapArgs): MutatingRetu
newRundown[indexB] = newB;
(newRundown[indexB] as OntimeEvent).revision += 1;
- return { newRundown };
+ return { newRundown, didMutate: true };
}
/**
diff --git a/apps/server/src/setup/loadDb.ts b/apps/server/src/setup/loadDb.ts
index 81112f15e..4876dbc9b 100644
--- a/apps/server/src/setup/loadDb.ts
+++ b/apps/server/src/setup/loadDb.ts
@@ -25,7 +25,13 @@ const populateDb = (directory: string, filename: string): string => {
if (!existsSync(dbPath)) {
try {
const dbDirectory = resolveDbDirectory;
- const newFileDirectory = join(dbDirectory, pathToStartDb.split('/').pop());
+ const startDbName = pathToStartDb.split('/').pop();
+
+ if (!startDbName) {
+ throw new Error('Invalid path to start database');
+ }
+
+ const newFileDirectory = join(dbDirectory, startDbName);
copyFileSync(pathToStartDb, newFileDirectory);
dbPath = newFileDirectory;
diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts
index 988b1a7b5..002d09c86 100644
--- a/apps/server/src/utils/__tests__/parser.test.ts
+++ b/apps/server/src/utils/__tests__/parser.test.ts
@@ -318,7 +318,6 @@ describe('test parser edge cases', () => {
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const parseResponse = await parseJson(testData);
- expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
expect(parseResponse?.rundown.length).toBe(0);
});
@@ -332,7 +331,7 @@ describe('test parser edge cases', () => {
// @ts-expect-error -- we know this is wrong, testing imports outside domain
await parseJson(testData);
- expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping');
+ expect(console.log).toHaveBeenCalledWith('ERROR: unable to parse settings, missing app or version');
});
});
@@ -564,6 +563,7 @@ describe('test views import', () => {
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
dangerColor: '#ED3333',
+ freezeEnd: false,
endMessage: '',
overrideStyles: false,
};
@@ -581,7 +581,7 @@ describe('test views import', () => {
},
} as DatabaseModel;
const parsed = parseViewSettings(testData);
- expect(parsed).toStrictEqual({});
+ expect(parsed).toStrictEqual(dbModel.viewSettings);
});
});
@@ -591,7 +591,7 @@ describe('test import of v2 datamodel', () => {
rundown: [
{ type: SupportedEvent.Block, title: 'block-title', id: 'block-id' },
{ type: SupportedEvent.Delay, duration: 0 },
- { type: SupportedEvent.Event, title: 'block-title', id: 'block-id' },
+ { type: SupportedEvent.Event, title: 'event-title', id: 'event-id' },
],
project: {
title: '',
diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts
index 71963aa5e..c9317a23c 100644
--- a/apps/server/src/utils/parser.ts
+++ b/apps/server/src/utils/parser.ts
@@ -273,15 +273,26 @@ export const parseJson = async (jsonData: Partial): Promise): OntimeRundown => {
- let newRundown: OntimeRundown = [];
- if ('rundown' in data) {
- console.log('Found rundown definition, importing...');
- const rundown = [];
- try {
- let eventIndex = 0;
- const ids = [];
- for (const event of data.rundown) {
- // double check unique ids
- if (ids.includes(event?.id)) {
- console.log('ERROR: ID collision on import, skipping');
- continue;
- }
-
- if (isOntimeEvent(event)) {
- eventIndex += 1;
- const parsedEvent = createEvent(event, eventIndex.toString());
- if (event != null) {
- rundown.push(parsedEvent);
- ids.push(parsedEvent.id);
- }
- } else if (isOntimeDelay(event)) {
- rundown.push({
- ...delayDef,
- duration: event.duration,
- id: event.id || generateId(),
- });
- } else if (isOntimeBlock(event)) {
- rundown.push({ ...blockDef, title: event.title, id: event.id || generateId() });
- } else {
- console.log('ERROR: unkown event type, skipping');
- }
- }
- } catch (error) {
- console.log(`Error ${error}`);
- }
- // write to db
- newRundown = rundown;
- console.log(`Uploaded file with ${newRundown.length} entries`);
+ if (!data.rundown) {
+ return [];
}
- return newRundown;
+
+ console.log('Found rundown, importing...');
+
+ const rundown: OntimeRundown = [];
+ let eventIndex = 0;
+ const ids: string[] = [];
+
+ for (const event of data.rundown) {
+ if (ids.includes(event.id)) {
+ console.log('ERROR: ID collision on import, skipping');
+ continue;
+ }
+
+ const id = event.id || generateId();
+ let newEvent: OntimeEvent | OntimeDelay | OntimeBlock | null;
+
+ if (isOntimeEvent(event)) {
+ newEvent = createEvent(event, eventIndex.toString());
+ // skip if event is invalid
+ if (newEvent == null) {
+ continue;
+ }
+ eventIndex += 1;
+ } else if (isOntimeDelay(event)) {
+ newEvent = { ...delayDef, duration: event.duration, id };
+ } else if (isOntimeBlock(event)) {
+ newEvent = { ...blockDef, title: event.title, id };
+ } else {
+ console.log('ERROR: unknown event type, skipping');
+ continue;
+ }
+
+ if (newEvent) {
+ rundown.push(newEvent);
+ ids.push(id);
+ }
+ }
+
+ console.log(`Uploaded rundown with ${rundown.length} entries`);
+ return rundown;
};
+
/**
* Parse event portion of an entry
- * @param {object} data - data object
- * @returns {object} - event object data
*/
export const parseProject = (data: Partial): ProjectData => {
- let newProjectData: Partial = {};
- // we are adding this here to aid transition, should be removed once enough time has past that users have fully migrated
- if ('project' in data) {
- console.log('Found project data, importing...');
- const project = data.project;
-
- // filter known properties and write to db
- newProjectData = {
- ...dbModel.project,
- title: project.title || dbModel.project.title,
- description: project.description || dbModel.project.description,
- publicUrl: project.publicUrl || dbModel.project.publicUrl,
- publicInfo: project.publicInfo || dbModel.project.publicInfo,
- backstageUrl: project.backstageUrl || dbModel.project.backstageUrl,
- backstageInfo: project.backstageInfo || dbModel.project.backstageInfo,
- };
+ if (!data.project) {
+ return { ...dbModel.project };
}
- return newProjectData as ProjectData;
+
+ console.log('Found project data, importing...');
+
+ return {
+ title: data.project.title ?? dbModel.project.title,
+ description: data.project.description ?? dbModel.project.description,
+ publicUrl: data.project.publicUrl ?? dbModel.project.publicUrl,
+ publicInfo: data.project.publicInfo ?? dbModel.project.publicInfo,
+ backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
+ backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
+ };
};
/**
* Parse settings portion of an entry
- * @param {object} data - data object
- * @returns {object} - event object data
*/
-export const parseSettings = (data): Settings => {
- let newSettings: Partial = {};
- if ('settings' in data) {
- console.log('Found settings definition, importing...');
- const s = data.settings;
-
- // skip if file definition is missing
- if (s?.app !== 'ontime' || s?.version == null) {
- console.log('ERROR: unknown app version, skipping');
- } else {
- const settings = {
- version: dbModel.settings.version,
- serverPort: s.serverPort ?? dbModel.settings.serverPort,
- editorKey: s.editorKey ?? null,
- operatorKey: s.operatorKey ?? null,
- timeFormat: s.timeFormat ?? '24',
- language: s.language ?? 'en',
- };
-
- // write to db
- newSettings = {
- ...dbModel.settings,
- ...settings,
- };
- }
+export const parseSettings = (data: Partial): Settings => {
+ if (!data.settings) {
+ return { ...dbModel.settings };
}
- return newSettings as Settings;
+
+ // skip if file definition is missing
+ if (data.settings?.app !== 'ontime' || data.settings?.version == null) {
+ throw new Error('ERROR: unable to parse settings, missing app or version');
+ }
+
+ console.log('Found settings, importing...');
+
+ return {
+ app: dbModel.settings.app,
+ version: dbModel.settings.version,
+ serverPort: data.settings.serverPort ?? dbModel.settings.serverPort,
+ editorKey: data.settings.editorKey ?? null,
+ operatorKey: data.settings.operatorKey ?? null,
+ timeFormat: data.settings.timeFormat ?? '24',
+ language: data.settings.language ?? 'en',
+ };
};
/**
- * Parse settings portion of an entry
- * @param {object} data - data object
- * @returns {object} - event object data
+ * Parse view settings portion of an entry
*/
export const parseViewSettings = (data: Partial): ViewSettings => {
- let newViews: Partial = {};
- if ('viewSettings' in data) {
- console.log('Found view definition, importing...');
- const v = data.viewSettings;
-
- const viewSettings = {
- overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
- normalColor: v.normalColor ?? dbModel.viewSettings.normalColor,
- warningColor: v.warningColor ?? dbModel.viewSettings.warningColor,
- dangerColor: v.dangerColor ?? dbModel.viewSettings.dangerColor,
- endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
- };
-
- newViews = { ...viewSettings };
+ if (!data.viewSettings) {
+ return { ...dbModel.viewSettings };
}
- return newViews as ViewSettings;
+
+ console.log('Found view settings, importing...');
+
+ return {
+ dangerColor: data.viewSettings.dangerColor ?? dbModel.viewSettings.dangerColor,
+ endMessage: data.viewSettings.endMessage ?? dbModel.viewSettings.endMessage,
+ freezeEnd: data.viewSettings.freezeEnd ?? dbModel.viewSettings.freezeEnd,
+ normalColor: data.viewSettings.normalColor ?? dbModel.viewSettings.normalColor,
+ overrideStyles: data.viewSettings.overrideStyles ?? dbModel.viewSettings.overrideStyles,
+ warningColor: data.viewSettings.warningColor ?? dbModel.viewSettings.warningColor,
+ };
};
/**
@@ -170,20 +156,20 @@ export function sanitiseOscSubscriptions(subscriptions?: OscSubscription[]): Osc
/**
* Parse osc portion of an entry
*/
-export const parseOsc = (data: { osc?: Partial }): OSCSettings => {
- if ('osc' in data) {
- console.log('Found OSC definition, importing...');
-
- const loadedConfig = data.osc || {};
- return {
- portIn: loadedConfig.portIn ?? dbModel.osc.portIn,
- portOut: loadedConfig.portOut ?? dbModel.osc.portOut,
- targetIP: loadedConfig.targetIP ?? dbModel.osc.targetIP,
- enabledIn: loadedConfig.enabledIn ?? dbModel.osc.enabledIn,
- enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
- subscriptions: sanitiseOscSubscriptions(loadedConfig.subscriptions),
- };
+export const parseOsc = (data: Partial): OSCSettings => {
+ if (!data.osc) {
+ return { ...dbModel.osc };
}
+ console.log('Found OSC settings, importing...');
+
+ return {
+ portIn: data.osc.portIn ?? dbModel.osc.portIn,
+ portOut: data.osc.portOut ?? dbModel.osc.portOut,
+ targetIP: data.osc.targetIP ?? dbModel.osc.targetIP,
+ enabledIn: data.osc.enabledIn ?? dbModel.osc.enabledIn,
+ enabledOut: data.osc.enabledOut ?? dbModel.osc.enabledOut,
+ subscriptions: sanitiseOscSubscriptions(data.osc.subscriptions),
+ };
};
/**
@@ -206,66 +192,70 @@ export function sanitiseHttpSubscriptions(subscriptions?: HttpSubscription[]): H
/**
* Parse Http portion of an entry
- * @param {object} data - data object
- * @param {boolean} enforce - whether to create a definition if one is missing
- * @returns {object} - event object data
*/
-export const parseHttp = (data: { http?: Partial }): HttpSettings => {
- if ('http' in data) {
- console.log('Found HTTP definition, importing...');
-
- // TODO: this can be improved by only merging known keys
- const loadedConfig = data?.http || {};
-
- return {
- enabledOut: loadedConfig.enabledOut ?? dbModel.http.enabledOut,
- subscriptions: sanitiseHttpSubscriptions(loadedConfig.subscriptions),
- };
+export const parseHttp = (data: Partial): HttpSettings => {
+ if (!data.http) {
+ return { ...dbModel.http };
}
+
+ console.log('Found HTTP settings, importing...');
+
+ return {
+ enabledOut: data.http.enabledOut ?? dbModel.http.enabledOut,
+ subscriptions: sanitiseHttpSubscriptions(data.http.subscriptions),
+ };
};
/**
* Parse URL preset portion of an entry
- * @param {object} data - data object
- * @returns {object} - event object data
*/
export const parseUrlPresets = (data: Partial): URLPreset[] => {
- const newPresets: URLPreset[] = [];
- if ('urlPresets' in data) {
- console.log('Found URL presets definition, importing...');
- try {
- for (const preset of data.urlPresets) {
- const newPreset = {
- enabled: preset.enabled ?? false,
- alias: preset.alias ?? '',
- pathAndParams: preset.pathAndParams ?? '',
- };
- newPresets.push(newPreset);
- }
- console.log(`Uploaded ${newPresets.length} preset(s)`);
- } catch (error) {
- console.log(`Error: ${error}`);
- }
+ if (!data.urlPresets) {
+ return [];
}
+
+ console.log('Found URL presets, importing...');
+
+ const newPresets: URLPreset[] = [];
+
+ for (const preset of data.urlPresets) {
+ const newPreset = {
+ enabled: preset.enabled ?? false,
+ alias: preset.alias ?? '',
+ pathAndParams: preset.pathAndParams ?? '',
+ };
+ newPresets.push(newPreset);
+ }
+
+ console.log(`Uploaded ${newPresets.length} preset(s)`);
+
return newPresets;
};
/**
* Parse customFields entry
- * @param {object} data - data object
- * @returns {object} - event object data
*/
export const parseCustomFields = (data: Partial): CustomFields => {
- let newCustomFields: CustomFields = { ...dbModel.customFields };
-
- if ('customFields' in data) {
- console.log('Found Custom Fields definition, importing...');
- try {
- //TODO: validate
- newCustomFields = { ...dbModel.customFields, ...data.customFields };
- } catch (error) {
- console.log(`Error: ${error}`);
- }
+ if (typeof data.customFields !== 'object') {
+ return { ...dbModel.customFields };
}
- return { ...newCustomFields };
+
+ console.log('Found Custom Fields, importing...');
+
+ const newCustomFields: CustomFields = {};
+
+ for (const fieldLabel in data.customFields) {
+ const field = data.customFields[fieldLabel];
+ if (!field.label || !field.type || !field.colour) {
+ console.log('ERROR: missing required field, skipping');
+ continue;
+ }
+ newCustomFields[field.label] = {
+ type: field.type,
+ colour: field.colour,
+ label: field.label,
+ };
+ }
+
+ return newCustomFields;
};
diff --git a/apps/server/src/utils/parserUtils.ts b/apps/server/src/utils/parserUtils.ts
index 8eb85a0d3..90a128083 100644
--- a/apps/server/src/utils/parserUtils.ts
+++ b/apps/server/src/utils/parserUtils.ts
@@ -15,10 +15,9 @@ export const makeString = (val: unknown, fallback = ''): string => {
/**
* @description Delete file from system
- * @param {string} file - reference to file
*/
-export const deleteFile = async (file) => {
- unlink(file, (error) => {
+export const deleteFile = async (filePath: string) => {
+ unlink(filePath, (error) => {
if (error) {
console.error('Could not delete file:', error);
}
@@ -67,11 +66,12 @@ export function mergeObject(a: T, b: Partial): T {
* @description Removes undefined
* @param {object} obj
*/
-export const removeUndefined = (obj: object) => {
+export const removeUndefined = >(obj: T): Partial => {
return Object.keys(obj).reduce((patched, key) => {
if (typeof obj[key] !== 'undefined') {
+ // @ts-expect-error -- not sure how to type this
patched[key] = obj[key];
}
return patched;
- }, {});
+ }, {} as Partial);
};
From 8a1474e8d6ac821629538bba17cc891ca500ea1c Mon Sep 17 00:00:00 2001
From: Alex Christoffer Rasmussen
Date: Fri, 5 Apr 2024 23:06:40 +0200
Subject: [PATCH 8/9] Improve error handling in server (#812)
* add functions to convert all errors to `ErrorResponse` type
* replace all `{ message: error.toString() }` with `toErrorResponse(error)`
* refactor: all controllers to to use getErrorMessage and add types to the Response
* 404 for nonexistent api routes
---
.../panel/sources-panel/SourcesPanel.tsx | 4 +-
.../custom-fields/customFields.controller.ts | 20 ++++----
apps/server/src/api-data/db/db.controller.ts | 34 ++++++++-----
.../src/api-data/http/http.controller.ts | 4 +-
apps/server/src/api-data/index.ts | 5 ++
.../server/src/api-data/osc/osc.controller.ts | 4 +-
.../api-data/project/project.controller.ts | 4 +-
.../api-data/rundown/rundown.controller.ts | 25 ++++++---
.../api-data/settings/settings.controller.ts | 5 +-
.../src/api-data/sheets/sheets.controller.ts | 51 ++++++++++++++-----
.../url-presets/urlPresets.controller.ts | 4 +-
.../view-settings/viewSettings.controller.ts | 4 +-
.../src/api-integration/integration.router.ts | 10 ++--
packages/utils/index.ts | 2 +-
packages/utils/src/generic/generic.ts | 2 +-
15 files changed, 120 insertions(+), 58 deletions(-)
diff --git a/apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.tsx b/apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.tsx
index 7bb896df4..fb804052b 100644
--- a/apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.tsx
+++ b/apps/client/src/features/app-settings/panel/sources-panel/SourcesPanel.tsx
@@ -2,7 +2,7 @@ import { ChangeEvent, useRef, useState } from 'react';
import { Button, Input } from '@chakra-ui/react';
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
-import { ImportMap, unpackError } from 'ontime-utils';
+import { getErrorMessage, ImportMap } from 'ontime-utils';
import {
getWorksheetNames as getWorksheetNamesExcel,
@@ -59,7 +59,7 @@ export default function SourcesPanel() {
setImportFlow('excel');
setHasFile('done');
} catch (error) {
- const errorMessage = unpackError(error);
+ const errorMessage = getErrorMessage(error);
setError(`Error uploading file: ${errorMessage}`);
setWorksheets(null);
setHasFile('none');
diff --git a/apps/server/src/api-data/custom-fields/customFields.controller.ts b/apps/server/src/api-data/custom-fields/customFields.controller.ts
index 42c428c08..71c55f5f8 100644
--- a/apps/server/src/api-data/custom-fields/customFields.controller.ts
+++ b/apps/server/src/api-data/custom-fields/customFields.controller.ts
@@ -1,7 +1,8 @@
-import { CustomField, CustomFields } from 'ontime-types';
+import { CustomField, CustomFields, ErrorResponse } from 'ontime-types';
import type { Request, Response } from 'express';
+import { getErrorMessage } from 'ontime-utils';
import {
createCustomField,
editCustomField,
@@ -14,36 +15,37 @@ export async function getCustomFields(_req: Request, res: Response
res.json(customFields);
}
-// Expects { label: |