├── .gitignore ├── jsdoc.json ├── lib ├── index.js ├── Symbols.js ├── NRConfigNode.js ├── NRContainer.js ├── NRWire.js ├── NRSubflowInstance.js ├── NRSubflow.js ├── NRGroup.js ├── NRObject.js ├── NRFlow.js ├── NRFlowSet.js └── NRNode.js ├── jsdoc └── include-parent.js ├── package.json ├── .github └── workflows │ └── tests.yml ├── README.md ├── test ├── test.js ├── resources │ ├── test-NRNode.json │ ├── test-02.json │ └── test-01.json └── NRNode.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | .DS_Store 3 | node_modules 4 | .nyc_output 5 | coverage 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jsdoc/include-parent.js", 4 | "plugins/markdown" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const NRFlowSet = require("./NRFlowSet.js") 2 | 3 | /** 4 | * parseFlow - Parses a Node-RED Flow object 5 | * 6 | * @param {array} flowConfig An Flow JSON Array 7 | * @return {NRFlowSet} Description 8 | */ 9 | function parseFlow(flowConfig) { 10 | const flowSet = new NRFlowSet(flowConfig); 11 | return flowSet; 12 | } 13 | 14 | /** 15 | * Type symbols 16 | * @type ObjectTypes 17 | */ 18 | const Symbols = require("./Symbols"); 19 | 20 | module.exports = { 21 | parseFlow: parseFlow, 22 | types: Symbols 23 | } 24 | -------------------------------------------------------------------------------- /lib/Symbols.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ObjectTypes 3 | * @property {Symbol} Node - A flow node 4 | * @property {Symbol} ConfigNode - A Configuration node 5 | * @property {Symbol} Group - A Group 6 | * @property {Symbol} Flow - A Flow 7 | * @property {Symbol} Subflow - A Subflow 8 | * @global 9 | */ 10 | module.exports = { 11 | /** 12 | * A flow node 13 | * @memberof Types 14 | * */ 15 | Node: Symbol("NRNode"), 16 | /** A configuration node */ 17 | ConfigNode: Symbol("NRConfigNode"), 18 | /** A group */ 19 | Group: Symbol("NRGroup"), 20 | /** A flow */ 21 | Flow: Symbol("NRFlow"), 22 | /** A subflow */ 23 | Subflow: Symbol("NRSubflow") 24 | } 25 | -------------------------------------------------------------------------------- /lib/NRConfigNode.js: -------------------------------------------------------------------------------- 1 | const NRObject = require("./NRObject.js"); 2 | const Symbols = require("./Symbols"); 3 | 4 | /** 5 | * NRConfigNode - Description 6 | * @property {object} users - Nodes that reference this config node 7 | * @extends NRObject 8 | * @inheritdoc 9 | */ 10 | class NRConfigNode extends NRObject { 11 | 12 | /** 13 | * constructor - Description 14 | * 15 | * @param {type} config Description 16 | */ 17 | constructor(config) { 18 | super(config); 19 | this.TYPE = Symbols.ConfigNode; 20 | this.users = new Set(); 21 | } 22 | 23 | 24 | addUser(node) { 25 | this.users.add(node); 26 | } 27 | } 28 | 29 | module.exports = NRConfigNode; 30 | -------------------------------------------------------------------------------- /jsdoc/include-parent.js: -------------------------------------------------------------------------------- 1 | 2 | const doclets = {}; 3 | const propertyMap = {}; 4 | 5 | exports.handlers = { 6 | newDoclet: function(e) { 7 | if (e.doclet.kind === "class" && e.doclet.properties) { 8 | doclets[e.doclet.name] = e.doclet; 9 | e.doclet.properties = e.doclet.properties || []; 10 | } 11 | }, 12 | parseComplete: function(e) { 13 | e.doclets.forEach(doclet => { 14 | if (doclet.kind === 'class' && doclet.augments) { 15 | augmentWithParent(doclet,doclets[doclet.augments[0]]); 16 | } 17 | }) 18 | } 19 | 20 | }; 21 | 22 | function augmentWithParent(doclet,parentDoclet) { 23 | doclet.properties = Array.from(new Set([...parentDoclet.properties, ...doclet.properties])) 24 | 25 | if (!parentDoclet.augments || parentDoclet.augments.length === 0) { 26 | return; 27 | } else { 28 | augmentWithParent(doclet,doclets[parentDoclet.augments[0]]) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@node-red/flow-parser", 3 | "version": "1.0.2", 4 | "description": "Parser Utility for Node-RED Flow JSON", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/node-red/flow-parser.git" 8 | }, 9 | "main": "lib/index.js", 10 | "scripts": { 11 | "test": "nyc mocha", 12 | "coverage": "nyc report --reporter=lcov", 13 | "coverage-html": "nyc report --reporter=html", 14 | "docs": "npx jsdoc -c ./jsdoc.json -d docs -R README.md lib" 15 | }, 16 | "keywords": [ 17 | "node-red" 18 | ], 19 | "license": "Apache-2.0", 20 | "devDependencies": { 21 | "coveralls": "^3.1.0", 22 | "jsdoc": "^3.6.7", 23 | "mocha": "^8.4.0", 24 | "nyc": "^15.1.0", 25 | "should": "^13.2.3" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "Nick O'Leary" 30 | }, 31 | { 32 | "name": "Kunihiko Toumura" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/NRContainer.js: -------------------------------------------------------------------------------- 1 | const NRObject = require("./NRObject.js"); 2 | 3 | /** 4 | * NRContainer - Description 5 | * @property {Set} nodes - Flow nodes in this flow/subflow/group 6 | */ 7 | class NRContainer extends NRObject { 8 | /** 9 | * constructor - Description 10 | * 11 | * @param {type} config Description 12 | * 13 | */ 14 | constructor(config) { 15 | super(config); 16 | this.nodes = new Map(); 17 | } 18 | 19 | /** 20 | * addNode - Description 21 | * @param {NRNode} node Description 22 | */ 23 | addNode(node) { 24 | this.nodes.set(node.id, node); 25 | node.setParent(this); 26 | } 27 | 28 | exportContents() { 29 | let result = []; 30 | this.nodes.forEach((n,_) => { 31 | result.push(n.export()); 32 | }) 33 | return result; 34 | } 35 | 36 | walkContents(callback) { 37 | this.nodes.forEach(n => n.walk(callback)); 38 | } 39 | } 40 | 41 | module.exports = NRContainer; 42 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [12, 14, 16] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install Dependencies 25 | run: npm install 26 | - name: Run tests 27 | run: | 28 | npm run test 29 | npm run coverage 30 | - name: Publish to coveralls.io 31 | if: ${{ matrix.node-version == 14 }} 32 | uses: coverallsapp/github-action@v1.1.2 33 | with: 34 | github-token: ${{ github.token }} 35 | -------------------------------------------------------------------------------- /lib/NRWire.js: -------------------------------------------------------------------------------- 1 | const Symbols = require("./Symbols"); 2 | 3 | /** 4 | * NRWire - Description 5 | * @property {NRNode} sourceNode - source of the wire 6 | * @property {number} sourcePortIndex - index of the source port 7 | * @property {NRNode} destinationNode - destination of the wire 8 | * @property {number} destinationPortIndex - index of the destination port 9 | * @property {boolean} virtual - whether this is a virtual wire between link nodes 10 | */ 11 | class NRWire { 12 | /** 13 | * constructor - Description 14 | * 15 | * @param {type} config Description 16 | * 17 | */ 18 | constructor(sourceNode, sourcePortIndex, destinationNode, destinationPortIndex, virtual=false) { 19 | this.sourceNode = sourceNode; 20 | this.sourcePortIndex = sourcePortIndex; 21 | this.destinationNode = destinationNode; 22 | this.destinationPortIndex = destinationPortIndex; 23 | this.virtual = virtual; 24 | } 25 | 26 | toString() { 27 | return this.sourceNode.id+"["+this.sourcePortIndex+"] "+(this.virtual?"...":"---")+" "+this.destinationNode.id+"["+this.destinationPortIndex+"]" 28 | } 29 | } 30 | 31 | module.exports = NRWire; 32 | -------------------------------------------------------------------------------- /lib/NRSubflowInstance.js: -------------------------------------------------------------------------------- 1 | const NRNode = require("./NRNode.js"); 2 | 3 | /** 4 | * NRSubflowInstance - Description 5 | * @property {NRSubflow} subflow - The Subflow this is an instance of 6 | * @property {string} subflowId - The id of the Subflow this is an instance of 7 | * @extends NRNode 8 | */ 9 | class NRSubflowInstance extends NRNode { 10 | 11 | /** 12 | * constructor - Description 13 | * @param {type} config Description 14 | */ 15 | constructor(config) { 16 | if (!/^subflow:/.test(config.type)) { 17 | throw new Error("Not a subflow instance node "+config.id+" type:"+config.type); 18 | } 19 | super(config); 20 | 21 | this.subflowId = this.type.substring(8); 22 | // Set later by a call to setSubflow 23 | this.subflow = null; 24 | } 25 | 26 | 27 | /** 28 | * setSubflow - Description 29 | * @param {NRSubflow} subflow The Subflow this is an instance of 30 | */ 31 | setSubflow(subflow) { 32 | if (subflow.id !== this.subflowId) { 33 | throw new Error("Subflow id does not match expected type id type:"+this.type+" got "+subflow.id) 34 | } 35 | this.subflow = subflow; 36 | } 37 | 38 | } 39 | 40 | module.exports = NRSubflowInstance; 41 | -------------------------------------------------------------------------------- /lib/NRSubflow.js: -------------------------------------------------------------------------------- 1 | const NRFlow = require("./NRFlow.js"); 2 | const Symbols = require("./Symbols"); 3 | 4 | /** 5 | * NRSubflow - Description 6 | * @property {Map} instances - instances of the subflow 7 | * @property {String} category - if set, custom category this subflow is in 8 | * @property {String} color - if set, custom color for this subflow node 9 | * @property {String} icon - a custom icon for the subflow node, if set 10 | * @property {String[]} inputLabels - array of custom labels for the node inputs 11 | * @property {String[]} outputLabels - array of custom labels for the node outputs 12 | * @property {object} env - subflow properties definition 13 | * @property {object} meta - meta information about the subflow 14 | * 15 | * @extends NRFlow 16 | */ 17 | class NRSubflow extends NRFlow { 18 | 19 | /** 20 | * constructor - Description 21 | * 22 | * @param {type} config Description 23 | */ 24 | constructor(config) { 25 | super(config); 26 | this.TYPE = Symbols.Subflow; 27 | 28 | this.instances = new Map(); 29 | this._ownProperties = ["category", "in", "out", "env", "meta", "color", "inputLabels", "outputLabels", "icon"]; 30 | this._ownProperties.forEach(prop => { 31 | if (config.hasOwnProperty(prop)) { 32 | this[prop] = config[prop]; 33 | delete config[prop]; 34 | } 35 | }) 36 | 37 | delete config.category; 38 | } 39 | 40 | export() { 41 | let obj = super.export(); 42 | delete obj.disabled; 43 | this._ownProperties.forEach(prop => { 44 | if (this.hasOwnProperty(prop)) { 45 | obj[prop] = this[prop]; 46 | } 47 | }) 48 | return obj 49 | } 50 | 51 | /** 52 | * addInstance - Description 53 | * 54 | * @param {NRSubflowInstance} node Description 55 | */ 56 | addInstance(node) { 57 | node.setSubflow(this); 58 | this.instances.set(node.id,node); 59 | } 60 | } 61 | module.exports = NRSubflow; 62 | -------------------------------------------------------------------------------- /lib/NRGroup.js: -------------------------------------------------------------------------------- 1 | const NRContainer = require("./NRContainer.js"); 2 | const Symbols = require("./Symbols"); 3 | 4 | /** 5 | * NRGroup - Description 6 | * @property {String} groupId - if set, the id of the parent group this group is in (from `node.g`) 7 | * @property {NRGroup} group - the parent group 8 | * @property {String} info - any documentation for this group 9 | * @property {object} style - style properties for this group 10 | * @extends NRContainer 11 | */ 12 | class NRGroup extends NRContainer { 13 | 14 | /** 15 | * constructor - Description 16 | * 17 | * @param {type} config Description 18 | */ 19 | constructor(config) { 20 | super(config); 21 | this.TYPE = Symbols.Group; 22 | this.w = config.w; 23 | this.h = config.h; 24 | this.groupId = config.g; 25 | delete config.g; 26 | 27 | this.style = config.style; 28 | this.info = config.info; 29 | 30 | delete config.w; 31 | delete config.h; 32 | delete config.style; 33 | delete config.info; 34 | this._nodes = config.nodes; 35 | delete config.nodes; 36 | } 37 | 38 | setGroup(group) { 39 | this.groupId = group.id; 40 | this.group = group; 41 | } 42 | 43 | addNode(node) { 44 | // Override default NRContainer behaviour as we don't setParent of the node 45 | this.nodes.set(node.id, node); 46 | node.setGroup(this); 47 | } 48 | 49 | export() { 50 | let obj = super.export(); 51 | if (this.groupId) { 52 | obj.g = this.groupId; 53 | } 54 | obj.w = this.w; 55 | obj.h = this.h; 56 | if (this.style) { 57 | obj.style = this.style; 58 | } 59 | if (this.info !== undefined) { 60 | obj.info = this.info; 61 | } 62 | obj.nodes = Array.from(this.nodes.keys()) 63 | return obj; 64 | } 65 | 66 | exportContents() { 67 | // As groups exist orthogonally to flows, we don't 68 | // export this contents this way. 69 | return []; 70 | } 71 | } 72 | 73 | module.exports = NRGroup 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @node-red/flow-parser 2 | --- 3 | 4 | This module provides a set of utilities for working with Node-RED flows. 5 | 6 | 7 | ## Usage 8 | 9 | ``` 10 | const fs = require("fs"); 11 | const { parseFlow } = require("@node-red/flow-parser"); 12 | 13 | // Load the flow json from a local file and parse to an object 14 | const exampleFlow = JSON.parse(fs.readFileSync("flows.json", "utf-8")); 15 | 16 | // Parse the flow 17 | const flow = parseFlow(exampleFlow); 18 | 19 | // `flow` is now an object that can be used to explore the flow structure 20 | ``` 21 | 22 | 23 | ### Example - `walk` 24 | 25 | The `walk` function can be used to invoke a function on every object in the flow 26 | configuration in a reasonably well-defined order: 27 | 28 | - Subflow definitions 29 | - Config nodes scoped to this subflow 30 | - Groups 31 | - Nodes 32 | - Global Config nodes 33 | - Flows 34 | - Config nodes scoped to this flow 35 | - Groups 36 | - Nodes 37 | 38 | ``` 39 | const fs = require("fs"); 40 | const FlowParser = require("@node-red/flow-parser"); 41 | 42 | // Load the flow json from a local file and parse to an object 43 | const exampleFlow = JSON.parse(fs.readFileSync("flows.json", "utf-8")); 44 | 45 | const flow = FlowParser.parseFlow(exampleFlow); 46 | 47 | flow.walk(function(obj) { 48 | switch(obj.TYPE) { 49 | case FlowParser.types.Flow: 50 | // A flow object 51 | break; 52 | case FlowParser.types.Subflow: 53 | // A subflow definition 54 | break; 55 | case FlowParser.types.Group: 56 | // A group object 57 | break; 58 | case FlowParser.types.ConfigNode: 59 | // A config node 60 | break; 61 | case FlowParser.types.Node: 62 | // A flow node 63 | break; 64 | } 65 | }) 66 | 67 | ``` 68 | 69 | ### Example - `export` 70 | 71 | The `export` function gives back the JSON array for the flow. 72 | 73 | The following example will disable all Debug nodes in the flow: 74 | 75 | ``` 76 | const flow = parseFlow(exampleFlow); 77 | 78 | flow.walk(obj => { 79 | if (obj.type === 'debug') { 80 | obj.active = false; 81 | } 82 | }); 83 | 84 | const newFlow = flow.export(); 85 | ``` 86 | -------------------------------------------------------------------------------- /lib/NRObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NRObject - Description 3 | * @property {string} id - unique identifier for this object 4 | * @property {string} z - identifier of the flow/subflow this object is on 5 | * @property {string} type - the type of the object 6 | * @property {boolean} disabled - whether the object is disabled 7 | * @property {object} config - the item-specific configuration properties 8 | * @property {NRFlow|NRSubflow} parent - the parent object. This will be the NRFlow/NRSubflow that 'owns' this object. As identified by `node.z` 9 | */ 10 | class NRObject { 11 | /** 12 | * constructor - Description 13 | * @param {type} config Description 14 | * 15 | */ 16 | constructor(config) { 17 | this.id = config.id; 18 | this.z = config.z; 19 | this.type = config.type; 20 | this.disabled = !!config.d; 21 | delete config.id; 22 | delete config.z; 23 | delete config.type; 24 | delete config.d; 25 | this.config = config; 26 | // This is set by a later call to `setParent` providing the actual NRFlow/NRSubflow object 27 | this.parent = null; 28 | } 29 | 30 | /** 31 | * setParent - Sets the parent object of this object. Either a Flow or Subflow 32 | * @param {NRFlow|NRSubflow} parent Description 33 | */ 34 | setParent(parent) { 35 | this.z = parent.id; 36 | this.parent = parent; 37 | } 38 | 39 | 40 | /** 41 | * export - Exports this object as an Object that can be included in a flow 42 | * @return {object} the exported object 43 | */ 44 | export() { 45 | let obj = { 46 | id: this.id, 47 | type: this.type 48 | } 49 | if (this.z) { 50 | obj.z = this.z; 51 | } 52 | if (this.disabled) { 53 | obj.d = true; 54 | } 55 | for (let property in this.config) { 56 | if (this.config.hasOwnProperty(property)) { 57 | obj[property] = this.config[property]; 58 | } 59 | } 60 | return obj; 61 | } 62 | 63 | walk(callback) { 64 | if (this.id) { 65 | callback(this); 66 | } 67 | } 68 | 69 | walkContents(callback) { 70 | 71 | } 72 | } 73 | 74 | module.exports = NRObject; 75 | -------------------------------------------------------------------------------- /lib/NRFlow.js: -------------------------------------------------------------------------------- 1 | const NRContainer = require("./NRContainer.js"); 2 | const Symbols = require("./Symbols"); 3 | 4 | /** 5 | * NRFlow - Description 6 | * @property {Set} configs - Configuration nodes in this flow/subflow 7 | * @property {Set} subflows - Subflows in this flow/subflow 8 | * @property {Set} groups - Groups in this flow/subflow 9 | * @property {String} info - any documentation for this flow 10 | * @extends NRContainer 11 | */ 12 | class NRFlow extends NRContainer { 13 | 14 | /** 15 | * constructor - Description 16 | * 17 | * @param {type} config Description 18 | */ 19 | constructor(config) { 20 | super(config); 21 | this.TYPE = Symbols.Flow; 22 | this.disabled = !!config.disabled; 23 | this.info = config.info; 24 | 25 | delete config.disabled; 26 | delete config.info; 27 | 28 | this.configs = new Map(); 29 | this.subflows = new Map(); 30 | this.groups = new Map(); 31 | } 32 | 33 | export() { 34 | let obj = super.export(); 35 | obj.disabled = this.disabled; 36 | delete obj.d; 37 | obj.info = this.info; 38 | return obj 39 | } 40 | exportContents() { 41 | let result = []; 42 | this.configs.forEach((n,_) => { 43 | result.push(n.export()); 44 | }) 45 | this.subflows.forEach((n,_) => { 46 | result.push(n.export()) 47 | result = result.concat(n.exportContents()); 48 | }) 49 | this.groups.forEach((n,_) => { 50 | result.push(n.export()); 51 | }) 52 | // Nodes exported last 53 | result = result.concat(super.exportContents()); 54 | return result; 55 | } 56 | /** 57 | * addSubflow - Description 58 | * @param {NRSubflow} subflow Description 59 | */ 60 | addSubflow(subflow) { 61 | this.subflows.set(subflow.id, subflow); 62 | subflow.setParent(this); 63 | } 64 | 65 | /** 66 | * addConfigNode - Description 67 | * @param {NRConfigNode} configNode Description 68 | */ 69 | addConfigNode(configNode) { 70 | this.configs.set(configNode.id, configNode); 71 | configNode.setParent(this); 72 | } 73 | 74 | /** 75 | * addGroup - Description 76 | * @param {NRGroup} group Description 77 | */ 78 | addGroup(group) { 79 | this.groups.set(group.id, group); 80 | group.setParent(this); 81 | } 82 | 83 | walk(callback) { 84 | super.walk(callback); 85 | this.subflows.forEach(n => n.walk(callback)) 86 | this.configs.forEach(n => n.walk(callback)) 87 | this.groups.forEach(n => n.walk(callback)) 88 | super.walkContents(callback); 89 | } 90 | } 91 | module.exports = NRFlow; 92 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const assert = require("assert"); 3 | const FlowParser = require("../lib/index.js"); 4 | 5 | describe("Flow Parser tests", function() { 6 | it('passes flow 1', function() { 7 | runTests(__dirname+"/resources/test-01.json") 8 | }) 9 | it('passes flow 2', function() { 10 | runTests(__dirname+"/resources/test-02.json") 11 | }) 12 | 13 | function runTests(filename) { 14 | const original = JSON.parse(fs.readFileSync(filename,"utf-8")); 15 | const flowSet = FlowParser.parseFlow(original); 16 | const generated = flowSet.export(); 17 | 18 | 19 | 20 | // flowSet.walk(function(obj) { 21 | // switch(obj.TYPE) { 22 | // case FlowParser.types.Flow: 23 | // // A flow object 24 | // console.log("FLOW",obj.id) 25 | // break; 26 | // case FlowParser.types.Subflow: 27 | // // A subflow definition 28 | // console.log("SUBFLOW",obj.id) 29 | // break; 30 | // case FlowParser.types.Group: 31 | // // A group object 32 | // console.log(" Group",obj.id) 33 | // break; 34 | // case FlowParser.types.ConfigNode: 35 | // // A config node 36 | // console.log(" ConfigNode",obj.id) 37 | // break; 38 | // case FlowParser.types.Node: 39 | // // A flow node 40 | // console.log(" Node",obj.id) 41 | // break; 42 | // } 43 | // }) 44 | 45 | const knownIds = original.map(n => n.id).sort(); 46 | 47 | const walkedNodes = {}; 48 | flowSet.walk(n => { 49 | assert(!walkedNodes[n.id],"Walk visited same node id twice: "+n.id+" "+n.type) 50 | walkedNodes[n.id] = n; 51 | }); 52 | 53 | const generatedIds = Object.keys(walkedNodes) 54 | generatedIds.sort(); 55 | 56 | for (var i=0;i mangle(v)),"",2); 81 | const generatedSorted = JSON.stringify(generated.map(v => mangle(v)),"",2); 82 | 83 | assert(originalSorted === generatedSorted, "generated flow does not match source flow") 84 | // fs.writeFileSync("/tmp/flow-A.json",JSON.stringify(original,"",2)); 85 | // fs.writeFileSync("/tmp/flow-B.json",JSON.stringify(generated,"",2)); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /lib/NRFlowSet.js: -------------------------------------------------------------------------------- 1 | const NRConfigNode = require('./NRConfigNode.js'); 2 | const NRFlow = require('./NRFlow.js'); 3 | const NRGroup = require('./NRGroup.js'); 4 | const NRNode = require('./NRNode.js'); 5 | const NRSubflow = require('./NRSubflow.js'); 6 | const NRSubflowInstance = require('./NRSubflowInstance.js'); 7 | const NRWire = require('./NRWire.js'); 8 | 9 | 10 | /** 11 | * NRFlowSet - Description 12 | * @property {object} flows - all flows 13 | * @property {object} nodes - all flow nodes 14 | * @property {object} configNodes - all config nodes 15 | * @property {object} subflows - all subflow definitions 16 | * @property {object} groups - all groups 17 | * @property {object} wires - all wires 18 | */ 19 | class NRFlowSet { 20 | constructor(flowConfig) { 21 | 22 | flowConfig = JSON.parse(JSON.stringify(flowConfig)); 23 | 24 | this.flows = new Map(); 25 | this.nodes = new Map(); 26 | this.configNodes = new Map(); 27 | this.subflows = new Map(); 28 | this.groups = new Map(); 29 | this.wires = [ ]; 30 | 31 | this.globals = new NRFlow({}); 32 | 33 | // Keep track of some particular objects that will require special handling 34 | let linkNodes = []; 35 | let subflowInstances = []; 36 | 37 | // The goal is to parse the full flow configuration in as few steps 38 | // as possible. That means not doing multiple loops over the full array. 39 | 40 | // This first pass creates all of the main NR* objects 41 | flowConfig.forEach(config => { 42 | if (config.type === "tab") { 43 | this.flows.set(config.id, new NRFlow(config)); 44 | } else if (config.type === "subflow") { 45 | this.subflows.set(config.id, new NRSubflow(config)); 46 | } else if (config.type === "group") { 47 | this.groups.set(config.id, new NRGroup(config)); 48 | } else if (config.hasOwnProperty('x') && config.hasOwnProperty('y')) { 49 | if (/^subflow:/.test(config.type)) { 50 | this.nodes.set(config.id, new NRSubflowInstance(config)); 51 | subflowInstances.push(this.nodes.get(config.id)); 52 | } else { 53 | // config.{id,type} are removed in NRNode constructor 54 | const node = new NRNode(config) 55 | this.nodes.set(node.id, node); 56 | if (node.type === "link call" || node.type === "link in" || node.type === "link out") { 57 | linkNodes.push(node); 58 | } 59 | } 60 | } else { 61 | this.configNodes.set(config.id, new NRConfigNode(config)); 62 | } 63 | }); 64 | 65 | // Create wires 66 | this.nodes.forEach(node => { 67 | if (node.wires) { 68 | for (let portNumber = 0; portNumber < node.wires.length; portNumber++) { 69 | let portWires = node.wires[portNumber] || []; 70 | for (let wireNumber = 0; wireNumber < portWires.length; wireNumber++) { 71 | let destination = this.nodes.get(portWires[wireNumber]); 72 | if (destination) { 73 | let wire = new NRWire(node, portNumber, destination, 0); 74 | node.addOutboundWire(wire); 75 | destination.addInboundWire(wire); 76 | this.wires.push(wire); 77 | } 78 | } 79 | } 80 | } 81 | }) 82 | // Links Nodes 83 | let createdLinks = new Set(); 84 | linkNodes.forEach(linkNode => { 85 | const links = linkNode.config.links || []; 86 | links.forEach(remoteId => { 87 | const linkIdentifier = linkNode.type === "link in"?(remoteId+":"+linkNode.id):(linkNode.id+":"+remoteId); 88 | if (!createdLinks.has(linkIdentifier)) { 89 | createdLinks.add(linkIdentifier); 90 | let remoteNode = this.nodes.get(remoteId); 91 | if (remoteNode) { 92 | let sourceNode = linkNode.type === "link in"?remoteNode:linkNode; 93 | let destinationNode = linkNode.type === "link in"?linkNode:remoteNode; 94 | let wire = new NRWire(sourceNode, 0, destinationNode, 0, true); 95 | sourceNode.addOutboundWire(wire); 96 | destinationNode.addInboundWire(wire); 97 | this.wires.push(wire); 98 | } 99 | } 100 | }) 101 | }) 102 | 103 | // Set the parent objects and subflow instances 104 | const addToParent = (collection) => { 105 | collection.forEach((object,_) => { 106 | let parent = this.flows.get(object.z) || this.subflows.get(object.z) || this.globals; 107 | if (parent) { 108 | if (object instanceof NRConfigNode) { 109 | parent.addConfigNode(object); 110 | } else if (object instanceof NRGroup) { 111 | parent.addGroup(object); 112 | } else if (object instanceof NRSubflow) { 113 | parent.addSubflow(object); 114 | } else if (object instanceof NRSubflowInstance) { 115 | parent.addNode(object); 116 | const subflowTemplate = this.subflows.get(object.subflowId); 117 | if (subflowTemplate) { 118 | subflowTemplate.addInstance(object); 119 | } else { 120 | throw new Error("Cannot find subflow defintion "+object.subflowId+" used by subflow instance "+object.id); 121 | } 122 | } else { 123 | parent.addNode(object); 124 | } 125 | } else { 126 | throw new Error("Cannot find parent "+object.z+" for object "+object.id); 127 | } 128 | if (object.groupId) { 129 | let group = this.groups.get(object.groupId); 130 | if (group) { 131 | group.addNode(object) 132 | } 133 | } 134 | findConfigNodeReferences(object); 135 | }); 136 | } 137 | const findConfigNodeReferences = (node) => { 138 | let self = this; 139 | JSON.stringify(node.config, (_, value) => { 140 | if (typeof value === 'string') { 141 | if (value !== node.id && self.configNodes.has(value)) { 142 | let configNode = self.configNodes.get(value); 143 | configNode.addUser(node); 144 | } 145 | } 146 | return value; 147 | }) 148 | }; 149 | addToParent(this.nodes); 150 | addToParent(this.configNodes); 151 | addToParent(this.groups); 152 | addToParent(this.subflows); 153 | } 154 | 155 | 156 | /** 157 | * Export the flow as a Node Array that can be saves as JSON and loaded by Node-RED 158 | * 159 | * @return {Array} an array of node-red objects 160 | */ 161 | export() { 162 | let result = this.globals.exportContents(); 163 | this.flows.forEach(n => { 164 | result.push(n.export()); 165 | result = result.concat(n.exportContents()); 166 | }) 167 | 168 | return result; 169 | } 170 | 171 | 172 | /** 173 | * Call the provided callback function for every object in the flow. 174 | * The objects are recursively visited in the following order: 175 | * 176 | * - Subflow definitions 177 | * - Config nodes scoped to this subflow 178 | * - Groups 179 | * - Nodes 180 | * - Global Config nodes 181 | * - Flows 182 | * - Config nodes scoped to this flow 183 | * - Groups 184 | * - Nodes 185 | * 186 | * @param {Function} callback - A function to call for every object in the flow. 187 | * The function will receive the object as its 188 | * first argument 189 | */ 190 | walk(callback) { 191 | this.globals.walk(callback); 192 | this.flows.forEach(n => n.walk(callback)); 193 | } 194 | 195 | 196 | } 197 | 198 | 199 | module.exports = NRFlowSet 200 | -------------------------------------------------------------------------------- /test/resources/test-NRNode.json: -------------------------------------------------------------------------------- 1 | [{"id":"db74897287902cd2","type":"tab","label":"NRNode Tests","disabled":false,"info":""},{"id":"8a20407846f7e402","type":"group","z":"db74897287902cd2","name":"getPrevious/Next/Connected","style":{"label":true},"nodes":["724fe9d07c37fafd","158780993e2c1fc9","d57bcb659131fad9","77514aec80b72630","b9c5121509ed75df","13b9b793852eb613","87f685cd7f783227","f3bd472220ee4621","2d4827e276d97b08","7a84b1da5062315f","b1e7226ad19816e8","875acd7a4b38fe50","4cab5f27dd38d817"],"x":14,"y":79,"w":592,"h":322},{"id":"724fe9d07c37fafd","type":"inject","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow1-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":130,"y":120,"wires":[["158780993e2c1fc9"]]},{"id":"158780993e2c1fc9","type":"function","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow1-node2","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":120,"wires":[["d57bcb659131fad9"]]},{"id":"d57bcb659131fad9","type":"debug","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow1-node3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":490,"y":120,"wires":[]},{"id":"77514aec80b72630","type":"inject","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":130,"y":200,"wires":[["b9c5121509ed75df"]]},{"id":"b9c5121509ed75df","type":"function","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node2","func":"\nreturn msg;","outputs":3,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":220,"wires":[["13b9b793852eb613","f3bd472220ee4621"],[],["87f685cd7f783227"]]},{"id":"13b9b793852eb613","type":"debug","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":490,"y":200,"wires":[]},{"id":"87f685cd7f783227","type":"debug","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":490,"y":280,"wires":[]},{"id":"f3bd472220ee4621","type":"debug","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":490,"y":240,"wires":[]},{"id":"2d4827e276d97b08","type":"link out","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow3-link1","links":["7a84b1da5062315f"],"x":255,"y":360,"wires":[]},{"id":"7a84b1da5062315f","type":"link in","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow3-link2","links":["2d4827e276d97b08"],"x":375,"y":360,"wires":[["875acd7a4b38fe50"]]},{"id":"b1e7226ad19816e8","type":"inject","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow3-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":130,"y":360,"wires":[["2d4827e276d97b08"]]},{"id":"875acd7a4b38fe50","type":"debug","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow3-node2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":490,"y":360,"wires":[]},{"id":"4cab5f27dd38d817","type":"inject","z":"db74897287902cd2","g":"8a20407846f7e402","name":"flow2-node6","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":130,"y":260,"wires":[["b9c5121509ed75df"]]},{"id":"414665861c913aac","type":"inject","z":"db74897287902cd2","name":"flow4-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":110,"y":480,"wires":[["de34265bdcc04e92"]]},{"id":"de34265bdcc04e92","type":"function","z":"db74897287902cd2","name":"flow4-node2","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":480,"wires":[["a57cd58086bd92b4"]]},{"id":"0766558997b1b4b0","type":"debug","z":"db74897287902cd2","name":"flow4-node5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":750,"y":480,"wires":[]},{"id":"a57cd58086bd92b4","type":"function","z":"db74897287902cd2","name":"flow4-node3","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":480,"wires":[["826108a7caf06e24"]]},{"id":"826108a7caf06e24","type":"function","z":"db74897287902cd2","name":"flow4-node4","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":480,"wires":[["0766558997b1b4b0"]]},{"id":"50dac75758f57ff4","type":"inject","z":"db74897287902cd2","name":"flow5-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":110,"y":580,"wires":[["6b08ddb64fd0c6f9"]]},{"id":"6b08ddb64fd0c6f9","type":"function","z":"db74897287902cd2","name":"flow5-node2","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":580,"wires":[["ac8fa5125b7342d8"]]},{"id":"c46286ea55c75f49","type":"debug","z":"db74897287902cd2","name":"flow5-node5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":750,"y":580,"wires":[]},{"id":"ac8fa5125b7342d8","type":"function","z":"db74897287902cd2","name":"flow5-node3","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":600,"wires":[["4cac76c52432ec19","eb9dfa96ebef0ad7"]]},{"id":"4cac76c52432ec19","type":"function","z":"db74897287902cd2","name":"flow5-node4","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":580,"wires":[["c46286ea55c75f49"]]},{"id":"898862d5b95f91ca","type":"inject","z":"db74897287902cd2","name":"flow5-node6","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":110,"y":620,"wires":[["8dcc460855fa22e9"]]},{"id":"8dcc460855fa22e9","type":"function","z":"db74897287902cd2","name":"flow5-node7","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":620,"wires":[["ac8fa5125b7342d8"]]},{"id":"92d4023b4e1a10c2","type":"debug","z":"db74897287902cd2","name":"flow5-node9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":750,"y":620,"wires":[]},{"id":"eb9dfa96ebef0ad7","type":"function","z":"db74897287902cd2","name":"flow5-node8","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":620,"wires":[["92d4023b4e1a10c2"]]},{"id":"638a851824fe4e07","type":"inject","z":"db74897287902cd2","name":"flow5-node10","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":430,"y":660,"wires":[["eb9dfa96ebef0ad7"]]},{"id":"b7db5de72d9bfc9e","type":"inject","z":"db74897287902cd2","name":"flow6-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":110,"y":720,"wires":[["7a0c45184d36fe41"]]},{"id":"7a0c45184d36fe41","type":"function","z":"db74897287902cd2","name":"flow6-node2","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":720,"wires":[["4dea4a9eceeba22e","f5fb30a76ab6b99b"]]},{"id":"4dea4a9eceeba22e","type":"debug","z":"db74897287902cd2","name":"flow6-node3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":720,"wires":[]},{"id":"5ea0b22205d5a51d","type":"link in","z":"db74897287902cd2","name":"flow6-link2","links":["f5fb30a76ab6b99b"],"x":475,"y":780,"wires":[["cee852d2a11632d6"]]},{"id":"f5fb30a76ab6b99b","type":"link out","z":"db74897287902cd2","name":"flow6-link1","links":["5ea0b22205d5a51d"],"x":375,"y":780,"wires":[]},{"id":"cee852d2a11632d6","type":"debug","z":"db74897287902cd2","name":"flow6-node4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":780,"wires":[]},{"id":"559a8e333de82dfd","type":"inject","z":"db74897287902cd2","name":"flow7-node1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":110,"y":860,"wires":[["7a81dc480e6083b1"]]},{"id":"7a81dc480e6083b1","type":"function","z":"db74897287902cd2","name":"flow7-node2","func":"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":290,"y":860,"wires":[["6c74ac50b582560d"]]},{"id":"152839336280b91e","type":"debug","z":"db74897287902cd2","name":"flow7-node4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":880,"wires":[]},{"id":"6c74ac50b582560d","type":"function","z":"db74897287902cd2","name":"flow7-node3","func":"\nreturn msg;","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":900,"wires":[["152839336280b91e"],["7a81dc480e6083b1"]]}] 2 | -------------------------------------------------------------------------------- /lib/NRNode.js: -------------------------------------------------------------------------------- 1 | const NRObject = require("./NRObject.js"); 2 | const Symbols = require("./Symbols"); 3 | 4 | /** 5 | * NRNode - Description 6 | * @property {number} x - X location 7 | * @property {number} y - Y location 8 | * @property {String} groupId - if set, the id of the group this node is in (from `node.g`) 9 | * @property {NRGroup} group - group this node is part of, if set 10 | * @property {number} w - width (if known) 11 | * @property {number} h - height (if known) 12 | * @property {boolean} showLabel - whether the node displays its label (from `node.l`) 13 | * @property {String[][]} wires - the original `node.wires` property. 14 | * @property {number} outputCount - the number of outputs the node has (from `node.wires.length`) 15 | * @property {String} info - any documentation for this node 16 | * @property {String} icon - a custom icon for the node, if set 17 | * @property {String[]} inputLabels - array of custom labels for the node inputs 18 | * @property {String[]} outputLabels - array of custom labels for the node outputs 19 | * @property {NRWire[]} inboundWires - Array of wires connected to an input of this node 20 | * @property {NRWire[]} outboundWires - Array of wires connected to an output of this node 21 | * @extends NRObject 22 | */ 23 | class NRNode extends NRObject { 24 | 25 | /** 26 | * constructor - Description 27 | * 28 | * @param {type} config Description 29 | */ 30 | constructor(config) { 31 | super(config); 32 | this.TYPE = Symbols.Node; 33 | this.x = config.x; 34 | this.y = config.y; 35 | this.groupId = config.g; 36 | delete config.g; 37 | 38 | if (config.hasOwnProperty('w')) { 39 | this.w = config.w; 40 | } 41 | if (config.hasOwnProperty('h')) { 42 | this.h = config.h; 43 | } 44 | if (config.hasOwnProperty('l')) { 45 | this.showLabel = config.l; 46 | } else { 47 | this.showLabel = (this.type !== "link in" && this.type !== "link out") 48 | } 49 | this.inputLabels = config.inputLabels || []; 50 | this.outputLabels = config.outputLabels || []; 51 | if (config.hasOwnProperty('icon')) { 52 | this.icon = config.icon; 53 | } 54 | 55 | this.wires = config.wires || []; 56 | this.outputCount = this.wires.length; 57 | this.info = config.info; 58 | 59 | delete config.x; 60 | delete config.y; 61 | delete config.w; 62 | delete config.h; 63 | delete config.wires; 64 | delete config.l; 65 | delete config.inputLabels; 66 | delete config.outputLabels; 67 | delete config.icon; 68 | delete config.info; 69 | 70 | this.inboundWires = []; 71 | this.outboundWires = []; 72 | } 73 | 74 | /** 75 | * setGroup - Set the group this node is in 76 | * 77 | * @param {NRGroup} group the group this node is a member of 78 | * 79 | * @return {type} Description 80 | */ 81 | setGroup(group) { 82 | if (group) { 83 | this.groupId = group.id; 84 | this.group = group; 85 | } else { 86 | delete this.groupId; 87 | delete this.group; 88 | } 89 | } 90 | 91 | export() { 92 | let obj = super.export(); 93 | obj.x = this.x; 94 | obj.y = this.y; 95 | if (this.groupId) { 96 | obj.g = this.groupId; 97 | } 98 | if (this.type === "link in" || this.type === "link out") { 99 | if (this.showLabel) { 100 | obj.l = true; 101 | } 102 | } else if (!this.showLabel) { 103 | obj.l = false; 104 | } 105 | obj.wires = new Array(this.outputCount).fill(true).map(_ => []) 106 | this.outboundWires.forEach(wire => { 107 | if (!wire.virtual) { 108 | obj.wires[wire.sourcePortIndex] = obj.wires[wire.sourcePortIndex] || []; 109 | obj.wires[wire.sourcePortIndex].push(wire.destinationNode.id); 110 | } 111 | }) 112 | if (this.inputLabels.length > 0) { 113 | obj.inputLabels = this.inputLabels; 114 | } 115 | if (this.outputLabels.length > 0) { 116 | obj.outputLabels = this.outputLabels; 117 | } 118 | if (this.icon) { 119 | obj.icon = this.icon; 120 | } 121 | if (this.info) { 122 | obj.info = this.info; 123 | } 124 | return obj; 125 | } 126 | 127 | /** 128 | * getSiblingNodes - Get the nodes wired directly to this node 129 | * @param {boolean} followVirtual whether to follow Link node virtual wires 130 | * @return {string[]} An array of node ids 131 | */ 132 | getSiblingNodes(followVirtual) { 133 | return this.getPreviousNodes(followVirtual).concat(this.getNextNodes(followVirtual)); 134 | } 135 | 136 | /** 137 | * getPreviousNodes - Get the nodes wired to this node's inputs 138 | * @param {type} followVirtual whether to follow Link node virtual wires 139 | * @return {string[]} An array of node ids 140 | */ 141 | getPreviousNodes(followVirtual) { 142 | let result = []; 143 | this.inboundWires.forEach(wire => { 144 | if (!wire.virtual || followVirtual) { 145 | result.push(wire.sourceNode); 146 | } 147 | }); 148 | return result; 149 | } 150 | 151 | /** 152 | * getNextNodes - Get the nodes wired to this node's outputs 153 | * @param {type} followVirtual whether to follow Link node virtual wires 154 | * @return {string[]} An array of node ids 155 | */ 156 | getNextNodes(followVirtual) { 157 | let result = []; 158 | this.outboundWires.forEach(wire => { 159 | if (!wire.virtual || followVirtual) { 160 | result.push(wire.destinationNode); 161 | } 162 | }); 163 | return result; 164 | } 165 | 166 | /** 167 | * getDownstreamNodes - Get all nodes reachable from this nodes's outputs 168 | * @param {type} followVirtual whether to follow Link node virtual wires 169 | * @return {string[]} An array of node ids 170 | */ 171 | getDownstreamNodes(followVirtual) { 172 | const visited = { }; 173 | visited[this.id] = true; 174 | const result = [ ]; 175 | const stack = this.getNextNodes(followVirtual); 176 | while(stack.length > 0) { 177 | let node = stack.pop(); 178 | if (!visited[node.id]) { 179 | result.push(node); 180 | visited[node.id] = true; 181 | let next = node.getNextNodes(followVirtual); 182 | next.forEach(n => { 183 | if (!visited[n.id]) { 184 | stack.push(n); 185 | } 186 | }) 187 | } 188 | } 189 | return result; 190 | } 191 | 192 | /** 193 | * getUpstreamNodes - Get all nodes reachable from this nodes's inputs 194 | * @param {type} followVirtual whether to follow Link node virtual wires 195 | * @return {string[]} An array of node ids 196 | */ 197 | getUpstreamNodes(followVirtual) { 198 | const visited = { }; 199 | visited[this.id] = true; 200 | const result = [ ]; 201 | const stack = this.getPreviousNodes(followVirtual); 202 | while(stack.length > 0) { 203 | let node = stack.pop(); 204 | if (!visited[node.id]) { 205 | result.push(node); 206 | visited[node.id] = true; 207 | let previous = node.getPreviousNodes(followVirtual); 208 | previous.forEach(n => { 209 | if (!visited[n.id]) { 210 | stack.push(n); 211 | } 212 | }) 213 | } 214 | } 215 | return result; 216 | } 217 | 218 | /** 219 | * getConnectedNodes - Get all nodes, not including this one, reachable from its inputs and outputs 220 | * @param {type} followVirtual whether to follow Link node virtual wires 221 | * @return {string[]} An array of node ids 222 | */ 223 | getConnectedNodes(followVirtual) { 224 | const visited = { } 225 | visited[this.id] = true; 226 | const result = [ ]; 227 | const stack = this.getSiblingNodes(followVirtual); 228 | while(stack.length > 0) { 229 | let node = stack.pop(); 230 | if (!visited[node.id]) { 231 | result.push(node); 232 | visited[node.id] = true; 233 | let siblings = node.getSiblingNodes(followVirtual); 234 | siblings.forEach(sibling => { 235 | if (!visited[sibling.id]) { 236 | stack.push(sibling); 237 | } 238 | }) 239 | } 240 | } 241 | return result; 242 | } 243 | 244 | 245 | /** 246 | * addOutboundWire - Add an outbound wire to this node 247 | * @param {NRWire} wire the outbound wire 248 | */ 249 | addOutboundWire(wire) { 250 | this.outboundWires.push(wire); 251 | } 252 | 253 | /** 254 | * addInboundWire - Add an inbound wire to this node 255 | * @param {NRWire} wire the inbound wire 256 | */ 257 | addInboundWire(wire) { 258 | this.inboundWires.push(wire); 259 | } 260 | } 261 | 262 | module.exports = NRNode; 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /test/NRNode.js: -------------------------------------------------------------------------------- 1 | const should = require("should"); 2 | const fs = require("fs").promises; 3 | const assert = require("assert"); 4 | const FlowParser = require("../lib/index.js"); 5 | 6 | const FILENAME = __dirname+"/resources/test-NRNode.json"; 7 | 8 | describe("NRNode", function() { 9 | let flowSet; 10 | 11 | const nodeMap = {}; 12 | 13 | before(async function() { 14 | const original = JSON.parse(await fs.readFile(FILENAME,"utf-8")); 15 | flowSet = FlowParser.parseFlow(original); 16 | 17 | flowSet.walk(function(obj) { 18 | if (obj.config.name) { 19 | nodeMap[obj.config.name] = obj; 20 | } 21 | }) 22 | }); 23 | 24 | function convertNodeArrayToMap(nodes) { 25 | const result = {}; 26 | nodes.forEach(n => result[n.config.name] = true) 27 | return result; 28 | } 29 | function validateNodeList(nodeList, expectedNodes) { 30 | const nodeCount = Object.keys(nodeList).length; 31 | nodeCount.should.eql(expectedNodes.length); 32 | for (let i=0;i