UNPKG

@bernierllc/temporal-workflow-ui

Version:

Thin domain-specific wrapper around @bernierllc/generic-workflow-ui for Temporal workflows

315 lines (313 loc) 10.2 kB
"use strict"; /* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ALL_VALIDATION_RULES = exports.detectBlockUntilCycles = exports.requireUniqueQueueNames = exports.requireUniqueQueryNames = exports.requireUniqueSignalNames = exports.requireStageNames = exports.requireWorkflowName = exports.detectCircularDependencies = exports.requireValidTransitionReferences = exports.requireUniqueTransitionIds = exports.requireUniqueStageIds = exports.requireTriggerNode = void 0; /** * Rule: Workflow must have at least one trigger node */ const requireTriggerNode = (workflow) => { const triggers = workflow.stages.filter(s => s.type === 'trigger'); if (triggers.length === 0) { return [{ message: 'Workflow must have at least one trigger node', severity: 'error', }]; } return []; }; exports.requireTriggerNode = requireTriggerNode; /** * Rule: All stages must have unique IDs */ const requireUniqueStageIds = (workflow) => { const errors = []; const ids = new Set(); for (const stage of workflow.stages) { if (ids.has(stage.id)) { errors.push({ stageId: stage.id, message: `Duplicate stage ID: ${stage.id}`, severity: 'error', }); } ids.add(stage.id); } return errors; }; exports.requireUniqueStageIds = requireUniqueStageIds; /** * Rule: All transitions must have unique IDs */ const requireUniqueTransitionIds = (workflow) => { const errors = []; const ids = new Set(); for (const transition of workflow.transitions) { if (ids.has(transition.id)) { errors.push({ transitionId: transition.id, message: `Duplicate transition ID: ${transition.id}`, severity: 'error', }); } ids.add(transition.id); } return errors; }; exports.requireUniqueTransitionIds = requireUniqueTransitionIds; /** * Rule: All transitions must reference valid stages */ const requireValidTransitionReferences = (workflow) => { const errors = []; const stageIds = new Set(workflow.stages.map(s => s.id)); for (const transition of workflow.transitions) { if (!stageIds.has(transition.from)) { errors.push({ transitionId: transition.id, field: 'from', message: `Transition references non-existent stage: ${transition.from}`, severity: 'error', }); } if (!stageIds.has(transition.to)) { errors.push({ transitionId: transition.id, field: 'to', message: `Transition references non-existent stage: ${transition.to}`, severity: 'error', }); } } return errors; }; exports.requireValidTransitionReferences = requireValidTransitionReferences; /** * Rule: Detect circular dependencies in workflow */ const detectCircularDependencies = (workflow) => { const errors = []; const visited = new Set(); const recursionStack = new Set(); // Build adjacency list const graph = new Map(); for (const stage of workflow.stages) { graph.set(stage.id, []); } for (const transition of workflow.transitions) { const neighbors = graph.get(transition.from) || []; neighbors.push(transition.to); graph.set(transition.from, neighbors); } // DFS to detect cycles function hasCycle(nodeId) { visited.add(nodeId); recursionStack.add(nodeId); const neighbors = graph.get(nodeId) || []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { if (hasCycle(neighbor)) { return true; } } else if (recursionStack.has(neighbor)) { return true; } } recursionStack.delete(nodeId); return false; } for (const stage of workflow.stages) { if (!visited.has(stage.id)) { if (hasCycle(stage.id)) { errors.push({ stageId: stage.id, message: 'Circular dependency detected in workflow', severity: 'error', }); break; // Only report once } } } return errors; }; exports.detectCircularDependencies = detectCircularDependencies; /** * Rule: Workflow must have a name */ const requireWorkflowName = (workflow) => { if (!workflow.name || workflow.name.trim() === '') { return [{ field: 'name', message: 'Workflow must have a name', severity: 'error', }]; } return []; }; exports.requireWorkflowName = requireWorkflowName; /** * Rule: All stages must have a name */ const requireStageNames = (workflow) => { const errors = []; for (const stage of workflow.stages) { if (!stage.name || stage.name.trim() === '') { errors.push({ stageId: stage.id, field: 'name', message: 'Stage must have a name', severity: 'error', }); } } return errors; }; exports.requireStageNames = requireStageNames; /** * Rule: All signals must have unique names */ const requireUniqueSignalNames = (workflow) => { if (!workflow.signals || workflow.signals.length === 0) { return []; } const errors = []; const names = new Set(); for (const signal of workflow.signals) { if (names.has(signal.name)) { errors.push({ field: 'signals', message: `Duplicate signal name: ${signal.name}`, severity: 'error', }); } names.add(signal.name); } return errors; }; exports.requireUniqueSignalNames = requireUniqueSignalNames; /** * Rule: All queries must have unique names */ const requireUniqueQueryNames = (workflow) => { if (!workflow.queries || workflow.queries.length === 0) { return []; } const errors = []; const names = new Set(); for (const query of workflow.queries) { if (names.has(query.name)) { errors.push({ field: 'queries', message: `Duplicate query name: ${query.name}`, severity: 'error', }); } names.add(query.name); } return errors; }; exports.requireUniqueQueryNames = requireUniqueQueryNames; /** * Rule: All work queues must have unique names */ const requireUniqueQueueNames = (workflow) => { if (!workflow.workQueues || workflow.workQueues.length === 0) { return []; } const errors = []; const names = new Set(); for (const queue of workflow.workQueues) { if (names.has(queue.name)) { errors.push({ field: 'workQueues', message: `Duplicate work queue name: ${queue.name}`, severity: 'error', }); } names.add(queue.name); } return errors; }; exports.requireUniqueQueueNames = requireUniqueQueueNames; /** * Rule: Detect cycles in blockUntil dependencies */ const detectBlockUntilCycles = (workflow) => { const errors = []; // Build dependency graph from blockUntil const graph = new Map(); const stageIds = new Set(workflow.stages.map(s => s.id)); for (const stage of workflow.stages) { if (stage.type === 'child-workflow' && stage.metadata.nodeType === 'child-workflow') { const blockUntil = stage.metadata.blockUntil || []; graph.set(stage.id, blockUntil); // Validate that all blockUntil references exist for (const dep of blockUntil) { if (!stageIds.has(dep)) { errors.push({ stageId: stage.id, field: 'metadata.blockUntil', message: `blockUntil references non-existent stage: ${dep}`, severity: 'error', }); } } } } // DFS to detect cycles const visited = new Set(); const recursionStack = new Set(); function hasCycle(nodeId) { visited.add(nodeId); recursionStack.add(nodeId); const deps = graph.get(nodeId) || []; for (const dep of deps) { if (!visited.has(dep)) { if (hasCycle(dep)) { return true; } } else if (recursionStack.has(dep)) { return true; } } recursionStack.delete(nodeId); return false; } for (const [nodeId] of graph) { if (!visited.has(nodeId)) { if (hasCycle(nodeId)) { errors.push({ stageId: nodeId, field: 'metadata.blockUntil', message: 'Circular dependency detected in blockUntil', severity: 'error', }); break; // Only report once } } } return errors; }; exports.detectBlockUntilCycles = detectBlockUntilCycles; /** * All validation rules */ exports.ALL_VALIDATION_RULES = [ exports.requireWorkflowName, exports.requireTriggerNode, exports.requireUniqueStageIds, exports.requireUniqueTransitionIds, exports.requireValidTransitionReferences, exports.detectCircularDependencies, exports.requireStageNames, exports.requireUniqueSignalNames, exports.requireUniqueQueryNames, exports.requireUniqueQueueNames, exports.detectBlockUntilCycles, ];