From 7409cb3abb59f49821359cf9373861b9302a9db8 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 18 Apr 2018 17:09:31 +0100 Subject: [PATCH] Separate library api and runtime components --- red/api/editor/index.js | 8 +- red/api/editor/library.js | 178 ++++++++------------------- red/runtime-api/flows.js | 2 +- red/runtime-api/index.js | 1 + red/runtime-api/library.js | 102 +++++++++++++-- red/runtime-api/nodes.js | 21 +--- red/runtime/index.js | 3 + red/runtime/library/index.js | 100 +++++++++++++++ red/runtime/nodes/registry/loader.js | 8 +- 9 files changed, 263 insertions(+), 160 deletions(-) create mode 100644 red/runtime/library/index.js diff --git a/red/api/editor/index.js b/red/api/editor/index.js index 6412c2411..ee992bdf2 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -86,10 +86,12 @@ module.exports = { // Library var library = require("./library"); - library.init(editorApp,runtime); - editorApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,apiUtil.errorHandler); + library.init(editorApp,runtimeAPI); + editorApp.get("/library/flows",needsPermission("library.read"),library.getAll,apiUtil.errorHandler); - editorApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,apiUtil.errorHandler); + editorApp.get(/library\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry); + editorApp.post(/library\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry); + // Credentials var credentials = require("./credentials"); diff --git a/red/api/editor/library.js b/red/api/editor/library.js index d5cbb020f..be1b8ee5b 100644 --- a/red/api/editor/library.js +++ b/red/api/editor/library.js @@ -13,149 +13,71 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +var apiUtils = require("../util"); var fs = require('fs'); var fspath = require('path'); var when = require('when'); -var redApp = null; -var storage; -var log; -var redNodes; -var needsPermission = require("../auth").needsPermission; - -function createLibrary(type) { - if (redApp) { - redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),needsPermission("library.read"),function(req,res) { - var path = req.params[1]||""; - storage.getLibraryEntry(type,path).then(function(result) { - log.audit({event: "library.get",type:type},req); - if (typeof result === "string") { - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.write(result); - res.end(); - } else { - res.json(result); - } - }).catch(function(err) { - if (err) { - log.warn(log._("api.library.error-load-entry",{path:path,message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.get",type:type,error:"forbidden"},req); - res.status(403).end(); - return; - } - } - log.audit({event: "library.get",type:type,error:"not_found"},req); - res.status(404).end(); - }); - }); - - redApp.post(new RegExp("/library/"+type+"\/(.*)"),needsPermission("library.write"),function(req,res) { - var path = req.params[0]; - var meta = req.body; - var text = meta.text; - delete meta.text; - - storage.saveLibraryEntry(type,path,meta,text).then(function() { - log.audit({event: "library.set",type:type},req); - res.status(204).end(); - }).catch(function(err) { - log.warn(log._("api.library.error-save-entry",{path:path,message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.set",type:type,error:"forbidden"},req); - res.status(403).end(); - return; - } - log.audit({event: "library.set",type:type,error:"unexpected_error",message:err.toString()},req); - res.status(500).json({error:"unexpected_error", message:err.toString()}); - }); - }); - } -} +var runtimeAPI; module.exports = { - init: function(app,runtime) { - redApp = app; - log = runtime.log; - storage = runtime.storage; - redNodes = runtime.nodes; + init: function(app,_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, - register: createLibrary, getAll: function(req,res) { - storage.getAllFlows().then(function(flows) { - log.audit({event: "library.get.all",type:"flow"},req); - var examples = redNodes.getNodeExampleFlows(); - if (examples) { - flows.d = flows.d||{}; - flows.d._examples_ = redNodes.getNodeExampleFlows(); - } - res.json(flows); + var opts = { + user: req.user, + type: 'flows' + } + runtimeAPI.library.getEntries(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }, - get: function(req,res) { - if (req.params[0].indexOf("_examples_/") === 0) { - var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(req.params[0]); - if (m) { - var module = m[1]; - var path = m[2]; - var fullPath = redNodes.getNodeExampleFlowPath(module,path); - if (fullPath) { - try { - fs.statSync(fullPath); - log.audit({event: "library.get",type:"flow",path:req.params[0]},req); - return res.sendFile(fullPath,{ - headers:{ - 'Content-Type': 'application/json' - } - }) - } catch(err) { - console.log(err); - } - } - } - // IF we get here, we didn't find the file - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); - return res.status(404).end(); - } else { - storage.getFlow(req.params[0]).then(function(data) { - // data is already a JSON string - log.audit({event: "library.get",type:"flow",path:req.params[0]},req); - res.set('Content-Type', 'application/json'); - res.send(data); - }).catch(function(err) { - if (err) { - log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req); - res.status(403).end(); - return; - } - } - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); - res.status(404).end(); - }); + getEntry: function(req,res) { + var opts = { + user: req.user, + type: req.params[0], + path: req.params[1]||"" } + runtimeAPI.library.getEntry(opts).then(function(result) { + if (typeof result === "string") { + if (opts.type === 'flows') { + res.writeHead(200, {'Content-Type': 'application/json'}); + } else { + res.writeHead(200, {'Content-Type': 'text/plain'}); + } + res.write(result); + res.end(); + } else { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); }, - post: function(req,res) { - // if (req.params[0].indexOf("_examples_/") === 0) { - // log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"})); - // log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); - // return res.status(403).send({error:"unexpected_error", message:"forbidden"}); - // } - var flow = JSON.stringify(req.body); - storage.saveFlow(req.params[0],flow).then(function() { - log.audit({event: "library.set",type:"flow",path:req.params[0]},req); + saveEntry: function(req,res) { + var opts = { + user: req.user, + type: req.params[0], + path: req.params[1]||"" + } + // TODO: horrible inconsistencies between flows and all other types + if (opts.type === "flows") { + opts.meta = {}; + opts.body = JSON.stringify(req.body); + } else { + opts.meta = req.body; + opts.body = opts.meta.text; + delete opts.meta.text; + } + runtimeAPI.library.saveEntry(opts).then(function(result) { res.status(204).end(); }).catch(function(err) { - log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); - res.status(403).end(); - return; - } - log.audit({event: "library.set",type:"flow",path:req.params[0],error:"unexpected_error",message:err.toString()},req); - res.status(500).send({error:"unexpected_error", message:err.toString()}); + apiUtils.rejectHandler(req,res,err); }); } } diff --git a/red/runtime-api/flows.js b/red/runtime-api/flows.js index f27ab4b34..bd858864f 100644 --- a/red/runtime-api/flows.js +++ b/red/runtime-api/flows.js @@ -223,7 +223,7 @@ var api = module.exports = { */ getNodeCredentials: function(opts) { return new Promise(function(resolve,reject) { - log.audit({event: "credentials.get",type:opts.type,id:opts.id}); + runtime.log.audit({event: "credentials.get",type:opts.type,id:opts.id}); var credentials = runtime.nodes.getCredentials(opts.id); if (!credentials) { return resolve({}); diff --git a/red/runtime-api/index.js b/red/runtime-api/index.js index 6a75d8583..75ee05602 100644 --- a/red/runtime-api/index.js +++ b/red/runtime-api/index.js @@ -30,6 +30,7 @@ var api = module.exports = { api.flows.init(runtime); api.nodes.init(runtime); api.settings.init(runtime); + api.library.init(runtime); }, /** diff --git a/red/runtime-api/library.js b/red/runtime-api/library.js index fbd265f88..fb0658554 100644 --- a/red/runtime-api/library.js +++ b/red/runtime-api/library.js @@ -15,21 +15,107 @@ **/ /** - * @module red/library + * @namespace RED.library */ -module.exports = { +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, /** - * Does something + * Gets an entry from the library. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @param {String} opts.path - the path of the entry + * @return {Promise} - resolves when complete + * @memberof RED.library */ - setEnty: function() {}, + getEntry: function(opts) { + return new Promise(function(resolve,reject) { + runtime.library.getEntry(opts.type,opts.path).then(function(result) { + runtime.log.audit({event: "library.get",type:opts.type,path:opts.path}); + return resolve(result); + }).catch(function(err) { + if (err) { + runtime.log.warn(runtime.log._("api.library.error-load-entry",{path:opts.path,message:err.toString()})); + if (err.code === 'forbidden') { + err.status = 403; + return reject(err); + } else if (err.code === "not_found") { + err.status = 404; + } else { + err.status = 400; + } + runtime.log.audit({event: "library.get",type:opts.type,path:opts.path,error:err.code}); + return reject(err); + } + runtime.log.audit({event: "library.get",type:type,error:"not_found"}); + var error = new Error(); + error.code = "not_found"; + error.status = 404; + return reject(error); + }); + }) + }, + /** - * Does something + * Saves an entry to the library + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @param {String} opts.path - the path of the entry + * @param {Object} opts.meta - any meta data associated with the entry + * @param {String} opts.body - the body of the entry + * @return {Promise} - resolves when complete + * @memberof RED.library */ - getEntry: function() {}, + saveEntry: function(opts) { + return new Promise(function(resolve,reject) { + runtime.library.saveEntry(opts.type,opts.path,opts.meta,opts.body).then(function() { + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path}); + return resolve(); + }).catch(function(err) { + runtime.log.warn(runtime.log._("api.library.error-save-entry",{path:opts.path,message:err.toString()})); + if (err.code === 'forbidden') { + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"forbidden"}); + err.status = 403; + return reject(err); + } + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"unexpected_error",message:err.toString()}); + var error = new Error(); + error.code = "not_found"; + error.status = 400; + return reject(error); + }); + }) + }, /** - * Does something + * Returns a complete listing of all entries of a given type in the library. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @return {Promise} - the entry listing + * @memberof RED.library */ - getEntries: function() {} + getEntries: function(opts) { + return new Promise(function(resolve,reject) { + if (opts.type !== 'flows') { + return reject(new Error("API only supports flows")); + + } + runtime.storage.getAllFlows().then(function(flows) { + runtime.log.audit({event: "library.get.all",type:"flow"}); + var examples = runtime.nodes.getNodeExampleFlows(); + if (examples) { + flows.d = flows.d||{}; + flows.d._examples_ = examples; + } + return resolve(flows); + }); + }) + } } diff --git a/red/runtime-api/nodes.js b/red/runtime-api/nodes.js index 5eb48b285..714372e65 100644 --- a/red/runtime-api/nodes.js +++ b/red/runtime-api/nodes.js @@ -14,27 +14,12 @@ * limitations under the License. **/ "use strict" - /** - * @namespace RED.nodes - */ +/** + * @namespace RED.nodes + */ var runtime; -function putNode(node, enabled) { - var promise; - if (!node.err && node.enabled === enabled) { - promise = Promise.resolve(node); - } else { - if (enabled) { - promise = runtime.nodes.enableNode(node.id); - } else { - promise = runtime.nodes.disableNode(node.id); - } - } - return promise; -} - - var api = module.exports = { init: function(_runtime) { runtime = _runtime; diff --git a/red/runtime/index.js b/red/runtime/index.js index 6577ee59c..e8fc0a44b 100644 --- a/red/runtime/index.js +++ b/red/runtime/index.js @@ -18,6 +18,7 @@ var when = require('when'); var redNodes = require("./nodes"); var storage = require("./storage"); +var library = require("./library"); var log = require("./log"); var i18n = require("./i18n"); var events = require("./events"); @@ -65,6 +66,7 @@ function init(userSettings,_adminApi) { adminApi = _adminApi; } redNodes.init(runtime); + library.init(runtime); } var version; @@ -245,6 +247,7 @@ var runtime = module.exports = { storage: storage, events: events, nodes: redNodes, + library: library, util: require("./util"), get adminApi() { return adminApi }, get nodeApp() { return nodeApp }, diff --git a/red/runtime/library/index.js b/red/runtime/library/index.js new file mode 100644 index 000000000..ce95d66ff --- /dev/null +++ b/red/runtime/library/index.js @@ -0,0 +1,100 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var fs = require('fs'); +var fspath = require('path'); + +var runtime; +var knownTypes = {}; + +var storage; + +function init(_runtime) { + runtime = _runtime; + storage = runtime.storage; + knownTypes = {}; +} + +function registerType(id,type) { + if (knownTypes.hasOwnProperty(type)) { + throw new Error(`Library type '${type}' already registerd by ${id}'`) + } + knownTypes[type] = id; +} + +function getAllEntries(type) { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } +} +function getEntry(type,path) { + if (type !== 'flows') { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + return storage.getLibraryEntry(type,path); + } else { + return new Promise(function(resolve,reject) { + if (path.indexOf("_examples_/") === 0) { + var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(path); + if (m) { + var module = m[1]; + var entryPath = m[2]; + var fullPath = runtime.nodes.getNodeExampleFlowPath(module,entryPath); + if (fullPath) { + try { + fs.readFile(fullPath,'utf8',function(err, data) { + runtime.log.audit({event: "library.get",type:"flow",path:path}); + if (err) { + return reject(err); + } + return resolve(data); + }) + } catch(err) { + return reject(err); + } + } + } else { + // IF we get here, we didn't find the file + var error = new Error("not_found"); + error.code = "not_found"; + return reject(error); + } + } else { + resolve(storage.getFlow(path)); + } + }); + } +} +function saveEntry(type,path,meta,body) { + if (type !== 'flows') { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + return storage.saveLibraryEntry(type,path,meta,body); + } else { + return storage.saveFlow(path,body); + } +} + +module.exports = { + init: init, + registerType: registerType, + getAllEntries: getAllEntries, + getEntry: getEntry, + saveEntry: saveEntry + +} diff --git a/red/runtime/nodes/registry/loader.js b/red/runtime/nodes/registry/loader.js index f88463609..7f677f3da 100644 --- a/red/runtime/nodes/registry/loader.js +++ b/red/runtime/nodes/registry/loader.js @@ -80,7 +80,11 @@ function createNodeApi(node) { copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]); if (runtime.adminApi) { red.comms = runtime.adminApi.comms; - red.library = runtime.adminApi.library; + red.library = { + register: function(type) { + return runtime.library.registerType(node.id,type); + } + }; red.auth = runtime.adminApi.auth; red.httpAdmin = runtime.adminApi.adminApp; red.httpNode = runtime.nodeApp; @@ -377,7 +381,7 @@ function addModule(module) { function loadNodeHelp(node,lang) { var base = path.basename(node.template); - var localePath = undefined; + var localePath; if (node.module === 'node-red') { var cat_dir = path.dirname(node.template); var cat = path.basename(cat_dir);