Skip to content

Commit 5e7cd92

Browse files
committed
Merge branch 'develop'
* develop: improve error handling and allow use of api key/password clean database name and fix fields that start with _ in the document
2 parents d6d2356 + 908261f commit 5e7cd92

File tree

3 files changed

+163
-42
lines changed

3 files changed

+163
-42
lines changed

77-cloudant-cf.html

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
<script type="text/x-red" data-template-name="cloudant">
1818
<div class="form-row">
19-
<label for="node-config-input-hostname"><i class="fa fa-bookmark"></i> Host</label>
20-
<input class="input-append-left" type="text" id="node-config-input-hostname"
21-
placeholder="https://[username].cloudant.com">
19+
<label for="node-config-input-host"><i class="fa fa-bookmark"></i> Host</label>
20+
<input class="input-append-left" type="text" id="node-config-input-host"
21+
placeholder="[username].cloudant.com">
2222
</div>
2323

2424
<div class="form-row">
@@ -42,13 +42,13 @@
4242
category: "config",
4343
color: "rgb(114, 199, 231)",
4444
defaults: {
45-
hostname: { value: "", required: true },
45+
host: { value: "", required: true },
4646
name: { value: "" },
4747
//user -> credentials
4848
//pass -> credentials
4949
},
5050
label: function() {
51-
return this.name || this.hostname;
51+
return this.name || this.host;
5252
},
5353
oneditprepare: function() {
5454
$.getJSON("cloudant/"+this.id, function(data) {
@@ -186,7 +186,7 @@
186186
service: { value: "", required: true },
187187
cloudant: { type: "cloudant", validate: validateServer},
188188
name: { value: "" },
189-
database: { value: "", required: true },
189+
database: { value: "", required: true, validate: validateDatabase },
190190
payonly: { value: false },
191191
operation: { value: "insert" }
192192
},
@@ -208,7 +208,7 @@
208208
service : { value: "", required: true },
209209
cloudant: { type: "cloudant", validate: validateServer },
210210
name : { value: "" },
211-
database: { value: "", required: true },
211+
database: { value: "", required: true, validate: validateDatabase },
212212
search : { value: "_id_", required: true },
213213
design : { value: "" },
214214
index : { value: "" }
@@ -286,6 +286,12 @@
286286
function validateServer(v) {
287287
return this.service != "_ext_" || v != "_ADD_";
288288
}
289+
290+
// https://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
291+
function validateDatabase(v) {
292+
return (v.indexOf('_') !== 0) &&
293+
(v.search(/[A-Z\s\\/]/g) < 0)
294+
}
289295
</script>
290296

291297
<script type="text/x-red" data-help-name="cloudant out">
@@ -307,11 +313,25 @@
307313
</p>
308314
<p>
309315
It is also possible to <b>delete</b> documents from the database by
310-
providing values for <code>_id</b> and <code>_rev</code> and selecting
316+
providing values for <code>_id</code> and <code>_rev</code> and selecting
311317
the <b>remove</b> option for the node. You can pass these values in the
312318
<code>msg</code> object itself or as an object in the
313319
<code>msg.payload</code>.
314320
</p>
321+
<p>
322+
The <b>database name</b> must follow these rules:
323+
<ul>
324+
<li>No spaces</li>
325+
<li>All letters in small caps</li>
326+
<li>The first character can't be <code>_</code></li>
327+
</ul>
328+
</p>
329+
<p>
330+
Your document should avoid having top-level fields that start with
331+
<code>_</code>, with exceptions for <code>_id</code>, <code>_rev</code>
332+
and other <a href="https://wiki.apache.org/couchdb/HTTP_Document_API#Special_Fields">
333+
CouchDB reserved words</a>.
334+
</p>
315335
</script>
316336

317337
<script type="text/x-red" data-help-name="cloudant in">
@@ -338,15 +358,29 @@
338358
</p>
339359
<p>
340360
When querying using a <b>Search Index</b> you can pass the search
341-
parameters as an object in <code>msg.payload</code>. For example, you
342-
can pass an object like this: <code>{ query: "abc*", limit: 100 }</code>
361+
parameters as an object in <code>msg.payload</code>.
362+
</p>
363+
<p>
364+
For example, you can pass an object like this:
365+
<p>
366+
<code>{ query: "abc*", limit: 100 }</code>
367+
</p>
368+
<p>
343369
to change the value of <code>limit</code>. You can find more information
344-
on the accepted parameters in the <a
370+
about <b>Search Index</b> parameters in the <a
345371
href="https://docs.cloudant.com/api.html?http#queries" target="_blank">
346372
official Cloudant documentation</a>.
347373
</p>
348374
<p>
349375
The last method to retrieve documents is to simply get all of them by
350376
selecting the option <b>all documents</b>.
351377
</p>
378+
<p>
379+
The <b>database name</b> must follow these rules:
380+
<ul>
381+
<li>No spaces</li>
382+
<li>All letters in small caps</li>
383+
<li>The first character can't be <code>_</code></li>
384+
</ul>
385+
</p>
352386
</script>

77-cloudant-cf.js

Lines changed: 117 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -90,24 +90,23 @@ module.exports = function(RED) {
9090
function CloudantNode(n) {
9191
RED.nodes.createNode(this, n);
9292

93-
this.name = n.name;
94-
this.hostname = n.hostname;
93+
this.name = n.name;
94+
this.host = n.host;
95+
96+
// remove unnecessary parts from host value
97+
var parsedUrl = url.parse(this.host);
98+
if (parsedUrl.host) {
99+
this.host = parsedUrl.host;
100+
}
101+
102+
// extract only the account name
103+
this.account = this.host.substring(0, this.host.indexOf('.'));
95104

96105
var credentials = RED.nodes.getCredentials(n.id);
97106
if (credentials) {
98107
this.username = credentials.username;
99108
this.password = credentials.password;
100109
}
101-
102-
var parsedUrl = url.parse(this.hostname);
103-
var authUrl = parsedUrl.protocol+'//';
104-
105-
if (this.username && this.password) {
106-
authUrl += this.username + ":" + encodeURIComponent(this.password) + "@";
107-
}
108-
authUrl += parsedUrl.hostname;
109-
110-
this.url = authUrl;
111110
}
112111
RED.nodes.registerType("cloudant", CloudantNode);
113112

@@ -116,17 +115,18 @@ module.exports = function(RED) {
116115

117116
this.operation = n.operation;
118117
this.payonly = n.payonly || false;
119-
this.database = n.database;
118+
this.database = _cleanDatabaseName(n.database, this);
120119
this.cloudantConfig = _getCloudantConfig(n);
121120

122121
var node = this;
123122
var credentials = {
124-
account: node.cloudantConfig.credentials.username,
125-
password: node.cloudantConfig.credentials.password
123+
account: node.cloudantConfig.account,
124+
key: node.cloudantConfig.username,
125+
password: node.cloudantConfig.password
126126
};
127127

128128
Cloudant(credentials, function(err, cloudant) {
129-
if (err) { node.error(err); }
129+
if (err) { node.error(err.description, err); }
130130
else {
131131
// check if the database exists and create it if it doesn't
132132
createDatabase(cloudant, node);
@@ -139,10 +139,24 @@ module.exports = function(RED) {
139139

140140
function createDatabase(cloudant, node) {
141141
cloudant.db.list(function(err, all_dbs) {
142-
if (err) { node.error(err); }
142+
if (err) {
143+
if (err.error !== 'forbidden') {
144+
// if err.error is 'forbidden' then we are using an api
145+
// key, so we can assume the database already exists
146+
return;
147+
}
148+
node.error("Failed to list databases: " + err.description, err);
149+
}
143150
else {
144151
if (all_dbs && all_dbs.indexOf(node.database) < 0) {
145-
cloudant.db.create(node.database);
152+
cloudant.db.create(node.database, function(err, body) {
153+
if (err) {
154+
node.error(
155+
"Failed to create database: " + err.description,
156+
err
157+
);
158+
}
159+
});
146160
}
147161
}
148162
});
@@ -155,7 +169,12 @@ module.exports = function(RED) {
155169
var doc = parseMessage(msg, root);
156170

157171
insertDocument(cloudant, node, doc, MAX_ATTEMPTS, function(err, body) {
158-
if (err) { node.error(err); }
172+
if (err) {
173+
node.error(
174+
"Failed to insert document: " + err.description,
175+
err
176+
);
177+
}
159178
});
160179
}
161180
else if (node.operation === "delete") {
@@ -164,10 +183,16 @@ module.exports = function(RED) {
164183
if ("_rev" in doc && "_id" in doc) {
165184
var db = cloudant.use(node.database);
166185
db.destroy(doc._id, doc._rev, function(err, body) {
167-
if (err) { node.error(err); }
186+
if (err) {
187+
node.error(
188+
"Failed to delete document: " + err.description,
189+
err
190+
);
191+
}
168192
});
169193
} else {
170-
node.error("_rev and _id are required to delete a document");
194+
var err = new Error("_id and _rev are required to delete a document");
195+
node.error(err.message, err);
171196
}
172197
}
173198
}
@@ -187,9 +212,36 @@ module.exports = function(RED) {
187212
msg = JSON.parse('{"' + root + '":"' + msg + '"}');
188213
}
189214
}
215+
return cleanMessage(msg);
216+
}
217+
218+
// fix field values that start with _
219+
// https://wiki.apache.org/couchdb/HTTP_Document_API#Special_Fields
220+
function cleanMessage(msg) {
221+
for (var key in msg) {
222+
if (msg.hasOwnProperty(key) && !isFieldNameValid(key)) {
223+
// remove _ from the start of the field name
224+
var newKey = key.substring(1, msg.length);
225+
226+
msg[newKey] = msg[key];
227+
delete msg[key];
228+
229+
node.warn("Property '" + key + "' renamed to '" + newKey + "'.");
230+
}
231+
}
232+
190233
return msg;
191234
}
192235

236+
function isFieldNameValid(key) {
237+
var allowedWords = [
238+
'_id', '_rev', '_attachments', '_deleted', '_revisions',
239+
'_revs_info', '_conflicts', '_deleted_conflicts', '_local_seq'
240+
];
241+
242+
return key[0] !== '_' || allowedWords.indexOf(key) >= 0;
243+
}
244+
193245
// Inserts a document +doc+ in a database +db+ that migh not exist
194246
// beforehand. If the database doesn't exist, it will create one
195247
// with the name specified in +db+. To prevent loops, it only tries
@@ -214,20 +266,21 @@ module.exports = function(RED) {
214266
RED.nodes.createNode(this,n);
215267

216268
this.cloudantConfig = _getCloudantConfig(n);
217-
this.database = n.database;
269+
this.database = _cleanDatabaseName(n.database, this);
218270
this.search = n.search;
219271
this.design = n.design;
220272
this.index = n.index;
221273
this.inputId = "";
222274

223275
var node = this;
224276
var credentials = {
225-
account: node.cloudantConfig.credentials.username,
226-
password: node.cloudantConfig.credentials.password
277+
account: node.cloudantConfig.account,
278+
key: node.cloudantConfig.username,
279+
password: node.cloudantConfig.password
227280
};
228281

229282
Cloudant(credentials, function(err, cloudant) {
230-
if (err) { node.error(err); }
283+
if (err) { node.error(err.description, err); }
231284
else {
232285
node.on("input", function(msg) {
233286
var db = cloudant.use(node.database);
@@ -245,7 +298,7 @@ module.exports = function(RED) {
245298
options.query = options.query || options.q || formatSearchQuery(msg.payload);
246299
options.include_docs = options.include_docs || true;
247300
options.limit = options.limit || 200;
248-
301+
249302
if (options.sort) {
250303
options.sort = JSON.stringify(options.sort);
251304
}
@@ -312,10 +365,13 @@ module.exports = function(RED) {
312365
msg.payload = null;
313366

314367
if (err.description === "missing") {
315-
node.warn("Document '" + node.inputId + "' not found in database '" +
316-
node.database + "'.");
368+
node.warn(
369+
"Document '" + node.inputId +
370+
"' not found in database '" + node.database + "'.",
371+
err
372+
);
317373
} else {
318-
node.error(err.reason);
374+
node.error(err.description, err);
319375
}
320376
}
321377

@@ -324,11 +380,42 @@ module.exports = function(RED) {
324380
}
325381
RED.nodes.registerType("cloudant in", CloudantInNode);
326382

383+
// must return an object with, at least, values for account, username and
384+
// password for the Cloudant service at the top-level of the object
327385
function _getCloudantConfig(n) {
328386
if (n.service === "_ext_") {
329387
return RED.nodes.getNode(n.cloudant);
388+
330389
} else if (n.service !== "") {
331-
return appEnv.getService(n.service);
390+
var service = appEnv.getService(n.service);
391+
var cloudantConfig = { };
392+
393+
var host = service.credentials.host;
394+
395+
cloudantConfig.username = service.credentials.username;
396+
cloudantConfig.password = service.credentials.password;
397+
cloudantConfig.account = host.substring(0, host.indexOf('.'));
398+
399+
return cloudantConfig;
400+
}
401+
}
402+
403+
// remove invalid characters from the database name
404+
// https://wiki.apache.org/couchdb/HTTP_database_API#Naming_and_Addressing
405+
function _cleanDatabaseName(database, node) {
406+
var newDatabase = database;
407+
408+
// caps are not allowed
409+
newDatabase = newDatabase.toLowerCase();
410+
// remove trailing underscore
411+
newDatabase = newDatabase.replace(/^_/, '');
412+
// remove spaces and slashed
413+
newDatabase = newDatabase.replace(/[\s\\/]+/g, '-');
414+
415+
if (newDatabase !== database) {
416+
node.warn("Database renamed as '" + newDatabase + "'.");
332417
}
418+
419+
return newDatabase;
333420
}
334421
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name" : "node-red-node-cf-cloudant",
3-
"version" : "0.2.12",
3+
"version" : "0.2.13",
44
"description" : "A Node-RED node to access a Cloudant database on Bluemix",
55
"dependencies" : {
66
"cfenv" : "1.0.0",

0 commit comments

Comments
 (0)