@node-red/runtime 3.1.0-beta.2 → 3.1.0-beta.4

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.
package/lib/flows/Flow.js CHANGED
@@ -14,19 +14,20 @@
14
14
  * limitations under the License.
15
15
  **/
16
16
 
17
- var clone = require("clone");
18
- var redUtil = require("@node-red/util").util;
17
+ const clone = require("clone");
18
+ const redUtil = require("@node-red/util").util;
19
19
  const events = require("@node-red/util").events;
20
- var flowUtil = require("./util");
20
+ const flowUtil = require("./util");
21
21
  const context = require('../nodes/context');
22
22
  const hooks = require("@node-red/util").hooks;
23
23
  const credentials = require("../nodes/credentials");
24
24
 
25
- var Subflow;
26
- var Log;
25
+ let Subflow;
26
+ let Log;
27
+ let Group;
27
28
 
28
- var nodeCloseTimeout = 15000;
29
- var asyncMessageDelivery = true;
29
+ let nodeCloseTimeout = 15000;
30
+ let asyncMessageDelivery = true;
30
31
 
31
32
  /**
32
33
  * This class represents a flow within the runtime. It is responsible for
@@ -52,6 +53,8 @@ class Flow {
52
53
  this.isGlobalFlow = false;
53
54
  }
54
55
  this.id = this.flow.id || "global";
56
+ this.groups = {}
57
+ this.groupOrder = []
55
58
  this.activeNodes = {};
56
59
  this.subflowInstanceNodes = {};
57
60
  this.catchNodes = [];
@@ -59,6 +62,11 @@ class Flow {
59
62
  this.path = this.id;
60
63
  // Ensure a context exists for this flow
61
64
  this.context = context.getFlowContext(this.id,this.parent.id);
65
+
66
+ // env is an array of env definitions
67
+ // _env is an object for direct lookup of env name -> value
68
+ this.env = this.flow.env
69
+ this._env = {}
62
70
  }
63
71
 
64
72
  /**
@@ -136,7 +144,7 @@ class Flow {
136
144
  * @param {[type]} msg [description]
137
145
  * @return {[type]} [description]
138
146
  */
139
- start(diff) {
147
+ async start(diff) {
140
148
  this.trace("start "+this.TYPE+" ["+this.path+"]");
141
149
  var node;
142
150
  var newNode;
@@ -145,6 +153,52 @@ class Flow {
145
153
  this.statusNodes = [];
146
154
  this.completeNodeMap = {};
147
155
 
156
+
157
+ if (this.isGlobalFlow) {
158
+ // This is the global flow. It needs to go find the `global-config`
159
+ // node and extract any env properties from it
160
+ const configNodes = Object.keys(this.flow.configs);
161
+ for (let i = 0; i < configNodes.length; i++) {
162
+ const node = this.flow.configs[configNodes[i]]
163
+ if (node.type === 'global-config' && node.env) {
164
+ const nodeEnv = await flowUtil.evaluateEnvProperties(this, node.env, credentials.get(node.id))
165
+ this._env = { ...this._env, ...nodeEnv }
166
+ }
167
+ }
168
+ }
169
+
170
+ if (this.env) {
171
+ this._env = { ...this._env, ...await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) }
172
+ }
173
+
174
+ // Initialise the group objects. These must be done in the right order
175
+ // starting from outer-most to inner-most so that the parent hierarchy
176
+ // is maintained.
177
+ this.groups = {}
178
+ this.groupOrder = []
179
+ const groupIds = Object.keys(this.flow.groups || {})
180
+ while (groupIds.length > 0) {
181
+ const id = groupIds.shift()
182
+ const groupDef = this.flow.groups[id]
183
+ if (!groupDef.g || this.groups[groupDef.g]) {
184
+ // The parent of this group is available - either another group
185
+ // or the top-level flow (this)
186
+ const parent = this.groups[groupDef.g] || this
187
+ this.groups[groupDef.id] = new Group(parent, groupDef)
188
+ this.groupOrder.push(groupDef.id)
189
+ } else {
190
+ // Try again once we've processed the other groups
191
+ groupIds.push(id)
192
+ }
193
+ }
194
+
195
+ for (let i = 0; i < this.groupOrder.length; i++) {
196
+ // Start the groups in the right order so they
197
+ // can setup their env vars knowning their parent
198
+ // will have been started
199
+ await this.groups[this.groupOrder[i]].start()
200
+ }
201
+
148
202
  var configNodes = Object.keys(this.flow.configs);
149
203
  var configNodeAttempts = {};
150
204
  while (configNodes.length > 0) {
@@ -177,7 +231,7 @@ class Flow {
177
231
  }
178
232
  }
179
233
  if (readyToCreate) {
180
- newNode = flowUtil.createNode(this,node);
234
+ newNode = await flowUtil.createNode(this,node);
181
235
  if (newNode) {
182
236
  this.activeNodes[id] = newNode;
183
237
  }
@@ -203,7 +257,7 @@ class Flow {
203
257
  if (node.d !== true) {
204
258
  if (!node.subflow) {
205
259
  if (!this.activeNodes[id]) {
206
- newNode = flowUtil.createNode(this,node);
260
+ newNode = await flowUtil.createNode(this,node);
207
261
  if (newNode) {
208
262
  this.activeNodes[id] = newNode;
209
263
  }
@@ -221,7 +275,7 @@ class Flow {
221
275
  node
222
276
  );
223
277
  this.subflowInstanceNodes[id] = subflow;
224
- subflow.start();
278
+ await subflow.start();
225
279
  this.activeNodes[id] = subflow.node;
226
280
 
227
281
  // this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id});
@@ -404,8 +458,7 @@ class Flow {
404
458
  * @return {Node} group node
405
459
  */
406
460
  getGroupNode(id) {
407
- const groups = this.global.groups;
408
- return groups[id];
461
+ return this.groups[id];
409
462
  }
410
463
 
411
464
  /**
@@ -416,95 +469,8 @@ class Flow {
416
469
  return this.activeNodes;
417
470
  }
418
471
 
419
- /*!
420
- * Get value of environment variable defined in group node.
421
- * @param {String} group - group node
422
- * @param {String} name - name of variable
423
- * @return {Object} object containing the value in val property or null if not defined
424
- */
425
- getGroupEnvSetting(node, group, name) {
426
- if (group) {
427
- if (name === "NR_GROUP_NAME") {
428
- return [{
429
- val: group.name
430
- }, null];
431
- }
432
- if (name === "NR_GROUP_ID") {
433
- return [{
434
- val: group.id
435
- }, null];
436
- }
437
-
438
- if (group.credentials === undefined) {
439
- group.credentials = credentials.get(group.id) || {};
440
- }
441
- if (!name.startsWith("$parent.")) {
442
- if (group.env) {
443
- if (!group._env) {
444
- const envs = group.env;
445
- const entries = envs.map((env) => {
446
- if (env.type === "cred") {
447
- const cred = group.credentials;
448
- if (cred.hasOwnProperty(env.name)) {
449
- env.value = cred[env.name];
450
- }
451
- }
452
- return [env.name, env];
453
- });
454
- group._env = Object.fromEntries(entries);
455
- }
456
- const env = group._env[name];
457
- if (env) {
458
- let value = env.value;
459
- const type = env.type;
460
- if ((type !== "env") ||
461
- (value !== name)) {
462
- if (type === "env") {
463
- value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}");
464
- }
465
- if (type === "bool") {
466
- const val
467
- = ((value === "true") ||
468
- (value === true));
469
- return [{
470
- val: val
471
- }, null];
472
- }
473
- if (type === "cred") {
474
- return [{
475
- val: value
476
- }, null];
477
- }
478
- try {
479
- var val = redUtil.evaluateNodeProperty(value, type, node, null, null);
480
- return [{
481
- val: val
482
- }, null];
483
- }
484
- catch (e) {
485
- this.error(e);
486
- return [null, null];
487
- }
488
- }
489
- }
490
- }
491
- }
492
- else {
493
- name = name.substring(8);
494
- }
495
- if (group.g) {
496
- const parent = this.getGroupNode(group.g);
497
- return this.getGroupEnvSetting(node, parent, name);
498
- }
499
- }
500
- return [null, name];
501
- }
502
-
503
-
504
472
  /**
505
- * Get a flow setting value. This currently automatically defers to the parent
506
- * flow which, as defined in ./index.js returns `process.env[key]`.
507
- * This lays the groundwork for Subflow to have instance-specific settings
473
+ * Get a flow setting value.
508
474
  * @param {[type]} key [description]
509
475
  * @return {[type]} [description]
510
476
  */
@@ -516,54 +482,14 @@ class Flow {
516
482
  if (key === "NR_FLOW_ID") {
517
483
  return flow.id;
518
484
  }
519
- if (flow.credentials === undefined) {
520
- flow.credentials = credentials.get(flow.id) || {};
521
- }
522
- if (flow.env) {
523
- if (!key.startsWith("$parent.")) {
524
- if (!flow._env) {
525
- const envs = flow.env;
526
- const entries = envs.map((env) => {
527
- if (env.type === "cred") {
528
- const cred = flow.credentials;
529
- if (cred.hasOwnProperty(env.name)) {
530
- env.value = cred[env.name];
531
- }
532
- }
533
- return [env.name, env]
534
- });
535
- flow._env = Object.fromEntries(entries);
536
- }
537
- const env = flow._env[key];
538
- if (env) {
539
- let value = env.value;
540
- const type = env.type;
541
- if ((type !== "env") || (value !== key)) {
542
- if (type === "env") {
543
- value = value.replace(new RegExp("\\${"+key+"}","g"),"${$parent."+key+"}");
544
- }
545
- try {
546
- if (type === "bool") {
547
- const val = ((value === "true") ||
548
- (value === true));
549
- return val;
550
- }
551
- if (type === "cred") {
552
- return value;
553
- }
554
- var val = redUtil.evaluateNodeProperty(value, type, null, null, null);
555
- return val;
556
- }
557
- catch (e) {
558
- this.error(e);
559
- }
560
- }
561
- }
485
+ if (!key.startsWith("$parent.")) {
486
+ if (this._env.hasOwnProperty(key)) {
487
+ return this._env[key]
562
488
  }
563
- else {
489
+ } else {
564
490
  key = key.substring(8);
565
- }
566
491
  }
492
+ // Delegate to the parent flow.
567
493
  return this.parent.getSetting(key);
568
494
  }
569
495
 
@@ -601,15 +527,40 @@ class Flow {
601
527
  // Delegate status to any nodes using this config node
602
528
  for (let userNode in node.users) {
603
529
  if (node.users.hasOwnProperty(userNode)) {
604
- node.users[userNode]._flow.handleStatus(node,statusMessage,node.users[userNode],true);
530
+ handled = node.users[userNode]._flow.handleStatus(node,statusMessage,node.users[userNode],true) || handled;
605
531
  }
606
532
  }
607
- handled = true;
608
533
  } else {
609
- this.statusNodes.forEach(function(targetStatusNode) {
610
- if (targetStatusNode.scope && targetStatusNode.scope.indexOf(reportingNode.id) === -1) {
534
+ const candidateNodes = [];
535
+ this.statusNodes.forEach(targetStatusNode => {
536
+ if (targetStatusNode.g && targetStatusNode.scope === 'group' && !reportingNode.g) {
537
+ // Status node inside a group, reporting node not in a group - skip it
538
+ return
539
+ }
540
+ if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) {
611
541
  return;
612
542
  }
543
+ let distance = 0
544
+ if (reportingNode.g) {
545
+ // Reporting node inside a group. Calculate the distance between it and the status node
546
+ let containingGroup = this.groups[reportingNode.g]
547
+ while (containingGroup && containingGroup.id !== targetStatusNode.g) {
548
+ distance++
549
+ containingGroup = this.groups[containingGroup.g]
550
+ }
551
+ if (!containingGroup && targetStatusNode.g && targetStatusNode.scope === 'group') {
552
+ // This status node is in a group, but not in the same hierachy
553
+ // the reporting node is in
554
+ return
555
+ }
556
+ }
557
+ candidateNodes.push({ d: distance, n: targetStatusNode })
558
+ })
559
+ candidateNodes.sort((A,B) => {
560
+ return A.d - B.d
561
+ })
562
+ candidateNodes.forEach(candidate => {
563
+ const targetStatusNode = candidate.n
613
564
  var message = {
614
565
  status: clone(statusMessage)
615
566
  }
@@ -662,26 +613,50 @@ class Flow {
662
613
  // Delegate status to any nodes using this config node
663
614
  for (let userNode in node.users) {
664
615
  if (node.users.hasOwnProperty(userNode)) {
665
- node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]);
616
+ handled = node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]) || handled;
666
617
  }
667
618
  }
668
- handled = true;
669
619
  } else {
670
- var handledByUncaught = false;
671
-
672
- this.catchNodes.forEach(function(targetCatchNode) {
673
- if (targetCatchNode.scope && targetCatchNode.scope.indexOf(reportingNode.id) === -1) {
620
+ const candidateNodes = [];
621
+ this.catchNodes.forEach(targetCatchNode => {
622
+ if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
623
+ // Catch node inside a group, reporting node not in a group - skip it
624
+ return
625
+ }
626
+ if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) {
627
+ // Catch node has a scope set and it doesn't include the reporting node
674
628
  return;
675
629
  }
676
- if (!targetCatchNode.scope && targetCatchNode.uncaught && !handledByUncaught) {
630
+ let distance = 0
631
+ if (reportingNode.g) {
632
+ // Reporting node inside a group. Calculate the distance between it and the catch node
633
+ let containingGroup = this.groups[reportingNode.g]
634
+ while (containingGroup && containingGroup.id !== targetCatchNode.g) {
635
+ distance++
636
+ containingGroup = this.groups[containingGroup.g]
637
+ }
638
+ if (!containingGroup && targetCatchNode.g && targetCatchNode.scope === 'group') {
639
+ // This catch node is in a group, but not in the same hierachy
640
+ // the reporting node is in
641
+ return
642
+ }
643
+ }
644
+ candidateNodes.push({ d: distance, n: targetCatchNode })
645
+ })
646
+ candidateNodes.sort((A,B) => {
647
+ return A.d - B.d
648
+ })
649
+ let handledByUncaught = false
650
+ candidateNodes.forEach(candidate => {
651
+ const targetCatchNode = candidate.n
652
+ if (targetCatchNode.uncaught && !handledByUncaught) {
653
+ // This node only wants errors that haven't already been handled
677
654
  if (handled) {
678
- // This has been handled by a !uncaught catch node
679
- return;
655
+ return
680
656
  }
681
- // This is an uncaught error
682
- handledByUncaught = true;
657
+ handledByUncaught = true
683
658
  }
684
- var errorMessage;
659
+ let errorMessage;
685
660
  if (msg) {
686
661
  errorMessage = redUtil.cloneMessage(msg);
687
662
  } else {
@@ -808,7 +783,7 @@ function handlePreRoute(flow, sendEvent, reportError) {
808
783
  return;
809
784
  } else if (err !== false) {
810
785
  sendEvent.destination.node = flow.getNode(sendEvent.destination.id);
811
- if (sendEvent.destination.node) {
786
+ if (sendEvent.destination.node && typeof sendEvent.destination.node === 'object') {
812
787
  if (sendEvent.cloneMessage) {
813
788
  sendEvent.msg = redUtil.cloneMessage(sendEvent.msg);
814
789
  }
@@ -858,9 +833,10 @@ module.exports = {
858
833
  asyncMessageDelivery = !runtime.settings.runtimeSyncDelivery
859
834
  Log = runtime.log;
860
835
  Subflow = require("./Subflow");
836
+ Group = require("./Group").Group
861
837
  },
862
838
  create: function(parent,global,conf) {
863
- return new Flow(parent,global,conf);
839
+ return new Flow(parent,global,conf)
864
840
  },
865
841
  Flow: Flow
866
842
  }
@@ -0,0 +1,55 @@
1
+ const flowUtil = require("./util");
2
+ const credentials = require("../nodes/credentials");
3
+
4
+ /**
5
+ * This class represents a group within the runtime.
6
+ */
7
+ class Group {
8
+
9
+ /**
10
+ * Create a Group object.
11
+ * @param {[type]} parent The parent flow/group
12
+ * @param {[type]} groupDef This group's definition
13
+ */
14
+ constructor(parent, groupDef) {
15
+ this.TYPE = 'group'
16
+ this.name = groupDef.name
17
+ this.parent = parent
18
+ this.group = groupDef
19
+ this.id = this.group.id
20
+ this.g = this.group.g
21
+ this.env = this.group.env
22
+ this._env = {}
23
+ }
24
+
25
+ async start() {
26
+ if (this.env) {
27
+ this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id))
28
+ }
29
+ }
30
+ /**
31
+ * Get a group setting value.
32
+ * @param {[type]} key [description]
33
+ * @return {[type]} [description]
34
+ */
35
+ getSetting(key) {
36
+ if (key === "NR_GROUP_NAME") {
37
+ return this.name;
38
+ }
39
+ if (key === "NR_GROUP_ID") {
40
+ return this.id;
41
+ }
42
+ if (!key.startsWith("$parent.")) {
43
+ if (this._env.hasOwnProperty(key)) {
44
+ return this._env[key]
45
+ }
46
+ } else {
47
+ key = key.substring(8);
48
+ }
49
+ return this.parent.getSetting(key);
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ Group
55
+ }
@@ -119,7 +119,7 @@ class Subflow extends Flow {
119
119
  this.templateCredentials = credentials.get(subflowDef.id) || {};
120
120
  this.instanceCredentials = credentials.get(id) || {};
121
121
 
122
- var env = [];
122
+ var env = {};
123
123
  if (this.subflowDef.env) {
124
124
  this.subflowDef.env.forEach(e => {
125
125
  env[e.name] = e;
@@ -145,7 +145,7 @@ class Subflow extends Flow {
145
145
  }
146
146
  });
147
147
  }
148
- this.env = env;
148
+ this.env = Object.values(env);
149
149
  }
150
150
 
151
151
  /**
@@ -156,7 +156,7 @@ class Subflow extends Flow {
156
156
  * @param {[type]} diff [description]
157
157
  * @return {[type]} [description]
158
158
  */
159
- start(diff) {
159
+ async start(diff) {
160
160
  var self = this;
161
161
  // Create a subflow node to accept inbound messages and route appropriately
162
162
  var Node = require("../nodes/Node");
@@ -310,7 +310,7 @@ class Subflow extends Flow {
310
310
  }
311
311
  }
312
312
  }
313
- super.start(diff);
313
+ return super.start(diff);
314
314
  }
315
315
 
316
316
  /**
@@ -335,68 +335,35 @@ class Subflow extends Flow {
335
335
  }
336
336
  /**
337
337
  * Get environment variable of subflow
338
- * @param {String} name name of env var
338
+ * @param {String} key name of env var
339
339
  * @return {Object} val value of env var
340
340
  */
341
- getSetting(name) {
342
- if (!/^\$parent\./.test(name)) {
343
- var env = this.env;
344
- if (env && env.hasOwnProperty(name)) {
345
- var val = env[name];
346
- // If this is an env type property we need to be careful not
347
- // to get into lookup loops.
348
- // 1. if the value to lookup is the same as this one, go straight to parent
349
- // 2. otherwise, check if it is a compound env var ("foo $(bar)")
350
- // and if so, substitute any instances of `name` with $parent.name
351
- // See https://github.com/node-red/node-red/issues/2099
352
- if (val.type !== 'env' || val.value !== name) {
353
- let value = val.value;
354
- var type = val.type;
355
- if (type === 'env') {
356
- value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}");
357
- }
358
- try {
359
- return evaluateInputValue(value, type, this.node);
360
- }
361
- catch (e) {
362
- this.error(e);
363
- return undefined;
364
- }
365
- } else {
366
- // This _is_ an env property pointing at itself - go to parent
367
- }
368
- }
369
- } else {
370
- // name starts $parent. ... so delegate to parent automatically
371
- name = name.substring(8);
372
- }
341
+ getSetting(key) {
373
342
  const node = this.subflowInstance;
374
343
  if (node) {
375
- if (name === "NR_NODE_NAME") {
344
+ if (key === "NR_NODE_NAME" || key === "NR_SUBFLOW_NAME") {
376
345
  return node.name;
377
346
  }
378
- if (name === "NR_NODE_ID") {
347
+ if (key === "NR_NODE_ID" || key === "NR_SUBFLOW_ID") {
379
348
  return node.id;
380
349
  }
381
- if (name === "NR_NODE_PATH") {
350
+ if (key === "NR_NODE_PATH" || key === "NR_SUBFLOW_PATH") {
382
351
  return node._path;
383
352
  }
384
353
  }
385
- if (node.g) {
386
- const group = this.getGroupNode(node.g);
387
- const [result, newName] = this.getGroupEnvSetting(node, group, name);
388
- if (result) {
389
- return result.val;
354
+ if (!key.startsWith("$parent.")) {
355
+ if (this._env.hasOwnProperty(key)) {
356
+ return this._env[key]
390
357
  }
391
- name = newName;
358
+ } else {
359
+ key = key.substring(8);
392
360
  }
393
-
394
- var parent = this.parent;
395
- if (parent) {
396
- var val = parent.getSetting(name);
397
- return val;
361
+ // Push the request up to the parent.
362
+ // Unlike a Flow, the parent of a Subflow could be a Group
363
+ if (node.g) {
364
+ return this.parent.getGroupNode(node.g).getSetting(key)
398
365
  }
399
- return undefined;
366
+ return this.parent.getSetting(key)
400
367
  }
401
368
 
402
369
  /**