@fsai-flow/workflow 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/.eslintrc.json +33 -0
  2. package/README.md +11 -0
  3. package/dist/README.md +11 -0
  4. package/dist/package.json +42 -0
  5. package/dist/src/index.d.ts +21 -0
  6. package/dist/src/index.js +33 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/lib/Constants.d.ts +68 -0
  9. package/dist/src/lib/Constants.js +106 -0
  10. package/dist/src/lib/Constants.js.map +1 -0
  11. package/dist/src/lib/DeferredPromise.d.ts +6 -0
  12. package/dist/src/lib/DeferredPromise.js +11 -0
  13. package/dist/src/lib/DeferredPromise.js.map +1 -0
  14. package/dist/src/lib/Expression.d.ts +65 -0
  15. package/dist/src/lib/Expression.js +215 -0
  16. package/dist/src/lib/Expression.js.map +1 -0
  17. package/dist/src/lib/Interfaces.d.ts +1569 -0
  18. package/dist/src/lib/Interfaces.js +44 -0
  19. package/dist/src/lib/Interfaces.js.map +1 -0
  20. package/dist/src/lib/LoggerProxy.d.ts +9 -0
  21. package/dist/src/lib/LoggerProxy.js +40 -0
  22. package/dist/src/lib/LoggerProxy.js.map +1 -0
  23. package/dist/src/lib/MetadataUtils.d.ts +4 -0
  24. package/dist/src/lib/MetadataUtils.js +27 -0
  25. package/dist/src/lib/MetadataUtils.js.map +1 -0
  26. package/dist/src/lib/NodeErrors.d.ts +82 -0
  27. package/dist/src/lib/NodeErrors.js +289 -0
  28. package/dist/src/lib/NodeErrors.js.map +1 -0
  29. package/dist/src/lib/NodeHelpers.d.ts +198 -0
  30. package/dist/src/lib/NodeHelpers.js +1348 -0
  31. package/dist/src/lib/NodeHelpers.js.map +1 -0
  32. package/dist/src/lib/ObservableObject.d.ts +5 -0
  33. package/dist/src/lib/ObservableObject.js +61 -0
  34. package/dist/src/lib/ObservableObject.js.map +1 -0
  35. package/dist/src/lib/RoutingNode.d.ts +18 -0
  36. package/dist/src/lib/RoutingNode.js +508 -0
  37. package/dist/src/lib/RoutingNode.js.map +1 -0
  38. package/dist/src/lib/TelemetryHelpers.d.ts +3 -0
  39. package/dist/src/lib/TelemetryHelpers.js +69 -0
  40. package/dist/src/lib/TelemetryHelpers.js.map +1 -0
  41. package/dist/src/lib/TypeValidation.d.ts +21 -0
  42. package/dist/src/lib/TypeValidation.js +385 -0
  43. package/dist/src/lib/TypeValidation.js.map +1 -0
  44. package/dist/src/lib/VersionedNodeType.d.ts +9 -0
  45. package/dist/src/lib/VersionedNodeType.js +26 -0
  46. package/dist/src/lib/VersionedNodeType.js.map +1 -0
  47. package/dist/src/lib/Workflow.d.ts +248 -0
  48. package/dist/src/lib/Workflow.js +901 -0
  49. package/dist/src/lib/Workflow.js.map +1 -0
  50. package/dist/src/lib/WorkflowDataProxy.d.ts +87 -0
  51. package/dist/src/lib/WorkflowDataProxy.js +556 -0
  52. package/dist/src/lib/WorkflowDataProxy.js.map +1 -0
  53. package/dist/src/lib/WorkflowErrors.d.ts +9 -0
  54. package/dist/src/lib/WorkflowErrors.js +18 -0
  55. package/dist/src/lib/WorkflowErrors.js.map +1 -0
  56. package/dist/src/lib/WorkflowHooks.d.ts +11 -0
  57. package/dist/src/lib/WorkflowHooks.js +34 -0
  58. package/dist/src/lib/WorkflowHooks.js.map +1 -0
  59. package/dist/src/lib/errors/base/base.error.d.ts +30 -0
  60. package/dist/src/lib/errors/base/base.error.js +45 -0
  61. package/dist/src/lib/errors/base/base.error.js.map +1 -0
  62. package/dist/src/lib/errors/base/operational.error.d.ts +15 -0
  63. package/dist/src/lib/errors/base/operational.error.js +19 -0
  64. package/dist/src/lib/errors/base/operational.error.js.map +1 -0
  65. package/dist/src/lib/errors/error.types.d.ts +11 -0
  66. package/dist/src/lib/errors/error.types.js +3 -0
  67. package/dist/src/lib/errors/error.types.js.map +1 -0
  68. package/dist/src/lib/errors/index.d.ts +1 -0
  69. package/dist/src/lib/errors/index.js +6 -0
  70. package/dist/src/lib/errors/index.js.map +1 -0
  71. package/dist/src/lib/result.d.ts +19 -0
  72. package/dist/src/lib/result.js +36 -0
  73. package/dist/src/lib/result.js.map +1 -0
  74. package/dist/src/lib/utils.d.ts +50 -0
  75. package/dist/src/lib/utils.js +110 -0
  76. package/dist/src/lib/utils.js.map +1 -0
  77. package/eslint.config.js +19 -0
  78. package/jest.config.ts +10 -0
  79. package/package.json +40 -0
  80. package/project.json +19 -0
  81. package/src/index.ts +33 -0
  82. package/src/lib/Constants.ts +124 -0
  83. package/src/lib/DeferredPromise.ts +14 -0
  84. package/src/lib/Expression.ts +375 -0
  85. package/src/lib/Interfaces.ts +2229 -0
  86. package/src/lib/LoggerProxy.ts +43 -0
  87. package/src/lib/MetadataUtils.ts +34 -0
  88. package/src/lib/NodeErrors.ts +332 -0
  89. package/src/lib/NodeHelpers.ts +1666 -0
  90. package/src/lib/ObservableObject.ts +77 -0
  91. package/src/lib/RoutingNode.ts +862 -0
  92. package/src/lib/TelemetryHelpers.ts +86 -0
  93. package/src/lib/TypeValidation.ts +431 -0
  94. package/src/lib/VersionedNodeType.ts +30 -0
  95. package/src/lib/Workflow.ts +1266 -0
  96. package/src/lib/WorkflowDataProxy.ts +708 -0
  97. package/src/lib/WorkflowErrors.ts +18 -0
  98. package/src/lib/WorkflowHooks.ts +51 -0
  99. package/src/lib/errors/base/base.error.ts +68 -0
  100. package/src/lib/errors/base/operational.error.ts +21 -0
  101. package/src/lib/errors/error.types.ts +14 -0
  102. package/src/lib/errors/index.ts +1 -0
  103. package/src/lib/result.ts +34 -0
  104. package/src/lib/utils.ts +132 -0
  105. package/tests/Helpers.ts +667 -0
  106. package/tests/NodeHelpers.test.ts +3053 -0
  107. package/tests/ObservableObject.test.ts +171 -0
  108. package/tests/RoutingNode.test.ts +1680 -0
  109. package/tests/Workflow.test.ts +1284 -0
  110. package/tests/WorkflowDataProxy.test.ts +199 -0
  111. package/tsconfig.json +27 -0
  112. package/tsconfig.lib.json +11 -0
  113. package/tsconfig.spec.json +14 -0
@@ -0,0 +1,901 @@
1
+ "use strict";
2
+ /* eslint-disable no-await-in-loop */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
6
+ /* eslint-disable no-param-reassign */
7
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
8
+ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
9
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
10
+ /* eslint-disable @typescript-eslint/no-for-in-array */
11
+ /* eslint-disable no-prototype-builtins */
12
+ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
13
+ /* eslint-disable no-underscore-dangle */
14
+ /* eslint-disable no-continue */
15
+ /* eslint-disable no-restricted-syntax */
16
+ /* eslint-disable import/no-cycle */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.Workflow = void 0;
19
+ const __1 = require("..");
20
+ class Workflow {
21
+ id;
22
+ name;
23
+ nodes = {};
24
+ connectionsBySourceNode;
25
+ connectionsByDestinationNode;
26
+ nodeTypes;
27
+ expression;
28
+ active;
29
+ settings;
30
+ // To save workflow specific static data like for example
31
+ // ids of registred webhooks of nodes
32
+ staticData;
33
+ // constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
34
+ constructor(parameters) {
35
+ this.id = parameters?.id;
36
+ this.name = parameters?.name;
37
+ this.nodeTypes = parameters.nodeTypes;
38
+ // Save nodes in workflow as object to be able to get the
39
+ // nodes easily by its name.
40
+ // Also directly add the default values of the node type.
41
+ let nodeType;
42
+ for (const node of parameters.nodes) {
43
+ this.nodes[node.name] = node;
44
+ nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
45
+ if (nodeType === undefined) {
46
+ // Go on to next node when its type is not known.
47
+ // For now do not error because that causes problems with
48
+ // expression resolution also then when the unknown node
49
+ // does not get used.
50
+ continue;
51
+ // throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
52
+ }
53
+ // Add default values
54
+ const nodeParameters = __1.NodeHelpers.getNodeParameters(nodeType.description.properties, node.parameters, true, false);
55
+ node.parameters = nodeParameters !== null ? nodeParameters : {};
56
+ }
57
+ this.connectionsBySourceNode = parameters.connections;
58
+ // Save also the connections by the destionation nodes
59
+ this.connectionsByDestinationNode = this.__getConnectionsByDestination(parameters.connections);
60
+ this.active = parameters.active || false;
61
+ this.staticData = __1.ObservableObject.create(parameters.staticData || {}, undefined, {
62
+ ignoreEmptyOnFirstChild: true,
63
+ });
64
+ this.settings = parameters.settings || {};
65
+ this.expression = new __1.Expression(this);
66
+ }
67
+ /**
68
+ * The default connections are by source node. This function rewrites them by destination nodes
69
+ * to easily find parent nodes.
70
+ *
71
+ * @param {IConnections} connections
72
+ * @returns {IConnections}
73
+ * @memberof Workflow
74
+ */
75
+ __getConnectionsByDestination(connections) {
76
+ const returnConnection = {};
77
+ let connectionInfo;
78
+ let maxIndex;
79
+ for (const sourceNode in connections) {
80
+ if (!connections.hasOwnProperty(sourceNode)) {
81
+ continue;
82
+ }
83
+ for (const type in connections[sourceNode]) {
84
+ if (!connections[sourceNode].hasOwnProperty(type)) {
85
+ continue;
86
+ }
87
+ for (const inputIndex in connections[sourceNode][type]) {
88
+ if (!connections[sourceNode][type].hasOwnProperty(inputIndex)) {
89
+ continue;
90
+ }
91
+ for (connectionInfo of connections[sourceNode][type][inputIndex]) {
92
+ if (!returnConnection.hasOwnProperty(connectionInfo.node)) {
93
+ returnConnection[connectionInfo.node] = {};
94
+ }
95
+ if (!returnConnection[connectionInfo.node].hasOwnProperty(connectionInfo.type)) {
96
+ returnConnection[connectionInfo.node][connectionInfo.type] = [];
97
+ }
98
+ maxIndex = returnConnection[connectionInfo.node][connectionInfo.type].length - 1;
99
+ for (let j = maxIndex; j < connectionInfo.index; j++) {
100
+ returnConnection[connectionInfo.node][connectionInfo.type].push([]);
101
+ }
102
+ returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index].push({
103
+ node: sourceNode,
104
+ type,
105
+ index: parseInt(inputIndex, 10),
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return returnConnection;
112
+ }
113
+ /**
114
+ * A workflow can only be activated if it has a node which has either triggers
115
+ * or webhooks defined.
116
+ *
117
+ * @param {string[]} [ignoreNodeTypes] Node-types to ignore in the check
118
+ * @returns {boolean}
119
+ * @memberof Workflow
120
+ */
121
+ checkIfWorkflowCanBeActivated(ignoreNodeTypes) {
122
+ let node;
123
+ let nodeType;
124
+ for (const nodeName of Object.keys(this.nodes)) {
125
+ node = this.nodes[nodeName];
126
+ if (node.disabled === true) {
127
+ // Deactivated nodes can not trigger a run so ignore
128
+ continue;
129
+ }
130
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
131
+ if (ignoreNodeTypes !== undefined && ignoreNodeTypes.includes(node.type)) {
132
+ continue;
133
+ }
134
+ nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
135
+ if (nodeType === undefined) {
136
+ // Type is not known so check is not possible
137
+ continue;
138
+ }
139
+ if (nodeType.poll !== undefined ||
140
+ nodeType.trigger !== undefined ||
141
+ nodeType.webhook !== undefined) {
142
+ // Is a trigger node. So workflow can be activated.
143
+ return true;
144
+ }
145
+ }
146
+ return false;
147
+ }
148
+ /**
149
+ * Checks if everything in the workflow is complete
150
+ * and ready to be executed. If it returns null everything
151
+ * is fine. If there are issues it returns the issues
152
+ * which have been found for the different nodes.
153
+ * TODO: Does currently not check for credential issues!
154
+ *
155
+ * @returns {(IWorfklowIssues | null)}
156
+ * @memberof Workflow
157
+ */
158
+ checkReadyForExecution(inputData) {
159
+ let node;
160
+ let nodeType;
161
+ let nodeIssues = null;
162
+ const workflowIssues = {};
163
+ let checkNodes = [];
164
+ if (inputData.destinationNode) {
165
+ // If a destination node is given we have to check all the nodes
166
+ // leading up to it
167
+ checkNodes = this.getParentNodes(inputData.destinationNode);
168
+ checkNodes.push(inputData.destinationNode);
169
+ }
170
+ else if (inputData.startNode) {
171
+ // If a start node is given we have to check all nodes which
172
+ // come after it
173
+ checkNodes = this.getChildNodes(inputData.startNode);
174
+ checkNodes.push(inputData.startNode);
175
+ }
176
+ for (const nodeName of checkNodes) {
177
+ nodeIssues = null;
178
+ node = this.nodes[nodeName];
179
+ if (node.disabled === true) {
180
+ continue;
181
+ }
182
+ nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
183
+ if (nodeType === undefined) {
184
+ // Node type is not known
185
+ nodeIssues = {
186
+ typeUnknown: true,
187
+ };
188
+ }
189
+ else {
190
+ nodeIssues = __1.NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node);
191
+ }
192
+ if (nodeIssues !== null) {
193
+ workflowIssues[node.name] = nodeIssues;
194
+ }
195
+ }
196
+ if (Object.keys(workflowIssues).length === 0) {
197
+ return null;
198
+ }
199
+ return workflowIssues;
200
+ }
201
+ /**
202
+ * Returns the static data of the workflow.
203
+ * It gets saved with the workflow and will be the same for
204
+ * all workflow-executions.
205
+ *
206
+ * @param {string} type The type of data to return ("global"|"node")
207
+ * @param {INode} [node] If type is set to "node" then the node has to be provided
208
+ * @returns {IDataObject}
209
+ * @memberof Workflow
210
+ */
211
+ getStaticData(type, node) {
212
+ let key;
213
+ if (type === 'global') {
214
+ key = 'global';
215
+ }
216
+ else if (type === 'node') {
217
+ if (node === undefined) {
218
+ throw new Error(`The request data of context type "node" the node parameter has to be set!`);
219
+ }
220
+ key = `node:${node.name}`;
221
+ }
222
+ else {
223
+ throw new Error(`The context type "${type}" is not know. Only "global" and node" are supported!`);
224
+ }
225
+ if (this.staticData[key] === undefined) {
226
+ // Create it as ObservableObject that we can easily check if the data changed
227
+ // to know if the workflow with its data has to be saved afterwards or not.
228
+ this.staticData[key] = __1.ObservableObject.create({}, this.staticData);
229
+ }
230
+ return this.staticData[key];
231
+ }
232
+ /**
233
+ * Returns all the trigger nodes in the workflow.
234
+ *
235
+ * @returns {INode[]}
236
+ * @memberof Workflow
237
+ */
238
+ getTriggerNodes() {
239
+ return this.queryNodes((nodeType) => !!nodeType.trigger);
240
+ }
241
+ /**
242
+ * Returns all the poll nodes in the workflow
243
+ *
244
+ * @returns {INode[]}
245
+ * @memberof Workflow
246
+ */
247
+ getPollNodes() {
248
+ return this.queryNodes((nodeType) => !!nodeType.poll);
249
+ }
250
+ /**
251
+ * Returns all the nodes in the workflow for which the given
252
+ * checkFunction return true
253
+ *
254
+ * @param {(nodeType: INodeType) => boolean} checkFunction
255
+ * @returns {INode[]}
256
+ * @memberof Workflow
257
+ */
258
+ queryNodes(checkFunction) {
259
+ const returnNodes = [];
260
+ // Check if it has any of them
261
+ let node;
262
+ let nodeType;
263
+ for (const nodeName of Object.keys(this.nodes)) {
264
+ node = this.nodes[nodeName];
265
+ if (node.disabled === true) {
266
+ continue;
267
+ }
268
+ nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
269
+ if (nodeType !== undefined && checkFunction(nodeType)) {
270
+ returnNodes.push(node);
271
+ }
272
+ }
273
+ return returnNodes;
274
+ }
275
+ /**
276
+ * Returns the node with the given name if it exists else null
277
+ *
278
+ * @param {string} nodeName Name of the node to return
279
+ * @returns {(INode | null)}
280
+ * @memberof Workflow
281
+ */
282
+ getNode(nodeName) {
283
+ if (this.nodes.hasOwnProperty(nodeName)) {
284
+ return this.nodes[nodeName];
285
+ }
286
+ return null;
287
+ }
288
+ /**
289
+ * Renames nodes in expressions
290
+ *
291
+ * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue The parameters to check for expressions
292
+ * @param {string} currentName The current name of the node
293
+ * @param {string} newName The new name
294
+ * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
295
+ * @memberof Workflow
296
+ */
297
+ renameNodeInExpressions(parameterValue, currentName, newName) {
298
+ if (typeof parameterValue !== 'object') {
299
+ // Reached the actual value
300
+ if (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') {
301
+ // Is expression so has to be rewritten
302
+ // To not run the "expensive" regex stuff when it is not needed
303
+ // make a simple check first if it really contains the the node-name
304
+ if (parameterValue.includes(currentName)) {
305
+ // Really contains node-name (even though we do not know yet if really as $node-expression)
306
+ // In case some special characters are used in name escape them
307
+ const currentNameEscaped = currentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
308
+ parameterValue = parameterValue.replace(new RegExp(`(\\$node(\\.|\\["|\\['))${currentNameEscaped}((\\.|"\\]|'\\]))`, 'g'), `$1${newName}$3`);
309
+ }
310
+ }
311
+ return parameterValue;
312
+ }
313
+ if (Array.isArray(parameterValue)) {
314
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
+ const returnArray = [];
316
+ for (const currentValue of parameterValue) {
317
+ returnArray.push(this.renameNodeInExpressions(currentValue, currentName, newName));
318
+ }
319
+ return returnArray;
320
+ }
321
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
322
+ const returnData = {};
323
+ for (const parameterName of Object.keys(parameterValue || {})) {
324
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
325
+ returnData[parameterName] = this.renameNodeInExpressions(parameterValue[parameterName], currentName, newName);
326
+ }
327
+ return returnData;
328
+ }
329
+ /**
330
+ * Rename a node in the workflow
331
+ *
332
+ * @param {string} currentName The current name of the node
333
+ * @param {string} newName The new name
334
+ * @memberof Workflow
335
+ */
336
+ renameNode(currentName, newName) {
337
+ // Rename the node itself
338
+ if (this.nodes[currentName] !== undefined) {
339
+ this.nodes[newName] = this.nodes[currentName];
340
+ this.nodes[newName].name = newName;
341
+ delete this.nodes[currentName];
342
+ }
343
+ // Update the expressions which reference the node
344
+ // with its old name
345
+ for (const node of Object.values(this.nodes)) {
346
+ node.parameters = this.renameNodeInExpressions(node.parameters, currentName, newName);
347
+ }
348
+ // Change all source connections
349
+ if (this.connectionsBySourceNode.hasOwnProperty(currentName)) {
350
+ this.connectionsBySourceNode[newName] = this.connectionsBySourceNode[currentName];
351
+ delete this.connectionsBySourceNode[currentName];
352
+ }
353
+ // Change all destination connections
354
+ let sourceNode;
355
+ let type;
356
+ let sourceIndex;
357
+ let connectionIndex;
358
+ let connectionData;
359
+ for (sourceNode of Object.keys(this.connectionsBySourceNode)) {
360
+ for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) {
361
+ for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) {
362
+ for (connectionIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)])) {
363
+ connectionData =
364
+ this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][parseInt(connectionIndex, 10)];
365
+ if (connectionData.node === currentName) {
366
+ connectionData.node = newName;
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ // Use the updated connections to create updated connections by destionation nodes
373
+ this.connectionsByDestinationNode = this.__getConnectionsByDestination(this.connectionsBySourceNode);
374
+ }
375
+ /**
376
+ * Finds the highest parent nodes of the node with the given name
377
+ *
378
+ * @param {string} nodeName
379
+ * @param {string} [type='main']
380
+ * @param {number} [nodeConnectionIndex]
381
+ * @returns {string[]}
382
+ * @memberof Workflow
383
+ */
384
+ getHighestNode(nodeName, type = 'main', nodeConnectionIndex, checkedNodes) {
385
+ const currentHighest = [];
386
+ if (this.nodes[nodeName].disabled === false) {
387
+ // If the current node is not disabled itself is the highest
388
+ currentHighest.push(nodeName);
389
+ }
390
+ if (!this.connectionsByDestinationNode.hasOwnProperty(nodeName)) {
391
+ // Node does not have incoming connections
392
+ return currentHighest;
393
+ }
394
+ if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(type)) {
395
+ // Node does not have incoming connections of given type
396
+ return currentHighest;
397
+ }
398
+ checkedNodes = checkedNodes || [];
399
+ if (checkedNodes.includes(nodeName)) {
400
+ // Node got checked already before
401
+ return currentHighest;
402
+ }
403
+ checkedNodes.push(nodeName);
404
+ const returnNodes = [];
405
+ let addNodes;
406
+ let connectionsByIndex;
407
+ for (let connectionIndex = 0; connectionIndex < this.connectionsByDestinationNode[nodeName][type].length; connectionIndex++) {
408
+ if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) {
409
+ // If a connection-index is given ignore all other ones
410
+ continue;
411
+ }
412
+ connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
413
+ // eslint-disable-next-line @typescript-eslint/no-loop-func
414
+ connectionsByIndex.forEach((connection) => {
415
+ if (checkedNodes.includes(connection.node)) {
416
+ // Node got checked already before
417
+ return;
418
+ }
419
+ addNodes = this.getHighestNode(connection.node, type, undefined, checkedNodes);
420
+ if (addNodes.length === 0) {
421
+ // The checked node does not have any further parents so add it
422
+ // if it is not disabled
423
+ if (this.nodes[connection.node].disabled !== true) {
424
+ addNodes = [connection.node];
425
+ }
426
+ }
427
+ addNodes.forEach((name) => {
428
+ // Only add if node is not on the list already anyway
429
+ if (returnNodes.indexOf(name) === -1) {
430
+ returnNodes.push(name);
431
+ }
432
+ });
433
+ });
434
+ }
435
+ return returnNodes;
436
+ }
437
+ /**
438
+ * Returns all the after the given one
439
+ *
440
+ * @param {string} nodeName
441
+ * @param {string} [type='main']
442
+ * @param {*} [depth=-1]
443
+ * @returns {string[]}
444
+ * @memberof Workflow
445
+ */
446
+ getChildNodes(nodeName, type = 'main', depth = -1) {
447
+ return this.getConnectedNodes(this.connectionsBySourceNode, nodeName, type, depth);
448
+ }
449
+ /**
450
+ * Returns all the nodes before the given one
451
+ *
452
+ * @param {string} nodeName
453
+ * @param {string} [type='main']
454
+ * @param {*} [depth=-1]
455
+ * @returns {string[]}
456
+ * @memberof Workflow
457
+ */
458
+ getParentNodes(nodeName, type = 'main', depth = -1) {
459
+ return this.getConnectedNodes(this.connectionsByDestinationNode, nodeName, type, depth);
460
+ }
461
+ /**
462
+ * Gets all the nodes which are connected nodes starting from
463
+ * the given one
464
+ *
465
+ * @param {IConnections} connections
466
+ * @param {string} nodeName
467
+ * @param {string} [type='main']
468
+ * @param {*} [depth=-1]
469
+ * @param {string[]} [checkedNodes]
470
+ * @returns {string[]}
471
+ * @memberof Workflow
472
+ */
473
+ getConnectedNodes(connections, nodeName, type = 'main', depth = -1, checkedNodes) {
474
+ depth = depth === -1 ? -1 : depth;
475
+ const newDepth = depth === -1 ? depth : depth - 1;
476
+ if (depth === 0) {
477
+ // Reached max depth
478
+ return [];
479
+ }
480
+ if (!connections.hasOwnProperty(nodeName)) {
481
+ // Node does not have incoming connections
482
+ return [];
483
+ }
484
+ if (!connections[nodeName].hasOwnProperty(type)) {
485
+ // Node does not have incoming connections of given type
486
+ return [];
487
+ }
488
+ checkedNodes = checkedNodes || [];
489
+ if (checkedNodes.includes(nodeName)) {
490
+ // Node got checked already before
491
+ return [];
492
+ }
493
+ checkedNodes.push(nodeName);
494
+ const returnNodes = [];
495
+ let addNodes;
496
+ let nodeIndex;
497
+ let i;
498
+ let parentNodeName;
499
+ connections[nodeName][type].forEach((connectionsByIndex) => {
500
+ connectionsByIndex.forEach((connection) => {
501
+ if (checkedNodes.includes(connection.node)) {
502
+ // Node got checked already before
503
+ return;
504
+ }
505
+ returnNodes.unshift(connection.node);
506
+ addNodes = this.getConnectedNodes(connections, connection.node, type, newDepth, checkedNodes);
507
+ for (i = addNodes.length; i--; i > 0) {
508
+ // Because nodes can have multiple parents it is possible that
509
+ // parts of the tree is parent of both and to not add nodes
510
+ // twice check first if they already got added before.
511
+ parentNodeName = addNodes[i];
512
+ nodeIndex = returnNodes.indexOf(parentNodeName);
513
+ if (nodeIndex !== -1) {
514
+ // Node got found before so remove it from current location
515
+ // that node-order stays correct
516
+ returnNodes.splice(nodeIndex, 1);
517
+ }
518
+ returnNodes.unshift(parentNodeName);
519
+ }
520
+ });
521
+ });
522
+ return returnNodes;
523
+ }
524
+ /**
525
+ * Returns via which output of the parent-node the node
526
+ * is connected to.
527
+ *
528
+ * @param {string} nodeName The node to check how it is connected with parent node
529
+ * @param {string} parentNodeName The parent node to get the output index of
530
+ * @param {string} [type='main']
531
+ * @param {*} [depth=-1]
532
+ * @param {string[]} [checkedNodes]
533
+ * @returns {(number | undefined)}
534
+ * @memberof Workflow
535
+ */
536
+ getNodeConnectionOutputIndex(nodeName, parentNodeName, type = 'main', depth = -1, checkedNodes) {
537
+ const node = this.getNode(parentNodeName);
538
+ if (node === null) {
539
+ return undefined;
540
+ }
541
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
542
+ if (nodeType.description.outputs.length === 1) {
543
+ // If the parent node has only one output, it can only be connected
544
+ // to that one. So no further checking is required.
545
+ return 0;
546
+ }
547
+ depth = depth === -1 ? -1 : depth;
548
+ const newDepth = depth === -1 ? depth : depth - 1;
549
+ if (depth === 0) {
550
+ // Reached max depth
551
+ return undefined;
552
+ }
553
+ if (!this.connectionsByDestinationNode.hasOwnProperty(nodeName)) {
554
+ // Node does not have incoming connections
555
+ return undefined;
556
+ }
557
+ if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(type)) {
558
+ // Node does not have incoming connections of given type
559
+ return undefined;
560
+ }
561
+ checkedNodes = checkedNodes || [];
562
+ if (checkedNodes.includes(nodeName)) {
563
+ // Node got checked already before
564
+ return undefined;
565
+ }
566
+ checkedNodes.push(nodeName);
567
+ let outputIndex;
568
+ for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
569
+ for (const connection of connectionsByIndex) {
570
+ if (parentNodeName === connection.node) {
571
+ return connection.index;
572
+ }
573
+ if (checkedNodes.includes(connection.node)) {
574
+ // Node got checked already before so continue with the next one
575
+ continue;
576
+ }
577
+ outputIndex = this.getNodeConnectionOutputIndex(connection.node, parentNodeName, type, newDepth, checkedNodes);
578
+ if (outputIndex !== undefined) {
579
+ return outputIndex;
580
+ }
581
+ }
582
+ }
583
+ return undefined;
584
+ }
585
+ /**
586
+ * Returns from which of the given nodes the workflow should get started from
587
+ *
588
+ * @param {string[]} nodeNames The potential start nodes
589
+ * @returns {(INode | undefined)}
590
+ * @memberof Workflow
591
+ */
592
+ __getStartNode(nodeNames) {
593
+ // Check if there are any trigger or poll nodes and then return the first one
594
+ let node;
595
+ let nodeType;
596
+ for (const nodeName of nodeNames) {
597
+ node = this.nodes[nodeName];
598
+ nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
599
+ if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
600
+ if (node.disabled === true) {
601
+ continue;
602
+ }
603
+ return node;
604
+ }
605
+ }
606
+ // Check if there is the actual "start" node
607
+ const startNodeType = 'n8n-nodes-base.start';
608
+ for (const nodeName of nodeNames) {
609
+ node = this.nodes[nodeName];
610
+ if (node.type === startNodeType) {
611
+ return node;
612
+ }
613
+ }
614
+ return undefined;
615
+ }
616
+ /**
617
+ * Returns the start node to start the worfklow from
618
+ *
619
+ * @param {string} [destinationNode]
620
+ * @returns {(INode | undefined)}
621
+ * @memberof Workflow
622
+ */
623
+ getStartNode(destinationNode) {
624
+ if (destinationNode) {
625
+ // Find the highest parent nodes of the given one
626
+ const nodeNames = this.getHighestNode(destinationNode);
627
+ if (nodeNames.length === 0) {
628
+ // If no parent nodes have been found then only the destination-node
629
+ // is in the tree so add that one
630
+ nodeNames.push(destinationNode);
631
+ }
632
+ // Check which node to return as start node
633
+ const node = this.__getStartNode(nodeNames);
634
+ if (node !== undefined) {
635
+ return node;
636
+ }
637
+ // If none of the above did find anything simply return the
638
+ // first parent node in the list
639
+ return this.nodes[nodeNames[0]];
640
+ }
641
+ return this.__getStartNode(Object.keys(this.nodes));
642
+ }
643
+ /**
644
+ * Executes the Webhooks method of the node
645
+ *
646
+ * @param {WebhookSetupMethodNames} method The name of the method to execute
647
+ * @param {IWebhookData} webhookData
648
+ * @param {INodeExecuteFunctions} nodeExecuteFunctions
649
+ * @param {WorkflowExecuteMode} mode
650
+ * @returns {(Promise<boolean | undefined>)}
651
+ * @memberof Workflow
652
+ */
653
+ async runWebhookMethod(method, webhookData, nodeExecuteFunctions, mode, activation, isTest) {
654
+ const node = this.getNode(webhookData.node);
655
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
656
+ if (nodeType.webhookMethods === undefined) {
657
+ return;
658
+ }
659
+ if (nodeType.webhookMethods[webhookData.webhookDescription.name] === undefined) {
660
+ return;
661
+ }
662
+ if (nodeType.webhookMethods[webhookData.webhookDescription.name][method] === undefined) {
663
+ return;
664
+ }
665
+ const thisArgs = nodeExecuteFunctions.getExecuteHookFunctions(this, node, webhookData.workflowExecuteAdditionalData, mode, activation, isTest, webhookData);
666
+ // eslint-disable-next-line consistent-return
667
+ return nodeType.webhookMethods[webhookData.webhookDescription.name][method].call(thisArgs);
668
+ }
669
+ /**
670
+ * Runs the given trigger node so that it can trigger the workflow
671
+ * when the node has data.
672
+ *
673
+ * @param {INode} node
674
+ * @param {IGetExecuteTriggerFunctions} getTriggerFunctions
675
+ * @param {IWorkflowExecuteAdditionalData} additionalData
676
+ * @param {WorkflowExecuteMode} mode
677
+ * @returns {(Promise<ITriggerResponse | undefined>)}
678
+ * @memberof Workflow
679
+ */
680
+ async runTrigger(node, getTriggerFunctions, additionalData, mode, activation) {
681
+ const triggerFunctions = getTriggerFunctions(this, node, additionalData, mode, activation);
682
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
683
+ if (nodeType === undefined) {
684
+ throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
685
+ }
686
+ if (!nodeType.trigger) {
687
+ throw new Error(`The node type "${node.type}" of node "${node.name}" does not have a trigger function defined.`);
688
+ }
689
+ if (mode === 'manual') {
690
+ // In manual mode we do not just start the trigger function we also
691
+ // want to be able to get informed as soon as the first data got emitted
692
+ const triggerResponse = await nodeType.trigger.call(triggerFunctions);
693
+ // Add the manual trigger response which resolves when the first time data got emitted
694
+ triggerResponse.manualTriggerResponse = new Promise((resolve) => {
695
+ triggerFunctions.emit = ((resolveEmit) => (data, responsePromise) => {
696
+ additionalData.hooks.hookFunctions.sendResponse = [
697
+ async (response) => {
698
+ if (responsePromise) {
699
+ responsePromise.resolve(response);
700
+ }
701
+ },
702
+ ];
703
+ resolveEmit(data);
704
+ })(resolve);
705
+ });
706
+ return triggerResponse;
707
+ }
708
+ // In all other modes simply start the trigger
709
+ return nodeType.trigger.call(triggerFunctions);
710
+ }
711
+ /**
712
+ * Runs the given trigger node so that it can trigger the workflow
713
+ * when the node has data.
714
+ *
715
+ * @param {INode} node
716
+ * @param {IPollFunctions} pollFunctions
717
+ * @returns
718
+ * @memberof Workflow
719
+ */
720
+ async runPoll(node, pollFunctions) {
721
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
722
+ if (nodeType === undefined) {
723
+ throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
724
+ }
725
+ if (!nodeType.poll) {
726
+ throw new Error(`The node type "${node.type}" of node "${node.name}" does not have a poll function defined.`);
727
+ }
728
+ return nodeType.poll.call(pollFunctions);
729
+ }
730
+ /**
731
+ * Executes the webhook data to see what it should return and if the
732
+ * workflow should be started or not
733
+ *
734
+ * @param {INode} node
735
+ * @param {IWorkflowExecuteAdditionalData} additionalData
736
+ * @param {INodeExecuteFunctions} nodeExecuteFunctions
737
+ * @param {WorkflowExecuteMode} mode
738
+ * @param {IRunExecutionData | undefined} runExecutionData
739
+ * @returns {Promise<IWebhookResponseData>}
740
+ * @memberof Workflow
741
+ */
742
+ async runWebhook(webhookData, node, additionalData, nodeExecuteFunctions, mode, runExecutionData, workflow) {
743
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
744
+ if (nodeType === undefined) {
745
+ throw new Error(`The type of the webhook node "${node.name}" is not known.`);
746
+ }
747
+ else if (nodeType.webhook === undefined) {
748
+ throw new Error(`The node "${node.name}" does not have any webhooks defined.`);
749
+ }
750
+ const thisArgs = nodeExecuteFunctions.getExecuteWebhookFunctions(this, node, additionalData, mode, webhookData, runExecutionData);
751
+ return nodeType.webhook.call(thisArgs, workflow);
752
+ }
753
+ /**
754
+ * Executes the given node.
755
+ *
756
+ * @param {INode} node
757
+ * @param {ITaskDataConnections} inputData
758
+ * @param {IRunExecutionData} runExecutionData
759
+ * @param {number} runIndex
760
+ * @param {IWorkflowExecuteAdditionalData} additionalData
761
+ * @param {INodeExecuteFunctions} nodeExecuteFunctions
762
+ * @param {WorkflowExecuteMode} mode
763
+ * @returns {(Promise<INodeExecutionData[][] | null>)}
764
+ * @memberof Workflow
765
+ */
766
+ async runNode(node, inputData, runExecutionData, runIndex, additionalData, nodeExecuteFunctions, mode) {
767
+ if (node.disabled === true) {
768
+ // If node is disabled simply pass the data through
769
+ // return NodeRunHelpers.
770
+ if (inputData.hasOwnProperty('main') && inputData['main'].length > 0) {
771
+ // If the node is disabled simply return the data from the first main input
772
+ if (inputData['main'][0] === null) {
773
+ return undefined;
774
+ }
775
+ return [inputData['main'][0]];
776
+ }
777
+ return undefined;
778
+ }
779
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
780
+ if (nodeType === undefined) {
781
+ throw new Error(`Node type "${node.type}" is not known so can not run it!`);
782
+ }
783
+ let connectionInputData = [];
784
+ if (nodeType.execute ||
785
+ nodeType.executeSingle ||
786
+ (!nodeType.poll && !nodeType.trigger && !nodeType.webhook)) {
787
+ // Only stop if first input is empty for execute & executeSingle runs. For all others run anyways
788
+ // because then it is a trigger node. As they only pass data through and so the input-data
789
+ // becomes output-data it has to be possible.
790
+ if (inputData.hasOwnProperty('main') && inputData['main'].length > 0) {
791
+ // We always use the data of main input and the first input for executeSingle
792
+ connectionInputData = inputData['main'][0];
793
+ }
794
+ if (connectionInputData.length === 0) {
795
+ // No data for node so return
796
+ return undefined;
797
+ }
798
+ }
799
+ if (runExecutionData.resultData.lastNodeExecuted === node.name &&
800
+ runExecutionData.resultData.error !== undefined) {
801
+ // The node did already fail. So throw an error here that it displays and logs it correctly.
802
+ // Does get used by webhook and trigger nodes in case they throw an error that it is possible
803
+ // to log the error and display in Editor-UI.
804
+ const error = new Error(runExecutionData.resultData.error.message);
805
+ error.stack = runExecutionData.resultData.error.stack;
806
+ throw error;
807
+ }
808
+ if (node.executeOnce === true) {
809
+ // If node should be executed only once so use only the first input item
810
+ connectionInputData = connectionInputData.slice(0, 1);
811
+ const newInputData = {};
812
+ for (const inputName of Object.keys(inputData)) {
813
+ newInputData[inputName] = inputData[inputName].map((input) => {
814
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
815
+ return input && input.slice(0, 1);
816
+ });
817
+ }
818
+ inputData = newInputData;
819
+ }
820
+ if (nodeType.executeSingle) {
821
+ const returnPromises = [];
822
+ for (let itemIndex = 0; itemIndex < connectionInputData.length; itemIndex++) {
823
+ const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, itemIndex, additionalData, mode);
824
+ returnPromises.push(nodeType.executeSingle.call(thisArgs));
825
+ }
826
+ if (returnPromises.length === 0) {
827
+ return null;
828
+ }
829
+ let promiseResults;
830
+ try {
831
+ promiseResults = await Promise.all(returnPromises);
832
+ }
833
+ catch (error) {
834
+ return Promise.reject(error);
835
+ }
836
+ if (promiseResults) {
837
+ return [promiseResults];
838
+ }
839
+ }
840
+ else if (nodeType.execute) {
841
+ const closeFunctions = [];
842
+ const thisArgs = nodeExecuteFunctions.getExecuteFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, additionalData, mode, nodeType, closeFunctions);
843
+ const data = await nodeType.execute.call(thisArgs);
844
+ const closeFunctionsResults = await Promise.allSettled(closeFunctions.map(async (fn) => await fn()));
845
+ const closingErrors = closeFunctionsResults
846
+ .filter((result) => result.status === 'rejected')
847
+ .map((result) => result.reason);
848
+ if (closingErrors.length > 0) {
849
+ if (closingErrors[0] instanceof Error)
850
+ throw closingErrors[0];
851
+ throw new Error(`Error on execution node's close function(s). Node Name: ${node.name}, Node Type: ${node.type}, Errors: ${closingErrors.join(', ')}`);
852
+ }
853
+ return data;
854
+ }
855
+ else if (nodeType.poll) {
856
+ if (mode === 'manual') {
857
+ // In manual mode run the poll function
858
+ const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(this, node, additionalData, mode, 'manual');
859
+ return nodeType.poll.call(thisArgs);
860
+ }
861
+ // In any other mode pass data through as it already contains the result of the poll
862
+ return inputData['main'];
863
+ }
864
+ else if (nodeType.trigger) {
865
+ if (mode === 'manual') {
866
+ // In manual mode start the trigger
867
+ const triggerResponse = await this.runTrigger(node, nodeExecuteFunctions.getExecuteTriggerFunctions, additionalData, mode, 'manual');
868
+ if (triggerResponse === undefined) {
869
+ return null;
870
+ }
871
+ if (triggerResponse.manualTriggerFunction !== undefined) {
872
+ // If a manual trigger function is defined call it and wait till it did run
873
+ await triggerResponse.manualTriggerFunction();
874
+ }
875
+ const response = await triggerResponse.manualTriggerResponse;
876
+ // And then close it again after it did execute
877
+ if (triggerResponse.closeFunction) {
878
+ await triggerResponse.closeFunction();
879
+ }
880
+ if (response.length === 0) {
881
+ return null;
882
+ }
883
+ return response;
884
+ }
885
+ // For trigger nodes in any mode except "manual" do we simply pass the data through
886
+ return inputData['main'];
887
+ }
888
+ else if (nodeType.webhook) {
889
+ // For webhook nodes always simply pass the data through
890
+ return inputData['main'];
891
+ }
892
+ else {
893
+ // For nodes which have routing information on properties
894
+ const routingNode = new __1.RoutingNode(this, node, connectionInputData, runExecutionData ?? null, additionalData, mode);
895
+ return routingNode.runNode(inputData, runIndex, nodeType, nodeExecuteFunctions);
896
+ }
897
+ return null;
898
+ }
899
+ }
900
+ exports.Workflow = Workflow;
901
+ //# sourceMappingURL=Workflow.js.map