Typescript null (#869)

* refactor: improve null checking

* fix: return result of mutation
This commit is contained in:
Carlos Valente
2024-04-05 12:07:26 +02:00
committed by GitHub
parent 9f207dd39a
commit d432f1e3ff
12 changed files with 260 additions and 229 deletions
+7 -9
View File
@@ -116,16 +116,14 @@ export class SocketServer implements IAdapter {
// Protocol specific stuff handled above // Protocol specific stuff handled above
try { try {
const reply = dispatchFromAdapter( const reply = dispatchFromAdapter(type, { payload }, 'ws');
type,
{
payload,
},
'ws',
);
if (reply) { if (reply) {
const { payload } = reply; ws.send(
ws.send(type, payload); JSON.stringify({
type: 'ontime-change',
payload: reply.payload,
}),
);
} }
} catch (error) { } catch (error) {
logger.error(LogOrigin.Rx, `WS IN: ${error}`); logger.error(LogOrigin.Rx, `WS IN: ${error}`);
@@ -3,10 +3,13 @@
* Google Sheets * Google Sheets
*/ */
import { CustomFields, OntimeRundown } from 'ontime-types';
import { ImportMap } from 'ontime-utils';
import { extname } from 'path'; import { extname } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { ImportMap } from 'ontime-utils';
import xlsx from 'node-xlsx'; import xlsx from 'node-xlsx';
import { parseExcel } from '../../utils/parser.js'; import { parseExcel } from '../../utils/parser.js';
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js'; import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
import { deleteFile } from '../../utils/parserUtils.js'; import { deleteFile } from '../../utils/parserUtils.js';
@@ -29,7 +32,7 @@ export function listWorksheets() {
return excelData.map((value) => value.name); 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; const data = excelData.find(({ name }) => name.toLowerCase() === options.worksheet.toLowerCase())?.data;
if (!data) { if (!data) {
@@ -39,15 +42,14 @@ export function generateRundownPreview(options: ImportMap) {
const dataFromExcel = parseExcel(data, options); const dataFromExcel = parseExcel(data, options);
// we run the parsed data through an extra step to ensure the objects shape // we run the parsed data through an extra step to ensure the objects shape
const result = { rundown: [], customFields: {} }; const rundown = parseRundown(dataFromExcel);
result.rundown = parseRundown(dataFromExcel); if (rundown.length === 0) {
if (result.rundown.length < 1) {
throw new Error(`Could not find data to import in the worksheet: ${options.worksheet}`); 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 = []; excelData = [];
return result; return { rundown, customFields };
} }
@@ -64,6 +64,7 @@ export class SimpleTimer {
public update(timeNow: number): SimpleTimerState { public update(timeNow: number): SimpleTimerState {
if (this.state.playback === SimplePlayback.Start) { if (this.state.playback === SimplePlayback.Start) {
// we know startedAt is not null since we are in play mode
const elapsed = timeNow - this.startedAt; const elapsed = timeNow - this.startedAt;
if (this.state.direction === SimpleDirection.CountDown) { if (this.state.direction === SimpleDirection.CountDown) {
this.state.current = this.state.duration - elapsed; this.state.current = this.state.duration - elapsed;
+1 -1
View File
@@ -64,7 +64,7 @@ export class RestoreService {
private readonly filePath: MaybeString; private readonly filePath: MaybeString;
private readonly file: JSONFile<RestorePoint | null>; private readonly file: JSONFile<RestorePoint | null>;
private failedCreateAttempts: number; private failedCreateAttempts: number;
private savedState: RestorePoint; private savedState: RestorePoint | null;
constructor(filePath: string) { constructor(filePath: string) {
this.filePath = filePath; this.filePath = filePath;
@@ -75,7 +75,7 @@ export async function getProjectFiles(): Promise<ProjectFile[]> {
const allFiles = await getFilesFromFolder(resolveProjectsDirectory); const allFiles = await getFilesFromFolder(resolveProjectsDirectory);
const filteredFiles = filterProjectFiles(allFiles); const filteredFiles = filterProjectFiles(allFiles);
const projectFiles = []; const projectFiles: ProjectFile[] = [];
for (const file of filteredFiles) { for (const file of filteredFiles) {
const filePath = join(resolveProjectsDirectory, file); const filePath = join(resolveProjectsDirectory, file);
const stats = await stat(filePath); const stats = await stat(filePath);
@@ -22,20 +22,32 @@ import { runtimeService } from '../runtime-service/RuntimeService.js';
import * as cache from './rundownCache.js'; import * as cache from './rundownCache.js';
import { getPlayableEvents } from './rundownUtils.js'; import { getPlayableEvents } from './rundownUtils.js';
function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) { type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
// we discard any UI provided events and add our own
type CompleteEntry<T> = T extends Partial<OntimeEvent>
? OntimeEvent
: T extends Partial<OntimeDelay>
? OntimeDelay
: T extends Partial<OntimeBlock>
? OntimeBlock
: never;
function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>>(
eventData: T,
): CompleteEntry<T> {
// we discard any UI provided IDs and add our own
const id = cache.getUniqueId(); const id = cache.getUniqueId();
if (isOntimeEvent(eventData)) { if (isOntimeEvent(eventData)) {
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as OntimeEvent; return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as CompleteEntry<T>;
} }
if (isOntimeDelay(eventData)) { if (isOntimeDelay(eventData)) {
return { ...delayDef, duration: eventData.duration ?? 0, id } as OntimeDelay; return { ...delayDef, duration: eventData.duration ?? 0, id } as CompleteEntry<T>;
} }
if (isOntimeBlock(eventData)) { if (isOntimeBlock(eventData)) {
return { ...blockDef, title: eventData?.title ?? '', id } as OntimeBlock; return { ...blockDef, title: eventData?.title ?? '', id } as CompleteEntry<T>;
} }
throw new Error('Invalid event type'); throw new Error('Invalid event type');
@@ -46,9 +58,7 @@ function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> |
* @param {object} eventData * @param {object} eventData
* @return {OntimeRundownEntry} * @return {OntimeRundownEntry}
*/ */
export async function addEvent( export async function addEvent(eventData: PatchWithId & { after?: string }): Promise<OntimeRundownEntry> {
eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>,
): Promise<OntimeRundownEntry> {
// if the user didnt provide an index, we add the event to start // if the user didnt provide an index, we add the event to start
let atIndex = 0; let atIndex = 0;
if (eventData?.after !== undefined) { if (eventData?.after !== undefined) {
@@ -62,15 +72,16 @@ export async function addEvent(
// generate a fully formed event from the patch // generate a fully formed event from the patch
const eventToAdd = generateEvent(eventData); const eventToAdd = generateEvent(eventData);
// modify rundown // modify rundown
const scopedMutation = cache.mutateCache(cache.add); 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 // notify runtime that rundown has changed
updateRuntimeOnChange(); updateRuntimeOnChange();
// notify timer and external services of change // notify timer and external services of change
notifyChanges({ timer: [newEvent.id], external: true }); notifyChanges({ timer: [eventData.id], external: true });
return newEvent; return newEvent;
} }
@@ -81,7 +92,11 @@ export async function addEvent(
*/ */
export async function deleteEvent(eventId: string) { export async function deleteEvent(eventId: string) {
const scopedMutation = cache.mutateCache(cache.remove); const scopedMutation = cache.mutateCache(cache.remove);
await scopedMutation({ eventId }); const { didMutate } = await scopedMutation({ eventId });
if (didMutate === false) {
return;
}
// notify runtime that rundown has changed // notify runtime that rundown has changed
updateRuntimeOnChange(); updateRuntimeOnChange();
@@ -108,14 +123,18 @@ export async function deleteAllEvents() {
* Apply patch to an element in rundown * Apply patch to an element in rundown
* @param patch * @param patch
*/ */
export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) { export async function editEvent(patch: PatchWithId) {
if (isOntimeEvent(patch) && patch?.cue === '') { if (isOntimeEvent(patch) && patch?.cue === '') {
throw new Error('Cue value invalid'); throw new Error('Cue value invalid');
} }
const scopedMutation = cache.mutateCache(cache.edit); const scopedMutation = cache.mutateCache(cache.edit);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know patch has an id const { newEvent, didMutate } = await scopedMutation({ patch, eventId: patch.id });
const { newEvent } = await scopedMutation({ patch, eventId: patch.id! });
// short circuit if nothing changed
if (didMutate === false) {
return newEvent;
}
// notify runtime that rundown has changed // notify runtime that rundown has changed
updateRuntimeOnChange(); updateRuntimeOnChange();
@@ -83,7 +83,7 @@ export function generate(
let accumulatedDelay = 0; let accumulatedDelay = 0;
let daySpan = 0; let daySpan = 0;
let previousEnd: number; let previousEnd: MaybeNumber = null;
for (let i = 0; i < initialRundown.length; i++) { for (let i = 0; i < initialRundown.length; i++) {
const currentEvent = initialRundown[i]; const currentEvent = initialRundown[i];
@@ -106,7 +106,7 @@ export function generate(
lastEnd = updatedEvent.timeEnd; lastEnd = updatedEvent.timeEnd;
// check if we go over midnight, account for eventual gaps // 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; const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
if (gapOverMidnight || durationOverMidnight) { if (gapOverMidnight || durationOverMidnight) {
daySpan++; daySpan++;
@@ -135,7 +135,9 @@ export function generate(
isStale = false; isStale = false;
totalDelay = accumulatedDelay; 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 }; return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
} }
@@ -210,6 +212,7 @@ type MutationParams<T> = T & CommonParams;
type MutatingReturn = { type MutatingReturn = {
newRundown: OntimeRundown; newRundown: OntimeRundown;
newEvent?: OntimeRundownEntry; newEvent?: OntimeRundownEntry;
didMutate: boolean;
}; };
type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingReturn; type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingReturn;
@@ -227,7 +230,7 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
*/ */
isStale = true; isStale = true;
const { newEvent, newRundown } = mutation({ ...params, persistedRundown }); const { newEvent, newRundown, didMutate } = mutation({ ...params, persistedRundown });
revision = revision + 1; revision = revision + 1;
persistedRundown = newRundown; persistedRundown = newRundown;
@@ -244,7 +247,7 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
DataProvider.setRundown(persistedRundown); DataProvider.setRundown(persistedRundown);
}); });
return { newEvent }; return { newEvent, newRundown, didMutate };
} }
return scopedMutation; return scopedMutation;
@@ -256,7 +259,7 @@ export function add({ persistedRundown, atIndex, event }: AddArgs): Required<Mut
const newEvent: OntimeRundownEntry = { ...event }; const newEvent: OntimeRundownEntry = { ...event };
const newRundown = insertAtIndex(atIndex, newEvent, persistedRundown); const newRundown = insertAtIndex(atIndex, newEvent, persistedRundown);
return { newRundown, newEvent }; return { newRundown, newEvent, didMutate: true };
} }
type RemoveArgs = MutationParams<{ eventId: string }>; type RemoveArgs = MutationParams<{ eventId: string }>;
@@ -265,11 +268,11 @@ export function remove({ persistedRundown, eventId }: RemoveArgs): MutatingRetur
const atIndex = persistedRundown.findIndex((event) => event.id === eventId); const atIndex = persistedRundown.findIndex((event) => event.id === eventId);
const newRundown = deleteAtIndex(atIndex, persistedRundown); const newRundown = deleteAtIndex(atIndex, persistedRundown);
return { newRundown }; return { newRundown, didMutate: atIndex !== -1 };
} }
export function removeAll(): { newRundown: OntimeRundown } { export function removeAll(): MutatingReturn {
return { newRundown: [] }; return { newRundown: [], didMutate: true };
} }
/** /**
@@ -304,7 +307,7 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<M
const eventInMemory = persistedRundown[indexAt]; const eventInMemory = persistedRundown[indexAt];
if (!hasChanges(eventInMemory, patch)) { if (!hasChanges(eventInMemory, patch)) {
isStale = false; isStale = false;
return; return { newRundown: persistedRundown, newEvent: eventInMemory, didMutate: false };
} }
const newEvent = makeEvent(eventInMemory, patch); const newEvent = makeEvent(eventInMemory, patch);
@@ -312,6 +315,7 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<M
const newRundown = [...persistedRundown]; const newRundown = [...persistedRundown];
newRundown[indexAt] = newEvent; newRundown[indexAt] = newEvent;
// check whether the data warrants recalculation of cache
const makeStale = isDataStale(patch); const makeStale = isDataStale(patch);
if (!makeStale) { if (!makeStale) {
@@ -319,7 +323,7 @@ export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<M
} }
isStale = makeStale; isStale = makeStale;
return { newRundown, newEvent }; return { newRundown, newEvent, didMutate: true };
} }
type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>; type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>;
@@ -339,7 +343,7 @@ export function batchEdit({ persistedRundown, eventIds, patch }: BatchEditArgs):
newRundown.push(persistedRundown[i]); newRundown.push(persistedRundown[i]);
} }
} }
return { newRundown }; return { newRundown, didMutate: true };
} }
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>; 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; event.revision += 1;
} }
} }
return { newRundown, newEvent: newRundown.at(from) }; return { newRundown, newEvent: newRundown.at(from) as OntimeRundownEntry, didMutate: true };
} }
type ApplyDelayArgs = MutationParams<{ eventId: string }>; type ApplyDelayArgs = MutationParams<{ eventId: string }>;
export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn { export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn {
const newRundown = apply(eventId, persistedRundown); const newRundown = apply(eventId, persistedRundown);
return { newRundown }; return { newRundown, didMutate: true };
} }
type SwapArgs = MutationParams<{ fromId: string; toId: string }>; type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
@@ -388,7 +392,7 @@ export function swap({ persistedRundown, fromId, toId }: SwapArgs): MutatingRetu
newRundown[indexB] = newB; newRundown[indexB] = newB;
(newRundown[indexB] as OntimeEvent).revision += 1; (newRundown[indexB] as OntimeEvent).revision += 1;
return { newRundown }; return { newRundown, didMutate: true };
} }
/** /**
+7 -1
View File
@@ -25,7 +25,13 @@ const populateDb = (directory: string, filename: string): string => {
if (!existsSync(dbPath)) { if (!existsSync(dbPath)) {
try { try {
const dbDirectory = resolveDbDirectory; 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); copyFileSync(pathToStartDb, newFileDirectory);
dbPath = newFileDirectory; dbPath = newFileDirectory;
@@ -318,7 +318,6 @@ describe('test parser edge cases', () => {
// @ts-expect-error -- we know this is wrong, testing imports outside domain // @ts-expect-error -- we know this is wrong, testing imports outside domain
const parseResponse = await parseJson(testData); const parseResponse = await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
expect(parseResponse?.rundown.length).toBe(0); 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 // @ts-expect-error -- we know this is wrong, testing imports outside domain
await parseJson(testData); await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping'); expect(console.log).toHaveBeenCalledWith('ERROR: unable to parse settings, missing app or version');
}); });
}); });
@@ -564,6 +563,7 @@ describe('test views import', () => {
normalColor: '#ffffffcc', normalColor: '#ffffffcc',
warningColor: '#FFAB33', warningColor: '#FFAB33',
dangerColor: '#ED3333', dangerColor: '#ED3333',
freezeEnd: false,
endMessage: '', endMessage: '',
overrideStyles: false, overrideStyles: false,
}; };
@@ -581,7 +581,7 @@ describe('test views import', () => {
}, },
} as DatabaseModel; } as DatabaseModel;
const parsed = parseViewSettings(testData); const parsed = parseViewSettings(testData);
expect(parsed).toStrictEqual({}); expect(parsed).toStrictEqual(dbModel.viewSettings);
}); });
}); });
@@ -591,7 +591,7 @@ describe('test import of v2 datamodel', () => {
rundown: [ rundown: [
{ type: SupportedEvent.Block, title: 'block-title', id: 'block-id' }, { type: SupportedEvent.Block, title: 'block-title', id: 'block-id' },
{ type: SupportedEvent.Delay, duration: 0 }, { type: SupportedEvent.Delay, duration: 0 },
{ type: SupportedEvent.Event, title: 'block-title', id: 'block-id' }, { type: SupportedEvent.Event, title: 'event-title', id: 'event-id' },
], ],
project: { project: {
title: '', title: '',
+16 -5
View File
@@ -273,15 +273,26 @@ export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<Datab
return null; return null;
} }
let settings;
// check settings first to make sure we can parse it
try {
settings = parseSettings(jsonData);
} catch (error) {
// if we cant parse, return an empty project
console.log('ERROR: unable to parse settings, missing app or version');
return dbModel;
}
const returnData: DatabaseModel = { const returnData: DatabaseModel = {
rundown: parseRundown(jsonData), rundown: parseRundown(jsonData),
project: parseProject(jsonData) ?? dbModel.project, project: parseProject(jsonData),
settings: parseSettings(jsonData) ?? dbModel.settings, settings,
viewSettings: parseViewSettings(jsonData) ?? dbModel.viewSettings, viewSettings: parseViewSettings(jsonData),
urlPresets: parseUrlPresets(jsonData), urlPresets: parseUrlPresets(jsonData),
customFields: parseCustomFields(jsonData), customFields: parseCustomFields(jsonData),
osc: parseOsc(jsonData) ?? dbModel.osc, osc: parseOsc(jsonData),
http: parseHttp(jsonData) ?? dbModel.http, http: parseHttp(jsonData),
}; };
return returnData; return returnData;
+156 -166
View File
@@ -15,6 +15,9 @@ import {
isOntimeCycle, isOntimeCycle,
HttpSubscription, HttpSubscription,
URLPreset, URLPreset,
OntimeEvent,
OntimeBlock,
OntimeDelay,
} from 'ontime-types'; } from 'ontime-types';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js'; import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
@@ -22,135 +25,118 @@ import { dbModel } from '../models/dataModel.js';
import { createEvent } from './parser.js'; import { createEvent } from './parser.js';
/** /**
* Parse events array of an entry * Parse rundown array of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseRundown = (data: Partial<DatabaseModel>): OntimeRundown => { export const parseRundown = (data: Partial<DatabaseModel>): OntimeRundown => {
let newRundown: OntimeRundown = []; if (!data.rundown) {
if ('rundown' in data) { return [];
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`);
} }
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 * Parse event portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseProject = (data: Partial<DatabaseModel>): ProjectData => { export const parseProject = (data: Partial<DatabaseModel>): ProjectData => {
let newProjectData: Partial<ProjectData> = {}; if (!data.project) {
// we are adding this here to aid transition, should be removed once enough time has past that users have fully migrated return { ...dbModel.project };
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,
};
} }
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 * Parse settings portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseSettings = (data): Settings => { export const parseSettings = (data: Partial<DatabaseModel>): Settings => {
let newSettings: Partial<Settings> = {}; if (!data.settings) {
if ('settings' in data) { return { ...dbModel.settings };
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,
};
}
} }
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 * Parse view settings portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseViewSettings = (data: Partial<DatabaseModel>): ViewSettings => { export const parseViewSettings = (data: Partial<DatabaseModel>): ViewSettings => {
let newViews: Partial<ViewSettings> = {}; if (!data.viewSettings) {
if ('viewSettings' in data) { return { ...dbModel.viewSettings };
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 };
} }
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 * Parse osc portion of an entry
*/ */
export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => { export const parseOsc = (data: Partial<DatabaseModel>): OSCSettings => {
if ('osc' in data) { if (!data.osc) {
console.log('Found OSC definition, importing...'); return { ...dbModel.osc };
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),
};
} }
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 * 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> }): HttpSettings => { export const parseHttp = (data: Partial<DatabaseModel>): HttpSettings => {
if ('http' in data) { if (!data.http) {
console.log('Found HTTP definition, importing...'); return { ...dbModel.http };
// 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),
};
} }
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 * Parse URL preset portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseUrlPresets = (data: Partial<DatabaseModel>): URLPreset[] => { export const parseUrlPresets = (data: Partial<DatabaseModel>): URLPreset[] => {
const newPresets: URLPreset[] = []; if (!data.urlPresets) {
if ('urlPresets' in data) { return [];
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}`);
}
} }
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; return newPresets;
}; };
/** /**
* Parse customFields entry * Parse customFields entry
* @param {object} data - data object
* @returns {object} - event object data
*/ */
export const parseCustomFields = (data: Partial<DatabaseModel>): CustomFields => { export const parseCustomFields = (data: Partial<DatabaseModel>): CustomFields => {
let newCustomFields: CustomFields = { ...dbModel.customFields }; if (typeof data.customFields !== 'object') {
return { ...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}`);
}
} }
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;
}; };
+5 -5
View File
@@ -15,10 +15,9 @@ export const makeString = (val: unknown, fallback = ''): string => {
/** /**
* @description Delete file from system * @description Delete file from system
* @param {string} file - reference to file
*/ */
export const deleteFile = async (file) => { export const deleteFile = async (filePath: string) => {
unlink(file, (error) => { unlink(filePath, (error) => {
if (error) { if (error) {
console.error('Could not delete file:', error); console.error('Could not delete file:', error);
} }
@@ -67,11 +66,12 @@ export function mergeObject<T extends object>(a: T, b: Partial<T>): T {
* @description Removes undefined * @description Removes undefined
* @param {object} obj * @param {object} obj
*/ */
export const removeUndefined = (obj: object) => { export const removeUndefined = <T extends Record<string, unknown>>(obj: T): Partial<T> => {
return Object.keys(obj).reduce((patched, key) => { return Object.keys(obj).reduce((patched, key) => {
if (typeof obj[key] !== 'undefined') { if (typeof obj[key] !== 'undefined') {
// @ts-expect-error -- not sure how to type this
patched[key] = obj[key]; patched[key] = obj[key];
} }
return patched; return patched;
}, {}); }, {} as Partial<T>);
}; };