mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
import {
|
|
AutomationBlueprint,
|
|
AutomationFilter,
|
|
AutomationOutput,
|
|
HTTPOutput,
|
|
OSCOutput,
|
|
timerLifecycleValues,
|
|
} from 'ontime-types';
|
|
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { body, oneOf, param, validationResult } from 'express-validator';
|
|
|
|
import * as assert from '../../utils/assert.js';
|
|
|
|
import { isFilterOperator, isFilterRule } from './automation.utils.js';
|
|
|
|
export const paramContainsId = [
|
|
param('id').exists(),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
export const validateAutomationSettings = [
|
|
body('enabledAutomations').exists().isBoolean(),
|
|
body('enabledOscIn').exists().isBoolean(),
|
|
body('oscPortIn').exists().isPort(),
|
|
body('automations').optional().isArray(),
|
|
body('automations.*.title').optional().isString().trim(),
|
|
body('automations.*.trigger').optional().isIn(timerLifecycleValues),
|
|
body('automations.*.blueprintId').optional().isString().trim(),
|
|
body('blueprints').optional().custom(parseBluePrint),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
export const validateAutomation = [
|
|
body('title').exists().isString().trim(),
|
|
body('trigger').exists().isIn(timerLifecycleValues),
|
|
body('blueprintId').exists().isString().trim(),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
export const validateAutomationPatch = [
|
|
param('id').exists(),
|
|
body('title').optional().isString().trim(),
|
|
body('trigger').optional().isIn(timerLifecycleValues),
|
|
body('blueprintId').optional().isString().trim(),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
export const validateBlueprint = [
|
|
body().custom(parseBluePrint),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
export const validateBlueprintPatch = [
|
|
param('id').exists(),
|
|
body().custom(parseBluePrint),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Parses and validates a use given blueprint
|
|
*/
|
|
export function parseBluePrint(maybeBlueprint: unknown): AutomationBlueprint {
|
|
assert.isObject(maybeBlueprint);
|
|
assert.hasKeys(maybeBlueprint, ['title', 'filterRule', 'filters', 'outputs']);
|
|
|
|
const { title, filterRule, filters, outputs } = maybeBlueprint;
|
|
assert.isString(title);
|
|
assert.isString(filterRule);
|
|
if (!isFilterRule(filterRule)) {
|
|
throw new Error(`Invalid blueprint: unknown filter rule ${filterRule}`);
|
|
}
|
|
assert.isArray(filters);
|
|
validateFilters(filters);
|
|
|
|
assert.isArray(outputs);
|
|
validateOutput(outputs);
|
|
|
|
return maybeBlueprint as AutomationBlueprint;
|
|
}
|
|
|
|
function validateFilters(filters: Array<unknown>): filters is AutomationFilter[] {
|
|
filters.forEach((condition) => {
|
|
assert.isObject(condition);
|
|
|
|
assert.hasKeys(condition, ['field', 'operator', 'value']);
|
|
const { field, operator, value } = condition;
|
|
assert.isString(field);
|
|
assert.isString(operator);
|
|
assert.isString(value);
|
|
if (!isFilterOperator(operator)) {
|
|
throw new Error(`Invalid blueprint: unknown filter operator ${operator}`);
|
|
}
|
|
|
|
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
throw new Error(`Invalid automation: unhandled filter type ${typeof value}`);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function validateOutput(output: Array<unknown>): output is AutomationOutput[] {
|
|
output.forEach((payload) => {
|
|
assert.isObject(payload);
|
|
assert.hasKeys(payload, ['type']);
|
|
const { type } = payload;
|
|
assert.isString(type);
|
|
|
|
if (type === 'osc') {
|
|
validateOSCOutput(payload);
|
|
} else if (type === 'http') {
|
|
validateHttpOutput(payload);
|
|
} else {
|
|
throw new Error('Invalid automation');
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function validateOSCOutput(payload: object): payload is OSCOutput {
|
|
assert.hasKeys(payload, ['targetIP', 'targetPort', 'address', 'args']);
|
|
const { targetIP, targetPort, address, args } = payload;
|
|
assert.isString(targetIP);
|
|
assert.isNumber(targetPort);
|
|
assert.isString(address);
|
|
if (typeof args !== 'string' && typeof args !== 'number') {
|
|
throw new Error('Invalid automation');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function validateHttpOutput(payload: object): payload is HTTPOutput {
|
|
assert.hasKeys(payload, ['url']);
|
|
const { url } = payload;
|
|
assert.isString(url);
|
|
return true;
|
|
}
|
|
|
|
export const validateTestPayload = [
|
|
body('type').exists().isIn(['osc', 'http']),
|
|
|
|
// validation for OSC message
|
|
oneOf([
|
|
body('targetIP').if(body('type').equals('osc')).isIP(),
|
|
body('targetIP').if(body('type').equals('osc')).isFQDN(),
|
|
body('targetIP').if(body('type').equals('osc')).equals('localhost'),
|
|
]),
|
|
body('targetPort').if(body('type').equals('osc')).isPort(),
|
|
body('address').if(body('type').equals('osc')).isString().trim(),
|
|
body('args').if(body('type').equals('osc')).isString().trim(),
|
|
|
|
// validation for HTTP message
|
|
body('url').if(body('type').equals('http')).isURL({ require_tld: false }).trim(),
|
|
|
|
(req: Request, res: Response, next: NextFunction) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
next();
|
|
},
|
|
];
|