@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.
Files changed (84) hide show
  1. package/package.json +63 -0
  2. package/src/event/event.interface.ts +18 -0
  3. package/src/event/index.ts +1 -0
  4. package/src/execution/context.constants.ts +9 -0
  5. package/src/execution/context.interface.ts +39 -0
  6. package/src/execution/context.ts +72 -0
  7. package/src/execution/context.utils.ts +53 -0
  8. package/src/execution/index.ts +2 -0
  9. package/src/index.ts +6 -0
  10. package/src/integration/custom-integration.interface.ts +20 -0
  11. package/src/integration/index.ts +2 -0
  12. package/src/integration/integration-config.interface.ts +19 -0
  13. package/src/integration/integration.interface.ts +16 -0
  14. package/src/operator/index.ts +2 -0
  15. package/src/operator/operator.interface.ts +6 -0
  16. package/src/operator/operators/BooleanTrue.ts +20 -0
  17. package/src/operator/operators/StringContains.ts +27 -0
  18. package/src/resolvers/index.ts +2 -0
  19. package/src/resolvers/resolver.utils.ts +157 -0
  20. package/src/resolvers/resolvers.interface.ts +369 -0
  21. package/src/secret/index.ts +1 -0
  22. package/src/secret/secret.interface.ts +4 -0
  23. package/src/stateMachine/index.ts +2 -0
  24. package/src/stateMachine/stateMachine.constants.ts +12 -0
  25. package/src/stateMachine/stateMachine.interface.ts +145 -0
  26. package/src/stateMachine/stateMachine.utils.ts +733 -0
  27. package/src/steps/index.ts +3 -0
  28. package/src/steps/library/action/action.interface.ts +69 -0
  29. package/src/steps/library/action/action.step.ts +70 -0
  30. package/src/steps/library/action/index.ts +2 -0
  31. package/src/steps/library/conditional/conditional.interface.ts +82 -0
  32. package/src/steps/library/conditional/conditional.step.ts +96 -0
  33. package/src/steps/library/conditional/conditional.utils.ts +110 -0
  34. package/src/steps/library/conditional/index.ts +2 -0
  35. package/src/steps/library/delay/delay.interface.ts +71 -0
  36. package/src/steps/library/delay/delay.step.ts +51 -0
  37. package/src/steps/library/delay/index.ts +2 -0
  38. package/src/steps/library/fanout/fanout.interface.ts +46 -0
  39. package/src/steps/library/fanout/fanout.step.ts +68 -0
  40. package/src/steps/library/fanout/index.ts +2 -0
  41. package/src/steps/library/function/function.interface.ts +69 -0
  42. package/src/steps/library/function/function.step.ts +55 -0
  43. package/src/steps/library/function/index.ts +2 -0
  44. package/src/steps/library/index.ts +7 -0
  45. package/src/steps/library/integrationRequest/index.ts +2 -0
  46. package/src/steps/library/integrationRequest/integrationRequest.interface.ts +79 -0
  47. package/src/steps/library/integrationRequest/integrationRequest.step.ts +100 -0
  48. package/src/steps/library/request/index.ts +2 -0
  49. package/src/steps/library/request/request.interface.ts +159 -0
  50. package/src/steps/library/request/request.step.ts +117 -0
  51. package/src/steps/library/response/index.ts +2 -0
  52. package/src/steps/library/response/response.interface.ts +50 -0
  53. package/src/steps/library/response/response.step.ts +68 -0
  54. package/src/steps/step.constants.ts +4 -0
  55. package/src/steps/step.interface-base.ts +81 -0
  56. package/src/steps/step.interface.ts +31 -0
  57. package/src/steps/step.ts +136 -0
  58. package/src/steps/step.utils.ts +103 -0
  59. package/src/triggers/cron/cron.interface.ts +94 -0
  60. package/src/triggers/cron/cron.step.ts +52 -0
  61. package/src/triggers/cron/cron.utils.ts +117 -0
  62. package/src/triggers/cron/index.ts +3 -0
  63. package/src/triggers/endpoint/endpoint.interface.ts +66 -0
  64. package/src/triggers/endpoint/endpoint.step.ts +61 -0
  65. package/src/triggers/endpoint/index.ts +2 -0
  66. package/src/triggers/event/event.interface.ts +43 -0
  67. package/src/triggers/event/event.step.ts +41 -0
  68. package/src/triggers/event/index.ts +2 -0
  69. package/src/triggers/index.ts +4 -0
  70. package/src/triggers/integrationEnabled/index.ts +2 -0
  71. package/src/triggers/integrationEnabled/integrationEnabled.interface.ts +29 -0
  72. package/src/triggers/integrationEnabled/integrationEnabled.step.ts +33 -0
  73. package/src/triggers/trigger.interface.ts +28 -0
  74. package/src/triggers/trigger.ts +25 -0
  75. package/src/user/index.ts +2 -0
  76. package/src/user/user.interface.ts +4 -0
  77. package/src/user/user.ts +6 -0
  78. package/src/utils/index.ts +1 -0
  79. package/src/utils/utils.ts +10 -0
  80. package/src/workflow/index.ts +2 -0
  81. package/src/workflow/workflow.interface.ts +50 -0
  82. package/src/workflow/workflow.ts +132 -0
  83. package/tsconfig.json +9 -0
  84. 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
+ }