diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index 9ea5fc121..5c45a0b2e 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -3,12 +3,12 @@ import { Alias, DatabaseModel, GetInfo, - HTTPSettings, + HttpSettings, OntimeRundown, OSCSettings, + OscSubscription, ProjectData, Settings, - Subscription, UserFields, ViewSettings, } from 'ontime-types'; @@ -109,7 +109,7 @@ export async function getOSC(): Promise { * @description HTTP request to retrieve http settings * @return {Promise} */ -export async function getHTTP(): Promise { +export async function getHTTP(): Promise { const res = await axios.get(`${ontimeURL}/http`); return res.data; } @@ -118,7 +118,7 @@ export async function getHTTP(): Promise { * @description HTTP request to mutate http settings * @return {Promise} */ -export async function postHTTP(data: HTTPSettings) { +export async function postHTTP(data: HttpSettings) { return axios.post(`${ontimeURL}/http`, data); } @@ -134,7 +134,7 @@ export async function postOSC(data: OSCSettings) { * @description HTTP request to mutate osc subscriptions * @return {Promise} */ -export async function postOscSubscriptions(data: Subscription) { +export async function postOscSubscriptions(data: OscSubscription) { return axios.post(`${ontimeURL}/osc-subscriptions`, data); } diff --git a/apps/client/src/common/hooks-query/useHttpSettings.ts b/apps/client/src/common/hooks-query/useHttpSettings.ts index 47e1bfa94..6435eb4fb 100644 --- a/apps/client/src/common/hooks-query/useHttpSettings.ts +++ b/apps/client/src/common/hooks-query/useHttpSettings.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { HTTPSettings } from 'ontime-types'; +import { HttpSettings } from 'ontime-types'; import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { HTTP_SETTINGS } from '../api/apiConstants'; @@ -20,7 +20,7 @@ export function useHttpSettings() { }); // we need to jump through some hoops because of the type op port - return { data: data! as unknown as HTTPSettings, status, isFetching, isError, refetch }; + return { data: data! as unknown as HttpSettings, status, isFetching, isError, refetch }; } export function usePostHttpSettings() { diff --git a/apps/client/src/common/models/Http.ts b/apps/client/src/common/models/Http.ts index 262b5b96a..f2393a1dd 100644 --- a/apps/client/src/common/models/Http.ts +++ b/apps/client/src/common/models/Http.ts @@ -1,6 +1,6 @@ -import { HTTPSettings } from 'ontime-types'; +import { HttpSettings } from 'ontime-types'; -export const httpPlaceholder: HTTPSettings = { +export const httpPlaceholder: HttpSettings = { enabledOut: false, subscriptions: { onLoad: [], diff --git a/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx b/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx index c740dd735..450d4d232 100644 --- a/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx +++ b/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Switch } from '@chakra-ui/react'; -import type { HTTPSettings } from 'ontime-types'; +import type { HttpSettings } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types'; import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings'; @@ -24,7 +24,7 @@ export default function HttpIntegration() { register, reset, formState: { isSubmitting, isDirty, isValid }, - } = useForm({ + } = useForm({ mode: 'onBlur', defaultValues: data, values: data, @@ -45,9 +45,9 @@ export default function HttpIntegration() { reset(data); }; - const onSubmit = async (values: HTTPSettings) => { + const onSubmit = async (values: HttpSettings) => { try { - const newSettings: HTTPSettings = { + const newSettings: HttpSettings = { enabledOut: Boolean(values.enabledOut), subscriptions: { onLoad: values.subscriptions.onLoad ?? [], @@ -59,8 +59,6 @@ export default function HttpIntegration() { }, }; - console.log('debug will submit', newSettings); - await mutateAsync(newSettings); } catch (error) { emitError(`Error setting HTML: ${error}`); diff --git a/apps/client/src/features/modals/integration-modal/http/HttpSubscriptionRow.tsx b/apps/client/src/features/modals/integration-modal/http/HttpSubscriptionRow.tsx index 424382820..bd01eec05 100644 --- a/apps/client/src/features/modals/integration-modal/http/HttpSubscriptionRow.tsx +++ b/apps/client/src/features/modals/integration-modal/http/HttpSubscriptionRow.tsx @@ -2,7 +2,7 @@ import { Control, useFieldArray, UseFormRegister } from 'react-hook-form'; import { Button, IconButton, Input, Switch } from '@chakra-ui/react'; import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; -import { HTTPSettings, TimerLifeCycle } from 'ontime-types'; +import { HttpSettings, TimerLifeCycle } from 'ontime-types'; import { useEmitLog } from '../../../../common/stores/logger'; import { startsWithHttp } from '../../../../common/utils/regex'; @@ -16,8 +16,8 @@ interface SubscriptionRowProps { subtitle: string; visible: boolean; setShowSection: (cycle: TimerLifeCycle) => void; - register: UseFormRegister; - control: Control; + register: UseFormRegister; + control: Control; placeholder: string; } diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index a05ecdeec..c6cfb7187 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,4 +1,4 @@ -import { HTTPSettings, LogOrigin, OSCSettings } from 'ontime-types'; +import { HttpSettings, LogOrigin, OSCSettings } from 'ontime-types'; import 'dotenv/config'; import express from 'express'; @@ -194,7 +194,7 @@ export const startOSCServer = async (overrideConfig = null) => { /** * starts integrations */ -export const startIntegrations = async (config?: { osc: OSCSettings; http: HTTPSettings }) => { +export const startIntegrations = async (config?: { osc: OSCSettings; http: HttpSettings }) => { checkStart(OntimeStartOrder.InitIO); const { osc, http } = config ?? DataProvider.getData(); diff --git a/apps/server/src/controllers/ontimeController.ts b/apps/server/src/controllers/ontimeController.ts index cf80ec66b..a5a6d89a0 100644 --- a/apps/server/src/controllers/ontimeController.ts +++ b/apps/server/src/controllers/ontimeController.ts @@ -1,5 +1,5 @@ import { LogOrigin } from 'ontime-types'; -import type { Alias, DatabaseModel, GetInfo, HTTPSettings, ProjectData } from 'ontime-types'; +import type { Alias, DatabaseModel, GetInfo, HttpSettings, ProjectData } from 'ontime-types'; import { RequestHandler, Request, Response } from 'express'; import fs from 'fs'; @@ -335,7 +335,7 @@ export const postOscSubscriptions = async (req, res) => { }; // Create controller for GET request to '/ontime/http' -export const getHTTP = async (_req, res: Response) => { +export const getHTTP = async (_req, res: Response) => { const http = DataProvider.getHttp(); res.status(200).send(http); }; diff --git a/apps/server/src/controllers/ontimeController.validate.ts b/apps/server/src/controllers/ontimeController.validate.ts index 775e7cbad..be0d541eb 100644 --- a/apps/server/src/controllers/ontimeController.validate.ts +++ b/apps/server/src/controllers/ontimeController.validate.ts @@ -1,5 +1,9 @@ import { body, check, validationResult } from 'express-validator'; -import { validateOscObject, validateSubscriptionEntry } from '../utils/parserFunctions.js'; +import { + validateHttpSubscriptionObject, + validateOscSubscriptionObject, + validateOscSubscriptionCycle, +} from '../utils/parserFunctions.js'; /** * @description Validates object for POST /ontime/views @@ -82,7 +86,7 @@ export const validateOSC = [ body('enabledOut').exists().isBoolean(), body('subscriptions') .isObject() - .custom((value) => validateOscObject(value)), + .custom((value) => validateOscSubscriptionObject(value)), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); @@ -97,7 +101,7 @@ export const validateHTTP = [ body('enabledOut').exists().isBoolean(), body('subscriptions') .isObject() - .custom((value) => validateOscObject(value)), + .custom((value) => validateHttpSubscriptionObject(value)), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); @@ -108,25 +112,25 @@ export const validateHTTP = [ /** * @description Validates object for POST /ontime/osc-subscriptions */ -export const validateSubscription = [ +export const validateOscSubscription = [ body('onLoad') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), body('onStart') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), body('onPause') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), body('onStop') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), body('onUpdate') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), body('onFinish') .isArray() - .custom((value) => validateSubscriptionEntry(value)), + .custom((value) => validateOscSubscriptionCycle(value)), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); diff --git a/apps/server/src/routes/ontimeRouter.ts b/apps/server/src/routes/ontimeRouter.ts index 4ebc3997d..bc5939ecb 100644 --- a/apps/server/src/routes/ontimeRouter.ts +++ b/apps/server/src/routes/ontimeRouter.ts @@ -26,7 +26,7 @@ import { import { validateAliases, validateOSC, - validateSubscription, + validateOscSubscriptionObject, validatePatchProjectFile, validateSettings, validateUserFields, @@ -86,7 +86,7 @@ router.get('/osc', getOSC); router.post('/osc', validateOSC, postOSC); // create route between controller and '/ontime/osc-subscriptions' endpoint -router.post('/osc-subscriptions', validateSubscription, postOscSubscriptions); +router.post('/osc-subscriptions', validateOscSubscriptionObject, postOscSubscriptions); // create route between controller and '/ontime/http' endpoint router.get('/http', getHTTP); diff --git a/apps/server/src/services/integration-service/HttpIntegration.ts b/apps/server/src/services/integration-service/HttpIntegration.ts index 5e6ddde33..5912b05f7 100644 --- a/apps/server/src/services/integration-service/HttpIntegration.ts +++ b/apps/server/src/services/integration-service/HttpIntegration.ts @@ -1,11 +1,11 @@ //TODO: cleanup stuff left over from OSC copy import http from 'node:http'; -import { HTTPSettings, LogOrigin, Subscription } from 'ontime-types'; +import { HttpSettings, HttpSubscription, HttpSubscriptionOptions, LogOrigin } from 'ontime-types'; import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import { parseTemplateNested } from './integrationUtils.js'; import { dbModel } from '../../models/dataModel.js'; -import { validateHttpObject } from '../../utils/parserFunctions.js'; +import { validateHttpSubscriptionEntry } from '../../utils/parserFunctions.js'; import { logger } from '../../classes/Logger.js'; import { URL } from 'node:url'; @@ -15,9 +15,9 @@ type Action = TimerLifeCycleKey | string; * @description Class contains logic towards outgoing HTTP communications * @class */ -export class HttpIntegration implements IIntegration { +export class HttpIntegration implements IIntegration { protected httpAgent: null | http.Agent; - subscriptions: Subscription; + subscriptions: HttpSubscription; constructor() { this.httpAgent = null; @@ -27,7 +27,7 @@ export class HttpIntegration implements IIntegration { /** * Initializes httpClient */ - init(config: HTTPSettings) { + init(config: HttpSettings) { const { subscriptions, enabledOut } = config; if (!enabledOut) { @@ -60,8 +60,9 @@ export class HttpIntegration implements IIntegration { } } - initSubscriptions(subscriptionOptions: Subscription) { - if (validateHttpObject(subscriptionOptions)) { + initSubscriptions(subscriptionOptions: HttpSubscription) { + // TODO: here we have to validate the entire subscription object + if (validateHttpSubscriptionEntry(subscriptionOptions)) { this.subscriptions = { ...subscriptionOptions }; } } diff --git a/apps/server/src/services/integration-service/IIntegration.ts b/apps/server/src/services/integration-service/IIntegration.ts index fc537de0e..4e4d57b15 100644 --- a/apps/server/src/services/integration-service/IIntegration.ts +++ b/apps/server/src/services/integration-service/IIntegration.ts @@ -2,8 +2,8 @@ import { TimerLifeCycle, Subscription } from 'ontime-types'; export type TimerLifeCycleKey = keyof typeof TimerLifeCycle; -export default interface IIntegration { - subscriptions: Subscription; +export default interface IIntegration { + subscriptions: Subscription; init: (config: unknown) => OperationReturn; dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn; emit: (...args: unknown[]) => unknown; diff --git a/apps/server/src/services/integration-service/IntegrationService.ts b/apps/server/src/services/integration-service/IntegrationService.ts index aad3296d8..944409eab 100644 --- a/apps/server/src/services/integration-service/IntegrationService.ts +++ b/apps/server/src/services/integration-service/IntegrationService.ts @@ -2,17 +2,17 @@ import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import { eventStore } from '../../stores/EventStore.js'; class IntegrationService { - private integrations: IIntegration[]; + private integrations: IIntegration[]; constructor() { this.integrations = []; } - register(integrationService: IIntegration) { + register(integrationService: IIntegration) { this.integrations.push(integrationService); } - unregister(integrationService: IIntegration) { + unregister(integrationService: IIntegration) { this.integrations = this.integrations.filter((int) => int !== integrationService); } diff --git a/apps/server/src/services/integration-service/OscIntegration.ts b/apps/server/src/services/integration-service/OscIntegration.ts index b6e5621d0..6cf8fe958 100644 --- a/apps/server/src/services/integration-service/OscIntegration.ts +++ b/apps/server/src/services/integration-service/OscIntegration.ts @@ -1,11 +1,11 @@ import { ArgumentType, Client, Message } from 'node-osc'; -import { OSCSettings, Subscription } from 'ontime-types'; +import { OSCSettings, OscSubscription, OscSubscriptionOptions } from 'ontime-types'; import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import { parseTemplateNested } from './integrationUtils.js'; import { isObject } from '../../utils/varUtils.js'; import { dbModel } from '../../models/dataModel.js'; -import { validateOscObject } from '../../utils/parserFunctions.js'; +import { validateOscSubscriptionObject } from '../../utils/parserFunctions.js'; type Action = TimerLifeCycleKey | string; @@ -13,9 +13,9 @@ type Action = TimerLifeCycleKey | string; * @description Class contains logic towards outgoing OSC communications * @class */ -export class OscIntegration implements IIntegration { +export class OscIntegration implements IIntegration { protected oscClient: null | Client; - subscriptions: Subscription; + subscriptions: OscSubscription; constructor() { this.oscClient = null; @@ -65,8 +65,8 @@ export class OscIntegration implements IIntegration { } } - initSubscriptions(subscriptionOptions: Subscription) { - if (validateOscObject(subscriptionOptions)) { + initSubscriptions(subscriptionOptions: OscSubscription) { + if (validateOscSubscriptionObject(subscriptionOptions)) { this.subscriptions = { ...subscriptionOptions }; } } diff --git a/apps/server/src/utils/__tests__/parserFunctions.test.js b/apps/server/src/utils/__tests__/parserFunctions.test.js deleted file mode 100644 index b95cbd644..000000000 --- a/apps/server/src/utils/__tests__/parserFunctions.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { validateOscObject } from '../parserFunctions.ts'; - -test('validateOscSubscription()', () => { - it('should return true when given a valid OscSubscription', () => { - const validSubscription = { - onLoad: [{ id: '1', message: 'test', enabled: true }], - onStart: [{ id: '2', message: 'test', enabled: false }], - onPause: [{ id: '3', message: 'test', enabled: true }], - onStop: [{ id: '4', message: 'test', enabled: false }], - onUpdate: [{ id: '5', message: 'test', enabled: true }], - onFinish: [{ id: '6', message: 'test', enabled: false }], - }; - - const result = validateOscObject(validSubscription); - - expect(result).toBe(true); - }); - - it('should return false when given undefined', () => { - const result = validateOscObject(undefined); - expect(result).toBe(false); - }); - - it('should return false when given null', () => { - const result = validateOscObject(null); - expect(result).toBe(false); - }); - - it('should return false when given an empty object', () => { - const result = validateOscObject({}); - expect(result).toBe(false); - }); - - it('should return false when given an empty array', () => { - const result = validateOscObject([]); - expect(result).toBe(false); - }); - - it('should return false when given an object that is not an OscSubscription', () => { - const invalidObject = { foo: 'bar' }; - - const result = validateOscObject(invalidObject); - - expect(result).toBe(false); - }); - - it('should return false when given an OscSubscription with a missing property', () => { - const invalidSubscription = { - onLoad: [{ id: '1', message: 'test', enabled: true }], - onStart: [{ id: '2', message: 'test', enabled: false }], - onPause: [{ id: '3', message: 'test', enabled: true }], - // Missing onStop - onUpdate: [{ id: '5', message: 'test', enabled: true }], - onFinish: [{ id: '6', message: 'test', enabled: false }], - }; - - const result = validateOscObject(invalidSubscription); - - expect(result).toBe(false); - }); - - it('should return false when given an OscSubscription with an invalid property value', () => { - const invalidSubscription = { - onLoad: [{ id: '1', message: 'test', enabled: true }], - onStart: [{ id: '2', message: 'test', enabled: false }], - onPause: [{ id: '3', message: 'test', enabled: true }], - onStop: [{ id: '4', message: 'test', enabled: false }], - onUpdate: [{ id: '5', message: 'test', enabled: true }], - onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }], - }; - - const result = validateOscObject(invalidSubscription); - - expect(result).toBe(false); - }); - - it('should return true if the message field is empty', () => { - const invalidSubscription = { - onLoad: [{ id: '1', message: 'test', enabled: true }], - onStart: [{ id: '2', message: '', enabled: false }], - onPause: [{ id: '3', message: '', enabled: true }], - onStop: [{ id: '4', message: 'test', enabled: false }], - onUpdate: [{ id: '5', message: 'test', enabled: true }], - onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }], - }; - - const result = validateOscObject(invalidSubscription); - - expect(result).toBe(true); - }); -}); diff --git a/apps/server/src/utils/__tests__/parserFunctions.test.ts b/apps/server/src/utils/__tests__/parserFunctions.test.ts new file mode 100644 index 000000000..10970592f --- /dev/null +++ b/apps/server/src/utils/__tests__/parserFunctions.test.ts @@ -0,0 +1,161 @@ +import { HttpSubscription, OscSubscription } from 'ontime-types'; +import { + validateOscSubscriptionObject, + validateOscSubscriptionCycle, + validateHttpSubscriptionCycle, + validateHttpSubscriptionObject, +} from '../parserFunctions.js'; + +describe('validateOscSubscriptionCycle()', () => { + it('should return false when given an OscSubscription with an invalid property value', () => { + const invalidEntry = [{ message: 'test', enabled: 'not a boolean' }]; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionCycle(invalidEntry); + expect(result).toBe(false); + }); +}); + +describe('validateOscSubscriptionObject()', () => { + it('should return true when given a valid OscSubscription', () => { + const validSubscription: OscSubscription = { + onLoad: [{ message: 'test', enabled: true }], + onStart: [{ message: 'test', enabled: false }], + onPause: [{ message: 'test', enabled: true }], + onStop: [{ message: 'test', enabled: false }], + onUpdate: [{ message: 'test', enabled: true }], + onFinish: [{ message: 'test', enabled: false }], + }; + + const result = validateOscSubscriptionObject(validSubscription); + expect(result).toBe(true); + }); + + it('should return false when given undefined', () => { + const result = validateOscSubscriptionObject(undefined); + expect(result).toBe(false); + }); + + it('should return false when given null', () => { + const result = validateOscSubscriptionObject(null); + expect(result).toBe(false); + }); + + it('should return false when given an empty object', () => { + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionObject({}); + expect(result).toBe(false); + }); + + it('should return false when given an empty array', () => { + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionObject([]); + expect(result).toBe(false); + }); + + it('should return false when given an object that is not an OscSubscription', () => { + const invalidObject = { foo: 'bar' }; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionObject(invalidObject); + expect(result).toBe(false); + }); + + it('should return false when given an OscSubscription with a missing property', () => { + const invalidSubscription = { + onLoad: [{ message: 'test', enabled: true }], + onStart: [{ message: 'test', enabled: false }], + onPause: [{ message: 'test', enabled: true }], + // Missing onStop + onUpdate: [{ message: 'test', enabled: true }], + onFinish: [{ message: 'test', enabled: false }], + }; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionObject(invalidSubscription); + expect(result).toBe(false); + }); +}); + +describe('validateHttpSubscriptionCycle()', () => { + it('should return false when given an HttpSubscription with an invalid property value', () => { + const invalidBoolean = [{ message: 'http://', enabled: 'not a boolean' }]; + const invalidHttp = [{ message: 'test', enabled: true }]; + const noHttps = [{ message: 'https://test', enabled: true }]; + const noEmpty = [{ message: '', enabled: true }]; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + expect(validateHttpSubscriptionCycle(invalidBoolean)).toBe(false); + + expect(validateHttpSubscriptionCycle(invalidHttp)).toBe(false); + expect(validateHttpSubscriptionCycle(noHttps)).toBe(false); + expect(validateHttpSubscriptionCycle(noEmpty)).toBe(false); + }); + it('should return true when given an HttpSubscription matches definition', () => { + const validHttp = [{ message: 'http://', enabled: true }]; + + const result = validateHttpSubscriptionCycle(validHttp); + expect(result).toBe(true); + }); +}); + +describe('validateHttpSubscriptionObject()', () => { + it('should return true when given a valid HttpSubscription', () => { + const validSubscription: HttpSubscription = { + onLoad: [{ message: 'http://', enabled: true }], + onStart: [{ message: 'http://', enabled: false }], + onPause: [{ message: 'http://', enabled: true }], + onStop: [{ message: 'http://', enabled: false }], + onUpdate: [{ message: 'http://', enabled: true }], + onFinish: [{ message: 'http://', enabled: false }], + }; + + const result = validateHttpSubscriptionObject(validSubscription); + expect(result).toBe(true); + }); + + it('should return false when given undefined', () => { + const result = validateHttpSubscriptionObject(undefined); + expect(result).toBe(false); + }); + + it('should return false when given null', () => { + const result = validateHttpSubscriptionObject(null); + expect(result).toBe(false); + }); + + it('should return false when given an empty object', () => { + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateOscSubscriptionObject({}); + expect(result).toBe(false); + }); + + it('should return false when given an empty array', () => { + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateHttpSubscriptionObject([]); + expect(result).toBe(false); + }); + + it('should return false when given an object that is not an HttpSubscription', () => { + const invalidObject = { foo: 'bar' }; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateHttpSubscriptionObject(invalidObject); + expect(result).toBe(false); + }); + + it('should return false when given an HttpSubscription with a missing property', () => { + const invalidSubscription = { + onLoad: [{ message: 'http://', enabled: true }], + onStart: [{ message: 'http://', enabled: false }], + onPause: [{ message: 'http://', enabled: true }], + // Missing onStop + onUpdate: [{ message: 'http://', enabled: true }], + onFinish: [{ message: 'http://', enabled: false }], + }; + + // @ts-expect-error -- since this comes from the client, we check things that typescript would have caught + const result = validateHttpSubscriptionObject(invalidSubscription); + expect(result).toBe(false); + }); +}); diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index 1aac6d486..985017a8f 100644 --- a/apps/server/src/utils/parserFunctions.ts +++ b/apps/server/src/utils/parserFunctions.ts @@ -2,15 +2,17 @@ import { generateId } from 'ontime-utils'; import { Alias, OntimeRundown, - HTTPSettings, + HttpSettings, OSCSettings, - Subscription, - SubscriptionOptions, ProjectData, Settings, TimerLifeCycle, UserFields, ViewSettings, + OscSubscription, + HttpSubscription, + OscSubscriptionOptions, + HttpSubscriptionOptions, } from 'ontime-types'; import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js'; @@ -160,12 +162,12 @@ export const parseViewSettings = (data): ViewSettings => { }; /** - * Parses and validates subscription entry + * Parses and validates OSC subscription cycle options * @param data */ -export const validateSubscriptionEntry = (data: SubscriptionOptions): boolean => { - for (const subscription in data) { - if (typeof data[subscription].message !== 'string' || typeof data[subscription].enabled !== 'boolean') { +export const validateOscSubscriptionCycle = (data: OscSubscriptionOptions[]): boolean => { + for (const subscriptionOption of data) { + if (typeof subscriptionOption.message !== 'string' || typeof subscriptionOption.enabled !== 'boolean') { return false; } } @@ -173,22 +175,23 @@ export const validateSubscriptionEntry = (data: SubscriptionOptions): boolean => }; /** - * Parses and validates subscription object + * Parses and validates OSC subscription object * @param data */ -export const validateOscObject = (data: Subscription): boolean => { +export const validateOscSubscriptionObject = (data: OscSubscription): boolean => { if (!data) { return false; } + const timerKeys = Object.keys(TimerLifeCycle); for (const key of timerKeys) { + // must contains all keys and be an array if (!(key in data) || !Array.isArray(data[key])) { return false; } - for (const subscription of data[key]) { - if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') { - return false; - } + const isValid = validateOscSubscriptionCycle(data[key]); + if (!isValid) { + return false; } } return true; @@ -202,7 +205,7 @@ export const parseOsc = (data: { osc?: Partial }): OSCSettings => { console.log('Found OSC definition, importing...'); const loadedConfig = data.osc || {}; - const validatedSubscriptions = validateOscObject(loadedConfig.subscriptions) + const validatedSubscriptions = validateOscSubscriptionObject(loadedConfig.subscriptions) ? loadedConfig.subscriptions : dbModel.osc.subscriptions; @@ -218,22 +221,36 @@ export const parseOsc = (data: { osc?: Partial }): OSCSettings => { }; /** - * Parses and validates subscription object + * Parses and validates HTTP subscription cycle options * @param data */ -export const validateHttpObject = (data: Subscription): boolean => { +export const validateHttpSubscriptionCycle = (data: HttpSubscriptionOptions[]): boolean => { + for (const subscriptionOption of data) { + const isHttp = subscriptionOption.message?.startsWith('http://'); + if (typeof subscriptionOption.message !== 'string' || !isHttp || typeof subscriptionOption.enabled !== 'boolean') { + return false; + } + } + return true; +}; + +/** + * Parses and validates HTTP subscription object + * @param data + */ +export const validateHttpSubscriptionObject = (data: HttpSubscription): boolean => { if (!data) { return false; } const timerKeys = Object.keys(TimerLifeCycle); + // must contains all keys and be an array for (const key of timerKeys) { if (!(key in data) || !Array.isArray(data[key])) { return false; } - for (const subscription of data[key]) { - if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') { - return false; - } + const isValid = validateHttpSubscriptionCycle(data[key]); + if (!isValid) { + return false; } } return true; @@ -245,12 +262,12 @@ export const validateHttpObject = (data: Subscription): boolean => { * @param {boolean} enforce - whether to create a definition if one is missing * @returns {object} - event object data */ -export const parseHttp = (data: { http?: Partial }): HTTPSettings => { +export const parseHttp = (data: { http?: Partial }): HttpSettings => { if ('http' in data) { console.log('Found HTTP definition, importing...'); const loadedConfig = data?.http || {}; - const validatedSubscriptions = validateHttpObject(loadedConfig.subscriptions) + const validatedSubscriptions = validateHttpSubscriptionObject(loadedConfig.subscriptions) ? loadedConfig.subscriptions : dbModel.http.subscriptions; diff --git a/packages/types/src/definitions/DataModel.type.ts b/packages/types/src/definitions/DataModel.type.ts index 156f8aff1..b72509771 100644 --- a/packages/types/src/definitions/DataModel.type.ts +++ b/packages/types/src/definitions/DataModel.type.ts @@ -5,7 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js'; import { Settings } from './core/Settings.type.js'; import { UserFields } from './core/UserFields.type.js'; import { ViewSettings } from './core/Views.type.js'; -import { HTTPSettings } from '../index.js'; +import { HttpSettings } from '../index.js'; export type DatabaseModel = { rundown: OntimeRundown; @@ -15,5 +15,5 @@ export type DatabaseModel = { aliases: Alias[]; userFields: UserFields; osc: OSCSettings; - http: HTTPSettings; + http: HttpSettings; }; diff --git a/packages/types/src/definitions/core/HttpSettings.type.ts b/packages/types/src/definitions/core/HttpSettings.type.ts index 0109a5a8b..39c0898ad 100644 --- a/packages/types/src/definitions/core/HttpSettings.type.ts +++ b/packages/types/src/definitions/core/HttpSettings.type.ts @@ -1,9 +1,9 @@ import { Subscription } from './Subscription.type.js'; -type HttpSubscriptionOptions = { message: string; enabled: boolean }; +export type HttpSubscriptionOptions = { message: string; enabled: boolean }; export type HttpSubscription = Subscription; -export interface HTTPSettings { +export interface HttpSettings { enabledOut: boolean; subscriptions: HttpSubscription; } diff --git a/packages/types/src/definitions/core/OscSettings.type.ts b/packages/types/src/definitions/core/OscSettings.type.ts index e080498c2..49e9bd076 100644 --- a/packages/types/src/definitions/core/OscSettings.type.ts +++ b/packages/types/src/definitions/core/OscSettings.type.ts @@ -1,6 +1,6 @@ import { Subscription } from './Subscription.type.js'; -type OscSubscriptionOptions = { message: string; enabled: boolean }; +export type OscSubscriptionOptions = { message: string; enabled: boolean }; export type OscSubscription = Subscription; export interface OSCSettings { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 67520d8e4..d0b5ef704 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -32,9 +32,10 @@ export type { UserFields } from './definitions/core/UserFields.type.js'; export type { Subscription } from './definitions/core/Subscription.type.js'; // ---> OSC -export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js'; +export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './definitions/core/OscSettings.type.js'; + // ---> HTTP -export type { HTTPSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js'; +export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js'; // SERVER RESPONSES export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';