/
h0tnanny
/
IotPlatform
Обзор
Документация
Войти
/
h0tnanny
/
IotPlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
src/services/WorkflowValidationService.ts
355 строк
10 KB
h0tnanny
Добавление функциональных требований
08 фев 2026, 23:02
08 фев 2026, 23:02
a8a23c5
Код
Авторство
О чём код?
/** * Сервис валидации workflow * Проверяет целостность графа, ссылки между узлами, достижимость и переменные */ interface NodeData { id: string; name: string; type: 'ActionNode' | 'ConditionNode' | 'LoopNode'; // ActionNode code?: string; nextNodeId?: string | null; // ConditionNode fieldName?: string; operator?: string; compareValue?: unknown; trueNextId?: string | null; falseNextId?: string | null; // LoopNode bodyNodeId?: string; exitNodeId?: string | null; maxIterations?: number; } interface VariableData { name: string; type: string; } export interface ValidationError { type: 'error' | 'warning'; nodeId?: string; message: string; } export interface ValidationResult { valid: boolean; errors: ValidationError[]; warnings: ValidationError[]; } export class WorkflowValidationService { /** * Полная валидация workflow */ static validate( nodes: NodeData[], startNodeId: string, variableList: VariableData[] ): ValidationResult { const errors: ValidationError[] = []; const warnings: ValidationError[] = []; const nodeMap = new Map<string, NodeData>(); for (const node of nodes) { nodeMap.set(node.id, node); } const variableNames = new Set(variableList.map(v => v.name)); // 1. Проверка существования startNodeId this.validateStartNode(startNodeId, nodeMap, errors); // 2. Проверка валидности ссылок this.validateReferences(nodes, nodeMap, errors); // 3. Проверка переменных в ConditionNode и LoopNode this.validateVariables(nodes, variableNames, warnings); // 4. Проверка достижимости узлов this.validateReachability(startNodeId, nodes, nodeMap, warnings); // 5. Проверка циклов (кроме LoopNode) this.validateCycles(startNodeId, nodes, nodeMap, warnings); // 6. Проверка конфигурации узлов this.validateNodeConfig(nodes, errors); return { valid: errors.length === 0, errors, warnings, }; } /** * Проверяет, что startNodeId ссылается на существующий узел */ private static validateStartNode( startNodeId: string, nodeMap: Map<string, NodeData>, errors: ValidationError[] ): void { if (!startNodeId) { errors.push({ type: 'error', message: 'Не указан стартовый узел (startNodeId)', }); return; } if (!nodeMap.has(startNodeId)) { errors.push({ type: 'error', message: `Стартовый узел "${startNodeId}" не существует в графе`, }); } } /** * Проверяет, что все ссылки между узлами указывают на существующие узлы */ private static validateReferences( nodes: NodeData[], nodeMap: Map<string, NodeData>, errors: ValidationError[] ): void { for (const node of nodes) { switch (node.type) { case 'ActionNode': if (node.nextNodeId && !nodeMap.has(node.nextNodeId)) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": nextNodeId "${node.nextNodeId}" не найден`, }); } break; case 'ConditionNode': if (node.trueNextId && !nodeMap.has(node.trueNextId)) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": trueNextId "${node.trueNextId}" не найден`, }); } if (node.falseNextId && !nodeMap.has(node.falseNextId)) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": falseNextId "${node.falseNextId}" не найден`, }); } break; case 'LoopNode': if (node.bodyNodeId && !nodeMap.has(node.bodyNodeId)) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": bodyNodeId "${node.bodyNodeId}" не найден`, }); } if (node.exitNodeId && !nodeMap.has(node.exitNodeId)) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": exitNodeId "${node.exitNodeId}" не найден`, }); } break; } } } /** * Проверяет, что переменные, используемые в ConditionNode/LoopNode, существуют */ private static validateVariables( nodes: NodeData[], variableNames: Set<string>, warnings: ValidationError[] ): void { for (const node of nodes) { if (node.type === 'ConditionNode' || node.type === 'LoopNode') { if (node.fieldName && !variableNames.has(node.fieldName)) { warnings.push({ type: 'warning', nodeId: node.id, message: `Узел "${node.name}": переменная "${node.fieldName}" не найдена в списке переменных workflow`, }); } } } } /** * Проверяет, что все узлы достижимы из startNodeId */ private static validateReachability( startNodeId: string, nodes: NodeData[], nodeMap: Map<string, NodeData>, warnings: ValidationError[] ): void { if (!nodeMap.has(startNodeId)) return; const reachable = new Set<string>(); const queue: string[] = [startNodeId]; while (queue.length > 0) { const currentId = queue.shift()!; if (reachable.has(currentId)) continue; reachable.add(currentId); const node = nodeMap.get(currentId); if (!node) continue; const nextIds = this.getNextNodeIds(node); for (const nextId of nextIds) { if (nextId && !reachable.has(nextId)) { queue.push(nextId); } } } for (const node of nodes) { if (!reachable.has(node.id)) { warnings.push({ type: 'warning', nodeId: node.id, message: `Узел "${node.name}" недостижим из стартового узла`, }); } } } /** * Обнаруживает циклы в графе (кроме LoopNode → bodyNodeId, что допустимо) */ private static validateCycles( startNodeId: string, _nodes: NodeData[], nodeMap: Map<string, NodeData>, warnings: ValidationError[] ): void { if (!nodeMap.has(startNodeId)) return; const visited = new Set<string>(); const inStack = new Set<string>(); const dfs = (nodeId: string, path: string[]): void => { if (inStack.has(nodeId)) { // Нашли цикл — проверяем, идёт ли он через LoopNode const cycleStart = path.indexOf(nodeId); const cyclePath = path.slice(cycleStart); // Проверяем, что хотя бы один узел в цикле — LoopNode const hasLoopNode = cyclePath.some(id => { const n = nodeMap.get(id); return n?.type === 'LoopNode'; }); if (!hasLoopNode) { const cycleNames = cyclePath.map(id => nodeMap.get(id)?.name || id); warnings.push({ type: 'warning', nodeId, message: `Обнаружен цикл без LoopNode: ${cycleNames.join(' → ')} → ${nodeMap.get(nodeId)?.name || nodeId}`, }); } return; } if (visited.has(nodeId)) return; visited.add(nodeId); inStack.add(nodeId); const node = nodeMap.get(nodeId); if (node) { const nextIds = this.getNextNodeIds(node); for (const nextId of nextIds) { if (nextId) { dfs(nextId, [...path, nodeId]); } } } inStack.delete(nodeId); }; dfs(startNodeId, []); } /** * Проверяет конфигурацию отдельных узлов */ private static validateNodeConfig( nodes: NodeData[], errors: ValidationError[] ): void { for (const node of nodes) { switch (node.type) { case 'ConditionNode': if (!node.fieldName) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": не указана переменная для сравнения (fieldName)`, }); } if (!node.operator) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": не указан оператор сравнения`, }); } if (!node.trueNextId && !node.falseNextId) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": не указаны переходы (trueNextId и falseNextId)`, }); } break; case 'LoopNode': if (!node.fieldName) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": не указана переменная для условия цикла`, }); } if (!node.bodyNodeId) { errors.push({ type: 'error', nodeId: node.id, message: `Узел "${node.name}": не указан узел тела цикла (bodyNodeId)`, }); } break; } } } /** * Возвращает все ID следующих узлов для данного узла */ private static getNextNodeIds(node: NodeData): (string | null | undefined)[] { switch (node.type) { case 'ActionNode': return [node.nextNodeId]; case 'ConditionNode': return [node.trueNextId, node.falseNextId]; case 'LoopNode': return [node.bodyNodeId, node.exitNodeId]; default: return []; } } }