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] 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); };