Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit ec9acec

Browse files
authored
Merge pull request #150 from glynnbird/plugin
Plugin support
2 parents 67b0a65 + 1fd4f41 commit ec9acec

File tree

9 files changed

+350
-46
lines changed

9 files changed

+350
-46
lines changed

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This is the official Cloudant library for Node.js.
88
* [Callback Signature](#callback-signature)
99
* [Password Authentication](#password-authentication)
1010
* [Cloudant Local](#cloudant-local)
11+
* [Request Plugins](#request-plugins)
1112
* [API Reference](#api-reference)
1213
* [Authorization and API Keys](#authorization-and-api-keys)
1314
* [Generate an API key](#generate-an-api-key)
@@ -206,6 +207,69 @@ Cloudant({hostname:"companycloudant.local", username:"somebody", password:"someb
206207
})
207208
~~~
208209

210+
### Request Plugins
211+
212+
This library can be used with one of three `request` plugins:
213+
214+
1. `default` - the default [request](https://www.npmjs.com/package/request) library plugin. This uses Node.js's callbacks to communicate Cloudant's replies
215+
back to your app and can be used to stream data using the Node.js [Stream API](https://nodejs.org/api/stream.html).
216+
2. `promises` - if you'd prefer to write code in the Promises style then the "promises" plugin turns each request into a Promise. This plugin cannot be used
217+
stream data because instead of returning the HTTP request, we are simply returning a Promise instead.
218+
3. `retry` - on occasion, Cloudant's multi-tenant offerring may reply with an HTTP 429 response because you've exceed the number of API requests in a given amount of time.
219+
The "retry" plugin will automatically retry your request with exponential back-off.
220+
4. custom plugin - you may also supply your own function which will be called to make API calls.
221+
222+
#### The 'promises' Plugins
223+
224+
When initialising the Cloudant library, you can opt to use the 'promises' plugin:
225+
226+
```js
227+
var cloudant = Cloudant({url: myurl, plugin:'promises'});
228+
var mydb = cloudant.db.use('mydb');
229+
```
230+
231+
Then the library will return a Promise for every asynchronous call:
232+
233+
```js
234+
mydb.list().then(function(data) {
235+
console.log(data);
236+
}).catch(function(err) {
237+
console.log('something went wrong', err);
238+
});
239+
```
240+
241+
#### The 'retry' plugin
242+
243+
When initialising the Cloudant library, you can opt to use the 'retry' plugin:
244+
245+
```js
246+
var cloudant = Cloudant({url: myurl, plugin:'retry'});
247+
var mydb = cloudant.db.use('mydb');
248+
```
249+
250+
Then use the Cloudant library normally. You may also opt to configure the retry parameters:
251+
252+
- retryAttempts - the maximum number of times the request will be attempted (default 3)
253+
- retryTimeout - the number of milliseconds after the first attempt that the second request will be tried; the timeout doubling with each subsequent attempt (default 500)
254+
255+
```js
256+
var cloudant = Cloudant({url: myurl, plugin:'retry', retryAttempts:5, retryTimeout:1000 });
257+
var mydb = cloudant.db.use('mydb');
258+
```
259+
260+
#### Custom plugin
261+
262+
When initialising the Cloudant library, you can supply your own plugin function:
263+
264+
```js
265+
var doNothingPlugin = function(opts, callback) {
266+
// don't do anything, just pretend that everything's ok.
267+
callback(null, { statusCode:200 }, { ok: true});
268+
};
269+
var cloudant = Cloudant({url: myurl, plugin: doNothingPlugin});
270+
```
271+
272+
Whenever the Cloudant library wishes to make an outgoing HTTP request, it will call your function instead of `request`.
209273

210274
## API Reference
211275

cloudant.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,53 @@ var nanodebug = require('debug')('nano');
2525
var reconfigure = require('./lib/reconfigure.js')
2626

2727
// This IS the Cloudant API. It is mostly nano, with a few functions.
28-
function Cloudant(credentials, callback) {
29-
debug('Initialize', credentials);
28+
function Cloudant(options, callback) {
29+
debug('Initialize', options);
3030

3131
// Save the username and password for potential conversion to cookie auth.
32-
var login = reconfigure.getCredentials(credentials);
32+
var login = reconfigure.getOptions(options);
3333

3434
// Convert the credentials into a URL that will work for cloudant. The
3535
// credentials object will become squashed into a string, which is fine
3636
// except for the .cookie option.
37-
var cookie = credentials.cookie;
37+
var cookie = options.cookie;
3838

3939
var pkg = require('./package.json');
4040
var useragent = "nodejs-cloudant/" + pkg.version + " (Node.js " + process.version + ")";
4141
var requestDefaults = { headers: { "User-agent": useragent}, gzip:true };
42-
if (typeof credentials == "object") {
43-
if (credentials.requestDefaults) {
44-
requestDefaults = credentials.requestDefaults;
45-
delete credentials.requestDefaults;
42+
var theurl = null;
43+
if (typeof options == "object") {
44+
if (options.requestDefaults) {
45+
requestDefaults = options.requestDefaults;
46+
delete options.requestDefaults;
4647
}
47-
credentials = reconfigure(credentials);
48+
theurl = reconfigure(options);
4849
} else {
49-
credentials = reconfigure({ url: credentials})
50+
theurl = reconfigure({ url: options})
5051
}
5152

5253
// keep connections alive by default
5354
if (requestDefaults && !requestDefaults.agent) {
54-
var protocol = (credentials.match(/^https/))? require('https') : require('http');
55+
var protocol = (theurl.match(/^https/))? require('https') : require('http');
5556
var agent = new protocol.Agent({ keepAlive:true });
5657
requestDefaults.agent = agent;
5758
}
5859

59-
debug('Create underlying Nano instance, credentials=%j requestDefaults=%j', credentials, requestDefaults);
60-
var nano = Nano({url:credentials, requestDefaults: requestDefaults, cookie: cookie, log: nanodebug});
60+
// plugin a request library
61+
var plugin = null;
62+
if (options.plugin) {
63+
if(typeof options.plugin === 'string') {
64+
var plugintype = options.plugin || 'default';
65+
debug('Using the "' + plugintype + '" plugin');
66+
plugin = require('./plugins/' + plugintype)(options);
67+
} else if (typeof options.plugin === 'function') {
68+
debug('Using a custom plugin');
69+
plugin = options.plugin;
70+
}
71+
}
72+
73+
debug('Create underlying Nano instance, options=%j requestDefaults=%j', options, requestDefaults);
74+
var nano = Nano({url:theurl, request: plugin, requestDefaults: requestDefaults, cookie: cookie, log: nanodebug});
6175

6276
// our own implementation of 'use' e.g. nano.use or nano.db.use
6377
// it includes all db-level functions

lib/reconfigure.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ module.exports = function(config) {
4747
if (match)
4848
config.account = match[1];
4949

50-
var credentials = getCredentials(config);
51-
var username = credentials.username;
52-
var password = credentials.password;
50+
var options = getOptions(config);
51+
var username = options.username;
52+
var password = options.password;
5353

5454
// Configure for Cloudant, either authenticated or anonymous.
5555
if (config.account && password) {
@@ -76,8 +76,8 @@ module.exports = function(config) {
7676
return outUrl;
7777
};
7878

79-
module.exports.getCredentials = getCredentials;
80-
function getCredentials(config) {
79+
module.exports.getOptions = getOptions;
80+
function getOptions(config) {
8181
// The username is the account ("foo" for "foo.cloudant.com")
8282
// or the third-party API key.
8383
var result = {password:config.password, username: config.key || config.username || config.account};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
"database"
1818
],
1919
"dependencies": {
20+
"async": "^2.0.1",
2021
"debug": "2.2.0",
21-
"nano": "6.2.0"
22+
"nano": "6.2.0",
23+
"request": "^2.53.0"
2224
},
2325
"devDependencies": {
2426
"dotenv": "1.2.0",

plugins/default.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2015 IBM Cloudant, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11+
* either express or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
// this the the 'default' request handler.
16+
// It is simply an instance of the popular 'request' npm module.
17+
// This is the simplest module to use as it supports JavaScript callbacks
18+
// and can be used for with the Node.js streaming API.
19+
20+
module.exports = function(options) {
21+
var requestDefaults = options.requestDefaults || {jar: false};
22+
return require('request').defaults(requestDefaults);
23+
}

plugins/promises.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) 2015 IBM Cloudant, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11+
* either express or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
// this the the 'promises' request handler.
16+
// It is a function that returns a Promise and resolves the promise on success
17+
// or rejects the Promise on failure
18+
19+
var nullcallback = function() {};
20+
21+
module.exports = function(options) {
22+
23+
var requestDefaults = options.requestDefaults || {jar: false};
24+
var request = require('request').defaults(requestDefaults);
25+
var myrequest = function(req, callback) {
26+
if (typeof callback !== 'function') {
27+
callback = nullcallback;
28+
}
29+
return new Promise(function(resolve, reject) {
30+
request(req, function(err, h, b) {
31+
var statusCode = h && h.statusCode || 500;
32+
if (statusCode >= 200 && statusCode < 400) {
33+
callback(null, h, b);
34+
return resolve(b);
35+
}
36+
reject(err || b);
37+
callback(err, h, b);
38+
})
39+
});
40+
};
41+
42+
return myrequest;
43+
};
44+
45+

plugins/retry.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright (c) 2015 IBM Cloudant, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11+
* either express or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
// this the the 'retry' request handler.
16+
// If CouchDB/Cloudant responds with a 429 HTTP code
17+
// the library will retry the request up to three
18+
// times with exponential backoff.
19+
// This module is unsuitable for streaming requests.
20+
var async = require('async');
21+
var debug = require('debug')('cloudant');
22+
23+
module.exports = function(options) {
24+
var requestDefaults = options.requestDefaults || {jar: false};
25+
var request = require('request').defaults(requestDefaults);
26+
27+
var myrequest = function(req, callback) {
28+
var attempts = 0;
29+
var maxAttempts = options.retryAttempts || 3;
30+
var firstTimeout = options.retryTimeout || 500; // ms
31+
var timeout = 0; // ms
32+
var statusCode = null;
33+
34+
// do the first function until the second function returns true
35+
async.doUntil(function(done) {
36+
attempts++;
37+
if (attempts > 1) {
38+
debug('attempt', attempts, 'timeout', timeout);
39+
}
40+
setTimeout(function() {
41+
request(req, function(e, h, b) {
42+
statusCode = h && h.statusCode || 500;
43+
done(null, [e, h, b]);
44+
});
45+
}, timeout);
46+
}, function() {
47+
// this function returns false for the first 'maxAttempts' 429s receieved
48+
if (statusCode === 429 && attempts < maxAttempts) {
49+
if (attempts === 1) {
50+
timeout = firstTimeout;
51+
} else {
52+
timeout *= 2;
53+
}
54+
return false;
55+
}
56+
return true;
57+
}, function(e, results) {
58+
callback(results[0], results[1], results[2])
59+
});
60+
};
61+
62+
return myrequest;
63+
};
64+
65+
66+

0 commit comments

Comments
 (0)