132 lines
3.4 KiB
TypeScript
132 lines
3.4 KiB
TypeScript
import { Workflow, WorkflowNode } from './types';
|
|
|
|
export interface ValidationError {
|
|
nodeId: string;
|
|
field: string;
|
|
message: string;
|
|
}
|
|
|
|
export function validateWorkflow(workflow: Workflow): ValidationError[] {
|
|
const errors: ValidationError[] = [];
|
|
|
|
// Check if workflow has at least a start and end node
|
|
const hasStart = workflow.nodes.some(n => n.type === 'start');
|
|
const hasEnd = workflow.nodes.some(n => n.type === 'end');
|
|
|
|
if (!hasStart) {
|
|
errors.push({
|
|
nodeId: 'workflow',
|
|
field: 'nodes',
|
|
message: 'Workflow must have a Start node',
|
|
});
|
|
}
|
|
|
|
// Validate each node
|
|
workflow.nodes.forEach((node) => {
|
|
const nodeErrors = validateNode(node);
|
|
errors.push(...nodeErrors);
|
|
});
|
|
|
|
// Check for disconnected nodes (except end nodes)
|
|
workflow.nodes.forEach((node) => {
|
|
if (node.type !== 'start' && node.type !== 'end') {
|
|
const hasIncoming = workflow.edges.some(e => e.target === node.id);
|
|
if (!hasIncoming) {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'connections',
|
|
message: 'Node is not connected to workflow',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return errors;
|
|
}
|
|
|
|
export function validateNode(node: WorkflowNode): ValidationError[] {
|
|
const errors: ValidationError[] = [];
|
|
const nodeType = (node.data as any).nodeType || node.type;
|
|
|
|
switch (nodeType) {
|
|
case 'agent':
|
|
if (!node.data.instructions || node.data.instructions.trim() === '') {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'instructions',
|
|
message: 'Agent must have instructions',
|
|
});
|
|
}
|
|
if (!node.data.model) {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'model',
|
|
message: 'Agent must have a model selected',
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'mcp':
|
|
if (!node.data.mcpServers || node.data.mcpServers.length === 0) {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'mcpServers',
|
|
message: 'MCP node must have at least one server configured',
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'if-else':
|
|
case 'if / else':
|
|
if (!node.data.condition || node.data.condition.trim() === '') {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'condition',
|
|
message: 'If/Else must have a condition',
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'while':
|
|
if (!node.data.condition || node.data.condition.trim() === '') {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'condition',
|
|
message: 'While loop must have a condition',
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'transform':
|
|
if (!node.data.transformScript || node.data.transformScript.trim() === '') {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'transformScript',
|
|
message: 'Transform must have a script',
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'set-state':
|
|
case 'set state':
|
|
if (!node.data.stateKey || node.data.stateKey.trim() === '') {
|
|
errors.push({
|
|
nodeId: node.id,
|
|
field: 'stateKey',
|
|
message: 'Set State must have a variable name',
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
export function getNodeValidationStatus(node: WorkflowNode): 'valid' | 'warning' | 'error' {
|
|
const errors = validateNode(node);
|
|
|
|
if (errors.length === 0) return 'valid';
|
|
if (errors.some(e => e.field !== 'connections')) return 'error';
|
|
return 'warning';
|
|
}
|