Source: index.js

/* 
 * Copyright (C) 2016 Alexis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';
var DEFAULT_DESIGN_DOC = 'pouchdb-dump-select';

//Module loading
var PouchDB = require("pouchdb");
var Args = require("vargs").Constructor;
var URL = require("url");
var Clone = require("clone");
var Promise = require("lie");
var Equal = require("deep-equal");
var Merge = require("merge");
/**
 * Constructor for the DumpSelect object.
 * @constructor
 * @param {String} url The url of the PouchDB database.
 * @param {String} username [] The username of the remote database if required.
 * @param {String} password [] The password of the remote database if required.
 */
var DumpSelect = function (url, username, password) {

	url = this._buildUrl(url, username, password);
	//Init

	/**
	 * @public
	 */

	this.db = new PouchDB(url);
};


/**
 * Get all the document according to the ids.
 * @param {Array|String} ids The id or the ids of the documents to fetch.
 * @param {Object} queryOpts [{}]  Additionnal PouchDB query options.
 * @returns {Promise}	Returns a promise with the an array of rows as result.
 */
DumpSelect.prototype.getAll = function (ids, queryOpts) {
	var options = {include_docs: true};
	var dis = this;
	//Keys convert to string
	if ((ids !== null && ids !== undefined) && !Array.isArray(ids))
		ids = ids + "";
	if (ids) {
		if (!Array.isArray(ids))
			options.key = ids;
		else
			options.keys = ids;
	}
	if (queryOpts && typeof queryOpts === "object")
		options = Merge(queryOpts, options);
	return this.db.allDocs(options).then(function (result) {
		return new Promise(function (resolve, reject) {
			resolve(dis._cleanRows(result));
		});
	});
};


/**
 * Get the rows from a view.
 * @param {String} view		The view name without the _design. Eg : "global/by_name".
 * @param {Array|String} keys	The key or keys to fetch from the view.
 * @param {Object} queryOptions [{}]	Additionnal PouchDB query options.
 * @returns {Promise}	Returns a promise with the rows as result.
 */
DumpSelect.prototype.getByView = function (view, keys, queryOptions) {
	var options = {include_docs: true};
	var dis = this;

	//Keys convert to string
	if ((keys !== null && keys !== undefined) && !Array.isArray(keys))
		keys = keys + "";
	if (keys)
		options['key' + (Array.isArray(keys) ? 's' : '')] = keys;

	if (queryOptions && typeof queryOptions === "object")
		options = Merge(queryOptions, options);
	return this.db.query(view, options).then(function (result) {
		return new Promise(function (resolve, reject) {
			resolve(dis._cleanRows(result));
		});
	}).catch(function (err) {
		console.error(err);
	});
};

/**
 * Get rows by a key-values conditions.
 * @param {String} key	The key of each doc that will be compared.
 * @param {Array|String} values	The value(s) to fetch from the key-value condition.
 * @param {Object} queryOptions [{}]	Additionnal  PouchDB query options. 
 * @param {Boolean} useDesignDoc [false] Determine if a design document will be created or no.
 * @returns {Promise}	Return a promise with the rows that matched the key-value condition.
 */
DumpSelect.prototype.getByKeyValue = function (key, values, queryOptions, useDesignDoc) {
	//Callback reference fix
	var dis = this;

	//Parameter validation
	if (!key)
		throw new TypeError("The key parameter must not be null");
	if (arguments.length >= 4) {
		if (typeof queryOptions !== "object")
			queryOptions = {};
		if (typeof useDesignDoc !== "boolean")
			useDesignDoc = false;
	} else {
		if (typeof queryOptions === "boolean") {
			useDesignDoc = queryOptions;
			queryOptions = {};
		} else {
			useDesignDoc = false;
		}
	}
	var defaultOpts = {include_docs: true};
	//Keys convert to string
	if ((values !== null && values !== undefined) && !Array.isArray(values))
		values = values + "";
	if (values)
		defaultOpts["key" + (Array.isArray(values) ? 's' : '')] = values;

	return this._getViewParameter(key, useDesignDoc).then(function (view) {
		queryOptions = Merge(queryOptions, defaultOpts);
		return dis.db.query(view, queryOptions);
	}).then(function (result) {
		return new Promise(function (resolve, reject) {
			resolve(dis._cleanRows(result));
		});
	}).catch(function (err) {
		console.error(err);
	});
};

//<editor-fold desc="Private functions" defaultstate="collapsed">

/**
 * Clean the result object of CouchDB by returning only valid rows.
 * @private
 * @param {Object} result
 * @returns {Array} Returns an array of valid rows.
 */
DumpSelect.prototype._cleanRows = function (result) {
	var rows = [];
	if (result && result.rows && result.rows.length > 0)
		for (var i = 0; i < result.rows.length; i++)
			if (!result.rows[i].error && result.rows[i].doc)
				rows.push(result.rows[i].doc);
	return rows;
};

/**
 * Create a view on the defined design document.
 * @private
 * @throws {TypeError} Throws a TypeError if the supplied parameters are invalids.
 * @param {String} name	The name of the view to add
 * @param {Function|String} map The map function or string.
 * @param {Function|String} reduce	The reduce string or function
 * @returns {Promise}	Returns a promise.
 */
DumpSelect.prototype._getOrCreateView = function (name, map, reduce) {
	var dis = this;
	if (!name)
		throw new TypeError("You must specified the name of your view");

	//Check if it exists
	return dis._getDesignDoc(this.db, DEFAULT_DESIGN_DOC).then(function (doc) {
		newDoc = dis._putView(doc, name, map, reduce);
		if (Equal(doc, newDoc)) {
			return new Promise(function (resolve, reject) {
				resolve(newDoc);
			});
		} else
			return dis.db.put(doc);

	}).then(function (result) {
		return new Promise(function (resolve, reject) {
			resolve(DEFAULT_DESIGN_DOC + '/' + name);
		});
	}).catch(function (err) {
		console.error("An error occured" + err);
	});
};

/**
 * Put a view into a design document
 * @private
 * @param {Object} doc	The document to add the view to.
 * @param {String} name	The name of the view to add or update
 * @param {String|Function} map	The map function or the map string
 * @param {String|Function} reduce	The reduce function or the reduce string.
 * @returns {Object}	Returns the document updated if possible.
 */
DumpSelect.prototype._putView = function (doc, name, map, reduce) {
	if (!doc || typeof doc !== "object")
		return doc;
	//Document validation
	var newDoc = Clone(doc);
	if (!newDoc.views)
		newDoc.views = {};
	//Create the view
	if (!newDoc.views[name])
		newDoc.views[name] = {};
	//Validate map and reduce props.
	if (!newDoc.views[name].map)
		newDoc.views[name] = {map: "", reduce: ""};
	//Add map
	if (map && newDoc.views.map != map.toString()) //Update if necessary
		newDoc.views[name].map = map.toString();
	//Add reduce
	if (reduce && newDoc.views.reduce != map.toString())
		newDoc.views[reduce].reduce = reduce.toString();
	return newDoc;
};

/**
 * Get the design doc and creates it if it's not existing
 * @private
 * @param {type} name	The name of the design document
 * @returns {Promise}
 */
DumpSelect.prototype._getDesignDoc = function (name) {
	var id = '_design/' + name;
	var dis = this;
	this.db.get(id).catch(function (err) {
		if (err.status === '404')
			return dis.put({
				_id: '_design/' + name,
				language: "javascript"
			});
	});
};

/**
 * Returns a promise with the view parameter of the .query() function. 
 * This could be either a design/view name or a map function.
 * @param {String} key	The key used for the mapping
 * @param {type} useDesignDoc [false] Determine if a design document will be created in the database.
 *  If not, it will be filtered locally.
 * @returns {Promise}	Returns a promise with the first parameter.
 */
DumpSelect.prototype._getViewParameter = function (key, useDesignDoc) {
		var mapFn = "function(doc){if(doc." + key + ")emit(doc." + key + ");}";
	if (useDesignDoc){//We try to fetch a _design/view string
		return this._getOrCreateView('by_' + key, mapFn);
	}
	else {//We use local filtering with a javascript function
		
		return new Promise(function (resolve, reject) {
			resolve({map:mapFn});
		});
	}
};

/**
 * Build the database URL for PouchDB.
 * @private
 * @throws {TypeError} Throws a type error if the username and password are not well supplied.
 * @param {String} url The url of the database
 * @param {String} username [] The username for the remote database if required.
 * @param {String} password [] The password for the remote database if required.
 */
DumpSelect.prototype._buildUrl = function (url, username, password) {
	if ((password && !username) || (!password && username))
		throw new TypeError("Both username and password must be defined. You can't provide only one of them.");
	if (username) {
		var parsedUrl = URL.parse(url);
		if (!parsedUrl.protocol)
			throw new TypeError("Username/Password are only for remote databases");
		url = parsedUrl.protocol + '//' + encodeURIComponent(username) + ':' + encodeURIComponent(password) + '@' + parsedUrl.host + parsedUrl.path;
	}
	return url;
};

//</editor-fold>

module.exports = DumpSelect;