refactor: update timers (#729)

* refactor: remove duplication

* refactor: previous times

* refactor: event patching
This commit is contained in:
Carlos Valente
2024-01-25 13:36:45 +01:00
committed by GitHub
parent e748bff0dd
commit 74e1ae609c
32 changed files with 448 additions and 555 deletions
@@ -1,4 +1,4 @@
import { LogOrigin, OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent } from 'ontime-types';
import { LogOrigin, OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, isOntimeEvent } from 'ontime-types';
import { generateId, getCueCandidate } from 'ontime-utils';
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
@@ -18,7 +18,7 @@ import {
delayedRundownCacheKey,
} from './delayedRundown.utils.js';
import { logger } from '../../classes/Logger.js';
import { validateEvent } from '../../utils/parser.js';
import { createEvent } from '../../utils/parser.js';
import { stateMutations } from '../../state.js';
import { runtimeService } from '../runtime-service/RuntimeService.js';
@@ -56,7 +56,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
switch (eventData.type) {
case SupportedEvent.Event: {
newEvent = validateEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
newEvent = createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
break;
}
case SupportedEvent.Delay:
@@ -80,7 +80,7 @@ export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeD
}
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
if (eventData.type === SupportedEvent.Event && eventData?.cue === '') {
if (isOntimeEvent(eventData) && eventData?.cue === '') {
throw new Error('Cue value invalid');
}
@@ -16,6 +16,7 @@ import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
import { isProduction } from '../../setup.js';
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
import { _applyDelay } from '../delayUtils.js';
import { createPatch } from '../../utils/parser.js';
/**
* Keep incremental revision number of rundown for runtime
@@ -109,6 +110,16 @@ export async function cachedEdit(
eventId: string,
patchObject: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>,
) {
const makeEvent = (eventFromRundown: OntimeRundownEntry): OntimeRundownEntry => {
if (isOntimeEvent(eventFromRundown)) {
const newEvent = createPatch(eventFromRundown, patchObject as OntimeEvent);
newEvent.revision++;
return newEvent;
}
return { ...eventFromRundown, ...patchObject } as OntimeRundownEntry;
};
const indexInMemory = DataProvider.getIndexOf(eventId);
if (indexInMemory < 0) {
throw new Error('No event with ID found');
@@ -125,10 +136,7 @@ export async function cachedEdit(
return eventFromRundown;
}
const newEvent = { ...eventFromRundown, ...patchObject } as OntimeRundownEntry;
if (isOntimeEvent(newEvent)) {
newEvent.revision++;
}
const newEvent = makeEvent(eventFromRundown);
updatedRundown[indexInMemory] = newEvent;
let newDelayedRundown = getDelayedRundown();
@@ -4,7 +4,7 @@ import { vi } from 'vitest';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { parseExcel, parseJson, validateEvent } from '../parser.js';
import { parseExcel, parseJson, createEvent } from '../parser.js';
import { makeString } from '../parserUtils.js';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
@@ -340,7 +340,7 @@ describe('test parser edge cases', () => {
};
const parseResponse = await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: undefined event type, skipping');
expect(console.log).toHaveBeenCalledWith('ERROR: unkown event type, skipping');
expect(parseResponse?.rundown.length).toBe(0);
});
@@ -464,7 +464,7 @@ describe('test event validator', () => {
const event = {
title: 'test',
};
const validated = validateEvent(event, 'test');
const validated = createEvent(event, 'test');
expect(validated).toEqual(
expect.objectContaining({
@@ -497,7 +497,7 @@ describe('test event validator', () => {
it('fails an empty object', () => {
const event = {};
const validated = validateEvent(event, 'none');
const validated = createEvent(event, 'none');
expect(validated).toEqual(null);
});
@@ -509,7 +509,7 @@ describe('test event validator', () => {
note: '1899-12-30T08:00:10.000Z',
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event, 'not-used');
const validated = createEvent(event, 'not-used');
expect(typeof validated.title).toEqual('string');
expect(typeof validated.subtitle).toEqual('string');
expect(typeof validated.presenter).toEqual('string');
@@ -522,7 +522,7 @@ describe('test event validator', () => {
timeEnd: '2',
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event);
const validated = createEvent(event);
expect(typeof validated.timeStart).toEqual('number');
expect(validated.timeStart).toEqual(0);
expect(typeof validated.timeEnd).toEqual('number');
@@ -534,7 +534,7 @@ describe('test event validator', () => {
title: {},
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event);
const validated = createEvent(event);
expect(typeof validated.title).toEqual('string');
});
});
+60 -55
View File
@@ -43,20 +43,25 @@ import { coerceBoolean } from './coerceType.js';
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
export const JSON_MIME = 'application/json';
type ExcelData = Pick<DatabaseModel, 'rundown' | 'project' | 'userFields'> & {
projectMetadata: Record<string, { row: number; col: number }>;
rundownMetadata: Record<string, { row: number; col: number }>;
};
/**
* @description Excel array parser
* @param {array} excelData - array with excel sheet
* @param {ExcelImportOptions} options - an object that contains the import map
* @returns {object} - parsed object
*/
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>): ExcelData => {
const projectMetadata = {};
const rundownMetadata = {};
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
for (const [key, value] of Object.entries(importMap)) {
importMap[key] = value.toLocaleLowerCase();
}
const projectData: Partial<ProjectData> = {
const projectData: ProjectData = {
title: '',
description: '',
publicUrl: '',
@@ -64,7 +69,7 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
backstageUrl: '',
backstageInfo: '',
};
const customUserFields: Partial<UserFields> = {
const customUserFields: UserFields = {
user0: importMap.user0,
user1: importMap.user1,
user2: importMap.user2,
@@ -350,10 +355,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
return {
rundown,
project: projectData,
settings: {
app: 'ontime',
version: '2.0.0',
},
userFields: customUserFields,
projectMetadata,
rundownMetadata,
@@ -393,61 +394,66 @@ export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
return returnData as DatabaseModel;
};
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
if (Object.keys(patchEvent).length === 0) {
return originalEvent;
}
const { timeStart, timeEnd, duration } = validateTimes(
patchEvent?.timeStart ?? originalEvent.timeStart,
patchEvent?.timeEnd ?? originalEvent.timeEnd,
patchEvent?.duration ?? originalEvent.duration,
);
return {
id: originalEvent.id,
type: SupportedEvent.Event,
title: makeString(patchEvent.title, originalEvent.title),
subtitle: makeString(patchEvent.subtitle, originalEvent.subtitle),
presenter: makeString(patchEvent.presenter, originalEvent.presenter),
timeStart,
timeEnd,
duration,
endAction: validateEndAction(patchEvent.endAction, EndAction.None),
timerType: validateTimerType(patchEvent.timerType, TimerType.CountDown),
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
note: makeString(patchEvent.note, originalEvent.note),
user0: makeString(patchEvent.user0, originalEvent.user0),
user1: makeString(patchEvent.user1, originalEvent.user1),
user2: makeString(patchEvent.user2, originalEvent.user2),
user3: makeString(patchEvent.user3, originalEvent.user3),
user4: makeString(patchEvent.user4, originalEvent.user4),
user5: makeString(patchEvent.user5, originalEvent.user5),
user6: makeString(patchEvent.user6, originalEvent.user6),
user7: makeString(patchEvent.user7, originalEvent.user7),
user8: makeString(patchEvent.user8, originalEvent.user8),
user9: makeString(patchEvent.user9, originalEvent.user9),
colour: makeString(patchEvent.colour, originalEvent.colour),
cue: makeString(patchEvent.cue, originalEvent.cue),
revision: originalEvent.revision,
timeWarning: patchEvent.timeWarning,
timeDanger: patchEvent.timeDanger,
};
}
/**
* @description Enforces formatting for events
* @param {object} eventArgs - attributes of event
* @param cueFallback
* @returns {object|null} - formatted object or null in case is invalid
*/
export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string) => {
// ensure id is defined and unique
const id = eventArgs.id || generateId();
let event = null;
// return if object is empty
if (Object.keys(eventArgs).length > 0) {
// make sure all properties exits
// dont load any extra properties than the ones known
const e = eventArgs;
const d = eventDef;
const { timeStart, timeEnd, duration } = validateTimes(e.timeStart, e.timeEnd, e.duration);
event = {
...d,
title: makeString(e.title, d.title),
subtitle: makeString(e.subtitle, d.subtitle),
presenter: makeString(e.presenter, d.presenter),
timeStart,
timeEnd,
duration,
endAction: validateEndAction(e.endAction, EndAction.None),
timerType: validateTimerType(e.timerType, TimerType.CountDown),
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
note: makeString(e.note, d.note),
user0: makeString(e.user0, d.user0),
user1: makeString(e.user1, d.user1),
user2: makeString(e.user2, d.user2),
user3: makeString(e.user3, d.user3),
user4: makeString(e.user4, d.user4),
user5: makeString(e.user5, d.user5),
user6: makeString(e.user6, d.user6),
user7: makeString(e.user7, d.user7),
user8: makeString(e.user8, d.user8),
user9: makeString(e.user9, d.user9),
colour: makeString(e.colour, d.colour),
cue: makeString(e.cue, cueFallback),
id,
type: 'event',
timeWarning: e.timeWarning,
timeDanger: e.timeDanger,
};
export const createEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: string): OntimeEvent | null => {
if (Object.keys(eventArgs).length === 0) {
return null;
}
const baseEvent = {
id: eventArgs?.id ?? generateId(),
cue: cueFallback,
...eventDef,
};
const event = createPatch(baseEvent, eventArgs);
return event;
};
@@ -497,7 +503,6 @@ export const fileHandler = async (file: string, options: ExcelImportOptions): Pr
}
if (file.endsWith('.json')) {
// if json check version
const rawdata = fs.readFileSync(file).toString();
let uploadedJson = null;
+30 -26
View File
@@ -13,11 +13,15 @@ import {
HttpSubscription,
OscSubscriptionOptions,
HttpSubscriptionOptions,
DatabaseModel,
isOntimeEvent,
isOntimeDelay,
isOntimeBlock,
} from 'ontime-types';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
import { dbModel } from '../models/dataModel.js';
import { validateEvent } from './parser.js';
import { createEvent } from './parser.js';
import { MAX_EVENTS } from '../settings.js';
/**
@@ -25,7 +29,7 @@ import { MAX_EVENTS } from '../settings.js';
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseRundown = (data): OntimeRundown => {
export const parseRundown = (data: Partial<DatabaseModel>): OntimeRundown => {
let newRundown: OntimeRundown = [];
if ('rundown' in data) {
console.log('Found rundown definition, importing...');
@@ -33,7 +37,7 @@ export const parseRundown = (data): OntimeRundown => {
try {
let eventIndex = 0;
const ids = [];
for (const e of data.rundown) {
for (const event of data.rundown) {
// cap number of events
if (rundown.length >= MAX_EVENTS) {
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
@@ -41,28 +45,28 @@ export const parseRundown = (data): OntimeRundown => {
}
// double check unique ids
if (ids.includes(e?.id)) {
if (ids.includes(event?.id)) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
if (isOntimeEvent(event)) {
eventIndex += 1;
const event = validateEvent(e, eventIndex.toString());
const parsedEvent = createEvent(event, eventIndex.toString());
if (event != null) {
rundown.push(event);
ids.push(event.id);
rundown.push(parsedEvent);
ids.push(parsedEvent.id);
}
} else if (e.type === 'delay') {
} else if (isOntimeDelay(event)) {
rundown.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
duration: event.duration,
id: event.id || generateId(),
});
} else if (e.type === 'block') {
rundown.push({ ...blockDef, title: e.title, id: e.id || generateId() });
} else if (isOntimeBlock(event)) {
rundown.push({ ...blockDef, title: event.title, id: event.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
console.log('ERROR: unkown event type, skipping');
}
}
} catch (error) {
@@ -112,16 +116,16 @@ export const parseSettings = (data): Settings => {
const s = data.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
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',
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
@@ -287,15 +291,15 @@ export const parseAliases = (data): Alias[] => {
if ('aliases' in data) {
console.log('Found Aliases definition, importing...');
try {
for (const a of data.aliases) {
for (const alias of data.aliases) {
const newAlias = {
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
enabled: alias.enabled ?? false,
alias: alias.alias ?? '',
pathAndParams: alias.pathAndParams ?? '',
};
newAliases.push(newAlias);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
console.log(`Uploaded ${newAliases.length} alias(es)`);
} catch (error) {
console.log(`Error: ${error}`);
}
@@ -328,4 +332,4 @@ export const parseUserFields = (data): UserFields => {
}
}
return { ...newUserFields };
};
};