@5minds/node-red-contrib-processcube 1.16.1-feature-5e019f-mhojvcvu → 1.16.1-feature-91fab0-mhvqo2et

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.
@@ -3,10 +3,10 @@ module.exports = function (RED) {
3
3
  RED.nodes.createNode(this, config);
4
4
  var node = this;
5
5
 
6
- node.on('input', function (msg) {
6
+ node.on('input', function (msg) {
7
7
  const flowNodeInstanceId = msg.flowNodeInstanceId;
8
8
  const etw_input_node_id = msg.etw_input_node_id;
9
-
9
+
10
10
  if (!etw_input_node_id) {
11
11
  node.error('Error: The message did not contain the required etw_input_node_id.');
12
12
  } else {
@@ -24,7 +24,7 @@ module.exports = function (RED) {
24
24
  if (msgError.code) {
25
25
  errorCode = msgError.code;
26
26
  errorMessage = msgError.message;
27
- }
27
+ }
28
28
  } else if (msg.errorCode) {
29
29
  errorCode = msg.errorCode;
30
30
  errorMessage = msg.errorMessage;
@@ -1,9 +1,128 @@
1
1
  const EventEmitter = require('node:events');
2
2
 
3
+ class ExternalTaskNodeStates {
4
+ constructor(flowNodeInstanceId) {
5
+ this.flowNodeInstanceId = flowNodeInstanceId;
6
+ this.nodeStades = {}; // Track send calls per nodeId
7
+ }
8
+
9
+ markSended(nodeId) {
10
+ if (!this.nodeStades[nodeId]) {
11
+ this.nodeStades[nodeId] = { gotSend: false, gotCompleted: false };
12
+ }
13
+
14
+ console.log(`[DEBUG] markSended - flowNodeInstanceId: ${this.flowNodeInstanceId}, nodeId: ${nodeId}, before: ${JSON.stringify(this.nodeStades[nodeId])}`);
15
+ this.nodeStades[nodeId].gotSend = true;
16
+ console.log(`[DEBUG] markSended - after: ${JSON.stringify(this.nodeStades[nodeId])}`);
17
+ }
18
+
19
+ markCompleted(nodeId) {
20
+ if (!this.nodeStades[nodeId]) {
21
+ this.nodeStades[nodeId] = { gotSend: false, gotCompleted: false };
22
+ }
23
+
24
+ console.log(`[DEBUG] markCompleted - flowNodeInstanceId: ${this.flowNodeInstanceId}, nodeId: ${nodeId}, before: ${JSON.stringify(this.nodeStades[nodeId])}`);
25
+ this.nodeStades[nodeId].gotCompleted = true;
26
+ console.log(`[DEBUG] markCompleted - after: ${JSON.stringify(this.nodeStades[nodeId])}`);
27
+ }
28
+
29
+ checkIfCompletedWithoutSend(nodeId) {
30
+ const nodeState = this.nodeStades[nodeId];
31
+ const result = (nodeState && nodeState.gotCompleted && !nodeState.gotSend);
32
+
33
+ console.log(`[DEBUG] checkIfCompletedWithoutSend - flowNodeInstanceId: ${this.flowNodeInstanceId}, nodeId: ${nodeId}, nodeState: ${JSON.stringify(nodeState)}, result: ${result}`);
34
+ return result;
35
+ }
36
+ }
37
+
3
38
  module.exports = function (RED) {
4
39
 
5
40
  const os = require('os');
6
41
 
42
+ // Global dictionary for tracking external tasks by flowNodeInstanceId
43
+ const globalExternalTaskStates = {};
44
+
45
+ const raiseExternalTaskError = (flowNodeInstanceId, etwInputNodeId, nodeId) => {
46
+ const fullNode = RED.nodes.getNode(nodeId);
47
+
48
+ const wires = fullNode?.wires;
49
+ const hasConnectedOutputs = wires && wires.some(wireArray => wireArray && wireArray.length > 0);
50
+
51
+ console.log(`[DEBUG] raiseExternalTaskError called for flowNodeInstanceId: ${flowNodeInstanceId}, nodeId: ${nodeId}, hasConnectedOutputs: ${hasConnectedOutputs}`);
52
+
53
+ if (hasConnectedOutputs) {
54
+ const inputNode = RED.nodes.getNode(etwInputNodeId);
55
+
56
+ if (inputNode && inputNode.eventEmitter) {
57
+ const errorMessage = `Node ${nodeId} (${fullNode.name || fullNode.type}) completed without sending output`;
58
+ const error = new Error(errorMessage);
59
+ error.errorCode = 'NODE_NO_OUTPUT';
60
+ error.errorDetails = RED.util.encodeObject({
61
+ flowNodeInstanceId: flowNodeInstanceId,
62
+ nodeId: nodeId,
63
+ nodeName: fullNode.name,
64
+ nodeType: fullNode.type
65
+ });
66
+
67
+ console.log(`[DEBUG] Emitting error event for flowNodeInstanceId: ${flowNodeInstanceId}, error: ${errorMessage}`);
68
+ inputNode.eventEmitter.emit(`handle-${flowNodeInstanceId}`, error, true);
69
+ } else {
70
+ console.log(`[DEBUG] Cannot raise error - inputNode or eventEmitter not found for etwInputNodeId: ${etwInputNodeId}`);
71
+ }
72
+ }
73
+ };
74
+
75
+ // Example synchronous onSend hook
76
+ RED.hooks.add("onSend", (sendEvents) => {
77
+ for (const sendEvent of sendEvents) {
78
+
79
+ // Call send method on ExternalTaskState if this message has a flowNodeInstanceId
80
+ if (sendEvent.msg?.flowNodeInstanceId) {
81
+ let externalTaskNodeStates = globalExternalTaskStates[sendEvent.msg.flowNodeInstanceId];
82
+
83
+ console.log(`[DEBUG] onSend - flowNodeInstanceId: ${sendEvent.msg.flowNodeInstanceId}, nodeId: ${sendEvent.source.node.id}, stateExists: ${!!externalTaskNodeStates}`);
84
+
85
+ if (!externalTaskNodeStates) {
86
+ console.log(`[DEBUG] onSend - Creating NEW ExternalTaskNodeStates for flowNodeInstanceId: ${sendEvent.msg.flowNodeInstanceId}`);
87
+ externalTaskNodeStates = new ExternalTaskNodeStates(sendEvent.msg.flowNodeInstanceId);
88
+ globalExternalTaskStates[sendEvent.msg.flowNodeInstanceId] = externalTaskNodeStates;
89
+ }
90
+
91
+ externalTaskNodeStates.markSended(sendEvent.source.node.id)
92
+
93
+ if (externalTaskNodeStates.checkIfCompletedWithoutSend(sendEvent.source.node.id)) {
94
+ console.log(`[DEBUG] onSend - Node completed without send detected! Raising error for nodeId: ${sendEvent.source.node.id}`);
95
+ raiseExternalTaskError(sendEvent.msg.flowNodeInstanceId, sendEvent.msg.etw_input_node_id, sendEvent.source.node.id);
96
+ }
97
+ }
98
+ }
99
+ });
100
+
101
+ const onCompleted = (completeEvent) => {
102
+
103
+ // Check if this is an external task message
104
+ if (completeEvent.msg?.flowNodeInstanceId) {
105
+ let externalTaskNodeStates = globalExternalTaskStates[completeEvent.msg.flowNodeInstanceId];
106
+
107
+ console.log(`[DEBUG] onComplete - flowNodeInstanceId: ${completeEvent.msg.flowNodeInstanceId}, nodeId: ${completeEvent.node.id}, stateExists: ${!!externalTaskNodeStates}`);
108
+
109
+ if (!externalTaskNodeStates) {
110
+ console.log(`[DEBUG] onComplete - Creating NEW ExternalTaskNodeStates for flowNodeInstanceId: ${completeEvent.msg.flowNodeInstanceId}`);
111
+ externalTaskNodeStates = new ExternalTaskNodeStates(completeEvent.msg.flowNodeInstanceId);
112
+ globalExternalTaskStates[completeEvent.msg.flowNodeInstanceId] = externalTaskNodeStates;
113
+ }
114
+
115
+ externalTaskNodeStates.markCompleted(completeEvent.node.id);
116
+
117
+ if (externalTaskNodeStates.checkIfCompletedWithoutSend(completeEvent.node.id)) {
118
+ console.log(`[DEBUG] onComplete - Node completed without send detected! Raising error for nodeId: ${completeEvent.node.id}`);
119
+ raiseExternalTaskError(completeEvent.msg.flowNodeInstanceId, completeEvent.msg.etw_input_node_id, completeEvent.node.id);
120
+ }
121
+ }
122
+ }
123
+
124
+ RED.hooks.add("onComplete", onCompleted);
125
+
7
126
  function ExternalTaskInput(config) {
8
127
  RED.nodes.createNode(this, config);
9
128
  var node = this;
@@ -349,15 +468,34 @@ module.exports = function (RED) {
349
468
  return;
350
469
  }
351
470
  const etwCallback = async (payload, externalTask) => {
471
+
472
+ console.log(`[DEBUG] etwCallback - NEW External Task received! flowNodeInstanceId: ${externalTask.flowNodeInstanceId}, processInstanceId: ${externalTask.processInstanceId}`);
473
+ console.log(`[DEBUG] etwCallback - Creating NEW ExternalTaskNodeStates for flowNodeInstanceId: ${externalTask.flowNodeInstanceId}`);
474
+ globalExternalTaskStates[externalTask.flowNodeInstanceId] = new ExternalTaskNodeStates(externalTask.flowNodeInstanceId);
475
+
352
476
  const saveHandleCallback = (data, callback, msg) => {
353
477
  try {
354
478
  callback(data);
355
479
  node.log(`send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}`);
480
+
481
+ // Remove ExternalTaskState from global dictionary
482
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
483
+ console.log(`[DEBUG] saveHandleCallback SUCCESS - Deleting ExternalTaskNodeStates for flowNodeInstanceId: ${externalTask.flowNodeInstanceId}`);
484
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
485
+ }
486
+
356
487
  node.setFinishHandlingTaskStatus(externalTask);
357
488
  } catch (error) {
489
+ // Remove ExternalTaskState from global dictionary on error as well
490
+ if (globalExternalTaskStates[externalTask.flowNodeInstanceId]) {
491
+ console.log(`[DEBUG] saveHandleCallback ERROR - Deleting ExternalTaskNodeStates for flowNodeInstanceId: ${externalTask.flowNodeInstanceId}, error: ${error?.message}`);
492
+ delete globalExternalTaskStates[externalTask.flowNodeInstanceId];
493
+ }
494
+
358
495
  node.setErrorFinishHandlingTaskStatus(externalTask, error);
359
496
  msg.error = error;
360
497
  node.error(`failed send to engine *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}', topic '${node.topic}' and *processInstanceId* ${externalTask.processInstanceId}: ${error?.message}`, msg);
498
+ callback(error);
361
499
  }
362
500
  };
363
501
 
@@ -378,6 +516,7 @@ module.exports = function (RED) {
378
516
  };
379
517
 
380
518
  const handleErrorTask = (error) => {
519
+ console.log(`[DEBUG] handleErrorTask - flowNodeInstanceId: ${externalTask.flowNodeInstanceId}, errorCode: ${error?.errorCode}, errorMessage: ${error?.message}`);
381
520
  node.log(
382
521
  `handle error event for *external task flowNodeInstanceId* '${externalTask.flowNodeInstanceId}' and *processInstanceId* '${externalTask.processInstanceId}' on *msg._msgid* '${error.errorDetails?._msgid}'.`
383
522
  );
@@ -390,6 +529,8 @@ module.exports = function (RED) {
390
529
  };
391
530
 
392
531
  node.eventEmitter.once(`handle-${externalTask.flowNodeInstanceId}`, (msg, isError = false) => {
532
+ console.log(`[DEBUG] eventEmitter handle event - flowNodeInstanceId: ${externalTask.flowNodeInstanceId}, isError: ${isError}, msgId: ${msg._msgid}`);
533
+
393
534
  try {
394
535
  msg.etw_finished_at = new Date().toISOString();
395
536
 
@@ -397,7 +538,7 @@ module.exports = function (RED) {
397
538
  msg.etw_duration = new Date(msg.etw_finished_at) - new Date(msg.etw_started_at);
398
539
  }
399
540
  } catch (error) {
400
- node.error(`failed to calculate duration: ${error?.message}`, msg);
541
+ node.error(`failed to calculate duration: ${error?.message}`, msg);
401
542
  }
402
543
 
403
544
  node.log(
@@ -406,8 +547,10 @@ module.exports = function (RED) {
406
547
 
407
548
 
408
549
  if (isError) {
550
+ console.log(`[DEBUG] Routing to handleErrorTask`);
409
551
  handleErrorTask(msg);
410
552
  } else {
553
+ console.log(`[DEBUG] Routing to handleFinishTask`);
411
554
  handleFinishTask(msg);
412
555
  }
413
556
  });
@@ -429,6 +572,7 @@ module.exports = function (RED) {
429
572
  );
430
573
 
431
574
  node.send(msg);
575
+ console.log(`[DEBUG] etwCallback - Sent message for flowNodeInstanceId: ${externalTask.flowNodeInstanceId}, msgId: ${msg._msgid}`);
432
576
  });
433
577
  };
434
578
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube",
3
- "version": "1.16.1-feature-5e019f-mhojvcvu",
3
+ "version": "1.16.1-feature-91fab0-mhvqo2et",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED nodes for ProcessCube",
6
6
  "scripts": {
@@ -39,7 +39,6 @@
39
39
  "DataobjectInstanceQuery": "dataobject-instance-query.js",
40
40
  "EndEventFinishedListener": "endevent-finished-listener.js",
41
41
  "externaltaskInput": "externaltask-input.js",
42
- "externaltaskInject": "externaltask-inject.js",
43
42
  "externaltaskOutput": "externaltask-output.js",
44
43
  "externaltaskError": "externaltask-error.js",
45
44
  "externaltaskEventListener": "externaltask-event-listener.js",
@@ -1,123 +0,0 @@
1
- <script type="text/javascript">
2
- RED.nodes.registerType('externaltask-inject', {
3
- category: 'ProcessCube',
4
- color: '#02AFD6',
5
- defaults: {
6
- name: { value: '' },
7
- payloadType: { value: 'json' },
8
- payload: { value: '{}' },
9
- taskType: { value: 'json' },
10
- task: { value: '{}' }
11
- },
12
- inputs: 0,
13
- outputs: 1,
14
- icon: 'inject.svg',
15
- label: function () {
16
- return this.name || 'externaltask-inject';
17
- },
18
- button: {
19
- onclick: function() {
20
- var node = this;
21
-
22
- $.ajax({
23
- type: "POST",
24
- url: "externaltask-inject/trigger/" + node.id,
25
- success: function(resp) {
26
- RED.notify("Message injected", "success");
27
- },
28
- error: function(xhr, status, err) {
29
- RED.notify("Error injecting message: " + err, "error");
30
- }
31
- });
32
- }
33
- },
34
- oneditprepare: function () {
35
- // Initialize the typed input for payload
36
- $('#node-input-payload').typedInput({
37
- default: 'json',
38
- types: ['str', 'num', 'bool', 'json', 'jsonata', 'flow', 'global']
39
- });
40
- // Restore the saved type for payload from hidden input
41
- var payloadType = $('#node-input-payloadType').val();
42
- if (payloadType) {
43
- $('#node-input-payload').typedInput('type', payloadType);
44
- }
45
-
46
- // Initialize the typed input for task
47
- $('#node-input-task').typedInput({
48
- default: 'json',
49
- types: ['str', 'num', 'bool', 'json', 'jsonata', 'flow', 'global']
50
- });
51
- // Restore the saved type for task from hidden input
52
- var taskType = $('#node-input-taskType').val();
53
- if (taskType) {
54
- $('#node-input-task').typedInput('type', taskType);
55
- }
56
- },
57
- oneditsave: function () {
58
- var payloadValue = $('#node-input-payload').typedInput('value');
59
- var payloadType = $('#node-input-payload').typedInput('type');
60
- this.payload = payloadValue;
61
- this.payloadType = payloadType;
62
- // Persist type to hidden input field
63
- $('#node-input-payloadType').val(payloadType);
64
-
65
- var taskValue = $('#node-input-task').typedInput('value');
66
- var taskType = $('#node-input-task').typedInput('type');
67
- this.task = taskValue;
68
- this.taskType = taskType;
69
- // Persist type to hidden input field
70
- $('#node-input-taskType').val(taskType);
71
- }
72
- });
73
- </script>
74
-
75
- <script type="text/html" data-template-name="externaltask-inject">
76
- <div class="form-row">
77
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
78
- <input type="text" id="node-input-name" placeholder="Name" />
79
- </div>
80
- <div class="form-row">
81
- <label for="node-input-payload">msg.payload</label>
82
- <input type="hidden" id="node-input-payloadType">
83
- <input type="text" id="node-input-payload" style="width: 70%;">
84
- </div>
85
- <div class="form-row">
86
- <label for="node-input-task">msg.task</label>
87
- <input type="hidden" id="node-input-taskType">
88
- <input type="text" id="node-input-task" style="width: 70%;">
89
- </div>
90
- </script>
91
-
92
- <script type="text/markdown" data-help-name="externaltask-inject">
93
- A lightweight alternative to the standard Node-RED inject node for testing external task workflows. Use this node to inject test messages directly into an external task workflow without needing a real external task from the ProcessCube engine.
94
-
95
- This node is ideal for small-scale testing and debugging of external task workflows. For larger-scale testing with test cases or CI/CD integration, use the **externaltask-testing** node instead.
96
-
97
- ## Features
98
-
99
- - Injects messages with configurable payload and task metadata
100
- - Automatically generates `flowNodeInstanceId` and `processInstanceId` if not provided
101
- - Sets required fields for compatibility with externaltask-output and externaltask-error nodes
102
- - Supports multiple value types (JSON, string, number, boolean, JSONata, flow/global context)
103
-
104
- ## Configs
105
-
106
- : name (string) : The name of the node
107
- : payload (string | {}) : The payload to inject into the workflow
108
- : task (string | {}) : The task object to inject
109
-
110
- ## Outputs
111
-
112
- : payload (string | {}) : The injected payload with metadata (flowNodeInstanceId, processInstanceId, task)
113
- : task (object) : The injected task object
114
- : flowNodeInstanceId (string) : Unique identifier for the external task
115
- : processInstanceId (string) : Reference to the process instance
116
- : etw_input_node_id (string) : Reference to the injecting node
117
- : etw_started_at (string) : ISO timestamp of when the task was injected
118
-
119
- ## References
120
-
121
- - [The ProcessCube&copy; Developer Network](https://processcube.io) - All documentation for the ProcessCube&copy; platform
122
- - [ProcessCube&copy; LowCode Integration](https://processcube.io/docs/node-red) - LowCode integration in ProcessCube&copy;
123
- </script>
@@ -1,98 +0,0 @@
1
- const { v4: uuidv4 } = require('uuid');
2
- const EventEmitter = require('node:events');
3
-
4
- module.exports = function (RED) {
5
- function ExternalTaskInject(config) {
6
- RED.nodes.createNode(this, config);
7
- var node = this;
8
- node.config = config;
9
- node.eventEmitter = new EventEmitter();
10
-
11
- node.on('input', function (msg, send, done) {
12
- msg.flowNodeInstanceId = msg.flowNodeInstanceId ?? uuidv4();
13
- msg.processInstanceId = msg.processInstanceId ?? uuidv4();
14
-
15
- if (!msg.task) {
16
- // Use configured task if available, otherwise create default
17
- if (config.task) {
18
- msg.task = config.task;
19
- } else {
20
- msg.task = {
21
- flowNodeInstanceId: msg.flowNodeInstanceId,
22
- processInstanceId: msg.processInstanceId,
23
- task: {}
24
- }
25
- }
26
- }
27
- msg.etw_started_at = new Date().toISOString();
28
- msg.etw_input_node_id = node.id;
29
-
30
- send(msg);
31
- if (done) done();
32
- });
33
-
34
- }
35
-
36
- RED.nodes.registerType('externaltask-inject', ExternalTaskInject);
37
-
38
- // Helper function to parse typed values
39
- function parseTypedValue(value, type) {
40
- switch(type) {
41
- case 'str':
42
- return value || '';
43
- case 'num':
44
- return Number(value) || 0;
45
- case 'bool':
46
- return value === 'true' || value === true;
47
- case 'json':
48
- try {
49
- return JSON.parse(value || '{}');
50
- } catch(parseErr) {
51
- console.error("Invalid JSON: " + parseErr.message);
52
- return {};
53
- }
54
- case 'jsonata':
55
- // JSONata expressions would need to be evaluated in Node-RED context
56
- // For now, treat as string
57
- return value || '';
58
- case 'flow':
59
- case 'global':
60
- // These are context references - store as string for now
61
- return value || '';
62
- default:
63
- return value || '';
64
- }
65
- }
66
-
67
- // HTTP endpoint to handle button clicks
68
- RED.httpAdmin.post('/externaltask-inject/trigger/:id', function(req, res) {
69
- var node = RED.nodes.getNode(req.params.id);
70
- if (node !== null && typeof node !== 'undefined') {
71
- try {
72
- var payloadValue = node.config.payload;
73
- var payloadType = node.config.payloadType || 'json';
74
- var payloadData = parseTypedValue(payloadValue, payloadType);
75
-
76
- var taskValue = node.config.task;
77
- var taskType = node.config.taskType || 'json';
78
- var taskData = parseTypedValue(taskValue, taskType);
79
-
80
- // Create a message with the configured payload and task
81
- var msg = {
82
- payload: payloadData,
83
- task: taskData,
84
- _msgid: uuidv4()
85
- };
86
-
87
- // Trigger the node's input handler with the configured message
88
- node.receive(msg);
89
- res.sendStatus(200);
90
- } catch(err) {
91
- res.sendStatus(500);
92
- node.error("Error injecting message: " + err.message);
93
- }
94
- } else {
95
- res.sendStatus(404);
96
- }
97
- });
98
- };