/* eslint-env node */
'use strict';
/*global Promise:true*/
/**
* Promise class as provided by the bluedbird library. If a Promise is
* resolved it will yield a {@link CouchResult} if it is rejected it yields a
* {@link CouchError}.
*
* @class Promise
* @see {@link https://github.com/petkaantonov/bluebird.git|bluebird}
*/
var Promise = require('bluebird');
var urljoin = require('url-join');
var _ = require('lodash');
var Database = require('./database');
var util = require('./util');
var CouchResult = util.CouchResult;
var CouchError = util.CouchError;
var AuthError = util.AuthError;
/**
* Extracts non-null options from a given object and attach them to the other
* given object.
*
* @param opts_list {Array} an array of strings representing the option names
* @param src {Object} the object to check for set options
* @param dst {Object} the object to set found options on
*
* TODO: MA -- 2014/10/24 -- Think about migrating to own module
*/
function getOptions(opts_list, src, dst) {
if (src) {
_.forEach(opts_list, function (opt) {
if (src[opt]) {
dst[opt] = src[opt];
}
});
}
}
/**
* Creates a Connection to a CouchDB Instance.
*
* @class
* @classdesc A connection object aims to provide all RESTful HTTP verbs,
* supported by CouchDB via convenience functions which provide the require
* mechanisms for creating the proper HTTP requests.
*
* @param {String} url - The base URL to the CouchDB Instance
* @param {Object} request - An object supporting the 'request' interface
*/
function Connection(url, request, options) {
/**
* The url to the CouchDB instance
* @member {String}
*/
this.url = url;
/**
* Default request configuration. We set the accept header and configure
* request to encode/decode json for us.
*
* @member {Object}
*/
this.req_defaults = {
headers: { 'Accept': 'application/json' },
json: true
};
/**
* Indicates whether to use Cookie authentication
* @member {boolean}
*/
this.useCookie = false;
/**
* The username to use for connecting to the couchdb host
* @member {String}
*/
this.user = '';
/**
* Ths password to use for connecting to the CouchDB host
* @member {String}
*/
this.passwd = '';
// Variable initialization done --> connection setup
getOptions(['user', 'passwd', 'useCookie'], options, this);
if (!this.useCookie && this.user) {
// We do not use cookie auth -- configure basic auth
this.req_defaults.auth = {
user: this.user,
pass: this.passwd,
sendImmediately: true
};
}
/**
* An Object fulfilling the request API
* @member {Object}
* @see {@link https://github.com/request/request|request}
*/
this.request = request.defaults(this.req_defaults);
}
/**
* The bare-metal HTTP request handling function returning a promise.
*
* WARNING: it is recommended to use make_request as it can handle
* automatic cookie-based login/relogin.
*
* @param {String} method - The HTTP verb to be used in the request
* @param {String} path - The relative path for the request
* @param {Object} opts - An Object containing optional parameters for the request:
* @param {Object} opts.headers - Object containing key/value pairs of custom
* HTTP Headers
* @param {Object} opts.parms - Object containing key/value pairs of parameters to
* be appended to the URL must be JSON encodable
* @param {Object} opts.body - Object or Array to be JSON encoded and put into
* the request body
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.http_request = function http_request(method, path, opts) {
var self = this;
var req = {
method: method,
url: urljoin(this.url, path)
};
if (opts) {
if (opts.headers) {
req.headers = opts.headers;
}
if (opts.parms) {
req.qs = opts.parms;
}
if (opts.body) {
req.body = opts.body;
}
}
if (this.useCookie) {
req.jar = true;
if (!req.headers) {
req.headers = {};
}
req.headers['X-CouchDB-WWW-Authenticate'] = 'Cookie';
}
return new Promise(function do_request(resolve, reject) {
self.request(req, function handle_response (err, resp, body) {
if (err) {
return reject(err);
}
if (resp.statusCode < 400) {
resolve(new CouchResult(resp, body));
} else if (resp.statusCode === 401) {
// Special case for handling relogins with cooike auth
reject(new AuthError(resp, body));
} else {
reject(new CouchError(resp, body));
}
});
});
};
/**
* Do a cookie-based login to the CouchDB and then retry a given request.
*
* @returns {Promise} Returns a promise that either resolves successful to the
* result of the specified request or is rejected because
* of the failed authentication.
*/
Connection.prototype.login_and_retry = function login_and_retry (method, path, opts) {
var self = this;
var login = {
name: this.user,
password: this.passwd
};
return this.http_request('POST', '_session', { body: login })
.then(function () {
return self.http_request(method, path, opts);
});
};
/**
* The central workhorse for HTTP requests. Does automatic login and relogin
* in casse 'useCookie' is set on the connection and credentials have been
* supplied.
*
* @param {String} method - The HTTP verb to be used in the request
* @param {String} path - The relative path for the request
* @param {Object} opts - An Object containing optional parameters for the request:
* @param {Object} opts.headers - Object containing key/value pairs of custom
* HTTP Headers
* @param {Object} opts.parms - Object containing key/value pairs of parameters to
* be appended to the URL must be JSON encodable
* @param {Object} opts.body - Object or Array to be JSON encoded and put into
* the request body
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.make_request = function make_request(method, path, opts) {
var self = this;
if (this.useCookie) {
// Try to make the request but intercept auth failues
return this.http_request(method, path, opts).catch(AuthError, function () {
return self.login_and_retry(method, path, opts);
});
} else {
return this.http_request(method, path, opts);
}
};
/**
* Convenience method to send a GET request to a CouchDB host.
*
* @param {String} path - the path to the desired object below the host-URL
* @param {Object} parms - Object containing key/vale pairs of parametrers to
* be appended to the URL must be JSON encodable
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.get = function get(path, parms) {
return this.make_request('GET', path, { parms: parms });
};
/**
* Convenience method to send a POST request to a CouchDB host.
*
* @param {String} path - the path to the desired object below the host-URL
* @param {Object} body - Object or Array to be JSON encoded and put into
* the request body
* @param {Object} parms - Object containing key/vale pairs of parametrers to
* be appended to the URL must be JSON encodable
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.post = function post(path, body, parms) {
return this.make_request('POST', path, { body: body, parms: parms });
};
/**
* Convenience method to send a PUT request to a CouchDB host.
*
* @param {String} path - the path to the desired object below the host-URL
* @param {Object} body - Object or Array to be JSON encoded and put into
* the request body
* @param {Object} parms - Object containing key/vale pairs of parametrers to
* be appended to the URL must be JSON encodable
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.put = function put(path, body, parms) {
return this.make_request('PUT', path, { body: body, parms: parms });
};
/**
* Convenience method to send a DELETE request to a CouchDB host.
*
* @param {String} path - the path to the desired object below the host-URL
* @param {Object} parms - Object containing key/vale pairs of parametrers to
* be appended to the URL. Must be JSON encodable.
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.delete = function _delete(path, parms) {
return this.make_request('DELETE', path, { parms: parms });
};
/**
* Convenience method to send a HEAD request to a CouchDB host.
*
* @param {String} path - the path to the desired object below the host-URL
* @param {Object} parms - Object containing key/vale pairs of parametrers to
* be appended to the URL. Must be JSON encodable.
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link CouchResult} is returned, if it rejects a {@link CouchError} is
* returned.
*/
Connection.prototype.head = function head(path, parms) {
return this.make_request('HEAD', path, { parms: parms });
};
/**
* Convenience method to open a database on a CouchDB host.
*
* @param {String} dbname - the name of the database to be opened on the
* database host.
* @param {Object} options - options for creating the database.
* @param {bool} options.create - indicates whether the database should
* be created by the host if it is not yet
* existing.
*
* @returns {Promise} A {@link Promise} object. If the promise resolves a
* {@link Database} is returned. In case the database does not exist and
* cannot be created either, a {@link CouchError} is thrown.
*/
Connection.prototype.openDB = function openDB(dbname, options) {
var self = this;
var create = true;
if (options && options.create === false) {
create = false;
}
return self.head(dbname).then(function () {
return new Database(dbname, self);
}).catch(CouchError, function (error) {
if (error.statusCode === 404 && create) {
return self.put(dbname).then(function () {
return new Database(dbname, self);
});
} else {
throw error;
}
});
};
// Put Errors into Connection Object so clients can
// access the type information.
Connection.CouchError = CouchError;
Connection.CouchResult = CouchResult;
Connection.AuthError = AuthError;
module.exports = Connection;