@useparagon/core 0.0.1-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -0
- package/src/event/event.interface.ts +18 -0
- package/src/event/index.ts +1 -0
- package/src/execution/context.constants.ts +9 -0
- package/src/execution/context.interface.ts +39 -0
- package/src/execution/context.ts +72 -0
- package/src/execution/context.utils.ts +53 -0
- package/src/execution/index.ts +2 -0
- package/src/index.ts +6 -0
- package/src/integration/custom-integration.interface.ts +20 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/integration-config.interface.ts +19 -0
- package/src/integration/integration.interface.ts +16 -0
- package/src/operator/index.ts +2 -0
- package/src/operator/operator.interface.ts +6 -0
- package/src/operator/operators/BooleanTrue.ts +20 -0
- package/src/operator/operators/StringContains.ts +27 -0
- package/src/resolvers/index.ts +2 -0
- package/src/resolvers/resolver.utils.ts +157 -0
- package/src/resolvers/resolvers.interface.ts +369 -0
- package/src/secret/index.ts +1 -0
- package/src/secret/secret.interface.ts +4 -0
- package/src/stateMachine/index.ts +2 -0
- package/src/stateMachine/stateMachine.constants.ts +12 -0
- package/src/stateMachine/stateMachine.interface.ts +145 -0
- package/src/stateMachine/stateMachine.utils.ts +733 -0
- package/src/steps/index.ts +3 -0
- package/src/steps/library/action/action.interface.ts +69 -0
- package/src/steps/library/action/action.step.ts +70 -0
- package/src/steps/library/action/index.ts +2 -0
- package/src/steps/library/conditional/conditional.interface.ts +82 -0
- package/src/steps/library/conditional/conditional.step.ts +96 -0
- package/src/steps/library/conditional/conditional.utils.ts +110 -0
- package/src/steps/library/conditional/index.ts +2 -0
- package/src/steps/library/delay/delay.interface.ts +71 -0
- package/src/steps/library/delay/delay.step.ts +51 -0
- package/src/steps/library/delay/index.ts +2 -0
- package/src/steps/library/fanout/fanout.interface.ts +46 -0
- package/src/steps/library/fanout/fanout.step.ts +68 -0
- package/src/steps/library/fanout/index.ts +2 -0
- package/src/steps/library/function/function.interface.ts +69 -0
- package/src/steps/library/function/function.step.ts +55 -0
- package/src/steps/library/function/index.ts +2 -0
- package/src/steps/library/index.ts +7 -0
- package/src/steps/library/integrationRequest/index.ts +2 -0
- package/src/steps/library/integrationRequest/integrationRequest.interface.ts +79 -0
- package/src/steps/library/integrationRequest/integrationRequest.step.ts +100 -0
- package/src/steps/library/request/index.ts +2 -0
- package/src/steps/library/request/request.interface.ts +159 -0
- package/src/steps/library/request/request.step.ts +117 -0
- package/src/steps/library/response/index.ts +2 -0
- package/src/steps/library/response/response.interface.ts +50 -0
- package/src/steps/library/response/response.step.ts +68 -0
- package/src/steps/step.constants.ts +4 -0
- package/src/steps/step.interface-base.ts +81 -0
- package/src/steps/step.interface.ts +31 -0
- package/src/steps/step.ts +136 -0
- package/src/steps/step.utils.ts +103 -0
- package/src/triggers/cron/cron.interface.ts +94 -0
- package/src/triggers/cron/cron.step.ts +52 -0
- package/src/triggers/cron/cron.utils.ts +117 -0
- package/src/triggers/cron/index.ts +3 -0
- package/src/triggers/endpoint/endpoint.interface.ts +66 -0
- package/src/triggers/endpoint/endpoint.step.ts +61 -0
- package/src/triggers/endpoint/index.ts +2 -0
- package/src/triggers/event/event.interface.ts +43 -0
- package/src/triggers/event/event.step.ts +41 -0
- package/src/triggers/event/index.ts +2 -0
- package/src/triggers/index.ts +4 -0
- package/src/triggers/integrationEnabled/index.ts +2 -0
- package/src/triggers/integrationEnabled/integrationEnabled.interface.ts +29 -0
- package/src/triggers/integrationEnabled/integrationEnabled.step.ts +33 -0
- package/src/triggers/trigger.interface.ts +28 -0
- package/src/triggers/trigger.ts +25 -0
- package/src/user/index.ts +2 -0
- package/src/user/user.interface.ts +4 -0
- package/src/user/user.ts +6 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +10 -0
- package/src/workflow/index.ts +2 -0
- package/src/workflow/workflow.interface.ts +50 -0
- package/src/workflow/workflow.ts +132 -0
- package/tsconfig.json +9 -0
- package/tsconfig.release.json +8 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import { Choice, ConditionalStep } from '../steps/library/conditional';
|
|
2
|
+
import { FanOutStep } from '../steps/library/fanout';
|
|
3
|
+
import { IStep, StepMap, StepType } from '../steps/step.interface';
|
|
4
|
+
import {
|
|
5
|
+
concatAndDedupeStepLists,
|
|
6
|
+
isConditional,
|
|
7
|
+
isFanout,
|
|
8
|
+
isStepInStepList,
|
|
9
|
+
isTrigger,
|
|
10
|
+
} from '../steps/step.utils';
|
|
11
|
+
import { IWorkflow } from '../workflow/workflow.interface';
|
|
12
|
+
import { EMPTY_STATE_MACHINE } from './stateMachine.constants';
|
|
13
|
+
import {
|
|
14
|
+
StateMachine,
|
|
15
|
+
Sequence,
|
|
16
|
+
SequenceEdge,
|
|
17
|
+
SequenceEdgeType,
|
|
18
|
+
SequenceMap,
|
|
19
|
+
SequenceType,
|
|
20
|
+
StepEdge,
|
|
21
|
+
} from './stateMachine.interface';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* gets the last step within a sequence
|
|
25
|
+
*
|
|
26
|
+
* @param sequence
|
|
27
|
+
* @param stateMachine
|
|
28
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
export function getLastStepInSequence(
|
|
31
|
+
sequence: Sequence,
|
|
32
|
+
stateMachine: StateMachine,
|
|
33
|
+
): IStep | undefined {
|
|
34
|
+
const nextSequenceEdge: SequenceEdge | undefined =
|
|
35
|
+
sequence.sequenceEdges.find((edge: SequenceEdge) => edge.type === 'NEXT');
|
|
36
|
+
const nextSequence = stateMachine.sequenceMap[nextSequenceEdge?.to || ''];
|
|
37
|
+
if (nextSequence && nextSequence.stepIds.length > 0) {
|
|
38
|
+
return getLastStepInSequence(nextSequence, stateMachine);
|
|
39
|
+
}
|
|
40
|
+
return stateMachine.stepMap[sequence.stepIds[sequence.stepIds.length - 1]];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* creates a sequence and adds it to the `sequence` array provided
|
|
45
|
+
* which is being used as a store to keep track of sequences created
|
|
46
|
+
*
|
|
47
|
+
* @param withStep
|
|
48
|
+
* @param sequences
|
|
49
|
+
* @param type
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
function createSequence(
|
|
53
|
+
withStep: IStep | undefined,
|
|
54
|
+
sequences: Sequence[],
|
|
55
|
+
type: SequenceType,
|
|
56
|
+
): Sequence {
|
|
57
|
+
const sequence: Sequence = {
|
|
58
|
+
id: `sequence-${sequences.length}`,
|
|
59
|
+
start: withStep?.id,
|
|
60
|
+
stepIds: withStep ? [withStep.id] : [],
|
|
61
|
+
stepEdges: [],
|
|
62
|
+
sequenceEdges: [],
|
|
63
|
+
type,
|
|
64
|
+
conditionalBranchWidth: 0,
|
|
65
|
+
};
|
|
66
|
+
sequences.push(sequence);
|
|
67
|
+
return sequence;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* recursively creates the sequences in a state machine;
|
|
72
|
+
* populate them with their steps
|
|
73
|
+
*
|
|
74
|
+
* @param step
|
|
75
|
+
* @param sequence
|
|
76
|
+
* @param stepMap
|
|
77
|
+
* @param sequences
|
|
78
|
+
* @param traversed
|
|
79
|
+
* @param conditionalBranchWidth
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
function traverseSteps(
|
|
83
|
+
step: IStep,
|
|
84
|
+
sequence: Sequence,
|
|
85
|
+
stepMap: StepMap,
|
|
86
|
+
sequences: Sequence[],
|
|
87
|
+
traversed: string[],
|
|
88
|
+
conditionalBranchWidth: number = 0,
|
|
89
|
+
): number {
|
|
90
|
+
let maxConditionalBranchWidth: number = conditionalBranchWidth;
|
|
91
|
+
let nextStepId: string | undefined | null = step?.next ?? null;
|
|
92
|
+
let nextStep: IStep | undefined = nextStepId
|
|
93
|
+
? stepMap[nextStepId]
|
|
94
|
+
: undefined;
|
|
95
|
+
let choice: Choice;
|
|
96
|
+
let newSequence: Sequence;
|
|
97
|
+
let newSequenceEdge: SequenceEdge;
|
|
98
|
+
const newStepEdge: StepEdge = {
|
|
99
|
+
from: step.id,
|
|
100
|
+
to: nextStepId,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (traversed.includes(step.id)) {
|
|
104
|
+
return conditionalBranchWidth;
|
|
105
|
+
}
|
|
106
|
+
traversed.push(step.id);
|
|
107
|
+
sequence.stepEdges.push(newStepEdge);
|
|
108
|
+
|
|
109
|
+
if (nextStep && !isConditional(step) && !isFanout(step)) {
|
|
110
|
+
if (isTrigger(step)) {
|
|
111
|
+
// Create separate sequence for steps after the trigger
|
|
112
|
+
newSequence = createSequence(nextStep, sequences, SequenceType.MAIN);
|
|
113
|
+
sequence.sequenceEdges.push({
|
|
114
|
+
type: SequenceEdgeType.NEXT,
|
|
115
|
+
from: sequence.id,
|
|
116
|
+
to: newSequence.id,
|
|
117
|
+
});
|
|
118
|
+
traverseSteps(nextStep, newSequence, stepMap, sequences, traversed);
|
|
119
|
+
} else if (isConditional(nextStep) || isFanout(nextStep)) {
|
|
120
|
+
// Create separate sequence for a conditional step
|
|
121
|
+
newSequence = createSequence(nextStep, sequences, sequence.type);
|
|
122
|
+
sequence.sequenceEdges.push({
|
|
123
|
+
type: SequenceEdgeType.NEXT,
|
|
124
|
+
from: sequence.id,
|
|
125
|
+
to: newSequence.id,
|
|
126
|
+
});
|
|
127
|
+
conditionalBranchWidth += traverseSteps(
|
|
128
|
+
nextStep,
|
|
129
|
+
newSequence,
|
|
130
|
+
stepMap,
|
|
131
|
+
sequences,
|
|
132
|
+
traversed,
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
// Add this step to the current sequence
|
|
136
|
+
sequence.stepIds.push(nextStepId!);
|
|
137
|
+
conditionalBranchWidth = Math.max(
|
|
138
|
+
traverseSteps(nextStep, sequence, stepMap, sequences, traversed),
|
|
139
|
+
conditionalBranchWidth,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isConditional(step)) {
|
|
145
|
+
conditionalBranchWidth += 1;
|
|
146
|
+
// iterate through each choice in the step; create a new sequence and add the step to it
|
|
147
|
+
let i: number = 0;
|
|
148
|
+
const choices: Choice[] =
|
|
149
|
+
(step as ConditionalStep)?.parameters?.choices || [];
|
|
150
|
+
while (i < choices.length) {
|
|
151
|
+
choice = choices[i];
|
|
152
|
+
nextStepId = choice.next;
|
|
153
|
+
nextStep = nextStepId ? stepMap[nextStepId] : undefined;
|
|
154
|
+
newSequence = createSequence(nextStep, sequences, SequenceType.BRANCH);
|
|
155
|
+
newSequenceEdge = {
|
|
156
|
+
type: SequenceEdgeType.BRANCH,
|
|
157
|
+
from: sequence.id,
|
|
158
|
+
to: newSequence.id,
|
|
159
|
+
};
|
|
160
|
+
sequence.stepEdges.push({
|
|
161
|
+
label: choice.label,
|
|
162
|
+
from: step.id,
|
|
163
|
+
to: nextStepId,
|
|
164
|
+
});
|
|
165
|
+
sequence.sequenceEdges.push(newSequenceEdge);
|
|
166
|
+
if (nextStep) {
|
|
167
|
+
conditionalBranchWidth += traverseSteps(
|
|
168
|
+
nextStep,
|
|
169
|
+
newSequence,
|
|
170
|
+
stepMap,
|
|
171
|
+
sequences,
|
|
172
|
+
traversed,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create separate sequence for resolved condition
|
|
179
|
+
nextStepId = step.next ?? null;
|
|
180
|
+
nextStep = stepMap[nextStepId || ''];
|
|
181
|
+
newSequence = createSequence(nextStep, sequences, SequenceType.MAIN);
|
|
182
|
+
sequence.sequenceEdges.push({
|
|
183
|
+
type: SequenceEdgeType.NEXT,
|
|
184
|
+
from: sequence.id,
|
|
185
|
+
to: newSequence.id,
|
|
186
|
+
});
|
|
187
|
+
if (nextStep) {
|
|
188
|
+
maxConditionalBranchWidth = Math.max(
|
|
189
|
+
traverseSteps(nextStep, newSequence, stepMap, sequences, traversed),
|
|
190
|
+
conditionalBranchWidth,
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
maxConditionalBranchWidth = Math.max(
|
|
194
|
+
maxConditionalBranchWidth,
|
|
195
|
+
conditionalBranchWidth,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sequence.conditionalBranchWidth = conditionalBranchWidth;
|
|
200
|
+
} else if (isFanout(step)) {
|
|
201
|
+
sequence.conditionalBranchWidth = 0.5;
|
|
202
|
+
conditionalBranchWidth += 0.5;
|
|
203
|
+
|
|
204
|
+
nextStepId = step.parameters.nextToIterate;
|
|
205
|
+
nextStep = stepMap[nextStepId || ''];
|
|
206
|
+
newSequence = createSequence(nextStep, sequences, SequenceType.FANOUT);
|
|
207
|
+
sequence.sequenceEdges.push({
|
|
208
|
+
type: SequenceEdgeType.FANOUT,
|
|
209
|
+
from: sequence.id,
|
|
210
|
+
to: newSequence.id,
|
|
211
|
+
});
|
|
212
|
+
if (nextStep) {
|
|
213
|
+
sequence.conditionalBranchWidth = Math.max(
|
|
214
|
+
traverseSteps(nextStep, newSequence, stepMap, sequences, traversed),
|
|
215
|
+
sequence.conditionalBranchWidth,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
nextStep = stepMap[step.next || ''];
|
|
220
|
+
newSequence = createSequence(nextStep, sequences, SequenceType.MAIN);
|
|
221
|
+
sequence.sequenceEdges.push({
|
|
222
|
+
type: SequenceEdgeType.NEXT,
|
|
223
|
+
from: sequence.id,
|
|
224
|
+
to: newSequence.id,
|
|
225
|
+
});
|
|
226
|
+
if (nextStep) {
|
|
227
|
+
maxConditionalBranchWidth = Math.max(
|
|
228
|
+
traverseSteps(nextStep, newSequence, stepMap, sequences, traversed),
|
|
229
|
+
conditionalBranchWidth,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Math.max(maxConditionalBranchWidth, conditionalBranchWidth);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* ensures that a state machine is valid
|
|
239
|
+
*
|
|
240
|
+
* @param stateMachine the state machine to validate
|
|
241
|
+
*/
|
|
242
|
+
function verifyStateMachine(stateMachine: StateMachine): void {
|
|
243
|
+
const steps: IStep[] = Object.values(stateMachine.stepMap);
|
|
244
|
+
let message: string;
|
|
245
|
+
|
|
246
|
+
for (const step of steps) {
|
|
247
|
+
// ensure no step is pointing to itself
|
|
248
|
+
if (step.next === step.id) {
|
|
249
|
+
message =
|
|
250
|
+
"StateMachine invalid: step's 'next' property is pointing to itself.";
|
|
251
|
+
throw new Error(message);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parentSteps: IStep[] = getUpstreamSteps(step.id, stateMachine);
|
|
255
|
+
// TODO: uncomment this check to ensure no ghost steps are in the state machine
|
|
256
|
+
// ensure no dangling (ghost) steps
|
|
257
|
+
// if (!parentSteps.length && !isTrigger(step)) {
|
|
258
|
+
// message = 'StateMachine invalid: step has no parent steps.';
|
|
259
|
+
// danger(message, {
|
|
260
|
+
// stepId: step.id,
|
|
261
|
+
// workflowId: step.workflowId,
|
|
262
|
+
// });
|
|
263
|
+
// throw Error(message);
|
|
264
|
+
// }
|
|
265
|
+
|
|
266
|
+
// ensure no cyclical workflows (step pointing to a parent step)
|
|
267
|
+
for (const parentStep of parentSteps) {
|
|
268
|
+
if (step.next === parentStep.id) {
|
|
269
|
+
message = 'StateMachine invalid: Cyclical workflow detected.';
|
|
270
|
+
// TODO: once apps support importing same shared logger on dashboard + other services,
|
|
271
|
+
// uncomment this to help debugging
|
|
272
|
+
// error(message, {
|
|
273
|
+
// stepId: step.id,
|
|
274
|
+
// cyclicalStepId: parentStep.id,
|
|
275
|
+
// workflowId: step.workflowId,
|
|
276
|
+
// stateMachine: stateMachine,
|
|
277
|
+
// });
|
|
278
|
+
throw new Error(message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* recursively traverses a state machine and calls a function on each sequence
|
|
286
|
+
*
|
|
287
|
+
* @param stateMachine
|
|
288
|
+
* @param sequenceVisitor
|
|
289
|
+
* @param currentSequenceId
|
|
290
|
+
*/
|
|
291
|
+
export function traverseStateMachineBySequence(
|
|
292
|
+
stateMachine: StateMachine,
|
|
293
|
+
sequenceVisitor: (sequence: Sequence) => void,
|
|
294
|
+
currentSequenceId: string = stateMachine.start,
|
|
295
|
+
): void {
|
|
296
|
+
const currentSequence = stateMachine.sequenceMap[currentSequenceId];
|
|
297
|
+
sequenceVisitor(currentSequence);
|
|
298
|
+
currentSequence.sequenceEdges.forEach(({ to }: SequenceEdge) =>
|
|
299
|
+
traverseStateMachineBySequence(stateMachine, sequenceVisitor, to),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* given a step, it finds the step before it in the workflow
|
|
305
|
+
*
|
|
306
|
+
* @param stepsOrWorkflow
|
|
307
|
+
* @param step
|
|
308
|
+
* @param traversed
|
|
309
|
+
* @returns
|
|
310
|
+
*/
|
|
311
|
+
export function getStepBefore(
|
|
312
|
+
stepsOrWorkflow: IStep[] | IWorkflow,
|
|
313
|
+
step: IStep,
|
|
314
|
+
traversed: IStep[] = [],
|
|
315
|
+
): IStep | undefined {
|
|
316
|
+
if (!Array.isArray(stepsOrWorkflow)) {
|
|
317
|
+
stepsOrWorkflow = stepsOrWorkflow.steps;
|
|
318
|
+
}
|
|
319
|
+
return stepsOrWorkflow.find(
|
|
320
|
+
(s: IStep) =>
|
|
321
|
+
!traversed.filter((t: IStep) => t.id === s.id).length &&
|
|
322
|
+
(s.next === step.id ||
|
|
323
|
+
(s.type === StepType.IFELSE &&
|
|
324
|
+
s.parameters?.choices?.find(
|
|
325
|
+
(choice: Choice) => choice.next === step.id,
|
|
326
|
+
)) ||
|
|
327
|
+
(s.type === StepType.MAP && s.next === step.id) ||
|
|
328
|
+
(s.type === StepType.MAP && s.parameters.nextToIterate === step.id)),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function getUpstreamSteps(
|
|
333
|
+
stepId: string,
|
|
334
|
+
stateMachine: StateMachine,
|
|
335
|
+
): IStep[] {
|
|
336
|
+
if (!stateMachine.stepMap[stepId]) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// the reason this is within `getUpstreamSteps` is so we can
|
|
341
|
+
// recursively call it for different paths;
|
|
342
|
+
// also, we need it this way so this inner function can update the outer `steps` function
|
|
343
|
+
function recurseUp(recurseStartStep: IStep | undefined): void {
|
|
344
|
+
let currentStep: IStep | undefined = recurseStartStep;
|
|
345
|
+
|
|
346
|
+
// we want to save the trigger step for last so at the end when we reverse it
|
|
347
|
+
// it's first in the list
|
|
348
|
+
while (
|
|
349
|
+
currentStep &&
|
|
350
|
+
currentStep.id !== triggerStep?.id &&
|
|
351
|
+
!isStepInStepList(currentStep, upstreamSteps)
|
|
352
|
+
) {
|
|
353
|
+
if (
|
|
354
|
+
stepId !== currentStep.id &&
|
|
355
|
+
isConditional(currentStep) &&
|
|
356
|
+
!isStepInConditional(
|
|
357
|
+
initialStep,
|
|
358
|
+
currentStep as ConditionalStep,
|
|
359
|
+
stateMachine,
|
|
360
|
+
)
|
|
361
|
+
) {
|
|
362
|
+
// in this case, we've traversed up to a conditional
|
|
363
|
+
// however, the initial step isn't in either branch of the conditional
|
|
364
|
+
// this means all the steps on both sides of the conditional are upstream of the initial step
|
|
365
|
+
// so we need to recurse down both paths to add them
|
|
366
|
+
const downstreamSteps: IStep[] = getDownstreamSteps(
|
|
367
|
+
currentStep.id,
|
|
368
|
+
stateMachine,
|
|
369
|
+
stepId,
|
|
370
|
+
).reverse();
|
|
371
|
+
concatAndDedupeStepLists(upstreamSteps, downstreamSteps, false);
|
|
372
|
+
} else if (
|
|
373
|
+
stepId !== currentStep.id &&
|
|
374
|
+
isFanout(currentStep) &&
|
|
375
|
+
!isStepInFanout(initialStep, currentStep as FanOutStep, stateMachine)
|
|
376
|
+
) {
|
|
377
|
+
// in this case, we've traversed up to a fanout that the current step is not in
|
|
378
|
+
// since the current step isn't within the fanout, the fanout has steps we haven't traversed
|
|
379
|
+
// so we need to recurse down the fanout to add them
|
|
380
|
+
const downstreamSteps: IStep[] = getDownstreamSteps(
|
|
381
|
+
currentStep.id,
|
|
382
|
+
stateMachine,
|
|
383
|
+
stepId,
|
|
384
|
+
).reverse();
|
|
385
|
+
concatAndDedupeStepLists(upstreamSteps, downstreamSteps, false);
|
|
386
|
+
}
|
|
387
|
+
upstreamSteps.push(currentStep);
|
|
388
|
+
|
|
389
|
+
currentStep = getStepBefore(
|
|
390
|
+
workflowSteps,
|
|
391
|
+
stateMachine.stepMap[currentStep.id],
|
|
392
|
+
upstreamSteps,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const initialStep: IStep = stateMachine.stepMap[stepId];
|
|
398
|
+
const upstreamSteps: IStep[] = [];
|
|
399
|
+
const allSteps: IStep[] = Object.values(stateMachine.stepMap);
|
|
400
|
+
const triggerStep: IStep | undefined = allSteps.find(isTrigger);
|
|
401
|
+
const workflowSteps: IStep[] = triggerStep
|
|
402
|
+
? getDownstreamSteps(triggerStep.id, stateMachine)
|
|
403
|
+
: allSteps;
|
|
404
|
+
|
|
405
|
+
recurseUp(initialStep);
|
|
406
|
+
|
|
407
|
+
if (triggerStep && !isStepInStepList(triggerStep, upstreamSteps)) {
|
|
408
|
+
upstreamSteps.push(triggerStep);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
upstreamSteps.reverse();
|
|
412
|
+
|
|
413
|
+
return upstreamSteps.filter((s: IStep) => s.id !== stepId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* given a step id and a state machine, it returns an array of steps that are downstream of the step
|
|
418
|
+
*
|
|
419
|
+
* @param stepId
|
|
420
|
+
* @param stateMachine
|
|
421
|
+
* @param stopId if provided, traversing stops upon reaching this step
|
|
422
|
+
* @returns
|
|
423
|
+
*/
|
|
424
|
+
export function getDownstreamSteps(
|
|
425
|
+
stepId: string,
|
|
426
|
+
stateMachine: StateMachine,
|
|
427
|
+
stopId: string | null = null,
|
|
428
|
+
): IStep[] {
|
|
429
|
+
if (!stateMachine.stepMap[stepId]) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// the reason this is within `getUpstreamSteps` is so we can
|
|
434
|
+
// recursively call it for different paths while tracking the cached state
|
|
435
|
+
// as well as modify the final result
|
|
436
|
+
function recurseDown(initialStep: IStep | undefined): void {
|
|
437
|
+
let currentStep: IStep | undefined = initialStep;
|
|
438
|
+
while (
|
|
439
|
+
currentStep &&
|
|
440
|
+
// currentStep.id !== stopId &&
|
|
441
|
+
!isStepInStepList(currentStep, downstreamSteps)
|
|
442
|
+
) {
|
|
443
|
+
downstreamSteps.push(currentStep);
|
|
444
|
+
|
|
445
|
+
if (isConditional(currentStep)) {
|
|
446
|
+
// if the step is a conditional, then we need to recurse down two separate paths
|
|
447
|
+
// we're not going to exit though because the conditional step may also have a `.next` step
|
|
448
|
+
const conditionalStep: ConditionalStep = currentStep as ConditionalStep;
|
|
449
|
+
|
|
450
|
+
const choiceId0: string | null | undefined =
|
|
451
|
+
conditionalStep.parameters?.choices[0].next;
|
|
452
|
+
choiceId0 && recurseDown(stateMachine.stepMap[choiceId0]);
|
|
453
|
+
|
|
454
|
+
const choiceId1: string | null | undefined =
|
|
455
|
+
conditionalStep.parameters?.choices[1].next;
|
|
456
|
+
choiceId1 && recurseDown(stateMachine.stepMap[choiceId1]);
|
|
457
|
+
} else if (isFanout(currentStep)) {
|
|
458
|
+
// if the step is a fanout, then we need to recurse down its `.nextToIterate`
|
|
459
|
+
// we're not going to exit though because the fanout step may also have a `.next` step
|
|
460
|
+
const nextId: string | null =
|
|
461
|
+
(currentStep as FanOutStep).parameters.nextToIterate ?? null;
|
|
462
|
+
nextId && recurseDown(stateMachine.stepMap[nextId]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
currentStep = currentStep.next
|
|
466
|
+
? stateMachine.stepMap[currentStep.next]
|
|
467
|
+
: undefined;
|
|
468
|
+
if (currentStep && currentStep.id === stopId) {
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const initialStep: IStep = stateMachine.stepMap[stepId];
|
|
475
|
+
const downstreamSteps: IStep[] = [];
|
|
476
|
+
|
|
477
|
+
recurseDown(initialStep);
|
|
478
|
+
|
|
479
|
+
// if there's a requested stopping point for this method,
|
|
480
|
+
// then we need to get the downstream steps of that stopping point
|
|
481
|
+
// and filter those out of the returned items
|
|
482
|
+
// this is because a workflow can have multiple paths
|
|
483
|
+
// and the steps under the requested stopping point may have been traversed
|
|
484
|
+
// by a different path in which the stopping point is not on,
|
|
485
|
+
// such as a workflow with conditionals or fanouts
|
|
486
|
+
const filterStepIds: string[] = [
|
|
487
|
+
stateMachine.stepMap[stepId],
|
|
488
|
+
...(stopId ? getDownstreamSteps(stopId, stateMachine) : []),
|
|
489
|
+
].map((s: IStep) => s.id);
|
|
490
|
+
return downstreamSteps.filter((s: IStep) => !filterStepIds.includes(s.id));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* given a step, a conditional step, and a state machine,
|
|
495
|
+
* it determines if the step is contained within the conditional within the state machine
|
|
496
|
+
*
|
|
497
|
+
* @param step
|
|
498
|
+
* @param conditional
|
|
499
|
+
* @param stateMachine
|
|
500
|
+
* @returns
|
|
501
|
+
*/
|
|
502
|
+
export function isStepInConditional(
|
|
503
|
+
step: IStep,
|
|
504
|
+
conditional: ConditionalStep,
|
|
505
|
+
stateMachine: StateMachine,
|
|
506
|
+
): boolean {
|
|
507
|
+
if (step.id === conditional.id) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const downstreamSteps: IStep[] = getDownstreamSteps(
|
|
512
|
+
conditional.id,
|
|
513
|
+
stateMachine,
|
|
514
|
+
conditional.next,
|
|
515
|
+
);
|
|
516
|
+
const result: boolean =
|
|
517
|
+
downstreamSteps.filter((s: IStep) => s.id === step.id).length > 0;
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* given a step, a fanout step, and a state machine,
|
|
523
|
+
* it determines if the step is contained within the fanout within the state machine
|
|
524
|
+
*
|
|
525
|
+
* @param step
|
|
526
|
+
* @param mapStep
|
|
527
|
+
* @param stateMachine
|
|
528
|
+
* @returns
|
|
529
|
+
*/
|
|
530
|
+
export function isStepInFanout(
|
|
531
|
+
step: IStep,
|
|
532
|
+
mapStep: FanOutStep,
|
|
533
|
+
stateMachine: StateMachine,
|
|
534
|
+
): boolean {
|
|
535
|
+
if (step.id === mapStep.id) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const downstreamSteps: IStep[] = getDownstreamSteps(
|
|
540
|
+
mapStep.id,
|
|
541
|
+
stateMachine,
|
|
542
|
+
mapStep.next,
|
|
543
|
+
);
|
|
544
|
+
const result: boolean =
|
|
545
|
+
downstreamSteps.filter((s: IStep) => s.id === step.id).length > 0;
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* removes cyclical referenes, unused steps
|
|
551
|
+
*
|
|
552
|
+
* @param stateMachine the state machine to validate
|
|
553
|
+
*/
|
|
554
|
+
export function sanitizeStateMachine(stateMachine: StateMachine): StateMachine {
|
|
555
|
+
// remove unused steps from state machine
|
|
556
|
+
if (!stateMachine.unusedSteps) {
|
|
557
|
+
stateMachine.unusedSteps = [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for (const stepId of stateMachine.unusedSteps) {
|
|
561
|
+
for (const step of Object.values(stateMachine.stepMap)) {
|
|
562
|
+
if (stepId === step.id) {
|
|
563
|
+
delete stateMachine.stepMap[step.id];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const allSteps: IStep[] = Object.values(stateMachine.stepMap);
|
|
569
|
+
const triggerStep = allSteps.find(isTrigger);
|
|
570
|
+
|
|
571
|
+
// TODO(PARA-1245): #test write test to ensure step never points to itself or parent step
|
|
572
|
+
const steps: IStep[] = triggerStep
|
|
573
|
+
? getDownstreamSteps(triggerStep.id, stateMachine)
|
|
574
|
+
: allSteps;
|
|
575
|
+
|
|
576
|
+
for (const step of steps) {
|
|
577
|
+
// ensure steps aren't pointing to itself
|
|
578
|
+
if (step.id === step.next) {
|
|
579
|
+
stateMachine.stepMap[step.id].next = null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ensure next step exists
|
|
583
|
+
if (step.next && !stateMachine.stepMap[step.next]) {
|
|
584
|
+
stateMachine.stepMap[step.id].next = null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// if a fanout but "nextToIterate" is set but doesn't exist in the state machine,
|
|
588
|
+
// set it to undefined
|
|
589
|
+
if (
|
|
590
|
+
isFanout(step) &&
|
|
591
|
+
step.parameters.nextToIterate &&
|
|
592
|
+
!stateMachine.stepMap[step.parameters.nextToIterate]
|
|
593
|
+
) {
|
|
594
|
+
(stateMachine.stepMap[step.id] as FanOutStep).parameters.nextToIterate =
|
|
595
|
+
null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// if a conditional and a choice's next is set but doesn't exist in the state machine,
|
|
599
|
+
// set it to undefined
|
|
600
|
+
if (isConditional(step)) {
|
|
601
|
+
let index: number = -1;
|
|
602
|
+
for (const choice of (step as ConditionalStep).parameters.choices) {
|
|
603
|
+
index += 1;
|
|
604
|
+
if (choice.next && !stateMachine.stepMap[choice.next]) {
|
|
605
|
+
(stateMachine.stepMap[step.id] as ConditionalStep).parameters.choices[
|
|
606
|
+
index
|
|
607
|
+
].next = null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ensure no cyclical workflows (step pointing to a parent step)
|
|
613
|
+
const parentSteps: IStep[] = getUpstreamSteps(step.id, stateMachine);
|
|
614
|
+
for (const parentStep of parentSteps) {
|
|
615
|
+
if (step.next === parentStep.id && step.id !== parentStep.id) {
|
|
616
|
+
// TODO: once apps support importing same shared logger on dashboard + other services,
|
|
617
|
+
// uncomment this to help debugging
|
|
618
|
+
// warn('detected cyclical step', {
|
|
619
|
+
// stepId: step.id,
|
|
620
|
+
// cyclicalStepId: parentStep.id,
|
|
621
|
+
// workflowId: step.workflowId,
|
|
622
|
+
// stateMachine: stateMachine,
|
|
623
|
+
// });
|
|
624
|
+
stateMachine.stepMap[step.id].next = null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ensure no step is pointing to itself
|
|
629
|
+
if (step.next === step.id) {
|
|
630
|
+
stateMachine.stepMap[step.id].next = null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return stateMachine;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Creates a state machine representation of a workflow or workflow fragment
|
|
639
|
+
* from an array of IStep objects.
|
|
640
|
+
*
|
|
641
|
+
* @param steps IStep entities to create a state machine representation from
|
|
642
|
+
* @param connectConditionalNextSteps If true, sets `.next` properties of the last steps
|
|
643
|
+
* of conditional branches to the nearest resolving step
|
|
644
|
+
* @param startStepId If specified, forces the state machine representation to begin at
|
|
645
|
+
* this step, even if it's not a trigger-type step
|
|
646
|
+
*/
|
|
647
|
+
export function workflowStepsToStateMachine(
|
|
648
|
+
steps: IStep[],
|
|
649
|
+
connectConditionalNextSteps: boolean = false,
|
|
650
|
+
startStepId?: string,
|
|
651
|
+
): StateMachine {
|
|
652
|
+
if (steps.length === 0) {
|
|
653
|
+
return EMPTY_STATE_MACHINE;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const stepMap: StepMap = {};
|
|
657
|
+
steps.forEach((step: IStep) => (stepMap[step.id] = step));
|
|
658
|
+
|
|
659
|
+
// get the first step;
|
|
660
|
+
// initialize the first sequence
|
|
661
|
+
const sequences: Sequence[] = [];
|
|
662
|
+
const traversedSteps: string[] = [];
|
|
663
|
+
const triggerStep: IStep | undefined = steps.find(isTrigger);
|
|
664
|
+
if (!triggerStep) {
|
|
665
|
+
throw new Error('Trigger step not found');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
createSequence(triggerStep, sequences, SequenceType.TRIGGER);
|
|
669
|
+
traverseSteps(triggerStep, sequences[0], stepMap, sequences, traversedSteps);
|
|
670
|
+
|
|
671
|
+
const sequenceMap: SequenceMap = {};
|
|
672
|
+
sequences.forEach(
|
|
673
|
+
(sequence: Sequence) => (sequenceMap[sequence.id] = sequence),
|
|
674
|
+
);
|
|
675
|
+
const stateMachine: StateMachine = {
|
|
676
|
+
stepMap,
|
|
677
|
+
sequenceMap,
|
|
678
|
+
start: sequences[0].id,
|
|
679
|
+
activeStepId: startStepId ? startStepId : triggerStep.id,
|
|
680
|
+
finalStepId:
|
|
681
|
+
startStepId && startStepId !== triggerStep.id ? startStepId : undefined,
|
|
682
|
+
unusedSteps: steps
|
|
683
|
+
.map((step: IStep) => step.id)
|
|
684
|
+
.filter((id: string) => !traversedSteps.includes(id)),
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
if (connectConditionalNextSteps) {
|
|
688
|
+
let lastNextFromConditional: string | null = null;
|
|
689
|
+
const visitedConditionalIds: string[] = [];
|
|
690
|
+
const lastStep = getLastStepInSequence(
|
|
691
|
+
stateMachine.sequenceMap[stateMachine.start],
|
|
692
|
+
stateMachine,
|
|
693
|
+
);
|
|
694
|
+
traverseStateMachineBySequence(stateMachine, (sequence: Sequence) => {
|
|
695
|
+
if (
|
|
696
|
+
sequence.stepIds.length === 1 &&
|
|
697
|
+
stateMachine.stepMap[sequence.stepIds[0]].type === StepType.IFELSE
|
|
698
|
+
) {
|
|
699
|
+
const conditionalStep = stateMachine.stepMap[sequence.stepIds[0]];
|
|
700
|
+
if (visitedConditionalIds.includes(conditionalStep.id)) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (
|
|
704
|
+
!conditionalStep.next &&
|
|
705
|
+
lastStep?.id !== conditionalStep.id &&
|
|
706
|
+
conditionalStep.id !== lastNextFromConditional
|
|
707
|
+
) {
|
|
708
|
+
conditionalStep.next = lastNextFromConditional;
|
|
709
|
+
}
|
|
710
|
+
lastNextFromConditional = conditionalStep.next;
|
|
711
|
+
|
|
712
|
+
const branches = sequence.sequenceEdges.filter(
|
|
713
|
+
(edge: SequenceEdge) => edge.type === SequenceEdgeType.BRANCH,
|
|
714
|
+
);
|
|
715
|
+
branches.forEach(({ to }: SequenceEdge) => {
|
|
716
|
+
const lastStep = getLastStepInSequence(
|
|
717
|
+
stateMachine.sequenceMap[to],
|
|
718
|
+
stateMachine,
|
|
719
|
+
);
|
|
720
|
+
if (lastStep && lastStep.id !== conditionalStep.next) {
|
|
721
|
+
lastStep.next = conditionalStep.next;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
visitedConditionalIds.push(conditionalStep.id);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const finalStateMachine: StateMachine = sanitizeStateMachine(stateMachine);
|
|
730
|
+
verifyStateMachine(finalStateMachine);
|
|
731
|
+
|
|
732
|
+
return finalStateMachine;
|
|
733
|
+
}
|