File size: 4,787 Bytes
d9494a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { isDefined } from '@/utils';
import { WorkflowActionType } from '@/workflow/types/WorkflowActionType';
import {
  type IfElseStepInput,
  type IteratorStepInput,
  type ValidatableWorkflow,
  type ValidatableWorkflowStep,
  type WorkflowValidationIssue,
} from '@/workflow/validation/types/workflow-validation.type';
import { type WorkflowGraph } from '@/workflow/validation/utils/build-workflow-graph.util';
import { getStepInput } from '@/workflow/validation/utils/get-step-outgoing-step-ids.util';

export const validateWorkflowGraph = ({
  workflow,
  graph,
}: {
  workflow: ValidatableWorkflow;
  graph: WorkflowGraph;
}): WorkflowValidationIssue[] => {
  const issues: WorkflowValidationIssue[] = [];
  const steps = workflow.steps ?? [];
  const stepIds = new Set(steps.map((step) => step.id));

  const seenStepIds = new Set<string>();

  for (const step of steps) {
    if (seenStepIds.has(step.id)) {
      issues.push({
        severity: 'error',
        code: 'DUPLICATE_STEP_ID',
        message: `Duplicate step id "${step.id}". Every step id must be unique within the workflow.`,
        stepId: step.id,
      });
    }
    seenStepIds.add(step.id);
  }

  const triggerNextStepIds = (workflow.trigger?.nextStepIds ?? []).filter(
    isDefined,
  );

  if (steps.length > 0 && triggerNextStepIds.length === 0) {
    issues.push({
      severity: 'error',
      code: 'TRIGGER_HAS_NO_NEXT_STEP',
      message: `The trigger is not connected to any step. The trigger must have a "nextStepIds" array pointing to the first step (e.g. nextStepIds: ["${steps[0].id}"]). If you used edges, also set trigger.nextStepIds.`,
    });
  }

  for (const triggerNextStepId of triggerNextStepIds) {
    if (!stepIds.has(triggerNextStepId)) {
      issues.push({
        severity: 'error',
        code: 'DANGLING_REFERENCE',
        message: `The trigger references a non-existent step "${triggerNextStepId}".`,
      });
    }
  }

  for (const step of steps) {
    for (const outgoingStepId of graph.childrenByStepId.get(step.id) ?? []) {
      if (!stepIds.has(outgoingStepId)) {
        issues.push({
          severity: 'error',
          code: 'DANGLING_REFERENCE',
          message: `Step "${step.name ?? step.id}" references a non-existent step "${outgoingStepId}".`,
          stepId: step.id,
        });
      }
    }

    issues.push(...validateBranchingStep(step));
  }

  for (const step of steps) {
    if (!graph.reachableFromTrigger.has(step.id)) {
      issues.push({
        severity: 'error',
        code: 'UNREACHABLE_STEP',
        message: `Step "${step.name ?? step.id}" is not reachable from the trigger. Ensure a chain of nextStepIds connects the trigger to this step. Check that the preceding step includes this step's id ("${step.id}") in its nextStepIds array.`,
        stepId: step.id,
      });
    }
  }

  return issues;
};

const validateBranchingStep = (
  step: ValidatableWorkflowStep,
): WorkflowValidationIssue[] => {
  const issues: WorkflowValidationIssue[] = [];
  const input = getStepInput(step);

  if (step.type === WorkflowActionType.IF_ELSE) {
    const branchList = (input as Partial<IfElseStepInput> | undefined)
      ?.branches;
    const branches = Array.isArray(branchList) ? branchList : [];

    if (branches.length < 2) {
      issues.push({
        severity: 'warning',
        code: 'IF_ELSE_INSUFFICIENT_BRANCHES',
        message: `If/Else step "${step.name ?? step.id}" should have at least two branches (a condition branch and an else branch).`,
        stepId: step.id,
      });
    }

    for (const branch of branches) {
      const branchNextStepIds = branch?.nextStepIds;

      if (!Array.isArray(branchNextStepIds) || branchNextStepIds.length === 0) {
        issues.push({
          severity: 'error',
          code: 'IF_ELSE_BRANCH_HAS_NO_NEXT_STEP',
          message: `A branch of If/Else step "${step.name ?? step.id}" is not connected to any step.`,
          stepId: step.id,
        });
      }
    }
  }

  if (step.type === WorkflowActionType.ITERATOR) {
    const iteratorInput = input as Partial<IteratorStepInput> | undefined;
    const items = iteratorInput?.items;
    const hasConfiguredItems =
      (typeof items === 'string' && items.length > 0) ||
      (Array.isArray(items) && items.length > 0);
    const initialLoopStepIds = iteratorInput?.initialLoopStepIds;
    const hasLoopBody =
      Array.isArray(initialLoopStepIds) && initialLoopStepIds.length > 0;

    if (hasConfiguredItems && !hasLoopBody) {
      issues.push({
        severity: 'error',
        code: 'ITERATOR_MISSING_LOOP_BODY',
        message: `Iterator step "${step.name ?? step.id}" has items to iterate over but no steps inside the loop.`,
        stepId: step.id,
      });
    }
  }

  return issues;
};