diff --git a/apps/server/src/api-data/db/migration/db.migration.v3.ts b/apps/server/src/api-data/db/migration/db.migration.v3.ts index 84d7c3e72..c9e3b9b66 100644 --- a/apps/server/src/api-data/db/migration/db.migration.v3.ts +++ b/apps/server/src/api-data/db/migration/db.migration.v3.ts @@ -17,10 +17,15 @@ import { URLPreset, ViewSettings, } from 'ontime-types'; -import { customFieldLabelToKey, checkRegex, isKnownTimerType, validateEndAction } from 'ontime-utils'; +import { + customFieldLabelToKey, + checkRegex, + isKnownTimerType, + validateEndAction, + eventDef as eventModel, +} 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'; diff --git a/apps/server/src/api-data/excel/excel.parser.ts b/apps/server/src/api-data/excel/excel.parser.ts index 5b40efe9a..476eddb8e 100644 --- a/apps/server/src/api-data/excel/excel.parser.ts +++ b/apps/server/src/api-data/excel/excel.parser.ts @@ -18,12 +18,12 @@ import { isKnownTimerType, validateTimerType, validateEndAction, + makeString, } from 'ontime-utils'; import { Prettify } from 'ts-essentials'; import { is } from '../../utils/is.js'; -import { makeString } from '../../utils/parserUtils.js'; import { parseExcelDate } from '../../utils/time.js'; import { generateImportHandlers, getCustomFieldData, parseBooleanString, SheetMetadata } from './excel.utils.js'; diff --git a/apps/server/src/api-data/rundown/__tests__/rundown.utils.test.ts b/apps/server/src/api-data/rundown/__tests__/rundown.utils.test.ts index c563466d6..e344afcdd 100644 --- a/apps/server/src/api-data/rundown/__tests__/rundown.utils.test.ts +++ b/apps/server/src/api-data/rundown/__tests__/rundown.utils.test.ts @@ -1,5 +1,5 @@ import { TimeStrategy, EndAction, TimerType, OntimeEvent } from 'ontime-types'; -import { MILLIS_PER_HOUR } from 'ontime-utils'; +import { createEvent, MILLIS_PER_HOUR } from 'ontime-utils'; import { assertType } from 'vitest'; @@ -7,7 +7,6 @@ import { demoDb } from '../../../models/demoProject.js'; import { calculateDayOffset, - createEvent, deleteById, doesInvalidateMetadata, duplicateRundown, diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index 0cea576d5..91040af4c 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -26,7 +26,7 @@ import { Rundown, InsertOptions, } from 'ontime-types'; -import { addToRundown, customFieldLabelToKey, getInsertAfterId, insertAtIndex } from 'ontime-utils'; +import { addToRundown, customFieldLabelToKey, getInsertAfterId, insertAtIndex, createGroup } from 'ontime-utils'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { consoleError } from '../../utils/console.js'; @@ -35,7 +35,6 @@ import type { RundownMetadata } from './rundown.types.js'; import { applyPatchToEntry, cloneSimpleRundownEntry, - createGroup, deleteById, doesInvalidateMetadata, getUniqueId, diff --git a/apps/server/src/api-data/rundown/rundown.parser.ts b/apps/server/src/api-data/rundown/rundown.parser.ts index cb4269ae0..ed96e9027 100644 --- a/apps/server/src/api-data/rundown/rundown.parser.ts +++ b/apps/server/src/api-data/rundown/rundown.parser.ts @@ -17,13 +17,22 @@ import { OntimeMilestone, OntimeGroup, } from 'ontime-types'; -import { isObjectEmpty, generateId, getLinkedTimes, getTimeFrom, isNewLatest } from 'ontime-utils'; +import { + isObjectEmpty, + generateId, + getLinkedTimes, + getTimeFrom, + isNewLatest, + createDelay, + createEvent, + createGroup, + createMilestone, +} from 'ontime-utils'; -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'; +import { calculateDayOffset, cleanupCustomFields } from './rundown.utils.js'; import { RundownMetadata } from './rundown.types.js'; /** @@ -107,7 +116,7 @@ export function parseRundown( cleanupCustomFields(newEvent.custom, parsedCustomFields); eventIndex += 1; } else if (isOntimeDelay(event)) { - newEvent = { ...delayDef, duration: event.duration, id }; + newEvent = createDelay({ duration: event.duration, id }); } else if (isOntimeMilestone(event)) { newEvent = createMilestone({ ...event, id }); cleanupCustomFields(newEvent.custom, parsedCustomFields); @@ -134,7 +143,7 @@ export function parseRundown( cleanupCustomFields(newNestedEvent.custom, parsedCustomFields); eventIndex += 1; } else if (isOntimeDelay(nestedEvent)) { - newNestedEvent = { ...delayDef, duration: nestedEvent.duration, id: nestedEventId }; + newNestedEvent = createDelay({ duration: nestedEvent.duration, id: nestedEventId }); newNestedEvent.parent = event.id; } else if (isOntimeMilestone(nestedEvent)) { newNestedEvent = createMilestone({ ...nestedEvent, id: nestedEventId }); diff --git a/apps/server/src/api-data/rundown/rundown.utils.ts b/apps/server/src/api-data/rundown/rundown.utils.ts index 2945a3b84..811402e27 100644 --- a/apps/server/src/api-data/rundown/rundown.utils.ts +++ b/apps/server/src/api-data/rundown/rundown.utils.ts @@ -22,19 +22,16 @@ import { dayInMs, generateId, getCueCandidate, + createDelay, + createEvent, + createGroup, + createMilestone, + makeString, validateEndAction, validateTimerType, validateTimes, } from 'ontime-utils'; -import { - event as eventDef, - group as groupDef, - delay as delayDef, - milestone as milestoneDef, -} from '../../models/eventsDefinition.js'; -import { makeString } from '../../utils/parserUtils.js'; - import { RundownMetadata } from './rundown.types.js'; type CompleteEntry = @@ -61,7 +58,7 @@ export function generateEvent< const id = eventData.id || getUniqueId(rundown); if (isOntimeDelay(eventData)) { - return { ...delayDef, duration: eventData.duration ?? 0, id } as CompleteEntry; + return createDelay({ duration: eventData.duration ?? 0, id }) as CompleteEntry; } // TODO(v4): allow user to provide a larger patch of the group entry @@ -198,74 +195,6 @@ export function applyPatchToEntry(eventFromRundown: OntimeEntry, patch: Partial< return { ...eventFromRundown, ...patch } as OntimeDelay; } -/** - * @description Enforces formatting for events - * @param {object} eventArgs - attributes of event - * @param {number} eventIndex - can be a string when we pass the a suggested cue name - * @returns {object|null} - formatted object or null in case is invalid - */ -export const createEvent = (eventArgs: Partial, eventIndex: number | string): OntimeEvent | null => { - if (Object.keys(eventArgs).length === 0) { - return null; - } - - const cue = typeof eventIndex === 'number' ? String(eventIndex + 1) : eventIndex; - - const baseEvent = { - id: eventArgs?.id ?? generateId(), - cue, - ...eventDef, - }; - const event = createEventPatch(baseEvent, eventArgs); - return event; -}; - -/** - * Creates a new group from an optional patch - */ -export function createGroup(patch?: Partial): OntimeGroup { - if (!patch) { - return { ...groupDef, id: generateId() }; - } - - return { - id: patch.id ?? generateId(), - type: SupportedEntry.Group, - title: patch.title ?? '', - note: patch.note ?? '', - entries: patch.entries ?? [], - targetDuration: patch.targetDuration ?? null, - colour: makeString(patch.colour, ''), - custom: patch.custom ?? {}, - revision: 0, - timeStart: null, - timeEnd: null, - duration: 0, - isFirstLinked: false, - }; -} - -/** - * Creates a new milestone from an optional patch - */ -export function createMilestone(patch?: Partial): OntimeMilestone { - if (!patch) { - return { ...milestoneDef, id: generateId() }; - } - - return { - id: patch.id ?? generateId(), - type: SupportedEntry.Milestone, - cue: patch.cue ?? '', - title: patch.title ?? '', - note: patch.note ?? '', - colour: makeString(patch.colour, ''), - custom: patch.custom ?? {}, - parent: patch.parent ?? null, - revision: 0, - }; -} - /** * Function infers strategy for a patch with only partial timer data * @param end diff --git a/apps/server/src/utils/__tests__/parserUtils.test.ts b/apps/server/src/utils/__tests__/parserUtils.test.ts index ba7202cad..104d320ff 100644 --- a/apps/server/src/utils/__tests__/parserUtils.test.ts +++ b/apps/server/src/utils/__tests__/parserUtils.test.ts @@ -1,4 +1,6 @@ -import { isEmptyObject, makeString, removeUndefined } from '../parserUtils.js'; +import { makeString } from 'ontime-utils'; + +import { isEmptyObject, removeUndefined } from '../parserUtils.js'; describe('isEmptyObject()', () => { test('finds an empty object', () => { diff --git a/apps/server/src/utils/parserUtils.ts b/apps/server/src/utils/parserUtils.ts index 7edb76e22..115e087e6 100644 --- a/apps/server/src/utils/parserUtils.ts +++ b/apps/server/src/utils/parserUtils.ts @@ -1,17 +1,5 @@ export type ErrorEmitter = (message: string) => void; -/** - * @description Ensures variable is string, it skips object types - * @param val - variable to convert - * @param {string} [fallback=''] - fallback value - * @returns {string} - value as string or fallback if not possible - */ -export const makeString = (val: unknown, fallback = ''): string => { - if (typeof val === 'string') return val.trim(); - else if (val == null || val.constructor === Object) return fallback; - return val.toString().trim(); -}; - /** * @description Verifies if object is empty * @param {object} obj diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 8de7d4fe7..200f12752 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -30,6 +30,12 @@ export { swapEventData, } from './src/rundown-utils/rundownUtils.js'; export { getFirstRundown } from './src/rundown/rundown.utils.js'; +export { + event as eventDef, + group as groupDef, + milestone as milestoneDef, +} from './src/rundown-utils/entryDefinitions.js'; +export { createDelay, createEvent, createGroup, createMilestone, makeString } from './src/rundown-utils/entryUtils.js'; // time format utils export { diff --git a/apps/server/src/models/eventsDefinition.ts b/packages/utils/src/rundown-utils/entryDefinitions.ts similarity index 89% rename from apps/server/src/models/eventsDefinition.ts rename to packages/utils/src/rundown-utils/entryDefinitions.ts index c254aa813..2ea9ab0a2 100644 --- a/apps/server/src/models/eventsDefinition.ts +++ b/packages/utils/src/rundown-utils/entryDefinitions.ts @@ -1,13 +1,5 @@ -import { - EndAction, - OntimeGroup, - OntimeDelay, - OntimeEvent, - OntimeMilestone, - SupportedEntry, - TimeStrategy, - TimerType, -} from 'ontime-types'; +import type { OntimeDelay, OntimeEvent, OntimeGroup, OntimeMilestone } from 'ontime-types'; +import { EndAction, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types'; export const event: Omit = { type: SupportedEntry.Event, diff --git a/packages/utils/src/rundown-utils/entryUtils.ts b/packages/utils/src/rundown-utils/entryUtils.ts new file mode 100644 index 000000000..4e73eaea0 --- /dev/null +++ b/packages/utils/src/rundown-utils/entryUtils.ts @@ -0,0 +1,155 @@ +import type { OntimeDelay, OntimeEvent, OntimeGroup, OntimeMilestone } from 'ontime-types'; +import { SupportedEntry, TimeStrategy } from 'ontime-types'; + +import { generateId } from '../generate-id/generateId.js'; +import { validateEndAction, validateTimerType } from '../validate-events/validateEvent.js'; +import { validateTimes } from '../validate-times/validateTimes.js'; +import { event as eventDef, group as groupDef, milestone as milestoneDef } from './entryDefinitions.js'; + +/** + * @description Ensures variable is string, it skips object types + * @param val - variable to convert + * @param {string} [fallback=''] - fallback value + * @returns {string} - value as string or fallback if not possible + */ +export const makeString = (val: unknown, fallback = ''): string => { + if (typeof val === 'string') return val.trim(); + else if (val == null || val.constructor === Object) return fallback; + return val.toString().trim(); +}; + +/** + * Creates a new delay from an optional patch + */ +export function createDelay(patch?: Partial): OntimeDelay { + return { + type: SupportedEntry.Delay, + id: patch?.id ?? generateId(), + duration: patch?.duration ?? 0, + parent: patch?.parent ?? null, + }; +} + +/** + * @description Enforces formatting for events + * @param {object} eventArgs - attributes of event + * @param {number} eventIndex - can be a string when we pass the a suggested cue name + * @returns {object|null} - formatted object or null in case is invalid + */ +export const createEvent = (eventArgs: Partial, eventIndex: number | string = ''): OntimeEvent | null => { + if (Object.keys(eventArgs).length === 0) { + return null; + } + + const cue = typeof eventIndex === 'number' ? String(eventIndex + 1) : eventIndex; + + const baseEvent = { + id: eventArgs?.id ?? generateId(), + cue, + ...eventDef, + }; + const event = createEventPatch(baseEvent, eventArgs); + return event; +}; + +/** + * Creates a new group from an optional patch + */ +export function createGroup(patch?: Partial): OntimeGroup { + if (!patch) { + return { ...groupDef, id: generateId() }; + } + + return { + id: patch.id ?? generateId(), + type: SupportedEntry.Group, + title: patch.title ?? '', + note: patch.note ?? '', + entries: patch.entries ?? [], + targetDuration: patch.targetDuration ?? null, + colour: makeString(patch.colour, ''), + custom: patch.custom ?? {}, + revision: 0, + timeStart: null, + timeEnd: null, + duration: 0, + isFirstLinked: false, + }; +} + +/** + * Creates a new milestone from an optional patch + */ +export function createMilestone(patch?: Partial): OntimeMilestone { + if (!patch) { + return { ...milestoneDef, id: generateId() }; + } + + return { + id: patch.id ?? generateId(), + type: SupportedEntry.Milestone, + cue: patch.cue ?? '', + title: patch.title ?? '', + note: patch.note ?? '', + colour: makeString(patch.colour, ''), + custom: patch.custom ?? {}, + parent: patch.parent ?? null, + revision: 0, + }; +} + +/** + * Function infers strategy for a patch with only partial timer data + */ +function inferStrategy(end: unknown, duration: unknown, fallback: TimeStrategy): TimeStrategy { + if (end && !duration) { + return TimeStrategy.LockEnd; + } + + if (!end && duration) { + return TimeStrategy.LockDuration; + } + return fallback; +} + +function createEventPatch(originalEvent: OntimeEvent, patchEvent: Partial): OntimeEvent { + if (Object.keys(patchEvent).length === 0) { + return originalEvent; + } + + const { timeStart, timeEnd, duration, timeStrategy } = validateTimes( + patchEvent?.timeStart ?? originalEvent.timeStart, + patchEvent?.timeEnd ?? originalEvent.timeEnd, + patchEvent?.duration ?? originalEvent.duration, + patchEvent?.timeStrategy ?? inferStrategy(patchEvent?.timeEnd, patchEvent?.duration, originalEvent.timeStrategy), + ); + + return { + id: originalEvent.id, + type: SupportedEntry.Event, + flag: typeof patchEvent.flag === 'boolean' ? patchEvent.flag : originalEvent.flag, + title: makeString(patchEvent.title, originalEvent.title), + timeStart, + timeEnd, + duration, + timeStrategy, + linkStart: typeof patchEvent.linkStart === 'boolean' ? patchEvent.linkStart : originalEvent.linkStart, + endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction), + timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType), + countToEnd: typeof patchEvent.countToEnd === 'boolean' ? patchEvent.countToEnd : originalEvent.countToEnd, + skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip, + note: makeString(patchEvent.note, originalEvent.note), + colour: makeString(patchEvent.colour, originalEvent.colour), + delay: originalEvent.delay, // is regenerated if timer related data is changed + dayOffset: originalEvent.dayOffset, // is regenerated if timer related data is changed + gap: originalEvent.gap, // is regenerated if timer related data is changed + // short circuit empty string + cue: makeString(patchEvent.cue ?? null, originalEvent.cue), + parent: originalEvent.parent, + revision: originalEvent.revision, + timeWarning: patchEvent.timeWarning ?? originalEvent.timeWarning, + timeDanger: patchEvent.timeDanger ?? originalEvent.timeDanger, + custom: { ...originalEvent.custom, ...patchEvent.custom }, + triggers: patchEvent.triggers ?? originalEvent.triggers, + }; +}