node-red-node-cf-cloudant
Version:
A Node-RED node to access Cloudant and couchdb databases
385 lines (333 loc) • 14 kB
JavaScript
/**
* Copyright 2014,2016 IBM Corp.
*
* 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.
**/
module.exports = function(RED) {
"use strict";
var url = require('url');
var querystring = require('querystring');
var cfEnv = require("cfenv");
var Cloudant = require("cloudant");
var MAX_ATTEMPTS = 3;
var appEnv = cfEnv.getAppEnv();
var services = [];
// load the services bound to this application
for (var i in appEnv.services) {
if (appEnv.services.hasOwnProperty(i)) {
// filter the services to include only the Cloudant ones
if (i.match(/^(cloudant)/i)) {
services = services.concat(appEnv.services[i].map(function(v) {
return { name: v.name, label: v.label };
}));
}
}
}
//
// HTTP endpoints that will be accessed from the HTML file
//
RED.httpAdmin.get('/cloudant/vcap', function(req,res) {
res.send(JSON.stringify(services));
});
//
// Create and register nodes
//
function CloudantNode(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this.host = n.host;
this.url = n.host;
// remove unnecessary parts from host value
var parsedUrl = url.parse(this.host);
if (parsedUrl.host) {
this.host = parsedUrl.host;
}
if (this.host.indexOf("cloudant.com")!==-1) {
// extract only the account name
this.account = this.host.substring(0, this.host.indexOf('.'));
delete this.url;
}
var credentials = this.credentials;
if ((credentials) && (credentials.hasOwnProperty("username"))) { this.username = credentials.username; }
if ((credentials) && (credentials.hasOwnProperty("pass"))) { this.password = credentials.pass; }
}
RED.nodes.registerType("cloudant", CloudantNode, {
credentials: {
pass: {type:"password"},
username: {type:"text"}
}
});
function CloudantOutNode(n) {
RED.nodes.createNode(this,n);
this.operation = n.operation;
this.payonly = n.payonly || false;
this.database = _cleanDatabaseName(n.database, this);
this.cloudantConfig = _getCloudantConfig(n);
var node = this;
var credentials = {
account: node.cloudantConfig.account,
key: node.cloudantConfig.username,
password: node.cloudantConfig.password,
url: node.cloudantConfig.url
};
Cloudant(credentials, function(err, cloudant) {
if (err) { node.error(err.description, err); }
else {
// check if the database exists and create it if it doesn't
createDatabase(cloudant, node);
}
node.on("input", function(msg) {
if (err) {
return node.error(err.description, err);
}
delete msg._msgid;
handleMessage(cloudant, node, msg);
});
});
function createDatabase(cloudant, node) {
cloudant.db.list(function(err, all_dbs) {
if (err) {
if (err.status_code === 403) {
// if err.status_code is 403 then we are probably using
// an api key, so we can assume the database already exists
return;
}
node.error("Failed to list databases: " + err.description, err);
}
else {
if (all_dbs && all_dbs.indexOf(node.database) < 0) {
cloudant.db.create(node.database, function(err, body) {
if (err) {
node.error(
"Failed to create database: " + err.description,
err
);
}
});
}
}
});
}
function handleMessage(cloudant, node, msg) {
if (node.operation === "insert") {
var msg = node.payonly ? msg.payload : msg;
var root = node.payonly ? "payload" : "msg";
var doc = parseMessage(msg, root);
insertDocument(cloudant, node, doc, MAX_ATTEMPTS, function(err, body) {
if (err) {
console.trace();
console.log(node.error.toString());
node.error("Failed to insert document: " + err.description, msg);
}
});
}
else if (node.operation === "delete") {
var doc = parseMessage(msg.payload || msg, "");
if ("_rev" in doc && "_id" in doc) {
var db = cloudant.use(node.database);
db.destroy(doc._id, doc._rev, function(err, body) {
if (err) {
node.error("Failed to delete document: " + err.description, msg);
}
});
} else {
var err = new Error("_id and _rev are required to delete a document");
node.error(err.message, msg);
}
}
}
function parseMessage(msg, root) {
if (typeof msg !== "object") {
try {
msg = JSON.parse(msg);
// JSON.parse accepts numbers, so make sure that an
// object is return, otherwise create a new one
if (typeof msg !== "object") {
msg = JSON.parse('{"' + root + '":"' + msg + '"}');
}
} catch (e) {
// payload is not in JSON format
msg = JSON.parse('{"' + root + '":"' + msg + '"}');
}
}
return cleanMessage(msg);
}
// fix field values that start with _
// https://wiki.apache.org/couchdb/HTTP_Document_API#Special_Fields
function cleanMessage(msg) {
for (var key in msg) {
if (msg.hasOwnProperty(key) && !isFieldNameValid(key)) {
// remove _ from the start of the field name
var newKey = key.substring(1, msg.length);
msg[newKey] = msg[key];
delete msg[key];
node.warn("Property '" + key + "' renamed to '" + newKey + "'.");
}
}
return msg;
}
function isFieldNameValid(key) {
var allowedWords = [
'_id', '_rev', '_attachments', '_deleted', '_revisions',
'_revs_info', '_conflicts', '_deleted_conflicts', '_local_seq'
];
return key[0] !== '_' || allowedWords.indexOf(key) >= 0;
}
// Inserts a document +doc+ in a database +db+ that migh not exist
// beforehand. If the database doesn't exist, it will create one
// with the name specified in +db+. To prevent loops, it only tries
// +attempts+ number of times.
function insertDocument(cloudant, node, doc, attempts, callback) {
var db = cloudant.use(node.database);
db.insert(doc, function(err, body) {
if (err && err.status_code === 404 && attempts > 0) {
// status_code 404 means the database was not found
return cloudant.db.create(db.config.db, function() {
insertDocument(cloudant, node, doc, attempts-1, callback);
});
}
callback(err, body);
});
}
};
RED.nodes.registerType("cloudant out", CloudantOutNode);
function CloudantInNode(n) {
RED.nodes.createNode(this,n);
this.cloudantConfig = _getCloudantConfig(n);
this.database = _cleanDatabaseName(n.database, this);
this.search = n.search;
this.design = n.design;
this.index = n.index;
this.inputId = "";
var node = this;
var credentials = {
account: node.cloudantConfig.account,
key: node.cloudantConfig.username,
password: node.cloudantConfig.password,
url: node.cloudantConfig.url
};
Cloudant(credentials, function(err, cloudant) {
if (err) { node.error(err.description, err); }
node.on("input", function(msg) {
if (err) {
return node.error(err.description, err);
}
var db = cloudant.use(node.database);
var options = (typeof msg.payload === "object") ? msg.payload : {};
if (node.search === "_id_") {
var id = getDocumentId(msg.payload);
node.inputId = id;
db.get(id, function(err, body) {
sendDocumentOnPayload(err, body, msg);
});
}
else if (node.search === "_idx_") {
options.query = options.query || options.q || formatSearchQuery(msg.payload);
options.include_docs = options.include_docs || true;
options.limit = options.limit || 200;
db.search(node.design, node.index, options, function(err, body) {
sendDocumentOnPayload(err, body, msg);
});
}
else if (node.search === "_all_") {
options.include_docs = options.include_docs || true;
db.list(options, function(err, body) {
sendDocumentOnPayload(err, body, msg);
});
}
});
});
function getDocumentId(payload) {
if (typeof payload === "object") {
if ("_id" in payload || "id" in payload) {
return payload.id || payload._id;
}
}
return payload;
}
function formatSearchQuery(query) {
if (typeof query === "object") {
// useful when passing the query on HTTP params
if ("q" in query) { return query.q; }
var queryString = "";
for (var key in query) {
queryString += key + ":" + query[key] + " ";
}
return queryString.trim();
}
return query;
}
function sendDocumentOnPayload(err, body, msg) {
if (!err) {
msg.cloudant = body;
if ("rows" in body) {
msg.payload = body.rows.
map(function(el) {
if (el.doc._id.indexOf("_design/") < 0) {
return el.doc;
}
}).
filter(function(el) {
return el !== null && el !== undefined;
});
} else {
msg.payload = body;
}
}
else {
msg.payload = null;
if (err.description === "missing") {
node.warn(
"Document '" + node.inputId +
"' not found in database '" + node.database + "'.",
err
);
} else {
node.error(err.description, err);
}
}
node.send(msg);
}
}
RED.nodes.registerType("cloudant in", CloudantInNode);
// must return an object with, at least, values for account, username and
// password for the Cloudant service at the top-level of the object
function _getCloudantConfig(n) {
if (n.service === "_ext_") {
return RED.nodes.getNode(n.cloudant);
} else if (n.service !== "") {
var service = appEnv.getService(n.service);
var cloudantConfig = { };
var host = service.credentials.host;
cloudantConfig.username = service.credentials.username;
cloudantConfig.password = service.credentials.password;
cloudantConfig.account = host.substring(0, host.indexOf('.'));
return cloudantConfig;
}
}
// remove invalid characters from the database name
// https://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
function _cleanDatabaseName(database, node) {
var newDatabase = database;
// caps are not allowed
newDatabase = newDatabase.toLowerCase();
// remove trailing underscore
newDatabase = newDatabase.replace(/^_/, '');
// remove spaces and slashed
newDatabase = newDatabase.replace(/[\s\\/]+/g, '-');
if (newDatabase !== database) {
node.warn("Database renamed as '" + newDatabase + "'.");
}
return newDatabase;
}
};