├── .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 |
--------------------------------------------------------------------------------