diff --git a/nodes/core/core/80-template.html b/nodes/core/core/80-template.html index 2c121b587..31dfe97ca 100644 --- a/nodes/core/core/80-template.html +++ b/nodes/core/core/80-template.html @@ -77,7 +77,7 @@ }

The resulting property will be:

Hello Fred. Today is Monday
-

It is possible to use a property from the flow context or global context. Just use {{flow.name}} or {{global.name}}. +

It is possible to use a property from the flow context or global context. Just use {{flow.name}} or {{global.name}}, or for persistable store store use {{flow[store].name}} or {{flobal[store].name}}.

Note: By default, mustache will escape any HTML entities in the values it substitutes. To prevent this, use {{{triple}}} braces. diff --git a/nodes/core/core/80-template.js b/nodes/core/core/80-template.js index e257dd2fa..f6e96a857 100644 --- a/nodes/core/core/80-template.js +++ b/nodes/core/core/80-template.js @@ -19,20 +19,39 @@ module.exports = function(RED) { var mustache = require("mustache"); var yaml = require("js-yaml"); + function parseContext(key) { + var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key); + if (match) { + var parts = {}; + parts.type = match[1]; + parts.store = (match[3] === '') ? "default" : match[3]; + parts.field = match[4]; + return parts; + } + return undefined; + } /** - * Custom Mustache Context capable to resolve message property and node + * Custom Mustache Context capable to collect message property and node * flow and global context */ - function NodeContext(msg, nodeContext, parent, escapeStrings) { + + function NodeContext(msg, nodeContext, parent, escapeStrings, promises, results) { this.msgContext = new mustache.Context(msg,parent); this.nodeContext = nodeContext; this.escapeStrings = escapeStrings; + this.promises = promises; + this.results = results; } NodeContext.prototype = new mustache.Context(); NodeContext.prototype.lookup = function (name) { + var results = this.results; + if (results) { + var val = results.shift(); + return val; + } // try message first: try { var value = this.msgContext.lookup(name); @@ -45,23 +64,40 @@ module.exports = function(RED) { value = value.replace(/\f/g, "\\f"); value = value.replace(/[\b]/g, "\\b"); } + this.promises.push(Promise.resolve(value)); return value; } - // try node context: - var dot = name.indexOf("."); - /* istanbul ignore else */ - if (dot > 0) { - var contextName = name.substr(0, dot); - var variableName = name.substr(dot + 1); - - if (contextName === "flow" && this.nodeContext.flow) { - return this.nodeContext.flow.get(variableName); + // try flow/global context: + var context = parseContext(name); + if (context) { + var type = context.type; + var store = context.store; + var field = context.field; + var target = this.nodeContext[type]; + if (target) { + var promise = new Promise((resolve, reject) => { + var callback = (err, val) => { + if (err) { + reject(err); + } else { + resolve(val); + } + }; + target.get(field, store, callback); + }); + this.promises.push(promise); + return ''; } - else if (contextName === "global" && this.nodeContext.global) { - return this.nodeContext.global.get(variableName); + else { + this.promises.push(Promise.resolve('')); + return ''; } } + else { + this.promises.push(Promise.resolve('')); + return ''; + } } catch(err) { throw err; @@ -69,7 +105,7 @@ module.exports = function(RED) { } NodeContext.prototype.push = function push (view) { - return new NodeContext(view, this.nodeContext,this.msgContext); + return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.promises, this.results); }; function TemplateNode(n) { @@ -83,8 +119,33 @@ module.exports = function(RED) { var node = this; node.on("input", function(msg) { + function output(value) { + /* istanbul ignore else */ + if (node.outputFormat === "json") { + value = JSON.parse(value); + } + /* istanbul ignore else */ + if (node.outputFormat === "yaml") { + value = yaml.load(value); + } + + if (node.fieldType === 'msg') { + RED.util.setMessageProperty(msg, node.field, value); + node.send(msg); + } else if ((node.fieldType === 'flow') || + (node.fieldType === 'global')) { + var context = RED.util.parseContextStore(node.field); + var target = node.context()[node.fieldType]; + target.set(context.key, value, context.store, function (err) { + if (err) { + node.error(err, msg); + } else { + node.send(msg); + } + }); + } + } try { - var value; /*** * Allow template contents to be defined externally * through inbound msg.template IFF node.template empty @@ -97,31 +158,18 @@ module.exports = function(RED) { } if (node.syntax === "mustache") { - if (node.outputFormat === "json") { - value = mustache.render(template,new NodeContext(msg, node.context(), null, true)); - } else { - value = mustache.render(template,new NodeContext(msg, node.context(), null, false)); - } + var is_json = (node.outputFormat === "json"); + var promises = []; + mustache.render(template, new NodeContext(msg, node.context(), null, is_json, promises, null)); + Promise.all(promises).then(function (values) { + var value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, null, values)); + output(value); + }).catch(function (err) { + node.error(err.message); + }); } else { - value = template; + output(template); } - /* istanbul ignore else */ - if (node.outputFormat === "json") { - value = JSON.parse(value); - } - /* istanbul ignore else */ - if (node.outputFormat === "yaml") { - value = yaml.load(value); - } - - if (node.fieldType === 'msg') { - RED.util.setMessageProperty(msg,node.field,value); - } else if (node.fieldType === 'flow') { - node.context().flow.set(node.field,value); - } else if (node.fieldType === 'global') { - node.context().global.set(node.field,value); - } - node.send(msg); } catch(err) { node.error(err.message); diff --git a/test/nodes/core/core/80-template_spec.js b/test/nodes/core/core/80-template_spec.js index 9abab1f90..1284b7dba 100644 --- a/test/nodes/core/core/80-template_spec.js +++ b/test/nodes/core/core/80-template_spec.js @@ -16,6 +16,7 @@ var should = require("should"); var templateNode = require("../../../../nodes/core/core/80-template.js"); +var Context = require("../../../../red/runtime/nodes/context"); var helper = require("node-red-node-test-helper"); describe('template node', function() { @@ -28,8 +29,32 @@ describe('template node', function() { helper.stopServer(done); }); + beforeEach(function(done) { + done(); + }); + + function initContext(done) { + Context.init({ + contextStorage: { + memory1: { + module: "memory" + }, + memory2: { + module: "memory" + } + } + }); + Context.load().then(function () { + done(); + }); + } + afterEach(function() { - helper.unload(); + helper.unload().then(function () { + return Context.clean({allNodes:{}}); + }).then(function () { + return Context.close(); + }); }); @@ -116,7 +141,6 @@ describe('template node', function() { }); }); - it('should modify payload from flow context', function(done) { var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{flow.value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; helper.load(templateNode, flow, function() { @@ -132,6 +156,44 @@ describe('template node', function() { }); }); + it('should modify payload from persistable flow context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{flow[memory1].value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo'); + done(); + }); + n1.context().flow.set("value","foo","memory1",function (err) { + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + }); + + it('should modify payload from two persistable flow context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{flow[memory1].value}}/{{flow[memory2].value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo/bar'); + done(); + }); + n1.context().flow.set("value","foo","memory1",function (err) { + n1.context().flow.set("value","bar","memory2",function (err) { + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + }); + }); + it('should modify payload from global context', function(done) { var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{global.value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; helper.load(templateNode, flow, function() { @@ -147,6 +209,64 @@ describe('template node', function() { }); }); + it('should modify payload from persistable global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{global[memory1].value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo'); + done(); + }); + n1.context().global.set("value","foo","memory1", function (err) { + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + }); + + it('should modify payload from two persistable global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{global[memory1].value}}/{{global[memory2].value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo/bar'); + done(); + }); + n1.context().global.set("value","foo","memory1", function (err) { + n1.context().global.set("value","bar","memory2", function (err) { + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + }); + }); + + it('should modify payload from persistable flow & global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{flow[memory1].value}}/{{global[memory1].value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo/bar'); + done(); + }); + n1.context().flow.set("value","foo","memory1", function (err) { + n1.context().global.set("value","bar","memory1", function (err) { + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + }); + }); + it('should handle missing node context', function(done) { // this is artificial test because in flow there is missing z property (probably never happen in real usage) var flow = [{id:"n1",type:"template", field:"payload", template:"payload={{flow.value}},{{global.value}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; @@ -206,6 +326,27 @@ describe('template node', function() { }); }); + it('should modify persistable flow context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"#:(memory1)::payload", fieldType:"flow", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + // mesage is intact + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + // result is in flow context + n2.context().flow.get("payload", "memory", function (err, val) { + val.should.equal("payload=foo"); + done(); + }); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + it('should modify global context', function(done) { var flow = [{id:"n1",z:"t1", type:"template", field:"payload", fieldType:"global", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; helper.load(templateNode, flow, function() { @@ -223,6 +364,27 @@ describe('template node', function() { }); }); + it('should modify persistable global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"#:(memory1)::payload", fieldType:"global", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + initContext(function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + // mesage is intact + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + // result is in global context + n2.context().global.get("payload", "memory", function (err, val) { + val.should.equal("payload=foo"); + done(); + }); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + }); + it('should handle if the field isn\'t set', function(done) { var flow = [{id:"n1", type:"template", template: "payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; helper.load(templateNode, flow, function() { @@ -264,6 +426,7 @@ describe('template node', function() { n1.receive({payload:{A:"abc"}}); }); }); + it('should raise error if passed bad template', function(done) { var flow = [{id:"n1", type:"template", field: "payload", template: "payload={{payload",wires:[["n2"]]},{id:"n2",type:"helper"}]; helper.load(templateNode, flow, function() {