diff --git a/apps/server/src/api-integration/__test__/integration.utils.test.ts b/apps/server/src/api-integration/__test__/integration.utils.test.ts index 4b9c6a1d6..ea639877b 100644 --- a/apps/server/src/api-integration/__test__/integration.utils.test.ts +++ b/apps/server/src/api-integration/__test__/integration.utils.test.ts @@ -1,35 +1,52 @@ -import { EntryCustomFields, OntimeEvent } from 'ontime-types'; +import { CustomFields, EntryCustomFields, OntimeDelay, OntimeEvent } from 'ontime-types'; import { isValidChangeProperty } from '../integration.utils.js'; describe('isValidChangeProperty()', () => { - test('correct value and property', () => { + test('allows changing a valid event property with valid value', () => { const testEvent = { id: 'test', duration: 111, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'duration', 123)).toBeTruthy(); + expect(isValidChangeProperty(testEvent, 'duration', 123, {})).toBeTruthy(); }); - test('correct property and undefined value', () => { + test('forbids changing a valid event property with undefined value', () => { const testEvent = { id: 'test', duration: 111, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'duration', undefined)).toBeFalsy(); + expect(isValidChangeProperty(testEvent, 'duration', undefined, {})).toBeFalsy(); }); - test('missing property and undefined value', () => { + test('forbids changing a non-existing property', () => { const testEvent = { id: 'test', duration: 111, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'missing', 123)).toBeFalsy(); + expect(isValidChangeProperty(testEvent, 'missing', 123, {})).toBeFalsy(); }); - test('non existing custom value', () => { + test('forbids changing customValues in event without custom fields', () => { + const testDelay = { + id: 'test', + duration: 111, + } as OntimeDelay; + + const testCustomFields = { + test: { + type: 'text', + colour: '#ffffff', + label: 'Test Field', + }, + } satisfies CustomFields; + + expect(isValidChangeProperty(testDelay, 'custom:test', 123, testCustomFields)).toBeFalsy(); + }); + + test('forbids changing a non-existing custom value', () => { const testEvent = { id: 'test', duration: 111, @@ -38,10 +55,10 @@ describe('isValidChangeProperty()', () => { } as EntryCustomFields, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeFalsy(); + expect(isValidChangeProperty(testEvent, 'custom:test', 123, {})).toBeFalsy(); }); - test('existing custom value', () => { + test('allows changing an existing custom value', () => { const testEvent = { id: 'test', duration: 111, @@ -50,10 +67,18 @@ describe('isValidChangeProperty()', () => { } as EntryCustomFields, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeTruthy(); + const testCustomFields = { + test: { + type: 'text', + colour: '#ffffff', + label: 'Test Field', + }, + } satisfies CustomFields; + + expect(isValidChangeProperty(testEvent, 'custom:test', 123, testCustomFields)).toBeTruthy(); }); - test('empty custom definition', () => { + test('forbids changing a custom field with a missing value', () => { const testEvent = { id: 'test', duration: 111, @@ -62,10 +87,18 @@ describe('isValidChangeProperty()', () => { } as EntryCustomFields, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'custom:', 123)).toBeFalsy(); + const testCustomFields = { + test: { + type: 'text', + colour: '#ffffff', + label: 'Test Field', + }, + } satisfies CustomFields; + + expect(isValidChangeProperty(testEvent, 'custom:', 123, testCustomFields)).toBeFalsy(); }); - test('build-in in custom', () => { + test('forbids references to prototype properties in custom object', () => { const testEvent = { id: 'test', duration: 111, @@ -74,10 +107,18 @@ describe('isValidChangeProperty()', () => { } as EntryCustomFields, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'custom:toString', 123)).toBeFalsy(); + const testCustomFields = { + test: { + type: 'text', + colour: '#ffffff', + label: 'Test Field', + }, + } satisfies CustomFields; + + expect(isValidChangeProperty(testEvent, 'custom:toString', 123, testCustomFields)).toBeFalsy(); }); - test('build-in in top object', () => { + test('forbids references to prototype properties in object', () => { const testEvent = { id: 'test', duration: 111, @@ -86,6 +127,14 @@ describe('isValidChangeProperty()', () => { } as EntryCustomFields, } as OntimeEvent; - expect(isValidChangeProperty(testEvent, 'toString', 123)).toBeFalsy(); + const testCustomFields = { + test: { + type: 'text', + colour: '#ffffff', + label: 'Test Field', + }, + } satisfies CustomFields; + + expect(isValidChangeProperty(testEvent, 'toString', 123, testCustomFields)).toBeFalsy(); }); }); diff --git a/apps/server/src/api-integration/integration.controller.ts b/apps/server/src/api-integration/integration.controller.ts index 08ca3a044..9d9f47393 100644 --- a/apps/server/src/api-integration/integration.controller.ts +++ b/apps/server/src/api-integration/integration.controller.ts @@ -23,7 +23,7 @@ import { socket } from '../adapters/WebsocketAdapter.js'; import { coerceEnum } from '../utils/coerceType.js'; import { editEntry } from '../api-data/rundown/rundown.service.js'; import { willCauseRegeneration } from '../api-data/rundown/rundown.utils.js'; -import { getCurrentRundown } from '../api-data/rundown/rundown.dao.js'; +import { getCurrentRundown, getProjectCustomFields } from '../api-data/rundown/rundown.dao.js'; let lastRequest: Date | null = null; @@ -65,6 +65,7 @@ const actionHandlers: Record = { } const { entries } = getCurrentRundown(); + const customFields = getProjectCustomFields(); const targetEntry = entries[id]; if (!targetEntry) { @@ -77,7 +78,7 @@ const actionHandlers: Record = { let shouldThrottle = false; Object.entries(data).forEach(([property, value]) => { - if (!isValidChangeProperty(targetEntry, property, value)) { + if (!isValidChangeProperty(targetEntry, property, value, customFields)) { throw new Error('Invalid property or value'); } const newObjectProperty = parseProperty(property, value); diff --git a/apps/server/src/api-integration/integration.utils.ts b/apps/server/src/api-integration/integration.utils.ts index d2bb401c3..d33b1702b 100644 --- a/apps/server/src/api-integration/integration.utils.ts +++ b/apps/server/src/api-integration/integration.utils.ts @@ -1,8 +1,7 @@ -import { EndAction, OntimeEntry, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types'; +import { CustomFields, EndAction, OntimeEntry, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types'; import { maxDuration } from 'ontime-utils'; import { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js'; -import { getDataProvider } from '../classes/data-provider/DataProvider.js'; /** * @@ -44,13 +43,14 @@ const propertyConversion: Record unknown> = { timeEnd: (value: unknown) => clampDuration(coerceNumber(value)), }; +/** + * coerces a property of an Ontime Entry + * @throws if the value does not conform to the expected type + */ export function parseProperty(property: string, value: unknown) { if (property.startsWith('custom:')) { + // custom fields have been validated when used here const customKey = property.split(':')[1]; - const customFields = getDataProvider().getCustomFields(); - if (!(customKey in customFields)) { - throw new Error(`Custom field ${customKey} not found`); - } const parserFn = propertyConversion.custom; return { custom: { [customKey]: parserFn(value) } }; } @@ -61,13 +61,21 @@ export function parseProperty(property: string, value: unknown) { return { [property]: parserFn(value) }; } -export function isValidChangeProperty(target: OntimeEntry, property: string, value: unknown): boolean { +/** + * Checks whether a valid property - value pair are applicable to an entry + */ +export function isValidChangeProperty( + target: OntimeEntry, + property: string, + value: unknown, + customFields: CustomFields, +): boolean { if (typeof property !== 'string') return false; if (value === undefined) return false; - if (property.startsWith('custom:') && 'custom' in target) { + if (property.startsWith('custom:') && Object.hasOwn(target, 'custom')) { const customProperty = property.slice('custom:'.length); if (!customProperty) return false; - return Object.hasOwn(target.custom, customProperty); + return Object.hasOwn(customFields, customProperty); } return Object.hasOwn(target, property); }