mam-project-templates
Version:
my personal templating projects
1,646 lines (1,472 loc) • 171 kB
JavaScript
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.PouchDB=e():"undefined"!=typeof global?global.PouchDB=e():"undefined"!=typeof self&&(self.PouchDB=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/*globals cordova */
"use strict";
var utils = require('./utils');
var merge = require('./merge');
var errors = require('./deps/errors');
var call = utils.call;
/*
* A generic pouch adapter
*/
// returns first element of arr satisfying callback predicate
function arrayFirst(arr, callback) {
for (var i = 0; i < arr.length; i++) {
if (callback(arr[i], i) === true) {
return arr[i];
}
}
return false;
}
// Wrapper for functions that call the bulkdocs api with a single doc,
// if the first result is an error, return an error
function yankError(callback) {
return function (err, results) {
if (err || results[0].error) {
call(callback, err || results[0]);
} else {
call(callback, null, results[0]);
}
};
}
// for every node in a revision tree computes its distance from the closest
// leaf
function computeHeight(revs) {
var height = {};
var edges = [];
merge.traverseRevTree(revs, function (isLeaf, pos, id, prnt) {
var rev = pos + "-" + id;
if (isLeaf) {
height[rev] = 0;
}
if (prnt !== undefined) {
edges.push({from: prnt, to: rev});
}
return rev;
});
edges.reverse();
edges.forEach(function (edge) {
if (height[edge.from] === undefined) {
height[edge.from] = 1 + height[edge.to];
} else {
height[edge.from] = Math.min(height[edge.from], 1 + height[edge.to]);
}
});
return height;
}
module.exports = function (Pouch) {
return function (opts, callback) {
var api = {};
var customApi = Pouch.adapters[opts.adapter](opts, function (err, db) {
if (err) {
if (callback) {
callback(err);
}
return;
}
for (var j in api) {
if (!db.hasOwnProperty(j)) {
db[j] = api[j];
}
}
// Don't call Pouch.open for ALL_DBS
// Pouch.open saves the db's name into ALL_DBS
if (opts.name === Pouch.prefix + Pouch.ALL_DBS) {
callback(err, db);
} else {
Pouch.open(opts, function (err) {
callback(err, db);
});
}
});
var auto_compaction = (opts.auto_compaction === true);
// wraps a callback with a function that runs compaction after each edit
function autoCompact(callback) {
if (!auto_compaction) {
return callback;
}
return function (err, res) {
if (err) {
call(callback, err);
} else {
var count = res.length;
var decCount = function () {
count--;
if (!count) {
call(callback, null, res);
}
};
res.forEach(function (doc) {
if (doc.ok) {
// TODO: we need better error handling
compactDocument(doc.id, 1, decCount);
} else {
decCount();
}
});
}
};
}
api.post = function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
return call(callback, errors.NOT_AN_OBJECT);
}
return customApi.bulkDocs({docs: [doc]}, opts,
autoCompact(yankError(callback)));
};
api.put = function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object') {
return call(callback, errors.NOT_AN_OBJECT);
}
if (!utils.isValidId(doc._id)) {
return call(callback, errors.MISSING_ID);
}
return customApi.bulkDocs({docs: [doc]}, opts,
autoCompact(yankError(callback)));
};
api.putAttachment = function (docId, attachmentId, rev, blob, type, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('putAttachment', arguments);
return;
}
if (typeof type === 'function') {
callback = type;
type = blob;
blob = rev;
rev = null;
}
if (typeof type === 'undefined') {
type = blob;
blob = rev;
rev = null;
}
function createAttachment(doc) {
doc._attachments = doc._attachments || {};
doc._attachments[attachmentId] = {
content_type: type,
data: blob
};
api.put(doc, callback);
}
api.get(docId, function (err, doc) {
// create new doc
if (err && err.error === errors.MISSING_DOC.error) {
createAttachment({_id: docId});
return;
}
if (err) {
call(callback, err);
return;
}
if (doc._rev !== rev) {
call(callback, errors.REV_CONFLICT);
return;
}
createAttachment(doc);
});
};
api.removeAttachment = function (docId, attachmentId, rev, callback) {
api.get(docId, function (err, obj) {
if (err) {
call(callback, err);
return;
}
if (obj._rev !== rev) {
call(callback, errors.REV_CONFLICT);
return;
}
if (!obj._attachments) {
return call(callback, null);
}
delete obj._attachments[attachmentId];
if (Object.keys(obj._attachments).length === 0) {
delete obj._attachments;
}
api.put(obj, callback);
});
};
api.remove = function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (opts === undefined) {
opts = {};
}
opts.was_delete = true;
var newDoc = {_id: doc._id, _rev: doc._rev};
newDoc._deleted = true;
return customApi.bulkDocs({docs: [newDoc]}, opts, yankError(callback));
};
api.revsDiff = function (req, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
var ids = Object.keys(req);
var count = 0;
var missing = {};
function addToMissing(id, revId) {
if (!missing[id]) {
missing[id] = {missing: []};
}
missing[id].missing.push(revId);
}
function processDoc(id, rev_tree) {
// Is this fast enough? Maybe we should switch to a set simulated by a map
var missingForId = req[id].slice(0);
merge.traverseRevTree(rev_tree, function (isLeaf, pos, revHash, ctx,
opts) {
var rev = pos + '-' + revHash;
var idx = missingForId.indexOf(rev);
if (idx === -1) {
return;
}
missingForId.splice(idx, 1);
if (opts.status !== 'available') {
addToMissing(id, rev);
}
});
// Traversing the tree is synchronous, so now `missingForId` contains
// revisions that were not found in the tree
missingForId.forEach(function (rev) {
addToMissing(id, rev);
});
}
ids.map(function (id) {
customApi._getRevisionTree(id, function (err, rev_tree) {
if (err && err.name === 'not_found' && err.message === 'missing') {
missing[id] = {missing: req[id]};
} else if (err) {
return call(callback, err);
} else {
processDoc(id, rev_tree);
}
if (++count === ids.length) {
return call(callback, null, missing);
}
});
});
};
// compact one document and fire callback
// by compacting we mean removing all revisions which
// are further from the leaf in revision tree than max_height
function compactDocument(docId, max_height, callback) {
customApi._getRevisionTree(docId, function (err, rev_tree) {
if (err) {
return call(callback);
}
var height = computeHeight(rev_tree);
var candidates = [];
var revs = [];
Object.keys(height).forEach(function (rev) {
if (height[rev] > max_height) {
candidates.push(rev);
}
});
merge.traverseRevTree(rev_tree, function (isLeaf, pos, revHash, ctx, opts) {
var rev = pos + '-' + revHash;
if (opts.status === 'available' && candidates.indexOf(rev) !== -1) {
opts.status = 'missing';
revs.push(rev);
}
});
customApi._doCompaction(docId, rev_tree, revs, callback);
});
}
// compact the whole database using single document
// compaction
api.compact = function (opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
api.changes({complete: function (err, res) {
if (err) {
call(callback); // TODO: silently fail
return;
}
var count = res.results.length;
if (!count) {
call(callback);
return;
}
res.results.forEach(function (row) {
compactDocument(row.id, 0, function () {
count--;
if (!count) {
call(callback);
}
});
});
}});
};
/* Begin api wrappers. Specific functionality to storage belongs in the _[method] */
api.get = function (id, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('get', arguments);
return;
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
var leaves = [];
function finishOpenRevs() {
var result = [];
var count = leaves.length;
if (!count) {
return call(callback, null, result);
}
// order with open_revs is unspecified
leaves.forEach(function (leaf) {
api.get(id, {rev: leaf, revs: opts.revs}, function (err, doc) {
if (!err) {
result.push({ok: doc});
} else {
result.push({missing: leaf});
}
count--;
if (!count) {
call(callback, null, result);
}
});
});
}
if (opts.open_revs) {
if (opts.open_revs === "all") {
customApi._getRevisionTree(id, function (err, rev_tree) {
if (err) {
// if there's no such document we should treat this
// situation the same way as if revision tree was empty
rev_tree = [];
}
leaves = merge.collectLeaves(rev_tree).map(function (leaf) {
return leaf.rev;
});
finishOpenRevs();
});
} else {
if (Array.isArray(opts.open_revs)) {
leaves = opts.open_revs;
for (var i = 0; i < leaves.length; i++) {
var l = leaves[i];
// looks like it's the only thing couchdb checks
if (!(typeof(l) === "string" && /^\d+-/.test(l))) {
return call(callback, errors.error(errors.BAD_REQUEST,
"Invalid rev format"));
}
}
finishOpenRevs();
} else {
return call(callback, errors.error(errors.UNKNOWN_ERROR,
'function_clause'));
}
}
return; // open_revs does not like other options
}
return customApi._get(id, opts, function (err, result) {
if (err) {
return call(callback, err);
}
var doc = result.doc;
var metadata = result.metadata;
var ctx = result.ctx;
if (opts.conflicts) {
var conflicts = merge.collectConflicts(metadata);
if (conflicts.length) {
doc._conflicts = conflicts;
}
}
if (opts.revs || opts.revs_info) {
var paths = merge.rootToLeaf(metadata.rev_tree);
var path = arrayFirst(paths, function (arr) {
return arr.ids.map(function (x) { return x.id; })
.indexOf(doc._rev.split('-')[1]) !== -1;
});
path.ids.splice(path.ids.map(function (x) {return x.id; })
.indexOf(doc._rev.split('-')[1]) + 1);
path.ids.reverse();
if (opts.revs) {
doc._revisions = {
start: (path.pos + path.ids.length) - 1,
ids: path.ids.map(function (rev) {
return rev.id;
})
};
}
if (opts.revs_info) {
var pos = path.pos + path.ids.length;
doc._revs_info = path.ids.map(function (rev) {
pos--;
return {
rev: pos + '-' + rev.id,
status: rev.opts.status
};
});
}
}
if (opts.local_seq) {
doc._local_seq = result.metadata.seq;
}
if (opts.attachments && doc._attachments) {
var attachments = doc._attachments;
var count = Object.keys(attachments).length;
if (count === 0) {
return call(callback, null, doc);
}
Object.keys(attachments).forEach(function (key) {
customApi._getAttachment(attachments[key], {encode: true, ctx: ctx}, function (err, data) {
doc._attachments[key].data = data;
if (!--count) {
call(callback, null, doc);
}
});
});
} else {
if (doc._attachments) {
for (var key in doc._attachments) {
doc._attachments[key].stub = true;
}
}
call(callback, null, doc);
}
});
};
api.getAttachment = function (docId, attachmentId, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('getAttachment', arguments);
return;
}
if (opts instanceof Function) {
callback = opts;
opts = {};
}
customApi._get(docId, opts, function (err, res) {
if (err) {
return call(callback, err);
}
if (res.doc._attachments && res.doc._attachments[attachmentId]) {
opts.ctx = res.ctx;
customApi._getAttachment(res.doc._attachments[attachmentId], opts, callback);
} else {
return call(callback, errors.MISSING_DOC);
}
});
};
api.allDocs = function (opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('allDocs', arguments);
return;
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if ('keys' in opts) {
if ('startkey' in opts) {
call(callback, errors.error(errors.QUERY_PARSE_ERROR,
'Query parameter `start_key` is not compatible with multi-get'
));
return;
}
if ('endkey' in opts) {
call(callback, errors.error(errors.QUERY_PARSE_ERROR,
'Query parameter `end_key` is not compatible with multi-get'
));
return;
}
}
if (typeof opts.skip === 'undefined') {
opts.skip = 0;
}
return customApi._allDocs(opts, callback);
};
function processChange(doc, metadata, opts) {
var changeList = [{rev: doc._rev}];
if (opts.style === 'all_docs') {
changeList = merge.collectLeaves(metadata.rev_tree)
.map(function (x) { return {rev: x.rev}; });
}
var change = {
id: metadata.id,
changes: changeList,
doc: doc
};
if (utils.isDeleted(metadata, doc._rev)) {
change.deleted = true;
}
if (opts.conflicts) {
change.doc._conflicts = merge.collectConflicts(metadata);
if (!change.doc._conflicts.length) {
delete change.doc._conflicts;
}
}
return change;
}
api.changes = function (opts) {
if (!api.taskqueue.ready()) {
var task = api.taskqueue.addTask('changes', arguments);
return {
cancel: function () {
if (task.task) {
return task.task.cancel();
}
if (Pouch.DEBUG) {
//console.log('Cancel Changes Feed');
}
task.parameters[0].aborted = true;
}
};
}
opts = utils.extend(true, {}, opts);
opts.processChange = processChange;
if (!opts.since) {
opts.since = 0;
}
if (opts.since === 'latest') {
var changes;
api.info(function (err, info) {
if (!opts.aborted) {
opts.since = info.update_seq - 1;
api.changes(opts);
}
});
// Return a method to cancel this method from processing any more
return {
cancel: function () {
if (changes) {
return changes.cancel();
}
if (Pouch.DEBUG) {
//console.log('Cancel Changes Feed');
}
opts.aborted = true;
}
};
}
if (opts.filter && typeof opts.filter === 'string') {
if (opts.filter === '_view') {
if (opts.view && typeof opts.view === 'string') {
// fetch a view from a design doc, make it behave like a filter
var viewName = opts.view.split('/');
api.get('_design/' + viewName[0], function (err, ddoc) {
if (ddoc && ddoc.views && ddoc.views[viewName[1]]) {
/*jshint evil: true */
var filter = eval('(function () {' +
' return function (doc) {' +
' var emitted = false;' +
' var emit = function (a, b) {' +
' emitted = true;' +
' };' +
' var view = ' + ddoc.views[viewName[1]].map + ';' +
' view(doc);' +
' if (emitted) {' +
' return true;' +
' }' +
' }' +
'})()');
if (!opts.aborted) {
opts.filter = filter;
api.changes(opts);
}
} else {
var msg = ddoc.views ? 'missing json key: ' + viewName[1] :
'missing json key: views';
err = err || errors.error(errors.MISSING_DOC, msg);
utils.call(opts.complete, err);
}
});
} else {
var err = errors.error(errors.BAD_REQUEST,
'`view` filter parameter is not provided.');
utils.call(opts.complete, err);
}
} else {
// fetch a filter from a design doc
var filterName = opts.filter.split('/');
api.get('_design/' + filterName[0], function (err, ddoc) {
if (ddoc && ddoc.filters && ddoc.filters[filterName[1]]) {
/*jshint evil: true */
var filter = eval('(function () { return ' +
ddoc.filters[filterName[1]] + ' })()');
if (!opts.aborted) {
opts.filter = filter;
api.changes(opts);
}
} else {
var msg = (ddoc && ddoc.filters) ? 'missing json key: ' + filterName[1]
: 'missing json key: filters';
err = err || errors.error(errors.MISSING_DOC, msg);
utils.call(opts.complete, err);
}
});
}
// Return a method to cancel this method from processing any more
return {
cancel: function () {
if (Pouch.DEBUG) {
console.log('Cancel Changes Feed');
}
opts.aborted = true;
}
};
}
if (!('descending' in opts)) {
opts.descending = false;
}
// 0 and 1 should return 1 document
opts.limit = opts.limit === 0 ? 1 : opts.limit;
return customApi._changes(opts);
};
api.close = function (callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('close', arguments);
return;
}
return customApi._close(callback);
};
api.info = function (callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('info', arguments);
return;
}
return customApi._info(callback);
};
api.id = function () {
return customApi._id();
};
api.type = function () {
return (typeof customApi._type === 'function') ? customApi._type() : opts.adapter;
};
api.bulkDocs = function (req, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('bulkDocs', arguments);
return;
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (!opts) {
opts = {};
} else {
opts = utils.extend(true, {}, opts);
}
if (!req || !req.docs || req.docs.length < 1) {
return call(callback, errors.MISSING_BULK_DOCS);
}
if (!Array.isArray(req.docs)) {
return call(callback, errors.QUERY_PARSE_ERROR);
}
for (var i = 0; i < req.docs.length; ++i) {
if (typeof req.docs[i] !== 'object' || Array.isArray(req.docs[i])) {
return call(callback, errors.NOT_AN_OBJECT);
}
}
req = utils.extend(true, {}, req);
if (!('new_edits' in opts)) {
opts.new_edits = true;
}
return customApi._bulkDocs(req, opts, autoCompact(callback));
};
/* End Wrappers */
var taskqueue = {};
taskqueue.ready = false;
taskqueue.queue = [];
api.taskqueue = {};
api.taskqueue.execute = function (db) {
if (taskqueue.ready) {
taskqueue.queue.forEach(function (d) {
d.task = db[d.name].apply(null, d.parameters);
});
}
};
api.taskqueue.ready = function () {
if (arguments.length === 0) {
return taskqueue.ready;
}
taskqueue.ready = arguments[0];
};
api.taskqueue.addTask = function (name, parameters) {
var task = { name: name, parameters: parameters };
taskqueue.queue.push(task);
return task;
};
api.replicate = {};
api.replicate.from = function (url, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
return Pouch.replicate(url, customApi, opts, callback);
};
api.replicate.to = function (dbName, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
return Pouch.replicate(customApi, dbName, opts, callback);
};
for (var j in api) {
if (!customApi.hasOwnProperty(j)) {
customApi[j] = api[j];
}
}
// Http adapter can skip setup so we force the db to be ready and execute any jobs
if (opts.skipSetup) {
api.taskqueue.ready(true);
api.taskqueue.execute(api);
}
if (utils.isCordova()) {
//to inform websql adapter that we can use api
cordova.fireWindowEvent(opts.name + "_pouch", {});
}
return customApi;
};
};
},{"./deps/errors":8,"./merge":13,"./utils":16}],2:[function(require,module,exports){
"use strict";
var utils = require('../utils');
var errors = require('../deps/errors');
// parseUri 1.2.2
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
function parseUri(str) {
var o = parseUri.options;
var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str);
var uri = {};
var i = 14;
while (i--) {
uri[o.key[i]] = m[i] || "";
}
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) {
uri[o.q.name][$1] = $2;
}
});
return uri;
}
function encodeDocId(id) {
if (/^_(design|local)/.test(id)) {
return id;
}
return encodeURIComponent(id);
}
parseUri.options = {
strictMode: false,
key: ["source", "protocol", "authority", "userInfo", "user", "password", "host",
"port", "relative", "path", "directory", "file", "query", "anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
// Get all the information you possibly can about the URI given by name and
// return it as a suitable object.
function getHost(name, opts) {
// If the given name contains "http:"
if (/http(s?):/.test(name)) {
// Prase the URI into all its little bits
var uri = parseUri(name);
// Store the fact that it is a remote URI
uri.remote = true;
// Store the user and password as a separate auth object
if (uri.user || uri.password) {
uri.auth = {username: uri.user, password: uri.password};
}
// Split the path part of the URI into parts using '/' as the delimiter
// after removing any leading '/' and any trailing '/'
var parts = uri.path.replace(/(^\/|\/$)/g, '').split('/');
// Store the first part as the database name and remove it from the parts
// array
uri.db = parts.pop();
// Restore the path by joining all the remaining parts (all the parts
// except for the database name) with '/'s
uri.path = parts.join('/');
opts = opts || {};
uri.headers = opts.headers || {};
if (opts.auth || uri.auth) {
var nAuth = opts.auth || uri.auth;
var token = utils.btoa(nAuth.username + ':' + nAuth.password);
uri.headers.Authorization = 'Basic ' + token;
}
if (opts.headers) {
uri.headers = opts.headers;
}
return uri;
}
// If the given name does not contain 'http:' then return a very basic object
// with no host, the current path, the given name as the database name and no
// username/password
return {host: '', path: '/', db: name, auth: false};
}
// Generate a URL with the host data given by opts and the given path
function genDBUrl(opts, path) {
// If the host is remote
if (opts.remote) {
// If the host already has a path, then we need to have a path delimiter
// Otherwise, the path delimiter is the empty string
var pathDel = !opts.path ? '' : '/';
// Return the URL made up of all the host's information and the given path
return opts.protocol + '://' + opts.host + ':' + opts.port + '/' +
opts.path + pathDel + opts.db + '/' + path;
}
// If the host is not remote, then return the URL made up of just the
// database name and the given path
return '/' + opts.db + '/' + path;
}
// Generate a URL with the host data given by opts and the given path
function genUrl(opts, path) {
if (opts.remote) {
// If the host already has a path, then we need to have a path delimiter
// Otherwise, the path delimiter is the empty string
var pathDel = !opts.path ? '' : '/';
// If the host already has a path, then we need to have a path delimiter
// Otherwise, the path delimiter is the empty string
return opts.protocol + '://' + opts.host + ':' + opts.port + '/' + opts.path + pathDel + path;
}
return '/' + path;
}
// Implements the PouchDB API for dealing with CouchDB instances over HTTP
function HttpPouch(opts, callback) {
// Parse the URI given by opts.name into an easy-to-use object
var host = getHost(opts.name, opts);
// Generate the database URL based on the host
var db_url = genDBUrl(host, '');
// The functions that will be publically available for HttpPouch
var api = {};
var ajaxOpts = opts.ajax || {};
function ajax(options, callback) {
return utils.ajax(utils.extend({}, ajaxOpts, options), callback);
}
var uuids = {
list: [],
get: function (opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {count: 10};
}
var cb = function (err, body) {
if (err || !('uuids' in body)) {
utils.call(callback, err || errors.UNKNOWN_ERROR);
} else {
uuids.list = uuids.list.concat(body.uuids);
utils.call(callback, null, "OK");
}
};
var params = '?count=' + opts.count;
ajax({
headers: host.headers,
method: 'GET',
url: genUrl(host, '_uuids') + params
}, cb);
}
};
// Create a new CouchDB database based on the given opts
var createDB = function () {
ajax({headers: host.headers, method: 'PUT', url: db_url}, function (err, ret) {
// If we get an "Unauthorized" error
if (err && err.status === 401) {
// Test if the database already exists
ajax({headers: host.headers, method: 'HEAD', url: db_url}, function (err, ret) {
// If there is still an error
if (err) {
// Give the error to the callback to deal with
utils.call(callback, err);
} else {
// Continue as if there had been no errors
utils.call(callback, null, api);
}
});
// If there were no errros or if the only error is "Precondition Failed"
// (note: "Precondition Failed" occurs when we try to create a database
// that already exists)
} else if (!err || err.status === 412) {
// Continue as if there had been no errors
utils.call(callback, null, api);
} else {
utils.call(callback, errors.UNKNOWN_ERROR);
}
});
};
if (!opts.skipSetup) {
ajax({headers: host.headers, method: 'GET', url: db_url}, function (err, ret) {
//check if the db exists
if (err) {
if (err.status === 404) {
//if it doesn't, create it
createDB();
} else {
utils.call(callback, err);
}
} else {
//go do stuff with the db
utils.call(callback, null, api);
}
});
}
api.type = function () {
return 'http';
};
// The HttpPouch's ID is its URL
api.id = function () {
return genDBUrl(host, '');
};
api.request = function (options, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('request', arguments);
return;
}
options.headers = host.headers;
options.url = genDBUrl(host, options.url);
ajax(options, callback);
};
// Sends a POST request to the host calling the couchdb _compact function
// version: The version of CouchDB it is running
api.compact = function (opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('compact', arguments);
return;
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
ajax({
headers: host.headers,
url: genDBUrl(host, '_compact'),
method: 'POST'
}, function () {
function ping() {
api.info(function (err, res) {
if (!res.compact_running) {
utils.call(callback, null);
} else {
setTimeout(ping, opts.interval || 200);
}
});
}
// Ping the http if it's finished compaction
if (typeof callback === "function") {
ping();
}
});
};
// Calls GET on the host, which gets back a JSON string containing
// couchdb: A welcome string
// version: The version of CouchDB it is running
api.info = function (callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('info', arguments);
return;
}
ajax({
headers: host.headers,
method: 'GET',
url: genDBUrl(host, '')
}, callback);
};
// Get the document with the given id from the database given by host.
// The id could be solely the _id in the database, or it may be a
// _design/ID or _local/ID path
api.get = function (id, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('get', arguments);
return;
}
// If no options were given, set the callback to the second parameter
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (opts.auto_encode === undefined) {
opts.auto_encode = true;
}
// List of parameters to add to the GET request
var params = [];
// If it exists, add the opts.revs value to the list of parameters.
// If revs=true then the resulting JSON will include a field
// _revisions containing an array of the revision IDs.
if (opts.revs) {
params.push('revs=true');
}
// If it exists, add the opts.revs_info value to the list of parameters.
// If revs_info=true then the resulting JSON will include the field
// _revs_info containing an array of objects in which each object
// representing an available revision.
if (opts.revs_info) {
params.push('revs_info=true');
}
if (opts.local_seq) {
params.push('local_seq=true');
}
// If it exists, add the opts.open_revs value to the list of parameters.
// If open_revs=all then the resulting JSON will include all the leaf
// revisions. If open_revs=["rev1", "rev2",...] then the resulting JSON
// will contain an array of objects containing data of all revisions
if (opts.open_revs) {
if (opts.open_revs !== "all") {
opts.open_revs = JSON.stringify(opts.open_revs);
}
params.push('open_revs=' + opts.open_revs);
}
// If it exists, add the opts.attachments value to the list of parameters.
// If attachments=true the resulting JSON will include the base64-encoded
// contents in the "data" property of each attachment.
if (opts.attachments) {
params.push('attachments=true');
}
// If it exists, add the opts.rev value to the list of parameters.
// If rev is given a revision number then get the specified revision.
if (opts.rev) {
params.push('rev=' + opts.rev);
}
// If it exists, add the opts.conflicts value to the list of parameters.
// If conflicts=true then the resulting JSON will include the field
// _conflicts containing all the conflicting revisions.
if (opts.conflicts) {
params.push('conflicts=' + opts.conflicts);
}
// Format the list of parameters into a valid URI query string
params = params.join('&');
params = params === '' ? '' : '?' + params;
if (opts.auto_encode) {
id = encodeDocId(id);
}
// Set the options for the ajax call
var options = {
headers: host.headers,
method: 'GET',
url: genDBUrl(host, id + params)
};
// If the given id contains at least one '/' and the part before the '/'
// is NOT "_design" and is NOT "_local"
// OR
// If the given id contains at least two '/' and the part before the first
// '/' is "_design".
// TODO This second condition seems strange since if parts[0] === '_design'
// then we already know that parts[0] !== '_local'.
var parts = id.split('/');
if ((parts.length > 1 && parts[0] !== '_design' && parts[0] !== '_local') ||
(parts.length > 2 && parts[0] === '_design' && parts[0] !== '_local')) {
// Binary is expected back from the server
options.binary = true;
}
// Get the document
ajax(options, function (err, doc, xhr) {
// If the document does not exist, send an error to the callback
if (err) {
return utils.call(callback, err);
}
// Send the document to the callback
utils.call(callback, null, doc, xhr);
});
};
// Delete the document given by doc from the database given by host.
api.remove = function (doc, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('remove', arguments);
return;
}
// If no options were given, set the callback to be the second parameter
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
// Delete the document
ajax({
headers: host.headers,
method: 'DELETE',
url: genDBUrl(host, encodeDocId(doc._id)) + '?rev=' + doc._rev
}, callback);
};
// Get the attachment
api.getAttachment = function (docId, attachmentId, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (opts.auto_encode === undefined) {
opts.auto_encode = true;
}
if (opts.auto_encode) {
docId = encodeDocId(docId);
}
opts.auto_encode = false;
api.get(docId + '/' + attachmentId, opts, callback);
};
// Remove the attachment given by the id and rev
api.removeAttachment = function (docId, attachmentId, rev, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('removeAttachment', arguments);
return;
}
ajax({
headers: host.headers,
method: 'DELETE',
url: genDBUrl(host, encodeDocId(docId) + '/' + attachmentId) + '?rev=' + rev
}, callback);
};
// Add the attachment given by blob and its contentType property
// to the document with the given id, the revision given by rev, and
// add it to the database given by host.
api.putAttachment = function (docId, attachmentId, rev, blob, type, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('putAttachment', arguments);
return;
}
if (typeof type === 'function') {
callback = type;
type = blob;
blob = rev;
rev = null;
}
if (typeof type === 'undefined') {
type = blob;
blob = rev;
rev = null;
}
var id = encodeDocId(docId) + '/' + attachmentId;
var url = genDBUrl(host, id);
if (rev) {
url += '?rev=' + rev;
}
var opts = {
headers: host.headers,
method: 'PUT',
url: url,
processData: false,
body: blob,
timeout: 60000
};
opts.headers['Content-Type'] = type;
// Add the attachment
ajax(opts, callback);
};
// Add the document given by doc (in JSON string format) to the database
// given by host. This fails if the doc has no _id field.
api.put = function (doc, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('put', arguments);
return;
}
// If no options were given, set the callback to be the second parameter
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object') {
return utils.call(callback, errors.NOT_AN_OBJECT);
}
if (!utils.isValidId(doc._id)) {
return utils.call(callback, errors.MISSING_ID);
}
// List of parameter to add to the PUT request
var params = [];
// If it exists, add the opts.new_edits value to the list of parameters.
// If new_edits = false then the database will NOT assign this document a
// new revision number
if (opts && typeof opts.new_edits !== 'undefined') {
params.push('new_edits=' + opts.new_edits);
}
// Format the list of parameters into a valid URI query string
params = params.join('&');
if (params !== '') {
params = '?' + params;
}
// Add the document
ajax({
headers: host.headers,
method: 'PUT',
url: genDBUrl(host, encodeDocId(doc._id)) + params,
body: doc
}, callback);
};
// Add the document given by doc (in JSON string format) to the database
// given by host. This does not assume that doc is a new document (i.e. does not
// have a _id or a _rev field.
api.post = function (doc, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('post', arguments);
return;
}
// If no options were given, set the callback to be the second parameter
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object') {
return utils.call(callback, errors.NOT_AN_OBJECT);
}
if (! ("_id" in doc)) {
if (uuids.list.length > 0) {
doc._id = uuids.list.pop();
api.put(doc, opts, callback);
} else {
uuids.get(function (err, resp) {
if (err) {
return utils.call(callback, errors.UNKNOWN_ERROR);
}
doc._id = uuids.list.pop();
api.put(doc, opts, callback);
});
}
} else {
api.put(doc, opts, callback);
}
};
// Update/create multiple documents given by req in the database
// given by host.
api.bulkDocs = function (req, opts, callback) {
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('bulkDocs', arguments);
return;
}
// If no options were given, set the callback to be the second parameter
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (!opts) {
opts = {};
}
// If opts.new_edits exists add it to the document data to be
// send to the database.
// If new_edits=false then it prevents the database from creating
// new revision numbers for the documents. Instead it just uses
// the old ones. This is used in database replication.
if (typeof opts.new_edits !== 'undefined') {
req.new_edits = opts.new_edits;
}
// Update/create the documents
ajax({
headers: host.headers,
method: 'POST',
url: genDBUrl(host, '_bulk_docs'),
body: req
}, callback);
};
// Get a listing of the documents in the database given
// by host and ordered by increasing id.
api.allDocs = function (opts, callback) {
// If no options were given, set the callback to be the second parameter
if (!api.taskqueue.ready()) {
api.taskqueue.addTask('allDocs', arguments);
return;
}
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
// List of parameters to add to the GET request
var params = [];
var body;
var method = 'GET';
// TODO I don't see conflicts as a valid parameter for a
// _all_docs request (see http://wiki.apache.org/couchdb/HTTP_Document_API#all_docs)
if (opts.conflicts) {
params.push('conflicts=true');
}
// If opts.descending is truthy add it to params
if (opts.descending) {
params.push('descending=true');
}
// If opts.include_docs exists, add the include_docs value to the
// list of parameters.
// If include_docs=true then include the associated document with each
// result.
if (opts.include_docs) {
params.push('include_docs=true');
}
// If opts.startkey exists, add the startkey value to the list of
// parameters.
// If startkey is given then the returned list of documents will
// start with the document whose id is startkey.
if (opts.startkey) {
params.push('startkey=' +
encodeURIComponent(JSON.stringify(opts.startkey)));
}
// If opts.endkey exists, add the endkey value to the list of parameters.
// If endkey is given then the returned list of docuemnts will
// end with the document whose id is endkey.
if (opts.endkey) {
params.push('endkey=' + encodeURIComponent(JSON.stringify(opts.endkey)));
}
// If opts.limit exists, add the limit value to the parameter list.
if (opts.limit) {
params.push('limit=' + opts.limit);
}
if (typeof opts.skip !== 'undefined') {
params.push('skip=' + opts.skip);
}
// Format the list of parameters into a valid URI query string
params = params.join('&');
if (params !== '') {
params = '?' + params;
}
// If keys are supplied, issue a POST request to circumvent GET query string limits
// see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
if (typeof opts.keys !== 'undefined') {
method = 'POST';
body = JSON.stringify({keys: opts.keys});
}
// Get the document listing
ajax({
headers: host.headers,
method: method,
url: genDBUrl(host, '_all_docs' + params),
body: body
}, callback);
};
// Get a list of changes made to documents in the database given by host.
// TODO According to the README, there should be two other methods here,
// api.changes.addListener and api.changes.removeListener.
api.changes = function (opts) {
// We internally page the results of a changes request, this means
// if there is a large set of changes to be returned we can start
// processing them quicker instead of waiting on the entire
// set of changes to return and attempting to process them at once
var CHANGES_LIMIT = 25;
if (!api.taskqueue.ready()) {
var task = api.taskqueue.addTask('changes', arguments);
return {
cancel: function () {
if (task.task) {
return task.task.cancel();
}
//console.log(db_url + ': Cancel Changes Feed');
task.parameters[0].aborted = true;
}
};
}
if (opts.since === 'latest') {
var changes;
api.info(function (err, info) {
if (!opts.aborted) {
opts.since = info.update_seq;
changes = api.changes(opts);
}
});
// Return a method to cancel this method from processing any more
return {
cancel: function () {
if (changes) {
return changes.cancel();
}
//console.log(db_url + ': Cancel Changes Feed');
opts.aborted = true;
}
};
}
//console.log(db_url + ': Start Changes Feed: continuous=' + opts.continuous);
var params = {};
var limit = (typeof opts.limit !== 'undefined') ? opts.limit : false;
if (limit === 0) {
limit = 1;
}
//
var leftToFetch = limit;
if (opts.style) {
params.style = opts.style;
}
if (opts.include_docs || opts.filter && typeof opts.filter === 'function') {
params.include_docs = true;
}
if (opts.continuous) {
params.feed = 'longpoll';
}
if (opts.conflicts) {
params.conflicts = true;
}
if (opts.descending) {
params.descending = true;
}
if (opts.filter && typeof opts.filter === 'string') {
params.filter = opts.filter;
if (opts.filter === '_view' && opts.view && typeof opts.view === 'string') {
params.view = opts.view;
}
}
// If opts.query_params exists, pass it through to the changes request.
// These parameters may be used by the filter on the source database.
if (opts.query_params && typeof opts.query_params === 'object') {
for (var param_name in opts.query_params) {
if (opts.query_params.hasOwnProperty(param_name)) {
params[param_name] = opts.query_params[param_name];
}
}
}
var xhr;
var lastFetchedSeq;
var remoteLastSeq;
var pagingCount;
// Get all the changes starting wtih the one immediately after the
// sequence number given by since.
var fetch = function (since, callback) {
params.since = since;
if (!opts.continuous && !pagingCount) {
pagingCount = remoteLastSeq;
}
params.limit = (!limit || leftToFetch > CHANGES_LIMIT) ?
CHANGES_LIMIT : leftToFetch;
var paramStr = '?' + Object.keys(params).map(function (k) {
return k + '=' + params[k];
}).join('&');
// Set the options for the ajax call
var xhrOpts = {
headers: host.headers,
method: 'GET',
url: genDBUrl(host, '_changes' + paramStr),
// _changes can take a long time to generate, especially when filtered
timeout: null
};
lastFetchedSeq = since;
if (opts.aborted) {
return;
}
// Get the changes
xhr = ajax(xhrOpts, callback);
};
// If opts.since exists, get all the changes from the sequence
// number given by opts.since. Otherwise, get all the changes
// from the sequence number 0.
var fetchTimeout = 10;
var fetchRetryCount = 0;
var results = {results: []};
var fetched = function (err, res) {
// If the result of the ajax call (res) contains changes (res.results)
if (res && res.results) {
results.last_seq = res.last_seq;
// For each change
var req = {};
req.query = opts.query_params;
res.results = res.results.filter(function (c) {
leftToFetch--;
var ret = utils.filterChange(opts)(c);
if (ret) {
results.results.push(c);
utils.call(opts.onChange, c);
}
return ret;
});
} else if (err) {
// In case of an error, stop listening for changes and call opts.complete
opts.aborted = true;
utils.call(opts.complete, err, null);
}
// The changes feed may have timed out with no results
// if so reuse last update sequence
if (res && res.last_seq) {