@bernierllc/temporal-workflow-ui
Version:
Thin domain-specific wrapper around @bernierllc/generic-workflow-ui for Temporal workflows
315 lines (313 loc) • 10.2 kB
JavaScript
"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,
];