├── .gitignore ├── .npmignore ├── .cfignore ├── project.json ├── icons └── cloudant.png ├── .editorconfig ├── package.json ├── README.md ├── 77-cloudant-cf.html └── 77-cloudant-cf.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .cfignore 3 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | launchConfigurations/ 2 | .git/ 3 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | {"Name":"laoqui2 | node-red-node-cf-cloudant"} -------------------------------------------------------------------------------- /icons/cloudant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/node-red-node-cf-cloudant/master/icons/cloudant.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-node-cf-cloudant", 3 | "version": "0.2.17", 4 | "description": "A Node-RED node to access Cloudant and couchdb databases", 5 | "dependencies": { 6 | "cfenv": "1.0.0", 7 | "cloudant": "1.4.*" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/lgfa29/node-red-node-cf-cloudant.git" 12 | }, 13 | "license": "Apache-2.0", 14 | "keywords": [ 15 | "node-red", 16 | "cloudant", 17 | "couchdb", 18 | "bluemix" 19 | ], 20 | "node-red": { 21 | "nodes": { 22 | "cloudant-cf": "77-cloudant-cf.js" 23 | } 24 | }, 25 | "author": { 26 | "name": "Luiz Gustavo Ferraz Aoqui", 27 | "email": "laoqui@ca.ibm.com" 28 | }, 29 | "contributors": [ 30 | { 31 | "name": "Túlio Pascoal" 32 | }, 33 | { 34 | "name": "Igor Leão" 35 | } 36 | ], 37 | "maintainers": [ 38 | { 39 | "name": "luiz_aoqui", 40 | "email": "laoqui@ca.ibm.com" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-red-node-cf-cloudant 2 | ========================= 3 | A pair of [Node-RED](http://nodered.org) nodes to work with documents 4 | in a [Cloudant](http://cloudant.com) database that is integrated with 5 | [IBM Bluemix](http://bluemix.net). 6 | 7 | Install 8 | ------- 9 | Install from [npm](http://npmjs.org) 10 | ``` 11 | npm install node-red-node-cf-cloudant 12 | ``` 13 | 14 | Usage 15 | ----- 16 | Allows basic access to a [Cloudant](http://cloudant.com) database to 17 | `insert`, `update`, `delete` and `search` for documents. 18 | 19 | To **insert** a new document into the database you have the option to store 20 | the entire `msg` object or just the `msg.payload`. If the input value is not 21 | in JSON format, it will be transformed before being stored. 22 | 23 | For **update** and **delete**, you must pass the `_id` and the `_rev`as part 24 | of the input `msg` object. 25 | 26 | To **search** for a document you have two options: get a document directly by 27 | its `_id` or use an existing [search index](https://cloudant.com/for-developers/search/) 28 | from the database. For both cases, the query should be passed in the 29 | `msg.payload` input object as a string. 30 | 31 | When getting documents by id, the `payload` will be the desired `_id` value. 32 | For `search indexes`, the query should follow the format `indexName:value`. 33 | 34 | Authors 35 | ------- 36 | * Luiz Gustavo Ferraz Aoqui - [laoqui@ca.ibm.com](mailto:laoqui@ca.ibm.com) 37 | * Túlio Pascoal 38 | -------------------------------------------------------------------------------- /77-cloudant-cf.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | 36 | 53 | 54 | 95 | 96 | 97 | 132 | 133 | 242 | 243 | 282 | 283 | 333 | -------------------------------------------------------------------------------- /77-cloudant-cf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014,2016 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | module.exports = function(RED) { 17 | "use strict"; 18 | var url = require('url'); 19 | var querystring = require('querystring'); 20 | var cfEnv = require("cfenv"); 21 | var Cloudant = require("cloudant"); 22 | 23 | var MAX_ATTEMPTS = 3; 24 | 25 | var appEnv = cfEnv.getAppEnv(); 26 | var services = []; 27 | 28 | // load the services bound to this application 29 | for (var i in appEnv.services) { 30 | if (appEnv.services.hasOwnProperty(i)) { 31 | // filter the services to include only the Cloudant ones 32 | if (i.match(/^(cloudant)/i)) { 33 | services = services.concat(appEnv.services[i].map(function(v) { 34 | return { name: v.name, label: v.label }; 35 | })); 36 | } 37 | } 38 | } 39 | 40 | // 41 | // HTTP endpoints that will be accessed from the HTML file 42 | // 43 | RED.httpAdmin.get('/cloudant/vcap', function(req,res) { 44 | res.send(JSON.stringify(services)); 45 | }); 46 | 47 | // 48 | // Create and register nodes 49 | // 50 | function CloudantNode(n) { 51 | RED.nodes.createNode(this, n); 52 | this.name = n.name; 53 | this.host = n.host; 54 | this.url = n.host; 55 | 56 | // remove unnecessary parts from host value 57 | var parsedUrl = url.parse(this.host); 58 | if (parsedUrl.host) { 59 | this.host = parsedUrl.host; 60 | } 61 | if (this.host.indexOf("cloudant.com")!==-1) { 62 | // extract only the account name 63 | this.account = this.host.substring(0, this.host.indexOf('.')); 64 | delete this.url; 65 | } 66 | var credentials = this.credentials; 67 | if ((credentials) && (credentials.hasOwnProperty("username"))) { this.username = credentials.username; } 68 | if ((credentials) && (credentials.hasOwnProperty("pass"))) { this.password = credentials.pass; } 69 | } 70 | RED.nodes.registerType("cloudant", CloudantNode, { 71 | credentials: { 72 | pass: {type:"password"}, 73 | username: {type:"text"} 74 | } 75 | }); 76 | 77 | 78 | function CloudantOutNode(n) { 79 | RED.nodes.createNode(this,n); 80 | 81 | this.operation = n.operation; 82 | this.payonly = n.payonly || false; 83 | this.database = _cleanDatabaseName(n.database, this); 84 | this.cloudantConfig = _getCloudantConfig(n); 85 | 86 | var node = this; 87 | var credentials = { 88 | account: node.cloudantConfig.account, 89 | key: node.cloudantConfig.username, 90 | password: node.cloudantConfig.password, 91 | url: node.cloudantConfig.url 92 | }; 93 | 94 | Cloudant(credentials, function(err, cloudant) { 95 | if (err) { node.error(err.description, err); } 96 | else { 97 | // check if the database exists and create it if it doesn't 98 | createDatabase(cloudant, node); 99 | } 100 | 101 | node.on("input", function(msg) { 102 | if (err) { 103 | return node.error(err.description, err); 104 | } 105 | 106 | delete msg._msgid; 107 | handleMessage(cloudant, node, msg); 108 | }); 109 | }); 110 | 111 | function createDatabase(cloudant, node) { 112 | cloudant.db.list(function(err, all_dbs) { 113 | if (err) { 114 | if (err.status_code === 403) { 115 | // if err.status_code is 403 then we are probably using 116 | // an api key, so we can assume the database already exists 117 | return; 118 | } 119 | node.error("Failed to list databases: " + err.description, err); 120 | } 121 | else { 122 | if (all_dbs && all_dbs.indexOf(node.database) < 0) { 123 | cloudant.db.create(node.database, function(err, body) { 124 | if (err) { 125 | node.error( 126 | "Failed to create database: " + err.description, 127 | err 128 | ); 129 | } 130 | }); 131 | } 132 | } 133 | }); 134 | } 135 | 136 | function handleMessage(cloudant, node, msg) { 137 | if (node.operation === "insert") { 138 | var msg = node.payonly ? msg.payload : msg; 139 | var root = node.payonly ? "payload" : "msg"; 140 | var doc = parseMessage(msg, root); 141 | 142 | insertDocument(cloudant, node, doc, MAX_ATTEMPTS, function(err, body) { 143 | if (err) { 144 | console.trace(); 145 | console.log(node.error.toString()); 146 | node.error("Failed to insert document: " + err.description, msg); 147 | } 148 | }); 149 | } 150 | else if (node.operation === "delete") { 151 | var doc = parseMessage(msg.payload || msg, ""); 152 | 153 | if ("_rev" in doc && "_id" in doc) { 154 | var db = cloudant.use(node.database); 155 | db.destroy(doc._id, doc._rev, function(err, body) { 156 | if (err) { 157 | node.error("Failed to delete document: " + err.description, msg); 158 | } 159 | }); 160 | } else { 161 | var err = new Error("_id and _rev are required to delete a document"); 162 | node.error(err.message, msg); 163 | } 164 | } 165 | } 166 | 167 | function parseMessage(msg, root) { 168 | if (typeof msg !== "object") { 169 | try { 170 | msg = JSON.parse(msg); 171 | // JSON.parse accepts numbers, so make sure that an 172 | // object is return, otherwise create a new one 173 | if (typeof msg !== "object") { 174 | msg = JSON.parse('{"' + root + '":"' + msg + '"}'); 175 | } 176 | } catch (e) { 177 | // payload is not in JSON format 178 | msg = JSON.parse('{"' + root + '":"' + msg + '"}'); 179 | } 180 | } 181 | return cleanMessage(msg); 182 | } 183 | 184 | // fix field values that start with _ 185 | // https://wiki.apache.org/couchdb/HTTP_Document_API#Special_Fields 186 | function cleanMessage(msg) { 187 | for (var key in msg) { 188 | if (msg.hasOwnProperty(key) && !isFieldNameValid(key)) { 189 | // remove _ from the start of the field name 190 | var newKey = key.substring(1, msg.length); 191 | msg[newKey] = msg[key]; 192 | delete msg[key]; 193 | node.warn("Property '" + key + "' renamed to '" + newKey + "'."); 194 | } 195 | } 196 | return msg; 197 | } 198 | 199 | function isFieldNameValid(key) { 200 | var allowedWords = [ 201 | '_id', '_rev', '_attachments', '_deleted', '_revisions', 202 | '_revs_info', '_conflicts', '_deleted_conflicts', '_local_seq' 203 | ]; 204 | return key[0] !== '_' || allowedWords.indexOf(key) >= 0; 205 | } 206 | 207 | // Inserts a document +doc+ in a database +db+ that migh not exist 208 | // beforehand. If the database doesn't exist, it will create one 209 | // with the name specified in +db+. To prevent loops, it only tries 210 | // +attempts+ number of times. 211 | function insertDocument(cloudant, node, doc, attempts, callback) { 212 | var db = cloudant.use(node.database); 213 | db.insert(doc, function(err, body) { 214 | if (err && err.status_code === 404 && attempts > 0) { 215 | // status_code 404 means the database was not found 216 | return cloudant.db.create(db.config.db, function() { 217 | insertDocument(cloudant, node, doc, attempts-1, callback); 218 | }); 219 | } 220 | 221 | callback(err, body); 222 | }); 223 | } 224 | }; 225 | RED.nodes.registerType("cloudant out", CloudantOutNode); 226 | 227 | 228 | function CloudantInNode(n) { 229 | RED.nodes.createNode(this,n); 230 | 231 | this.cloudantConfig = _getCloudantConfig(n); 232 | this.database = _cleanDatabaseName(n.database, this); 233 | this.search = n.search; 234 | this.design = n.design; 235 | this.index = n.index; 236 | this.inputId = ""; 237 | 238 | var node = this; 239 | var credentials = { 240 | account: node.cloudantConfig.account, 241 | key: node.cloudantConfig.username, 242 | password: node.cloudantConfig.password, 243 | url: node.cloudantConfig.url 244 | }; 245 | 246 | Cloudant(credentials, function(err, cloudant) { 247 | if (err) { node.error(err.description, err); } 248 | 249 | node.on("input", function(msg) { 250 | if (err) { 251 | return node.error(err.description, err); 252 | } 253 | 254 | var db = cloudant.use(node.database); 255 | var options = (typeof msg.payload === "object") ? msg.payload : {}; 256 | 257 | if (node.search === "_id_") { 258 | var id = getDocumentId(msg.payload); 259 | node.inputId = id; 260 | 261 | db.get(id, function(err, body) { 262 | sendDocumentOnPayload(err, body, msg); 263 | }); 264 | } 265 | else if (node.search === "_idx_") { 266 | options.query = options.query || options.q || formatSearchQuery(msg.payload); 267 | options.include_docs = options.include_docs || true; 268 | options.limit = options.limit || 200; 269 | 270 | db.search(node.design, node.index, options, function(err, body) { 271 | sendDocumentOnPayload(err, body, msg); 272 | }); 273 | } 274 | else if (node.search === "_all_") { 275 | options.include_docs = options.include_docs || true; 276 | 277 | db.list(options, function(err, body) { 278 | sendDocumentOnPayload(err, body, msg); 279 | }); 280 | } 281 | }); 282 | }); 283 | 284 | function getDocumentId(payload) { 285 | if (typeof payload === "object") { 286 | if ("_id" in payload || "id" in payload) { 287 | return payload.id || payload._id; 288 | } 289 | } 290 | 291 | return payload; 292 | } 293 | 294 | function formatSearchQuery(query) { 295 | if (typeof query === "object") { 296 | // useful when passing the query on HTTP params 297 | if ("q" in query) { return query.q; } 298 | 299 | var queryString = ""; 300 | for (var key in query) { 301 | queryString += key + ":" + query[key] + " "; 302 | } 303 | 304 | return queryString.trim(); 305 | } 306 | return query; 307 | } 308 | 309 | function sendDocumentOnPayload(err, body, msg) { 310 | if (!err) { 311 | msg.cloudant = body; 312 | 313 | if ("rows" in body) { 314 | msg.payload = body.rows. 315 | map(function(el) { 316 | if (el.doc._id.indexOf("_design/") < 0) { 317 | return el.doc; 318 | } 319 | }). 320 | filter(function(el) { 321 | return el !== null && el !== undefined; 322 | }); 323 | } else { 324 | msg.payload = body; 325 | } 326 | } 327 | else { 328 | msg.payload = null; 329 | 330 | if (err.description === "missing") { 331 | node.warn( 332 | "Document '" + node.inputId + 333 | "' not found in database '" + node.database + "'.", 334 | err 335 | ); 336 | } else { 337 | node.error(err.description, err); 338 | } 339 | } 340 | 341 | node.send(msg); 342 | } 343 | } 344 | RED.nodes.registerType("cloudant in", CloudantInNode); 345 | 346 | // must return an object with, at least, values for account, username and 347 | // password for the Cloudant service at the top-level of the object 348 | function _getCloudantConfig(n) { 349 | if (n.service === "_ext_") { 350 | return RED.nodes.getNode(n.cloudant); 351 | 352 | } else if (n.service !== "") { 353 | var service = appEnv.getService(n.service); 354 | var cloudantConfig = { }; 355 | 356 | var host = service.credentials.host; 357 | 358 | cloudantConfig.username = service.credentials.username; 359 | cloudantConfig.password = service.credentials.password; 360 | cloudantConfig.account = host.substring(0, host.indexOf('.')); 361 | 362 | return cloudantConfig; 363 | } 364 | } 365 | 366 | // remove invalid characters from the database name 367 | // https://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing 368 | function _cleanDatabaseName(database, node) { 369 | var newDatabase = database; 370 | 371 | // caps are not allowed 372 | newDatabase = newDatabase.toLowerCase(); 373 | // remove trailing underscore 374 | newDatabase = newDatabase.replace(/^_/, ''); 375 | // remove spaces and slashed 376 | newDatabase = newDatabase.replace(/[\s\\/]+/g, '-'); 377 | 378 | if (newDatabase !== database) { 379 | node.warn("Database renamed as '" + newDatabase + "'."); 380 | } 381 | 382 | return newDatabase; 383 | } 384 | }; 385 | --------------------------------------------------------------------------------