fix: api change custom fields (#1888)

* fix: validate nested custom path

* chore: add test
This commit is contained in:
Alex Christoffer Rasmussen
2025-11-27 09:15:02 +01:00
committed by GitHub
parent 80ec2d1186
commit f128f7acd2
3 changed files with 105 additions and 4 deletions
@@ -0,0 +1,91 @@
import { EntryCustomFields, OntimeEvent } from 'ontime-types';
import { isValidChangeProperty } from '../integration.utils.js';
describe('isValidChangeProperty()', () => {
test('correct value and property', () => {
const testEvent = {
id: 'test',
duration: 111,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'duration', 123)).toBeTruthy();
});
test('correct property and undefined value', () => {
const testEvent = {
id: 'test',
duration: 111,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'duration', undefined)).toBeFalsy();
});
test('missing property and undefined value', () => {
const testEvent = {
id: 'test',
duration: 111,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'missing', 123)).toBeFalsy();
});
test('non existing custom value', () => {
const testEvent = {
id: 'test',
duration: 111,
custom: {
field: 'test',
} as EntryCustomFields,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeFalsy();
});
test('existing custom value', () => {
const testEvent = {
id: 'test',
duration: 111,
custom: {
test: 'test',
} as EntryCustomFields,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeTruthy();
});
test('empty custom definition', () => {
const testEvent = {
id: 'test',
duration: 111,
custom: {
test: 'test',
} as EntryCustomFields,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'custom:', 123)).toBeFalsy();
});
test('build-in in custom', () => {
const testEvent = {
id: 'test',
duration: 111,
custom: {
test: 'test',
} as EntryCustomFields,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'custom:toString', 123)).toBeFalsy();
});
test('build-in in top object', () => {
const testEvent = {
id: 'test',
duration: 111,
custom: {
test: 'test',
} as EntryCustomFields,
} as OntimeEvent;
expect(isValidChangeProperty(testEvent, 'toString', 123)).toBeFalsy();
});
});
@@ -18,7 +18,7 @@ import { validateMessage, validateTimerMessage } from '../services/message-servi
import { runtimeService } from '../services/runtime-service/runtime.service.js';
import { eventStore } from '../stores/EventStore.js';
import * as assert from '../utils/assert.js';
import { parseProperty } from './integration.utils.js';
import { parseProperty, isValidChangeProperty } from './integration.utils.js';
import { socket } from '../adapters/WebsocketAdapter.js';
import { throttle } from '../utils/throttle.js';
import { coerceEnum } from '../utils/coerceType.js';
@@ -77,10 +77,9 @@ const actionHandlers: Record<ApiActionTag, ActionHandler> = {
let shouldThrottle = false;
Object.entries(data).forEach(([property, value]) => {
if (typeof property !== 'string' || value === undefined || !(property in targetEntry)) {
if (!isValidChangeProperty(targetEntry, property, value)) {
throw new Error('Invalid property or value');
}
// parseProperty is async because of the data lock
const newObjectProperty = parseProperty(property, value);
const key = Object.keys(newObjectProperty)[0];
shouldThrottle = shouldThrottle || willCauseRegeneration(key);
@@ -1,4 +1,4 @@
import { EndAction, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types';
import { EndAction, OntimeEntry, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types';
import { maxDuration } from 'ontime-utils';
import { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js';
@@ -60,3 +60,14 @@ export function parseProperty(property: string, value: unknown) {
const parserFn = propertyConversion[property];
return { [property]: parserFn(value) };
}
export function isValidChangeProperty(target: OntimeEntry, property: string, value: unknown): boolean {
if (typeof property !== 'string') return false;
if (value === undefined) return false;
if (property.startsWith('custom:') && 'custom' in target) {
const customProperty = property.slice('custom:'.length);
if (!customProperty) return false;
return Object.hasOwn(target.custom, customProperty);
}
return Object.hasOwn(target, property);
}