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