@fsai-flow/core 0.0.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 (113) hide show
  1. package/README.md +11 -0
  2. package/dist/README.md +11 -0
  3. package/dist/package.json +44 -0
  4. package/dist/src/index.d.ts +15 -0
  5. package/dist/src/index.js +29 -0
  6. package/dist/src/index.js.map +1 -0
  7. package/dist/src/lib/ActiveWebhooks.d.ts +59 -0
  8. package/dist/src/lib/ActiveWebhooks.js +184 -0
  9. package/dist/src/lib/ActiveWebhooks.js.map +1 -0
  10. package/dist/src/lib/ActiveWorkflows.d.ts +58 -0
  11. package/dist/src/lib/ActiveWorkflows.js +244 -0
  12. package/dist/src/lib/ActiveWorkflows.js.map +1 -0
  13. package/dist/src/lib/BinaryDataManager/FileSystem.d.ts +26 -0
  14. package/dist/src/lib/BinaryDataManager/FileSystem.js +179 -0
  15. package/dist/src/lib/BinaryDataManager/FileSystem.js.map +1 -0
  16. package/dist/src/lib/BinaryDataManager/index.d.ts +21 -0
  17. package/dist/src/lib/BinaryDataManager/index.js +146 -0
  18. package/dist/src/lib/BinaryDataManager/index.js.map +1 -0
  19. package/dist/src/lib/ChangeCase.d.ts +9 -0
  20. package/dist/src/lib/ChangeCase.js +43 -0
  21. package/dist/src/lib/ChangeCase.js.map +1 -0
  22. package/dist/src/lib/Constants.d.ts +14 -0
  23. package/dist/src/lib/Constants.js +19 -0
  24. package/dist/src/lib/Constants.js.map +1 -0
  25. package/dist/src/lib/Credentials.d.ts +27 -0
  26. package/dist/src/lib/Credentials.js +89 -0
  27. package/dist/src/lib/Credentials.js.map +1 -0
  28. package/dist/src/lib/FileSystem.d.ts +26 -0
  29. package/dist/src/lib/FileSystem.js +179 -0
  30. package/dist/src/lib/FileSystem.js.map +1 -0
  31. package/dist/src/lib/InputConnectionDataLegacy.d.ts +2 -0
  32. package/dist/src/lib/InputConnectionDataLegacy.js +79 -0
  33. package/dist/src/lib/InputConnectionDataLegacy.js.map +1 -0
  34. package/dist/src/lib/Interfaces.d.ts +148 -0
  35. package/dist/src/lib/Interfaces.js +3 -0
  36. package/dist/src/lib/Interfaces.js.map +1 -0
  37. package/dist/src/lib/LoadNodeParameterOptions.d.ts +39 -0
  38. package/dist/src/lib/LoadNodeParameterOptions.js +150 -0
  39. package/dist/src/lib/LoadNodeParameterOptions.js.map +1 -0
  40. package/dist/src/lib/NodeExecuteFunctions.d.ts +226 -0
  41. package/dist/src/lib/NodeExecuteFunctions.js +2483 -0
  42. package/dist/src/lib/NodeExecuteFunctions.js.map +1 -0
  43. package/dist/src/lib/NodesLoader/constants.d.ts +5 -0
  44. package/dist/src/lib/NodesLoader/constants.js +106 -0
  45. package/dist/src/lib/NodesLoader/constants.js.map +1 -0
  46. package/dist/src/lib/NodesLoader/custom-directory-loader.d.ts +9 -0
  47. package/dist/src/lib/NodesLoader/custom-directory-loader.js +36 -0
  48. package/dist/src/lib/NodesLoader/custom-directory-loader.js.map +1 -0
  49. package/dist/src/lib/NodesLoader/directory-loader.d.ts +66 -0
  50. package/dist/src/lib/NodesLoader/directory-loader.js +325 -0
  51. package/dist/src/lib/NodesLoader/directory-loader.js.map +1 -0
  52. package/dist/src/lib/NodesLoader/index.d.ts +5 -0
  53. package/dist/src/lib/NodesLoader/index.js +12 -0
  54. package/dist/src/lib/NodesLoader/index.js.map +1 -0
  55. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.d.ts +7 -0
  56. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js +52 -0
  57. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js.map +1 -0
  58. package/dist/src/lib/NodesLoader/load-class-in-isolation.d.ts +1 -0
  59. package/dist/src/lib/NodesLoader/load-class-in-isolation.js +22 -0
  60. package/dist/src/lib/NodesLoader/load-class-in-isolation.js.map +1 -0
  61. package/dist/src/lib/NodesLoader/package-directory-loader.d.ts +17 -0
  62. package/dist/src/lib/NodesLoader/package-directory-loader.js +100 -0
  63. package/dist/src/lib/NodesLoader/package-directory-loader.js.map +1 -0
  64. package/dist/src/lib/NodesLoader/types.d.ts +14 -0
  65. package/dist/src/lib/NodesLoader/types.js +3 -0
  66. package/dist/src/lib/NodesLoader/types.js.map +1 -0
  67. package/dist/src/lib/UserSettings.d.ts +80 -0
  68. package/dist/src/lib/UserSettings.js +261 -0
  69. package/dist/src/lib/UserSettings.js.map +1 -0
  70. package/dist/src/lib/WorkflowExecute.d.ts +53 -0
  71. package/dist/src/lib/WorkflowExecute.js +835 -0
  72. package/dist/src/lib/WorkflowExecute.js.map +1 -0
  73. package/dist/src/lib/index.d.ts +21 -0
  74. package/dist/src/lib/index.js +146 -0
  75. package/dist/src/lib/index.js.map +1 -0
  76. package/dist/src/utils/crypto.d.ts +1 -0
  77. package/dist/src/utils/crypto.js +8 -0
  78. package/dist/src/utils/crypto.js.map +1 -0
  79. package/eslint.config.js +19 -0
  80. package/jest.config.ts +10 -0
  81. package/package.json +44 -0
  82. package/project.json +19 -0
  83. package/src/index.ts +27 -0
  84. package/src/lib/ActiveWebhooks.ts +245 -0
  85. package/src/lib/ActiveWorkflows.ts +304 -0
  86. package/src/lib/BinaryDataManager/FileSystem.ts +214 -0
  87. package/src/lib/BinaryDataManager/index.ts +187 -0
  88. package/src/lib/ChangeCase.ts +45 -0
  89. package/src/lib/Constants.ts +16 -0
  90. package/src/lib/Credentials.ts +108 -0
  91. package/src/lib/FileSystem.ts +214 -0
  92. package/src/lib/InputConnectionDataLegacy.ts +123 -0
  93. package/src/lib/Interfaces.ts +338 -0
  94. package/src/lib/LoadNodeParameterOptions.ts +235 -0
  95. package/src/lib/NodeExecuteFunctions.ts +3704 -0
  96. package/src/lib/NodesLoader/constants.ts +112 -0
  97. package/src/lib/NodesLoader/custom-directory-loader.ts +31 -0
  98. package/src/lib/NodesLoader/directory-loader.ts +458 -0
  99. package/src/lib/NodesLoader/index.ts +5 -0
  100. package/src/lib/NodesLoader/lazy-package-directory-loader.ts +55 -0
  101. package/src/lib/NodesLoader/load-class-in-isolation.ts +19 -0
  102. package/src/lib/NodesLoader/package-directory-loader.ts +107 -0
  103. package/src/lib/NodesLoader/types.ts +14 -0
  104. package/src/lib/UserSettings.ts +292 -0
  105. package/src/lib/WorkflowExecute.ts +1108 -0
  106. package/src/lib/index.ts +187 -0
  107. package/src/utils/crypto.ts +5 -0
  108. package/tests/Credentials.test.ts +88 -0
  109. package/tests/Helpers.ts +808 -0
  110. package/tests/WorkflowExecute.test.ts +1242 -0
  111. package/tsconfig.json +42 -0
  112. package/tsconfig.lib.json +10 -0
  113. package/tsconfig.spec.json +14 -0
@@ -0,0 +1,1108 @@
1
+
2
+ import PCancelable = require('p-cancelable');
3
+
4
+ import {
5
+ ExecutionError,
6
+ IConnection,
7
+ IDataObject,
8
+ IExecuteData,
9
+ INode,
10
+ INodeConnections,
11
+ INodeExecutionData,
12
+ IRun,
13
+ IRunData,
14
+ IRunExecutionData,
15
+ ITaskData,
16
+ ITaskDataConnections,
17
+ IWaitingForExecution,
18
+ IWorkflowExecuteAdditionalData,
19
+ LoggerProxy as Logger,
20
+ NodeApiError,
21
+ NodeOperationError,
22
+ Workflow,
23
+ WorkflowExecuteMode,
24
+ WorkflowOperationError,
25
+ } from '@fsai-flow/workflow';
26
+ import { get } from 'lodash';
27
+ import { NodeExecuteFunctions } from '../../src';
28
+
29
+ export class WorkflowExecute {
30
+ runExecutionData: IRunExecutionData;
31
+
32
+ private additionalData: IWorkflowExecuteAdditionalData;
33
+
34
+ private mode: WorkflowExecuteMode;
35
+
36
+ constructor(
37
+ additionalData: IWorkflowExecuteAdditionalData,
38
+ mode: WorkflowExecuteMode,
39
+ runExecutionData?: IRunExecutionData,
40
+ ) {
41
+ this.additionalData = additionalData;
42
+ this.mode = mode;
43
+ this.runExecutionData = runExecutionData || {
44
+ startData: {},
45
+ resultData: {
46
+ runData: {},
47
+ },
48
+ executionData: {
49
+ contextData: {},
50
+ nodeExecutionStack: [],
51
+ waitingExecution: {},
52
+ },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Executes the given workflow.
58
+ *
59
+ * @param {Workflow} workflow The workflow to execute
60
+ * @param {INode[]} [startNodes] Node to start execution from
61
+ * @param {string} [destinationNode] Node to stop execution at
62
+ * @returns {(Promise<string>)}
63
+ * @memberof WorkflowExecute
64
+ */
65
+ // IMPORTANT: Do not add "async" to this function, it will then convert the
66
+ // PCancelable to a regular Promise and does so not allow canceling
67
+ // active executions anymore
68
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
69
+ run(workflow: Workflow, startNode?: INode, destinationNode?: string): PCancelable<IRun> {
70
+ // Get the nodes to start workflow execution from
71
+ startNode = startNode || workflow.getStartNode(destinationNode);
72
+
73
+ if (startNode === undefined) {
74
+ throw new Error('No node to start the workflow from could be found!');
75
+ }
76
+
77
+ // If a destination node is given we only run the direct parent nodes and no others
78
+ let runNodeFilter: string[] | undefined;
79
+ if (destinationNode) {
80
+ runNodeFilter = workflow.getParentNodes(destinationNode);
81
+ runNodeFilter.push(destinationNode);
82
+ }
83
+
84
+ // Initialize the data of the start nodes
85
+ const nodeExecutionStack: IExecuteData[] = [
86
+ {
87
+ node: startNode,
88
+ data: {
89
+ main: [
90
+ [
91
+ {
92
+ json: {},
93
+ },
94
+ ],
95
+ ],
96
+ },
97
+ },
98
+ ];
99
+
100
+ this.runExecutionData = {
101
+ startData: {
102
+ destinationNode,
103
+ runNodeFilter,
104
+ },
105
+ resultData: {
106
+ runData: {},
107
+ },
108
+ executionData: {
109
+ contextData: {},
110
+ nodeExecutionStack,
111
+ waitingExecution: {},
112
+ },
113
+ };
114
+
115
+ return this.processRunExecutionData(workflow);
116
+ }
117
+
118
+ /**
119
+ * Executes the given workflow but only
120
+ *
121
+ * @param {Workflow} workflow The workflow to execute
122
+ * @param {IRunData} runData
123
+ * @param {string[]} startNodes Nodes to start execution from
124
+ * @param {string} destinationNode Node to stop execution at
125
+ * @returns {(Promise<string>)}
126
+ * @memberof WorkflowExecute
127
+ */
128
+ // IMPORTANT: Do not add "async" to this function, it will then convert the
129
+ // PCancelable to a regular Promise and does so not allow canceling
130
+ // active executions anymore
131
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
132
+ runPartialWorkflow(
133
+ workflow: Workflow,
134
+ runData: IRunData,
135
+ startNodes: string[],
136
+ destinationNode: string,
137
+ ): PCancelable<IRun> {
138
+ let incomingNodeConnections: INodeConnections | undefined;
139
+ let connection: IConnection;
140
+
141
+ const runIndex = 0;
142
+
143
+ // Initialize the nodeExecutionStack and waitingExecution with
144
+ // the data from runData
145
+ const nodeExecutionStack: IExecuteData[] = [];
146
+ const waitingExecution: IWaitingForExecution = {};
147
+ for (const startNode of startNodes) {
148
+ incomingNodeConnections = workflow.connectionsByDestinationNode[startNode];
149
+
150
+ const incomingData: INodeExecutionData[][] = [];
151
+
152
+ if (incomingNodeConnections === undefined) {
153
+ // If it has no incoming data add the default empty data
154
+ incomingData.push([
155
+ {
156
+ json: {},
157
+ },
158
+ ]);
159
+ } else {
160
+ // Get the data of the incoming connections
161
+ for (const connections of incomingNodeConnections['main']) {
162
+ for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
163
+ connection = connections[inputIndex];
164
+ incomingData.push(
165
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
166
+ runData[connection.node][runIndex].data![connection.type][connection.index]!,
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ const executeData: IExecuteData = {
173
+ node: workflow.getNode(startNode) as INode,
174
+ data: {
175
+ main: incomingData,
176
+ },
177
+ };
178
+
179
+ nodeExecutionStack.push(executeData);
180
+
181
+ // Check if the destinationNode has to be added as waiting
182
+ // because some input data is already fully available
183
+ incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode];
184
+ if (incomingNodeConnections !== undefined) {
185
+ for (const connections of incomingNodeConnections['main']) {
186
+ for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
187
+ connection = connections[inputIndex];
188
+
189
+ if (waitingExecution[destinationNode] === undefined) {
190
+ waitingExecution[destinationNode] = {};
191
+ }
192
+ if (waitingExecution[destinationNode][runIndex] === undefined) {
193
+ waitingExecution[destinationNode][runIndex] = {};
194
+ }
195
+ if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) {
196
+ waitingExecution[destinationNode][runIndex][connection.type] = [];
197
+ }
198
+
199
+ if (runData[connection.node] !== undefined) {
200
+ // Input data exists so add as waiting
201
+ // incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]);
202
+ waitingExecution[destinationNode][runIndex][connection.type].push(
203
+ runData[connection.node][runIndex].data![connection.type][connection.index],
204
+ );
205
+ } else {
206
+ waitingExecution[destinationNode][runIndex][connection.type].push(null);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Only run the parent nodes and no others
214
+ let runNodeFilter: string[] | undefined;
215
+ // eslint-disable-next-line prefer-const
216
+ runNodeFilter = workflow.getParentNodes(destinationNode);
217
+ runNodeFilter.push(destinationNode);
218
+
219
+ this.runExecutionData = {
220
+ startData: {
221
+ destinationNode,
222
+ runNodeFilter,
223
+ },
224
+ resultData: {
225
+ runData,
226
+ },
227
+ executionData: {
228
+ contextData: {},
229
+ nodeExecutionStack,
230
+ waitingExecution,
231
+ },
232
+ };
233
+
234
+ return this.processRunExecutionData(workflow);
235
+ }
236
+
237
+ /**
238
+ * Executes the hook with the given name
239
+ *
240
+ * @param {string} hookName
241
+ * @param {any[]} parameters
242
+ * @returns {Promise<IRun>}
243
+ * @memberof WorkflowExecute
244
+ */
245
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
+ async executeHook(hookName: string, parameters: any[]): Promise<void> {
247
+ // tslint:disable-line:no-any
248
+ if (this.additionalData.hooks === undefined) {
249
+ return;
250
+ }
251
+
252
+ // eslint-disable-next-line consistent-return
253
+ return this.additionalData.hooks.executeHookFunctions(hookName, parameters);
254
+ }
255
+
256
+ /**
257
+ * Checks the incoming connection does not receive any data
258
+ */
259
+ incomingConnectionIsEmpty(
260
+ runData: IRunData,
261
+ inputConnections: IConnection[],
262
+ runIndex: number,
263
+ ): boolean {
264
+ // for (const inputConnection of workflow.connectionsByDestinationNode[nodeToAdd].main[0]) {
265
+ for (const inputConnection of inputConnections) {
266
+ const nodeIncomingData = get(
267
+ runData,
268
+ `[${inputConnection.node}][${runIndex}].data.main[${inputConnection.index}]`,
269
+ );
270
+ if (nodeIncomingData !== undefined && (nodeIncomingData as object[]).length !== 0) {
271
+ return false;
272
+ }
273
+ }
274
+ return true;
275
+ }
276
+
277
+ addNodeToBeExecuted(
278
+ workflow: Workflow,
279
+ connectionData: IConnection,
280
+ outputIndex: number,
281
+ parentNodeName: string,
282
+ nodeSuccessData: INodeExecutionData[][],
283
+ runIndex: number,
284
+ ): void {
285
+ let stillDataMissing = false;
286
+
287
+ // Check if node has multiple inputs as then we have to wait for all input data
288
+ // to be present before we can add it to the node-execution-stack
289
+ if (workflow.connectionsByDestinationNode[connectionData.node]['main'].length > 1) {
290
+ // Node has multiple inputs
291
+ let nodeWasWaiting = true;
292
+
293
+ // Check if there is already data for the node
294
+ if (
295
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined
296
+ ) {
297
+ // Node does not have data yet so create a new empty one
298
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
299
+ nodeWasWaiting = false;
300
+ }
301
+ if (
302
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] ===
303
+ undefined
304
+ ) {
305
+ // Node does not have data for runIndex yet so create also empty one and init it
306
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
307
+ main: [],
308
+ };
309
+ for (
310
+ let i = 0;
311
+ i < workflow.connectionsByDestinationNode[connectionData.node]['main'].length;
312
+ i++
313
+ ) {
314
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]['main'].push(null);
315
+ }
316
+ }
317
+
318
+ // Add the new data
319
+ if (nodeSuccessData === null) {
320
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]['main'][
321
+ connectionData.index
322
+ ] = null;
323
+ } else {
324
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]['main'][
325
+ connectionData.index
326
+ ] = nodeSuccessData[outputIndex];
327
+ }
328
+
329
+ // Check if all data exists now
330
+ let thisExecutionData: INodeExecutionData[] | null;
331
+ let allDataFound = true;
332
+ for (
333
+ let i = 0;
334
+ i <
335
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]['main']
336
+ .length;
337
+ i++
338
+ ) {
339
+ thisExecutionData =
340
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]['main'][
341
+ i
342
+ ];
343
+ if (thisExecutionData === null) {
344
+ allDataFound = false;
345
+ break;
346
+ }
347
+ }
348
+
349
+ if (allDataFound) {
350
+ // All data exists for node to be executed
351
+ // So add it to the execution stack
352
+ this.runExecutionData.executionData!.nodeExecutionStack.push({
353
+ node: workflow.nodes[connectionData.node],
354
+ data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][
355
+ runIndex
356
+ ],
357
+ });
358
+
359
+ // Remove the data from waiting
360
+ delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
361
+
362
+ if (
363
+ Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node])
364
+ .length === 0
365
+ ) {
366
+ // No more data left for the node so also delete that one
367
+ delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
368
+ }
369
+ return;
370
+ }
371
+ stillDataMissing = true;
372
+
373
+ if (!nodeWasWaiting) {
374
+ // Get a list of all the output nodes that we can check for siblings easier
375
+ const checkOutputNodes = [];
376
+ // eslint-disable-next-line @typescript-eslint/no-for-in-array
377
+ for (const outputIndexParent in workflow.connectionsBySourceNode[parentNodeName]['main']) {
378
+ if (
379
+ // eslint-disable-next-line no-prototype-builtins
380
+ !workflow.connectionsBySourceNode[parentNodeName]['main'].hasOwnProperty(outputIndexParent)
381
+ ) {
382
+ continue;
383
+ }
384
+ for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName]['main'][
385
+ outputIndexParent
386
+ ]) {
387
+ checkOutputNodes.push(connectionDataCheck.node);
388
+ }
389
+ }
390
+
391
+ // Node was not on "waitingExecution" so it is the first time it gets
392
+ // checked. So we have to go through all the inputs and check if they
393
+ // are already on the list to be processed.
394
+ // If that is not the case add it.
395
+ for (
396
+ let inputIndex = 0;
397
+ inputIndex < workflow.connectionsByDestinationNode[connectionData.node]['main'].length;
398
+ inputIndex++
399
+ ) {
400
+ for (const inputData of workflow.connectionsByDestinationNode[connectionData.node]['main'][
401
+ inputIndex
402
+ ]) {
403
+ if (inputData.node === parentNodeName) {
404
+ // Is the node we come from so its data will be available for sure
405
+ continue;
406
+ }
407
+
408
+ const executionStackNodes = this.runExecutionData.executionData!.nodeExecutionStack.map(
409
+ (stackData) => stackData.node.name,
410
+ );
411
+
412
+ // Check if that node is also an output connection of the
413
+ // previously processed one
414
+ if (inputData.node !== parentNodeName && checkOutputNodes.includes(inputData.node)) {
415
+ // So the parent node will be added anyway which
416
+ // will then process this node next. So nothing to do
417
+ // unless the incoming data of the node is empty
418
+ // because then it would not be executed
419
+ if (
420
+ !this.incomingConnectionIsEmpty(
421
+ this.runExecutionData.resultData.runData,
422
+ workflow.connectionsByDestinationNode[inputData.node]['main'][0],
423
+ runIndex,
424
+ )
425
+ ) {
426
+ continue;
427
+ }
428
+ }
429
+
430
+ // Check if it is already in the execution stack
431
+ if (executionStackNodes.includes(inputData.node)) {
432
+ // Node is already on the list to be executed
433
+ // so there is nothing to do
434
+ continue;
435
+ }
436
+
437
+ // Check if node got processed already
438
+ if (this.runExecutionData.resultData.runData[inputData.node] !== undefined) {
439
+ // Node got processed already so no need to add it
440
+ continue;
441
+ }
442
+
443
+ // Check if any of the parent nodes does not have any inputs. That
444
+ // would mean that it has to get added to the list of nodes to process.
445
+ const parentNodes = workflow.getParentNodes(inputData.node, 'main', -1);
446
+ let nodeToAdd: string | undefined = inputData.node;
447
+ parentNodes.push(inputData.node);
448
+ parentNodes.reverse();
449
+
450
+ for (const parentNode of parentNodes) {
451
+ // Check if that node is also an output connection of the
452
+ // previously processed one
453
+ if (inputData.node !== parentNode && checkOutputNodes.includes(parentNode)) {
454
+ // So the parent node will be added anyway which
455
+ // will then process this node next. So nothing to do.
456
+ nodeToAdd = undefined;
457
+ break;
458
+ }
459
+
460
+ // Check if it is already in the execution stack
461
+ if (executionStackNodes.includes(parentNode)) {
462
+ // Node is already on the list to be executed
463
+ // so there is nothing to do
464
+ nodeToAdd = undefined;
465
+ break;
466
+ }
467
+
468
+ // Check if node got processed already
469
+ if (this.runExecutionData.resultData.runData[parentNode] !== undefined) {
470
+ // Node got processed already so we can use the
471
+ // output data as input of this node
472
+ break;
473
+ }
474
+
475
+ nodeToAdd = parentNode;
476
+ }
477
+ const parentNodesNodeToAdd = workflow.getParentNodes(nodeToAdd as string);
478
+ if (
479
+ parentNodesNodeToAdd.includes(parentNodeName) &&
480
+ nodeSuccessData[outputIndex].length === 0
481
+ ) {
482
+ // We do not add the node if there is no input data and the node that should be connected
483
+ // is a child of the parent node. Because else it would run a node even though it should be
484
+ // specifically not run, as it did not receive any data.
485
+ nodeToAdd = undefined;
486
+ }
487
+
488
+ if (nodeToAdd === undefined) {
489
+ // No node has to get added so process
490
+ continue;
491
+ }
492
+
493
+ let addEmptyItem = false;
494
+
495
+ if (workflow.connectionsByDestinationNode[nodeToAdd] === undefined) {
496
+ // Add empty item if the node does not have any input connections
497
+ addEmptyItem = true;
498
+ } else if (
499
+ this.incomingConnectionIsEmpty(
500
+ this.runExecutionData.resultData.runData,
501
+ workflow.connectionsByDestinationNode[nodeToAdd]['main'][0],
502
+ runIndex,
503
+ )
504
+ ) {
505
+ // Add empty item also if the input data is empty
506
+ addEmptyItem = true;
507
+ }
508
+
509
+ if (addEmptyItem) {
510
+ // Add only node if it does not have any inputs because else it will
511
+ // be added by its input node later anyway.
512
+ this.runExecutionData.executionData!.nodeExecutionStack.push({
513
+ node: workflow.getNode(nodeToAdd) as INode,
514
+ data: {
515
+ main: [
516
+ [
517
+ {
518
+ json: {},
519
+ },
520
+ ],
521
+ ],
522
+ },
523
+ });
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
529
+
530
+ // Make sure the array has all the values
531
+ const connectionDataArray: Array<INodeExecutionData[] | null> = [];
532
+ for (let i: number = connectionData.index; i >= 0; i--) {
533
+ connectionDataArray[i] = null;
534
+ }
535
+
536
+ // Add the data of the current execution
537
+ if (nodeSuccessData === null) {
538
+ connectionDataArray[connectionData.index] = null;
539
+ } else {
540
+ connectionDataArray[connectionData.index] = nodeSuccessData[outputIndex];
541
+ }
542
+
543
+ if (stillDataMissing) {
544
+ // Additional data is needed to run node so add it to waiting
545
+ if (
546
+ // eslint-disable-next-line no-prototype-builtins
547
+ !this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)
548
+ ) {
549
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
550
+ }
551
+ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
552
+ main: connectionDataArray,
553
+ };
554
+ } else {
555
+ // All data is there so add it directly to stack
556
+ this.runExecutionData.executionData!.nodeExecutionStack.push({
557
+ node: workflow.nodes[connectionData.node],
558
+ data: {
559
+ main: connectionDataArray,
560
+ },
561
+ });
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Runs the given execution data.
567
+ *
568
+ * @param {Workflow} workflow
569
+ * @returns {Promise<string>}
570
+ * @memberof WorkflowExecute
571
+ */
572
+ // IMPORTANT: Do not add "async" to this function, it will then convert the
573
+ // PCancelable to a regular Promise and does so not allow canceling
574
+ // active executions anymore
575
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
576
+ processRunExecutionData(workflow: Workflow): PCancelable<IRun> {
577
+ Logger.verbose('Workflow execution started', { workflowId: workflow.id });
578
+ console.log(`Workflow execution started: ID: ${workflow.id} - Name: ${workflow.name} - Active: ${workflow.active ? 'active' : 'inactive'}`);
579
+
580
+ const startedAt = new Date();
581
+ const startNode = this.runExecutionData.executionData!.nodeExecutionStack[0].node.name;
582
+
583
+ let destinationNode: string | undefined;
584
+ if (this.runExecutionData.startData && this.runExecutionData.startData.destinationNode) {
585
+ destinationNode = this.runExecutionData.startData.destinationNode;
586
+ }
587
+
588
+ const workflowIssues = workflow.checkReadyForExecution({ startNode, destinationNode });
589
+ if (workflowIssues !== null) {
590
+ throw new Error(
591
+ 'The workflow has issues and can for that reason not be executed. Please fix them first.',
592
+ );
593
+ }
594
+
595
+ // Variables which hold temporary data for each node-execution
596
+ let executionData: IExecuteData;
597
+ let executionError: ExecutionError | undefined;
598
+ let executionNode: INode;
599
+ let nodeSuccessData: INodeExecutionData[][] | null | undefined;
600
+ let runIndex: number;
601
+ let startTime: number;
602
+ let taskData: ITaskData;
603
+
604
+ if (this.runExecutionData.startData === undefined) {
605
+ this.runExecutionData.startData = {};
606
+ }
607
+
608
+ if (this.runExecutionData.waitTill) {
609
+ const lastNodeExecuted = this.runExecutionData.resultData.lastNodeExecuted as string;
610
+ this.runExecutionData.executionData!.nodeExecutionStack[0].node.disabled = true;
611
+ this.runExecutionData.waitTill = undefined;
612
+ this.runExecutionData.resultData.runData[lastNodeExecuted].pop();
613
+ }
614
+
615
+ let currentExecutionTry = '';
616
+ let lastExecutionTry = '';
617
+
618
+ return new PCancelable((resolve: (value?: IRun | PromiseLike<IRun>) => void, reject: (reason?: unknown) => void, onCancel: (callback: () => void) => void) => {
619
+ let gotCancel = false;
620
+
621
+ (onCancel as any).shouldReject = false;
622
+ onCancel(() => {
623
+ gotCancel = true;
624
+ });
625
+
626
+ const returnPromise = (async () => {
627
+ try {
628
+ await this.executeHook('workflowExecuteBefore', [workflow]);
629
+ } catch (error) {
630
+ // Set the error that it can be saved correctly
631
+ executionError = {
632
+ ...(error as NodeOperationError | NodeApiError),
633
+ message: (error as NodeOperationError | NodeApiError).message,
634
+ stack: (error as NodeOperationError | NodeApiError).stack,
635
+ };
636
+
637
+ // Set the incoming data of the node that it can be saved correctly
638
+ // eslint-disable-next-line prefer-destructuring
639
+ executionData = this.runExecutionData.executionData!.nodeExecutionStack[0];
640
+ this.runExecutionData.resultData = {
641
+ runData: {
642
+ [executionData.node.name]: [
643
+ {
644
+ startTime,
645
+ executionTime: new Date().getTime() - startTime,
646
+ data: {
647
+ main: executionData.data['main'],
648
+ } as ITaskDataConnections,
649
+ },
650
+ ],
651
+ },
652
+ lastNodeExecuted: executionData.node.name,
653
+ error: executionError,
654
+ };
655
+
656
+ throw error;
657
+ }
658
+
659
+ executionLoop: while (
660
+ this.runExecutionData.executionData!.nodeExecutionStack.length !== 0
661
+ ) {
662
+ if (
663
+ this.additionalData.executionTimeoutTimestamp !== undefined &&
664
+ Date.now() >= this.additionalData.executionTimeoutTimestamp
665
+ ) {
666
+ gotCancel = true;
667
+ }
668
+
669
+ if (gotCancel) {
670
+ return Promise.resolve();
671
+ }
672
+
673
+ nodeSuccessData = null;
674
+ executionError = undefined;
675
+ executionData =
676
+ this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
677
+ executionNode = executionData.node;
678
+
679
+ Logger.debug(`Start processing node "${executionNode.name}"`, {
680
+ node: executionNode.name,
681
+ workflowId: workflow.id,
682
+ });
683
+ await this.executeHook('nodeExecuteBefore', [executionNode.name]);
684
+
685
+ // Get the index of the current run
686
+ runIndex = 0;
687
+ // eslint-disable-next-line no-prototype-builtins
688
+ if (this.runExecutionData.resultData.runData.hasOwnProperty(executionNode.name)) {
689
+ runIndex = this.runExecutionData.resultData.runData[executionNode.name].length;
690
+ }
691
+
692
+ currentExecutionTry = `${executionNode.name}:${runIndex}`;
693
+
694
+ if (currentExecutionTry === lastExecutionTry) {
695
+ throw new Error('Did stop execution because execution seems to be in endless loop.');
696
+ }
697
+
698
+ if (
699
+ this.runExecutionData.startData!.runNodeFilter !== undefined &&
700
+ this.runExecutionData.startData!.runNodeFilter.indexOf(executionNode.name) === -1
701
+ ) {
702
+ // If filter is set and node is not on filter skip it, that avoids the problem that it executes
703
+ // leafs that are parallel to a selected destinationNode. Normally it would execute them because
704
+ // they have the same parent and it executes all child nodes.
705
+ continue;
706
+ }
707
+
708
+ // Check if all the data which is needed to run the node is available
709
+ if (Object.prototype.hasOwnProperty.call(workflow.connectionsByDestinationNode, executionNode.name)) {
710
+ // Check if the node has incoming connections
711
+ if (Object.prototype.hasOwnProperty.call(workflow.connectionsByDestinationNode[executionNode.name], 'main')) {
712
+ let inputConnections: IConnection[][];
713
+ let connectionIndex: number;
714
+
715
+ // eslint-disable-next-line prefer-const
716
+ inputConnections = workflow.connectionsByDestinationNode[executionNode.name]['main'];
717
+
718
+ for (
719
+ connectionIndex = 0;
720
+ connectionIndex < inputConnections.length;
721
+ connectionIndex++
722
+ ) {
723
+ if (
724
+ workflow.getHighestNode(executionNode.name, 'main', connectionIndex).length === 0
725
+ ) {
726
+ // If there is no valid incoming node (if all are disabled)
727
+ // then ignore that it has inputs and simply execute it as it is without
728
+ // any data
729
+ continue;
730
+ }
731
+
732
+ if (!Object.prototype.hasOwnProperty.call(executionData.data, 'main')) {
733
+ // ExecutionData does not even have the connection set up so can
734
+ // not have that data, so add it again to be executed later
735
+ this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
736
+ lastExecutionTry = currentExecutionTry;
737
+ continue executionLoop;
738
+ }
739
+
740
+ // Check if it has the data for all the inputs
741
+ // The most nodes just have one but merge node for example has two and data
742
+ // of both inputs has to be available to be able to process the node.
743
+ if (
744
+ executionData.data['main']!.length < connectionIndex ||
745
+ executionData.data['main']![connectionIndex] === null
746
+ ) {
747
+ // Does not have the data of the connections so add back to stack
748
+ this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
749
+ lastExecutionTry = currentExecutionTry;
750
+ continue executionLoop;
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ // Clone input data that nodes can not mess up data of parallel nodes which receive the same data
757
+ // TODO: Should only clone if multiple nodes get the same data or when it gets returned to frontned
758
+ // is very slow so only do if needed
759
+ startTime = new Date().getTime();
760
+
761
+ let maxTries = 1;
762
+ if (executionData.node.retryOnFail === true) {
763
+ // TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
764
+ maxTries = Math.min(5, Math.max(2, executionData.node.maxTries || 3));
765
+ }
766
+
767
+ let waitBetweenTries = 0;
768
+ if (executionData.node.retryOnFail === true) {
769
+ // TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
770
+ waitBetweenTries = Math.min(
771
+ 5000,
772
+ Math.max(0, executionData.node.waitBetweenTries || 1000),
773
+ );
774
+ }
775
+
776
+ for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
777
+ if (gotCancel) {
778
+ return Promise.resolve();
779
+ }
780
+ try {
781
+ if (tryIndex !== 0) {
782
+ // Reset executionError from previous error try
783
+ executionError = undefined;
784
+ if (waitBetweenTries !== 0) {
785
+ // TODO: Improve that in the future and check if other nodes can
786
+ // be executed in the meantime
787
+ // eslint-disable-next-line @typescript-eslint/no-shadow
788
+ await new Promise((resolve) => {
789
+ setTimeout(() => {
790
+ resolve(undefined);
791
+ }, waitBetweenTries);
792
+ });
793
+ }
794
+ }
795
+
796
+ Logger.debug(`Running node "${executionNode.name}" started`, {
797
+ node: executionNode.name,
798
+ workflowId: workflow.id,
799
+ });
800
+ nodeSuccessData = await workflow.runNode(
801
+ executionData.node,
802
+ executionData.data,
803
+ this.runExecutionData,
804
+ runIndex,
805
+ this.additionalData,
806
+ NodeExecuteFunctions,
807
+ this.mode,
808
+ );
809
+ Logger.debug(`Running node "${executionNode.name}" finished successfully`, {
810
+ node: executionNode.name,
811
+ workflowId: workflow.id,
812
+ });
813
+
814
+ if (nodeSuccessData === undefined) {
815
+ // Node did not get executed
816
+ nodeSuccessData = null;
817
+ } else {
818
+ this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
819
+ }
820
+
821
+ if (nodeSuccessData === null || nodeSuccessData[0][0] === undefined) {
822
+ if (executionData.node.alwaysOutputData === true) {
823
+ nodeSuccessData = nodeSuccessData || [];
824
+ nodeSuccessData[0] = [
825
+ {
826
+ json: {},
827
+ },
828
+ ];
829
+ }
830
+ }
831
+
832
+ if (nodeSuccessData === null && !this.runExecutionData.waitTill!) {
833
+ // If null gets returned it means that the node did succeed
834
+ // but did not have any data. So the branch should end
835
+ // (meaning the nodes afterwards should not be processed)
836
+ continue executionLoop;
837
+ }
838
+
839
+ break;
840
+ } catch (error) {
841
+ this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
842
+
843
+ executionError = {
844
+ ...(error as NodeOperationError | NodeApiError),
845
+ message: (error as NodeOperationError | NodeApiError).message,
846
+ stack: (error as NodeOperationError | NodeApiError).stack,
847
+ };
848
+
849
+ Logger.debug(`Running node "${executionNode.name}" finished with error`, {
850
+ node: executionNode.name,
851
+ workflowId: workflow.id,
852
+ });
853
+ }
854
+ }
855
+
856
+ // Add the data to return to the user
857
+ // (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
858
+
859
+ if (!Object.prototype.hasOwnProperty.call(this.runExecutionData.resultData.runData, executionNode.name)) {
860
+ this.runExecutionData.resultData.runData[executionNode.name] = [];
861
+ }
862
+ taskData = {
863
+ startTime,
864
+ executionTime: new Date().getTime() - startTime,
865
+ };
866
+
867
+ if (executionError !== undefined) {
868
+ taskData.error = executionError;
869
+
870
+ if (executionData.node.continueOnFail === true) {
871
+ // Workflow should continue running even if node errors
872
+ if (Object.prototype.hasOwnProperty.call(executionData.data, 'main') && executionData.data['main'].length > 0) {
873
+ // Simply get the input data of the node if it has any and pass it through
874
+ // to the next node
875
+ if (executionData.data['main'][0] !== null) {
876
+ nodeSuccessData = [executionData.data['main'][0]];
877
+ }
878
+ }
879
+ } else {
880
+ // Node execution did fail so add error and stop execution
881
+ this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
882
+
883
+ // Add the execution data again so that it can get restarted
884
+ this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
885
+
886
+ await this.executeHook('nodeExecuteAfter', [
887
+ executionNode.name,
888
+ taskData,
889
+ this.runExecutionData,
890
+ ]);
891
+
892
+ break;
893
+ }
894
+ }
895
+
896
+ // Merge error information to default output for now
897
+ // As the new nodes can report the errors in
898
+ // the `error` property.
899
+ for (const execution of nodeSuccessData!) {
900
+ for (const lineResult of execution) {
901
+ if (
902
+ lineResult.json !== undefined &&
903
+ lineResult.json['$error'] !== undefined &&
904
+ lineResult.json['$error'] !== undefined
905
+ ) {
906
+ lineResult.error = lineResult.json['$error'] as NodeApiError | NodeOperationError;
907
+ lineResult.json = {
908
+ error: (lineResult.json['$error'] as NodeApiError | NodeOperationError).message,
909
+ };
910
+ } else if (lineResult.error !== undefined) {
911
+ lineResult.json = { error: lineResult.error.message };
912
+ }
913
+ }
914
+ }
915
+
916
+ // Node executed successfully. So add data and go on.
917
+ taskData.data = {
918
+ main: nodeSuccessData,
919
+ } as ITaskDataConnections;
920
+
921
+ this.runExecutionData.resultData.runData[executionNode.name].push(taskData);
922
+
923
+ if (this.runExecutionData.waitTill!) {
924
+ await this.executeHook('nodeExecuteAfter', [
925
+ executionNode.name,
926
+ taskData,
927
+ this.runExecutionData,
928
+ ]);
929
+
930
+ // Add the node back to the stack that the workflow can start to execute again from that node
931
+ this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
932
+
933
+ break;
934
+ }
935
+
936
+ if (
937
+ this.runExecutionData.startData &&
938
+ this.runExecutionData.startData.destinationNode &&
939
+ this.runExecutionData.startData.destinationNode === executionNode.name
940
+ ) {
941
+ // Before stopping, make sure we are executing hooks so
942
+ // That frontend is notified for example for manual executions.
943
+ await this.executeHook('nodeExecuteAfter', [
944
+ executionNode.name,
945
+ taskData,
946
+ this.runExecutionData,
947
+ ]);
948
+
949
+ // If destination node is defined and got executed stop execution
950
+ continue;
951
+ }
952
+
953
+ // Add the nodes to which the current node has an output connection to that they can
954
+ // be executed next
955
+ if (Object.prototype.hasOwnProperty.call(workflow.connectionsBySourceNode, executionNode.name)) {
956
+ if (Object.prototype.hasOwnProperty.call(workflow.connectionsBySourceNode[executionNode.name], 'main')) {
957
+ let outputIndex: string;
958
+ let connectionData: IConnection;
959
+ // Iterate over all the outputs
960
+
961
+ // Add the nodes to be executed
962
+ // eslint-disable-next-line @typescript-eslint/no-for-in-array
963
+ for (outputIndex in workflow.connectionsBySourceNode[executionNode.name]['main']) {
964
+ if (
965
+ !Object.prototype.hasOwnProperty.call(workflow.connectionsBySourceNode[executionNode.name]['main'],
966
+ outputIndex,
967
+ )
968
+ ) {
969
+ continue;
970
+ }
971
+
972
+ // Iterate over all the different connections of this output
973
+ for (connectionData of workflow.connectionsBySourceNode[executionNode.name]['main'][
974
+ outputIndex
975
+ ]) {
976
+ if (!Object.prototype.hasOwnProperty.call(workflow.nodes, connectionData.node)) {
977
+ return Promise.reject(
978
+ new Error(
979
+ `The node "${executionNode.name}" connects to not found node "${connectionData.node}"`,
980
+ ),
981
+ );
982
+ }
983
+
984
+ if (
985
+ nodeSuccessData![outputIndex] &&
986
+ (nodeSuccessData![outputIndex].length !== 0 || connectionData.index > 0)
987
+ ) {
988
+ // Add the node only if it did execute or if connected to second "optional" input
989
+ this.addNodeToBeExecuted(
990
+ workflow,
991
+ connectionData,
992
+ parseInt(outputIndex, 10),
993
+ executionNode.name,
994
+ nodeSuccessData!,
995
+ runIndex,
996
+ );
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+
1003
+ // If we got here, it means that we did not stop executing from manual executions / destination.
1004
+ // Execute hooks now to make sure that all hooks are executed properly
1005
+ // Await is needed to make sure that we don't fall into concurrency problems
1006
+ // When saving node execution data
1007
+ await this.executeHook('nodeExecuteAfter', [
1008
+ executionNode.name,
1009
+ taskData,
1010
+ this.runExecutionData,
1011
+ ]);
1012
+ }
1013
+
1014
+ return Promise.resolve();
1015
+ })()
1016
+ .then(async () => {
1017
+ if (gotCancel && executionError === undefined) {
1018
+ return this.processSuccessExecution(
1019
+ startedAt,
1020
+ workflow,
1021
+ new WorkflowOperationError('Workflow has been canceled or timed out!'),
1022
+ );
1023
+ }
1024
+ return this.processSuccessExecution(startedAt, workflow, executionError);
1025
+ })
1026
+ .catch(async (error) => {
1027
+ const fullRunData = this.getFullRunData(startedAt);
1028
+
1029
+ fullRunData.data.resultData.error = {
1030
+ ...error,
1031
+ message: error.message,
1032
+ stack: error.stack,
1033
+ };
1034
+
1035
+ // Check if static data changed
1036
+ let newStaticData: IDataObject | undefined;
1037
+ // eslint-disable-next-line no-underscore-dangle
1038
+ if (workflow.staticData['__dataChanged'] === true) {
1039
+ // Static data of workflow changed
1040
+ newStaticData = workflow.staticData;
1041
+ }
1042
+ await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch(
1043
+ // eslint-disable-next-line @typescript-eslint/no-shadow
1044
+ (error) => {
1045
+ // eslint-disable-next-line no-console
1046
+ console.error('There was a problem running hook "workflowExecuteAfter"', error);
1047
+ },
1048
+ );
1049
+
1050
+ return fullRunData;
1051
+ });
1052
+
1053
+ return returnPromise.then(resolve);
1054
+ });
1055
+ }
1056
+
1057
+ async processSuccessExecution(
1058
+ startedAt: Date,
1059
+ workflow: Workflow,
1060
+ executionError?: ExecutionError,
1061
+ ): Promise<IRun> {
1062
+ const fullRunData = this.getFullRunData(startedAt);
1063
+
1064
+ if (executionError !== undefined) {
1065
+ Logger.verbose(`Workflow execution finished with error`, {
1066
+ error: executionError,
1067
+ workflowId: workflow.id,
1068
+ });
1069
+ fullRunData.data.resultData.error = {
1070
+ ...executionError,
1071
+ message: executionError.message,
1072
+ stack: executionError.stack,
1073
+ } as ExecutionError;
1074
+ } else if (this.runExecutionData.waitTill!) {
1075
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1076
+ Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, {
1077
+ workflowId: workflow.id,
1078
+ });
1079
+ fullRunData.waitTill = this.runExecutionData.waitTill;
1080
+ } else {
1081
+ Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
1082
+ fullRunData.finished = true;
1083
+ }
1084
+
1085
+ // Check if static data changed
1086
+ let newStaticData: IDataObject | undefined;
1087
+ // eslint-disable-next-line no-underscore-dangle
1088
+ if (workflow.staticData['__dataChanged'] === true) {
1089
+ // Static data of workflow changed
1090
+ newStaticData = workflow.staticData;
1091
+ }
1092
+
1093
+ await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]);
1094
+
1095
+ return fullRunData;
1096
+ }
1097
+
1098
+ getFullRunData(startedAt: Date): IRun {
1099
+ const fullRunData: IRun = {
1100
+ data: this.runExecutionData,
1101
+ mode: this.mode,
1102
+ startedAt,
1103
+ stoppedAt: new Date(),
1104
+ };
1105
+
1106
+ return fullRunData;
1107
+ }
1108
+ }