diff --git a/apps/client/src/common/models/Info.ts b/apps/client/src/common/models/Info.ts index d05acdd2b..6d6dd09a3 100644 --- a/apps/client/src/common/models/Info.ts +++ b/apps/client/src/common/models/Info.ts @@ -2,7 +2,7 @@ import { GetInfo } from 'ontime-types'; export const ontimePlaceholderInfo: GetInfo = { networkInterfaces: [], - version: '2.0.0', + version: '4.0.0', serverPort: 4001, publicDir: '', }; diff --git a/apps/client/src/common/models/OntimeSettings.ts b/apps/client/src/common/models/OntimeSettings.ts index bb26409da..662a935c6 100644 --- a/apps/client/src/common/models/OntimeSettings.ts +++ b/apps/client/src/common/models/OntimeSettings.ts @@ -1,8 +1,7 @@ import { Settings } from 'ontime-types'; export const ontimePlaceholderSettings: Settings = { - app: 'ontime', - version: '2.0.0', + version: '4.0.0', serverPort: 4001, editorKey: null, operatorKey: null, diff --git a/apps/electron/src/main.js b/apps/electron/src/main.js index f200ac7e3..c6dffd907 100644 --- a/apps/electron/src/main.js +++ b/apps/electron/src/main.js @@ -49,9 +49,9 @@ async function startBackend() { const ontimeServer = require(nodePath); const { initAssets, startServer, startIntegrations } = ontimeServer; - await initAssets(); + await initAssets(escalateError); - const result = await startServer(escalateError); + const result = await startServer(); loaded = result.message; await startIntegrations(); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 423e782ac..954ab6c32 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -140,8 +140,11 @@ const checkStart = (currentState: OntimeStartOrder) => { } }; -export const initAssets = async () => { +export const initAssets = async (escalateErrorFn?: (error: string, unrecoverable: boolean) => void) => { checkStart(OntimeStartOrder.InitAssets); + // initialise logging service, escalateErrorFn only exists in electron + logger.init(escalateErrorFn); + await clearUploadfolder(); populateStyles(); await populateDemo(); @@ -152,12 +155,8 @@ export const initAssets = async () => { /** * Starts servers */ -export const startServer = async ( - escalateErrorFn?: (error: string, unrecoverable: boolean) => void, -): Promise<{ message: string; serverPort: number }> => { +export const startServer = async (): Promise<{ message: string; serverPort: number }> => { checkStart(OntimeStartOrder.InitServer); - // initialise logging service, escalateErrorFn only exists in electron - logger.init(escalateErrorFn); const settings = getDataProvider().getSettings(); const { serverPort: desiredPort } = settings; @@ -208,7 +207,6 @@ export const startServer = async ( // load restore point if it exists const maybeRestorePoint = await restoreService.load(); - // TODO: pass event store to rundownservice runtimeService.init(maybeRestorePoint); const nif = getNetworkInterfaces(); diff --git a/apps/server/src/classes/data-provider/DataProvider.ts b/apps/server/src/classes/data-provider/DataProvider.ts index 8ddb45695..4793a123d 100644 --- a/apps/server/src/classes/data-provider/DataProvider.ts +++ b/apps/server/src/classes/data-provider/DataProvider.ts @@ -23,6 +23,9 @@ type ReadonlyPromise = Promise>; let db = {} as Low; +/** + * Initialises the JSON adapter to persist data to a file + */ export async function initPersistence(filePath: string, fallbackData: DatabaseModel) { // eslint-disable-next-line no-unused-labels -- dev code path DEV: shouldCrashDev(!isPath(filePath), 'initPersistence should be called with a path'); diff --git a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts index c9c5494eb..c8bda5b23 100644 --- a/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts +++ b/apps/server/src/classes/data-provider/__tests__/DataProvider.utils.test.ts @@ -67,7 +67,6 @@ describe('safeMerge', () => { } as Settings, }); expect(mergedData.settings).toStrictEqual({ - app: 'ontime', version: 'new', serverPort: 3000, operatorKey: null, diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts index 8b16c8b04..c7157cb2f 100644 --- a/apps/server/src/models/dataModel.ts +++ b/apps/server/src/models/dataModel.ts @@ -23,7 +23,6 @@ export const dbModel: DatabaseModel = { projectLogo: null, }, settings: { - app: 'ontime', version: ONTIME_VERSION, serverPort: 4001, editorKey: null, diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index de0527f1b..28b991571 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -495,7 +495,6 @@ export const demoDb: DatabaseModel = { projectLogo: null, }, settings: { - app: 'ontime', version: '-', serverPort: 4001, editorKey: null, diff --git a/apps/server/src/services/project-service/ProjectService.ts b/apps/server/src/services/project-service/ProjectService.ts index dfc56281c..f80228e42 100644 --- a/apps/server/src/services/project-service/ProjectService.ts +++ b/apps/server/src/services/project-service/ProjectService.ts @@ -31,6 +31,7 @@ import { setLastLoadedProject, } from '../app-state-service/AppStateService.js'; import { runtimeService } from '../runtime-service/RuntimeService.js'; +import { getFirstRundown } from '../rundown-service/rundownUtils.js'; import { copyCorruptFile, @@ -40,7 +41,6 @@ import { moveCorruptFile, parseJsonFile, } from './projectServiceUtils.js'; -import { getFirstRundown } from '../rundown-service/rundownUtils.js'; type ProjectState = | { @@ -70,26 +70,50 @@ function init() { export async function getCurrentProject(): Promise<{ filename: string; pathToFile: string }> { if (currentProjectState.status === 'PENDING') { - const lastLoadedProject = await initialiseProject(); - currentProjectState = { - status: 'INITIALIZED', - currentProjectName: lastLoadedProject, - }; + await initialiseProject(); } - const pathToFile = getPathToProject(currentProjectState.currentProjectName); + // we know the project is loaded since we force initialisation above + const pathToFile = getPathToProject(currentProjectState.currentProjectName as string); - return { filename: currentProjectState.currentProjectName, pathToFile }; + return { filename: currentProjectState.currentProjectName as string, pathToFile }; +} + +/** + * Private function loads a project file and handles necessary side effects + */ +async function loadProject(projectData: DatabaseModel, projectName: string) { + // we need to make sure the file name is unique in the projects directory + const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, projectName); + + // change LowDB to point to new file + await initPersistence(getPathToProject(pathToNewFile), projectData); + logger.info(LogOrigin.Server, `Loaded project ${projectName}`); + + // stop the runtime service + runtimeService.stop(); + + // load the first rundown in the project + const firstRundown = getFirstRundown(projectData.rundowns); + await initRundown(firstRundown, projectData.customFields); + + // persist the project selection + const newName = getFileNameFromPath(pathToNewFile); + await setLastLoadedProject(newName); + + // update the service state + currentProjectState = { + status: 'INITIALIZED', + currentProjectName: newName, + }; + + return newName; } /** * Loads the demo project */ export async function loadDemoProject(): Promise { - const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.demoProject); - await initPersistence(getPathToProject(pathToNewFile), demoDb); - const newName = getFileNameFromPath(pathToNewFile); - await setLastLoadedProject(newName); - return newName; + return loadProject(demoDb, config.demoProject); } /** @@ -97,11 +121,7 @@ export async function loadDemoProject(): Promise { * to be composed in the loading functions */ async function loadNewProject(): Promise { - const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.newProject); - await initPersistence(getPathToProject(pathToNewFile), dbModel); - const newName = getFileNameFromPath(pathToNewFile); - await setLastLoadedProject(newName); - return newName; + return loadProject(dbModel, config.newProject); } /** @@ -129,46 +149,32 @@ export async function initialiseProject(): Promise { // check what was loaded before const previousProject = await getLastLoadedProject(); + // in normal circumstances we dont have a previous project if it is the first app start + // in which case we want to load a demo project if (!previousProject) { return loadDemoProject(); } - - // try and load the previous project - const filePath = doesProjectExist(previousProject); - if (filePath === null) { - logger.warning(LogOrigin.Server, `Previous project file ${previousProject} not found`); - return loadNewProject(); - } - try { - const fileData = await parseJsonFile(filePath); - const result = parseDatabaseModel(fileData); - let parsedFileName = previousProject; - let parsedFilePath = filePath; - - if (result.errors.length > 0) { - logger.warning(LogOrigin.Server, 'Project loaded with errors'); - parsedFileName = await handleCorruptedFile(filePath, previousProject); - parsedFilePath = getPathToProject(parsedFileName); - } - - await initPersistence(parsedFilePath, result.data); - await setLastLoadedProject(parsedFileName); - return parsedFileName; + const projectName = await loadProjectFile(previousProject); + return projectName; } catch (error) { + // if we are here, most likely the json parsing failed and the file is corrupt logger.warning(LogOrigin.Server, `Unable to load previous project ${previousProject}: ${getErrorMessage(error)}`); - await moveCorruptFile(filePath, previousProject).catch((_) => { + try { + const pathToFile = getPathToProject(previousProject); + await moveCorruptFile(pathToFile, previousProject); + } catch (_) { /* while we have to catch the error, we dont need to handle it */ - }); - - return loadNewProject(); + } } + + return loadNewProject(); } /** * Loads a data from a file into the runtime */ -export async function loadProjectFile(name: string) { +export async function loadProjectFile(name: string): Promise { const filePath = doesProjectExist(name); if (filePath === null) { throw new Error('Project file not found'); @@ -178,31 +184,14 @@ export async function loadProjectFile(name: string) { const fileData = await parseJsonFile(filePath); const result = parseDatabaseModel(fileData); let parsedFileName = name; - let parsedFilePath = filePath; if (result.errors.length > 0) { logger.warning(LogOrigin.Server, 'Project loaded with errors'); parsedFileName = await handleCorruptedFile(filePath, name); - parsedFilePath = getPathToProject(parsedFileName); } - // change LowDB to point to new file - await initPersistence(parsedFilePath, result.data); - logger.info(LogOrigin.Server, `Loaded project ${parsedFileName}`); - - // persist the project selection - await setLastLoadedProject(parsedFileName); - - // since load happens at runtime, we need to update the services that depend on the data - - // apply data model - runtimeService.stop(); - - const { rundowns, customFields } = result.data; - - // apply the rundown - const firstRundown = getFirstRundown(rundowns); - await initRundown(firstRundown, customFields); + const projectName = await loadProject(result.data, parsedFileName); + return projectName; } /** @@ -239,7 +228,7 @@ export async function duplicateProjectFile(originalFile: string, newFilename: st /** * Renames an existing project file */ -export async function renameProjectFile(originalFile: string, newFilename: string) { +export async function renameProjectFile(originalFile: string, newFilename: string): Promise { const projectFilePath = doesProjectExist(originalFile); if (projectFilePath === null) { throw new Error('Project file not found'); @@ -257,50 +246,23 @@ export async function renameProjectFile(originalFile: string, newFilename: strin const isLoaded = await isLastLoadedProject(originalFile); if (isLoaded) { const fileData = await parseJsonFile(pathToRenamed); - const result = parseDatabaseModel(fileData); + const projectData = parseDatabaseModel(fileData); - // change LowDB to point to new file - await initPersistence(pathToRenamed, result.data); - logger.info(LogOrigin.Server, `Loaded project ${newFilename}`); - - // persist the project selection - await setLastLoadedProject(newFilename); - - // apply data model - runtimeService.stop(); - - const { rundowns, customFields } = result.data; - - // apply the rundown - const firstRundown = getFirstRundown(rundowns); - await initRundown(firstRundown, customFields); + const newFileName = await loadProject(projectData.data, newFilename); + return newFileName; } + return newFilename; } /** * Creates a new project file and applies its result */ -export async function createProject(filename: string, initialData: Partial) { +export async function createProject(filename: string, initialData: Partial): Promise { const data = safeMerge(dbModel, initialData); const fileNameWithExtension = ensureJsonExtension(filename); - const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, fileNameWithExtension); - const newFile = getPathToProject(uniqueFileName); - - // change LowDB to point to new file - await initPersistence(newFile, data); - - // apply data to running services - // we dont need to parse since we are creating a new file - await patchCurrentProject(data); - - // update app state to point to new value - setLastLoadedProject(uniqueFileName); - - // update the service state - currentProjectState.currentProjectName = uniqueFileName; - - return uniqueFileName; + const newFilename = await loadProject(data, fileNameWithExtension); + return newFilename; } /** @@ -338,7 +300,7 @@ export async function patchCurrentProject(data: Partial) { * We currently ignore all other rundowns */ const firstRundown = getFirstRundown(result.rundowns); - initRundown(firstRundown, result.customFields); + await initRundown(firstRundown, result.customFields); } const updatedData = await getDataProvider().getData(); @@ -346,17 +308,13 @@ export async function patchCurrentProject(data: Partial) { } /** - * Changes the title of a project + * Changes the current project data * it handles invalidating the necessary data */ export async function editCurrentProjectData(newData: Partial) { const currentProjectData = getDataProvider().getProjectData(); const updatedProjectData = await getDataProvider().setProjectData(newData); - if (currentProjectData.title !== updatedProjectData.title) { - // something - } - // Delete the old logo if the logo has been removed if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) { const filePath = join(publicDir.logoDir, currentProjectData.projectLogo); diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index e32e5c7a2..cb6e1e34b 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -282,8 +282,8 @@ function notifyChanges(options: NotifyChangesOptions) { } /** - * Overrides the rundown with the given - * @param rundown + * Sets a new rundown in the cache + * and marks it as the currently loaded one */ export async function initRundown(rundown: Readonly, customFields: Readonly) { await cache.init(rundown, customFields); diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index 4ef18fc41..55348b759 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -34,11 +34,10 @@ beforeAll(() => { describe('generate()', () => { test('benchmark function execution time', () => { - const rundown = demoDb.rundowns.default; const t1 = performance.now(); let result: ProcessedRundownMetadata | null = null; for (let i = 0; i < 100; i++) { - result = generate(rundown); + result = generate(demoDb.rundowns.default, demoDb.customFields); } const t2 = performance.now(); console.warn( @@ -60,7 +59,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(3); expect(initResult.order).toStrictEqual(['1', '2', '3']); expect(initResult.entries['1'].type).toBe(SupportedEvent.Event); @@ -77,7 +76,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(2); expect((initResult.entries['2'] as OntimeEvent).delay).toBe(100); expect(initResult.totalDelay).toBe(100); @@ -97,7 +96,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(7); expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0); expect((initResult.entries['2'] as OntimeEvent).delay).toBe(200); @@ -117,7 +116,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start }); @@ -132,7 +131,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start }); @@ -167,7 +166,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start }); @@ -185,7 +184,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(7); expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0); expect((initResult.entries['2'] as OntimeEvent).delay).toBe(-200); @@ -227,7 +226,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(5); expect((initResult.entries['2'] as OntimeEvent).timeStart).toBe(2); expect((initResult.entries['2'] as OntimeEvent).timeEnd).toBe(12); @@ -248,7 +247,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(3); expect((initResult.entries['3'] as OntimeEvent).timeStart).toBe(2); }); @@ -264,7 +263,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(4); expect(initResult.totalDuration).toBe(500 - 100); }); @@ -280,7 +279,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.order.length).toBe(4); expect(initResult.totalDuration).toBe(500 - 100); }); @@ -310,7 +309,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR); }); @@ -333,7 +332,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR); expect(initResult.totalDuration).toBe(expectedDuration); }); @@ -369,7 +368,7 @@ describe('generate()', () => { }, }); - const initResult = generate(rundown); + const initResult = generate(rundown, {}); expect(initResult.entries).toMatchObject({ '1': { timeStart: 0, @@ -455,7 +454,7 @@ describe('generate() v4', () => { '300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }), }, }); - const generatedRundown = generate(rundown); + const generatedRundown = generate(rundown, {}); expect(generatedRundown.order).toMatchObject(['1']); expect(generatedRundown.totalDuration).toBe(300); @@ -495,7 +494,7 @@ describe('generate() v4', () => { '303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }), }, }); - const generatedRundown = generate(rundown); + const generatedRundown = generate(rundown, {}); expect(generatedRundown.order).toMatchObject(['0', '1', '2', '3']); expect(generatedRundown.totalDuration).toBe(1200); diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 8a90a115d..56ef348b2 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -21,7 +21,6 @@ import type { RundownMetadata } from './rundown.types.js'; import { apply } from './delayUtils.js'; import { hasChanges, isDataStale, makeRundownMetadata, type ProcessedRundownMetadata } from './rundownCache.utils.js'; -/** We hold the currently selected rundown and its metadata in memory */ let currentRundownId: EntryId = ''; let currentRundown: Rundown = { id: '', @@ -78,16 +77,14 @@ export let customFieldChangelog: Record = {}; * Receives a rundown which will be processed and used as the new current rundown */ export async function init(initialRundown: Readonly, customFields: Readonly) { - // TODO: do we need to clone? + // we clone this objects since we use mutating logic in the cache currentRundown = structuredClone(initialRundown); currentRundownId = initialRundown.id; projectCustomFields = structuredClone(customFields); - generate(); - // TODO: we may not need to persist this data since it should come from the database - // update the persisted data - await getDataProvider().setRundown(currentRundownId, currentRundown); - await getDataProvider().setCustomFields(customFields); + updateCache(); + + currentRundownId; } /** @@ -95,14 +92,9 @@ export async function init(initialRundown: Readonly, customFields: Read * @private should not be called outside of `rundownCache.ts`, exported for testing */ export function generate( - initialRundown: Readonly = currentRundown, - customFields: Readonly = projectCustomFields, + initialRundown: Readonly, + customFields: Readonly, ): ProcessedRundownMetadata { - // The stale state can only be cleared inside generate() - function clearIsStale() { - isStale = false; - } - const { process, getMetadata } = makeRundownMetadata(customFields, customFieldChangelog); for (let i = 0; i < initialRundown.order.length; i++) { @@ -154,9 +146,18 @@ export function generate( } } - const processedData = getMetadata(); - clearIsStale(); - customFieldChangelog = {}; + return getMetadata(); +} + +/** + * Runs the generate function in the currently loaded rundown and updates caches + */ +export function updateCache() { + // The stale state can only be cleared inside updateCache() + function clearIsStale() { + isStale = false; + } + const processedData = generate(currentRundown, projectCustomFields); // update the cache values // eslint-disable-next-line @typescript-eslint/no-unused-vars -- we are not interested in the iteration data @@ -164,15 +165,14 @@ export function generate( currentRundown.entries = entries; currentRundown.order = order; rundownMetadata = metadata; - - // The return value is used for testing - return processedData; + clearIsStale(); + customFieldChangelog = {}; } /** Returns an ID guaranteed to be unique */ export function getUniqueId(): string { if (isStale) { - generate(); + updateCache(); } let id = ''; do { @@ -184,7 +184,7 @@ export function getUniqueId(): string { /** Returns index of an event with a given id */ export function getIndexOf(eventId: EntryId) { if (isStale) { - generate(); + updateCache(); } return currentRundown.order.indexOf(eventId); } @@ -192,7 +192,7 @@ export function getIndexOf(eventId: EntryId) { /** Returns id of an event at a given index */ export function getIdOf(index: number) { if (isStale) { - generate(); + updateCache(); } return currentRundown.order.at(index); } @@ -213,7 +213,7 @@ type RundownCache = { */ export function get(): Readonly { if (isStale) { - generate(); + updateCache(); } return { id: currentRundown.id, @@ -232,7 +232,7 @@ export function get(): Readonly { */ export function getMetadata(): Readonly { if (isStale) { - generate(); + updateCache(); } return { @@ -252,7 +252,7 @@ export type RundownOrder = { */ export function getEventOrder(): Readonly { if (isStale) { - generate(); + updateCache(); } return { order: currentRundown.order, @@ -482,7 +482,7 @@ function invalidateIfUsed(label: CustomFieldLabel) { // ... and schedule a cache update // schedule a non priority cache update setImmediate(async () => { - generate(); + updateCache(); await getDataProvider().setRundown(currentRundownId, currentRundown); }); } diff --git a/apps/server/src/services/rundown-service/rundownUtils.ts b/apps/server/src/services/rundown-service/rundownUtils.ts index 0d4ce8a87..92829bb33 100644 --- a/apps/server/src/services/rundown-service/rundownUtils.ts +++ b/apps/server/src/services/rundown-service/rundownUtils.ts @@ -149,10 +149,18 @@ export function filterTimedEvents(rundown: Rundown, timedEventOrder: EntryId[]): /** * Gets the first rundown in the project - * We ensure that the projects always have a rundown + * We know that the project has at least one rundown */ export function getFirstRundown(rundowns: ProjectRundowns): Rundown { const firstKey = Object.keys(rundowns)[0]; + + // eslint-disable-next-line no-unused-labels -- dev code path + DEV: { + if (!firstKey) { + throw new Error('rundownUtils.getFirstRundown() No rundowns found'); + } + } + return rundowns[firstKey]; } diff --git a/apps/server/src/utils/__tests__/parser.test.ts b/apps/server/src/utils/__tests__/parser.test.ts index 5ba8eb270..ca7c29b6a 100644 --- a/apps/server/src/utils/__tests__/parser.test.ts +++ b/apps/server/src/utils/__tests__/parser.test.ts @@ -146,7 +146,6 @@ describe('test aliases import', () => { const testData = { rundown: [], settings: { - app: 'ontime', version: '2.0.0', }, urlPresets: [ @@ -171,7 +170,6 @@ describe('test views import', () => { const testData = { rundown: [], settings: { - app: 'ontime', version: '2.0.0', }, viewSettings: { @@ -205,7 +203,6 @@ describe('test views import', () => { const testData = { rundown: [], settings: { - app: 'ontime', version: '2.0.0', }, } as unknown as DatabaseModel; diff --git a/apps/server/src/utils/__tests__/parserFunctions.test.ts b/apps/server/src/utils/__tests__/parserFunctions.test.ts index 01a0b56f2..223d0425a 100644 --- a/apps/server/src/utils/__tests__/parserFunctions.test.ts +++ b/apps/server/src/utils/__tests__/parserFunctions.test.ts @@ -229,11 +229,10 @@ describe('parseSettings()', () => { expect(() => parseSettings({})).toThrow(); }); - it('returns an a base model as long as we have the app and version', () => { - const result = parseSettings({ settings: { app: 'ontime', version: '1' } as Settings }); + it('returns an a base model as long as we have the app version', () => { + const result = parseSettings({ settings: { version: '1' } as Settings }); expect(result).toBeTypeOf('object'); expect(result).toMatchObject({ - app: 'ontime', version: expect.any(String), serverPort: 4001, editorKey: null, diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index 3934fb6bc..84b1cbd0e 100644 --- a/apps/server/src/utils/parserFunctions.ts +++ b/apps/server/src/utils/parserFunctions.ts @@ -24,6 +24,7 @@ import { createEvent, type ErrorEmitter } from './parser.js'; /** * Parse a rundowns object along with the project custom fields + * Returns a default rundown if none exists */ export function parseRundowns( data: Partial, @@ -32,6 +33,8 @@ export function parseRundowns( // check custom fields first const parsedCustomFields = parseCustomFields(data, emitError); + // ensure there is always a rundown to import + // this is important since the rest of the app assumes this exist if (!data.rundowns || isObjectEmpty(data.rundowns)) { emitError?.('No data found to import'); return { @@ -193,14 +196,14 @@ export function parseProject(data: Partial, emitError?: ErrorEmit */ export function parseSettings(data: Partial): Settings { // skip if file definition is missing - if (!data.settings || data.settings?.app !== 'ontime' || data.settings?.version == null) { - throw new Error('ERROR: unable to parse settings, missing app or version'); + // TODO: skip parsing if the version is not correct + if (!data.settings || data.settings?.version == null) { + throw new Error('ERROR: unable to parse settings, missing or incorrect 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, diff --git a/packages/types/src/definitions/core/Settings.type.ts b/packages/types/src/definitions/core/Settings.type.ts index 33b9a49e8..eb423b41c 100644 --- a/packages/types/src/definitions/core/Settings.type.ts +++ b/packages/types/src/definitions/core/Settings.type.ts @@ -1,7 +1,6 @@ import type { TimeFormat } from './TimeFormat.type.js'; export type Settings = { - app: 'ontime'; version: string; serverPort: number; editorKey: null | string;