refactor: generate ids for new rundowns

This commit is contained in:
Carlos Valente
2025-12-11 18:22:28 +01:00
committed by Carlos Valente
parent b70ef80320
commit f2b78e0f32
13 changed files with 146 additions and 73 deletions
@@ -1,19 +1,21 @@
import { DatabaseModel, AutomationSettings, NormalisedAutomation, Trigger } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import type { ErrorEmitter } from '../../utils/parserUtils.js';
import { getPartialProject } from '../../models/dataModel.js';
export function parseAutomationSettings(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): AutomationSettings {
const defaultAutomation: AutomationSettings = getPartialProject('automation');
if (!data.automation) {
emitError?.('No data found to import');
return { ...dbModel.automation };
return defaultAutomation;
}
console.log('Found Automation settings, importing...');
return {
enabledAutomations: data.automation.enabledAutomations ?? dbModel.automation.enabledAutomations,
enabledOscIn: data.automation.enabledOscIn ?? dbModel.automation.enabledOscIn,
oscPortIn: data.automation.oscPortIn ?? dbModel.automation.oscPortIn,
enabledAutomations: data.automation.enabledAutomations ?? defaultAutomation.enabledAutomations,
enabledOscIn: data.automation.enabledOscIn ?? defaultAutomation.enabledOscIn,
oscPortIn: data.automation.oscPortIn ?? defaultAutomation.oscPortIn,
triggers: parseTriggers(data.automation.triggers),
automations: parseAutomations(data.automation.automations),
};
+3 -9
View File
@@ -49,15 +49,9 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
*/
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
try {
const newFileName = await projectService.createProject(req.body.filename || 'untitled', {
project: {
title: req.body?.title ?? '',
description: req.body?.description ?? '',
url: req.body?.url ?? '',
info: req.body?.info ?? '',
logo: req.body?.logo ?? null,
custom: req.body?.custom ?? [],
},
const { filename, ...project } = req.body;
const newFileName = await projectService.createProjectWithPatch(filename, {
project,
});
res.status(200).send({
@@ -17,11 +17,12 @@ import {
URLPreset,
ViewSettings,
} from 'ontime-types';
import { is } from '../../../utils/is.js';
import { dbModel } from '../../../models/dataModel.js';
import { customFieldLabelToKey, checkRegex, isKnownTimerType, validateEndAction } from 'ontime-utils';
import { is } from '../../../utils/is.js';
import { event as eventModel } from '../../../models/eventsDefinition.js';
import { ONTIME_VERSION } from '../../../ONTIME_VERSION.js';
import { getPartialProject } from '../../../models/dataModel.js';
// the methodology of the migrations is to just change the necessary keys to match with v4
// and then let the normal project parser handle ensuring the the file is correct
@@ -148,8 +149,8 @@ export function migrateProjectData(jsonData: object): ProjectData | undefined {
description,
url: backstageUrl,
info: backstageInfo,
logo: logo ?? dbModel.project.logo,
custom: custom ?? dbModel.project.custom,
logo: logo ?? null,
custom: custom ?? [],
};
}
}
@@ -244,7 +245,7 @@ export function migrateAutomations(jsonData: object): AutomationSettings | undef
}
let foundOldSetting = false;
const migratedOldStuff = structuredClone(dbModel.automation);
const migratedOldStuff: AutomationSettings = getPartialProject('automation');
const migratedAutomations: NormalisedAutomation = {};
const migratedTriggers: Trigger[] = [];
@@ -14,7 +14,7 @@ import {
ViewSettings,
} from 'ontime-types';
import * as v3 from './db.migration.v3.js';
import { dbModel } from '../../../models/dataModel.js';
import { ONTIME_VERSION } from '../../../ONTIME_VERSION.js';
describe('v3 to v4', () => {
@@ -469,7 +469,7 @@ describe('v3 to v4', () => {
const expectedAutomation: AutomationSettings = {
enabledAutomations: true,
enabledOscIn: true,
oscPortIn: dbModel.automation.oscPortIn,
oscPortIn: 8888,
triggers: [
{
id: '1ge4r8-T',
@@ -1,25 +1,27 @@
import { DatabaseModel, ProjectData } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { ErrorEmitter } from '../../utils/parserUtils.js';
import { getPartialProject } from '../../models/dataModel.js';
/**
* Parse event portion of an entry
*/
export function parseProjectData(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): ProjectData {
const defaultProject: ProjectData = getPartialProject('project');
if (!data.project) {
emitError?.('No data found to import');
return { ...dbModel.project };
return defaultProject;
}
console.log('Found project data, importing...');
return {
title: data.project.title ?? dbModel.project.title,
description: data.project.description ?? dbModel.project.description,
url: data.project.url ?? dbModel.project.url,
info: data.project.info ?? dbModel.project.info,
logo: data.project.logo ?? dbModel.project.logo,
custom: data.project.custom ?? dbModel.project.custom,
title: data.project.title ?? defaultProject.title,
description: data.project.description ?? defaultProject.description,
url: data.project.url ?? defaultProject.url,
info: data.project.info ?? defaultProject.info,
logo: data.project.logo ?? defaultProject.logo,
custom: data.project.custom ?? defaultProject.custom,
};
}
@@ -8,7 +8,7 @@ import {
OntimeMilestone,
} from 'ontime-types';
import { defaultRundown } from '../../../models/dataModel.js';
import { makeNewRundown } from '../../../models/dataModel.js';
const baseEvent = {
type: SupportedEntry.Event,
@@ -63,7 +63,7 @@ export function makeOntimeMilestone(patch: Partial<OntimeMilestone>): OntimeMile
*/
export function makeRundown(patch: Partial<Rundown>): Rundown {
return {
...defaultRundown,
...makeNewRundown(),
...patch,
};
}
@@ -1,15 +1,25 @@
import { SupportedEntry, OntimeEvent, OntimeGroup, Rundown, CustomFields } from 'ontime-types';
import { defaultRundown } from '../../../models/dataModel.js';
import { makeOntimeGroup, makeOntimeEvent, makeOntimeMilestone } from '../__mocks__/rundown.mocks.js';
import { makeNewRundown } from '../../../models/dataModel.js';
import { makeOntimeGroup, makeOntimeEvent, makeOntimeMilestone } from '../__mocks__/rundown.mocks.js';
import { parseRundowns, parseRundown, sanitiseCustomFields } from '../rundown.parser.js';
describe('parseRundowns()', () => {
it('returns a default project rundown if nothing is given', () => {
const errorEmitter = vi.fn();
const defaultRundown = makeNewRundown();
const result = parseRundowns({}, {}, errorEmitter);
expect(result).toStrictEqual({ default: defaultRundown });
const rundownIds = Object.keys(result);
expect(rundownIds).toHaveLength(1);
expect(result[rundownIds[0]]).toMatchObject({
id: rundownIds[0],
title: defaultRundown.title,
order: expect.any(Array),
entries: expect.any(Object),
});
// one for not having custom fields
// one for not having a rundown
expect(errorEmitter).toHaveBeenCalledTimes(1);
@@ -17,6 +27,7 @@ describe('parseRundowns()', () => {
it('ensures the rundown IDs are consistent', () => {
const errorEmitter = vi.fn();
const defaultRundown = makeNewRundown();
const r1 = { ...defaultRundown, id: '1' };
const r2 = { ...defaultRundown, id: '2' };
const result = parseRundowns(
@@ -19,8 +19,8 @@ import {
} from 'ontime-types';
import { isObjectEmpty, generateId, getLinkedTimes, getTimeFrom, isNewLatest } from 'ontime-utils';
import { defaultRundown } from '../../models/dataModel.js';
import { delay as delayDef } from '../../models/eventsDefinition.js';
import { makeNewRundown } from '../../models/dataModel.js';
import type { ErrorEmitter } from '../../utils/parserUtils.js';
import { calculateDayOffset, cleanupCustomFields, createGroup, createEvent, createMilestone } from './rundown.utils.js';
@@ -39,10 +39,9 @@ export function parseRundowns(
// this is important since the rest of the app assumes this exist
if (!data.rundowns || isObjectEmpty(data.rundowns)) {
emitError?.('No data found to import');
const defaultRundown = makeNewRundown();
return {
[defaultRundown.id]: {
...defaultRundown,
},
[defaultRundown.id]: defaultRundown,
};
}
@@ -1,9 +1,14 @@
import { ErrorResponse, OntimeEntry, ProjectRundownsList, Rundown } from 'ontime-types';
import { generateId, getErrorMessage } from 'ontime-utils';
import { getErrorMessage } from 'ontime-utils';
import type { Request, Response } from 'express';
import express from 'express';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { makeNewRundown } from '../../models/dataModel.js';
import { paramsWithId } from '../validation-utils/validationFunction.js';
import { getCurrentRundown } from './rundown.dao.js';
import {
addEntry,
@@ -30,9 +35,6 @@ import {
validateRundownMutation,
clonePostValidator,
} from './rundown.validation.js';
import { paramsWithId } from '../validation-utils/validationFunction.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { defaultRundown } from '../../models/dataModel.js';
import { duplicateRundown, normalisedToRundownArray } from './rundown.utils.js';
export const router = express.Router();
@@ -85,8 +87,9 @@ router.post('/:id/load', paramsWithId, async (req: Request, res: Response<Projec
*/
router.post('/', rundownPostValidator, async (req: Request, res: Response<ProjectRundownsList | ErrorResponse>) => {
try {
const id = generateId();
await getDataProvider().setRundown(id, { ...defaultRundown, id, title: req.body.title });
const emptyRundown = makeNewRundown();
emptyRundown.title = req.body.title;
await getDataProvider().setRundown(emptyRundown.id, emptyRundown);
const projectRundowns = getDataProvider().getProjectRundowns();
res.status(201).json({ loaded: getCurrentRundown().id, rundowns: normalisedToRundownArray(projectRundowns) });
@@ -1,11 +1,13 @@
import { DatabaseModel, Settings } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { getPartialProject } from '../../models/dataModel.js';
/**
* Parse settings portion of a project file
*/
export function parseSettings(data: Partial<DatabaseModel>): Settings {
const defaultSettings: Settings = getPartialProject('settings');
// skip if file definition is missing
// TODO: skip parsing if the version is not correct
if (!data.settings || data.settings?.version == null) {
@@ -15,11 +17,11 @@ export function parseSettings(data: Partial<DatabaseModel>): Settings {
console.log('Found settings, importing...');
return {
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',
version: defaultSettings.version,
serverPort: data.settings.serverPort ?? defaultSettings.serverPort,
editorKey: data.settings.editorKey ?? defaultSettings.editorKey,
operatorKey: data.settings.operatorKey ?? defaultSettings.operatorKey,
timeFormat: data.settings.timeFormat ?? defaultSettings.timeFormat,
language: data.settings.language ?? defaultSettings.language,
};
}
@@ -1,23 +1,25 @@
import { DatabaseModel, ViewSettings } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { getPartialProject } from '../../models/dataModel.js';
import { ErrorEmitter } from '../../utils/parserUtils.js';
/**
* Parse viewSettings portion of a project file
*/
export function parseViewSettings(data: Partial<DatabaseModel>, emitError?: ErrorEmitter): ViewSettings {
const defaultViewSettings: ViewSettings = getPartialProject('viewSettings');
if (!data.viewSettings) {
emitError?.('No data found to import');
return { ...dbModel.viewSettings };
return defaultViewSettings;
}
console.log('Found view settings, importing...');
return {
dangerColor: data.viewSettings.dangerColor ?? dbModel.viewSettings.dangerColor,
normalColor: data.viewSettings.normalColor ?? dbModel.viewSettings.normalColor,
overrideStyles: data.viewSettings.overrideStyles ?? dbModel.viewSettings.overrideStyles,
warningColor: data.viewSettings.warningColor ?? dbModel.viewSettings.warningColor,
dangerColor: data.viewSettings.dangerColor ?? defaultViewSettings.dangerColor,
normalColor: data.viewSettings.normalColor ?? defaultViewSettings.normalColor,
overrideStyles: data.viewSettings.overrideStyles ?? defaultViewSettings.overrideStyles,
warningColor: data.viewSettings.warningColor ?? defaultViewSettings.warningColor,
};
}
+36 -2
View File
@@ -1,7 +1,8 @@
import { DatabaseModel, Rundown } from 'ontime-types';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
import { generateId } from 'ontime-utils';
export const defaultRundown: Rundown = {
const defaultRundown: Rundown = {
id: 'default',
title: 'Default',
order: [],
@@ -10,7 +11,7 @@ export const defaultRundown: Rundown = {
revision: 0,
};
export const dbModel: DatabaseModel = {
const dbModel: DatabaseModel = {
rundowns: {
default: { ...defaultRundown },
},
@@ -46,3 +47,36 @@ export const dbModel: DatabaseModel = {
automations: {},
},
};
/**
* Creates a new project with a single rundown of given Id
*/
export function makeNewProject(defaultRundownId?: string): DatabaseModel {
const rundown = makeNewRundown(defaultRundownId);
const newProject = structuredClone(dbModel);
newProject.rundowns = {
[rundown.id]: rundown,
};
return newProject;
}
/**
* Creates a new rundown with given Id
*/
export function makeNewRundown(id?: string): Rundown {
const rundownId = id || generateId();
const newRundown = structuredClone(defaultRundown);
newRundown.id = rundownId;
return newRundown;
}
/**
* Get a top level property of the DatabaseModel
*/
export function getPartialProject<T extends keyof DatabaseModel>(key: T): DatabaseModel[T] {
if (Object.hasOwn(dbModel, key)) {
return structuredClone(dbModel[key]);
}
throw new Error(`Key ${key} does not exist on DatabaseModel`);
}
@@ -2,6 +2,7 @@ import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types'
import { getErrorMessage, getFirstRundown } from 'ontime-utils';
import { copyFile } from 'fs/promises';
import { join } from 'path';
import { logger } from '../../classes/Logger.js';
import { publicDir } from '../../setup/index.js';
@@ -15,15 +16,16 @@ import {
getFileNameFromPath,
removeFileExtension,
} from '../../utils/fileManagement.js';
import { dbModel } from '../../models/dataModel.js';
import { parseRundowns } from '../../api-data/rundown/rundown.parser.js';
import { demoDb } from '../../models/demoProject.js';
import { config } from '../../setup/config.js';
import { getDataProvider, initPersistence } from '../../classes/data-provider/DataProvider.js';
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
import { initRundown } from '../../api-data/rundown/rundown.service.js';
import { parseDatabaseModel } from '../../api-data/db/db.parser.js';
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
import { makeNewProject } from '../../models/dataModel.js';
import { safeMerge } from '../../classes/data-provider/DataProvider.utils.js';
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
import { getLastLoaded, isLastLoadedProject, setLastLoaded } from '../app-state-service/AppStateService.js';
import { runtimeService } from '../runtime-service/runtime.service.js';
@@ -35,7 +37,6 @@ import {
moveCorruptFile,
parseJsonFile,
} from './projectServiceUtils.js';
import { join } from 'path';
type ProjectState =
| {
@@ -94,10 +95,6 @@ async function loadProject(projectData: DatabaseModel, fileName: string, rundown
? projectData.rundowns[rundownId]
: getFirstRundown(projectData.rundowns);
if (!rundown) {
throw new Error('No rundown found in project');
}
await initRundown(rundown, projectData.customFields, true);
// persist the project selection
@@ -124,7 +121,8 @@ export async function loadDemoProject(): Promise<string> {
* to be composed in the loading functions
*/
async function loadNewProject(): Promise<string> {
return createProject(config.newProject, dbModel);
const emptyProject = makeNewProject();
return createProject(config.newProject, emptyProject);
}
/**
@@ -297,13 +295,23 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
* @param fileName file name of the project including the extension
* @param initialData db to initialize the project with
*/
export async function createProject(fileName: string, initialData: Partial<DatabaseModel>): Promise<string> {
const data = safeMerge(dbModel, initialData);
export async function createProject(fileName: string, initialData: DatabaseModel): Promise<string> {
const fileNameWithExtension = generateUniqueFileName(publicDir.projectsDir, ensureJsonExtension(fileName));
await loadProject(data, fileNameWithExtension);
await loadProject(initialData, fileNameWithExtension);
return fileNameWithExtension;
}
/**
* Creates a new project file from a patch and applies its result
* @param fileName file name of the project including the extension
* @param initialData patch of DB to initialize the project with
*/
export async function createProjectWithPatch(fileName: string, initialData: Partial<DatabaseModel>): Promise<string> {
const newProject = makeNewProject();
const sanitisedData = safeMerge(newProject, initialData);
return createProject(fileName, sanitisedData);
}
/**
* Deletes a project file
*/
@@ -328,6 +336,7 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we need to remove the fields before merging
const { rundowns, customFields, ...rest } = data;
// we can pass some stuff straight to the data provider
await getDataProvider().mergeIntoData(rest);
@@ -340,9 +349,23 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
// then we can check the rundown
if (rundowns) {
const parsedCustomFields = await getDataProvider().getCustomFields();
const parsedRundowns = parseRundowns(data, parsedCustomFields);
await getDataProvider().mergeIntoData({ rundowns: parsedRundowns });
// use the already parsed custom fields if available, otherwise fetch from provider
const projectCustomFields = getDataProvider().getCustomFields();
const parsedRundowns = parseRundowns(data, projectCustomFields);
const currentRundown = getCurrentRundown();
const mergedData = await getDataProvider().mergeIntoData({ rundowns: parsedRundowns });
// check if the currently loaded rundown was modified
const didOverrideCurrentRundown = currentRundown.id in parsedRundowns;
if (didOverrideCurrentRundown) {
// verify the rundown exists in the merged data before reinitializing
const updatedCurrentRundown = mergedData.rundowns[currentRundown.id];
if (updatedCurrentRundown) {
await initRundown(updatedCurrentRundown, projectCustomFields, true);
}
}
}
const updatedData = await getDataProvider().getData();