blob: b54f838c2ca1320aceaa30497b0cca6d1fd632e8 [file] [log] [blame]
"use strict";
var utils = require('./utils');
var merge = require('./merge');
var errors = require('./deps/errors');
var EventEmitter = require('events').EventEmitter;
var upsert = require('./deps/upsert');
var Changes = require('./changes');
var Promise = utils.Promise;
/*
* 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] && results[0].error)) {
callback(err || results[0]);
} else {
callback(null, results.length ? results[0] : results);
}
};
}
// 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;
}
function allDocsKeysQuery(api, opts, callback) {
var keys = ('limit' in opts) ?
opts.keys.slice(opts.skip, opts.limit + opts.skip) :
(opts.skip > 0) ? opts.keys.slice(opts.skip) : opts.keys;
if (opts.descending) {
keys.reverse();
}
if (!keys.length) {
return api._allDocs({limit: 0}, callback);
}
var finalResults = {
offset: opts.skip
};
return Promise.all(keys.map(function (key) {
var subOpts = utils.extend(true, {key: key, deleted: 'ok'}, opts);
['limit', 'skip', 'keys'].forEach(function (optKey) {
delete subOpts[optKey];
});
return new Promise(function (resolve, reject) {
api._allDocs(subOpts, function (err, res) {
if (err) {
return reject(err);
}
finalResults.total_rows = res.total_rows;
resolve(res.rows[0] || {key: key, error: 'not_found'});
});
});
})).then(function (results) {
finalResults.rows = results;
return finalResults;
});
}
utils.inherits(AbstractPouchDB, EventEmitter);
module.exports = AbstractPouchDB;
function AbstractPouchDB() {
var self = this;
EventEmitter.call(this);
var listeners = 0, changes;
var eventNames = ['change', 'delete', 'create', 'update'];
this.on('newListener', function (eventName) {
if (~eventNames.indexOf(eventName)) {
if (listeners) {
listeners++;
return;
} else {
listeners++;
}
} else {
return;
}
var lastChange = 0;
changes = this.changes({
conflicts: true,
include_docs: true,
continuous: true,
since: 'now',
onChange: function (change) {
if (change.seq <= lastChange) {
return;
}
lastChange = change.seq;
self.emit('change', change);
if (change.doc._deleted) {
self.emit('delete', change);
} else if (change.doc._rev.split('-')[0] === '1') {
self.emit('create', change);
} else {
self.emit('update', change);
}
}
});
});
this.on('removeListener', function (eventName) {
if (~eventNames.indexOf(eventName)) {
listeners--;
if (listeners) {
return;
}
} else {
return;
}
changes.cancel();
});
}
AbstractPouchDB.prototype.post =
utils.adapterFun('post', function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
return callback(errors.error(errors.NOT_AN_OBJECT));
}
this.bulkDocs({docs: [doc]}, opts, yankError(callback));
});
AbstractPouchDB.prototype.put =
utils.adapterFun('put', utils.getArguments(function (args) {
var temp, temptype, opts, callback;
var doc = args.shift();
var id = '_id' in doc;
if (typeof doc !== 'object' || Array.isArray(doc)) {
callback = args.pop();
return callback(errors.error(errors.NOT_AN_OBJECT));
}
doc = utils.clone(doc);
while (true) {
temp = args.shift();
temptype = typeof temp;
if (temptype === "string" && !id) {
doc._id = temp;
id = true;
} else if (temptype === "string" && id && !('_rev' in doc)) {
doc._rev = temp;
} else if (temptype === "object") {
opts = temp;
} else if (temptype === "function") {
callback = temp;
}
if (!args.length) {
break;
}
}
opts = opts || {};
var error = utils.invalidIdError(doc._id);
if (error) {
return callback(error);
}
if (utils.isLocalId(doc._id) && typeof this._putLocal === 'function') {
if (doc._deleted) {
return this._removeLocal(doc, callback);
} else {
return this._putLocal(doc, callback);
}
}
this.bulkDocs({docs: [doc]}, opts, yankError(callback));
}));
AbstractPouchDB.prototype.putAttachment =
utils.adapterFun('putAttachment', function (docId, attachmentId, rev,
blob, type, callback) {
var api = this;
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
};
return api.put(doc);
}
return api.get(docId).then(function (doc) {
if (doc._rev !== rev) {
throw errors.error(errors.REV_CONFLICT);
}
return createAttachment(doc);
}, function (err) {
// create new doc
if (err.reason === errors.MISSING_DOC.message) {
return createAttachment({_id: docId});
} else {
throw err;
}
});
});
AbstractPouchDB.prototype.removeAttachment =
utils.adapterFun('removeAttachment', function (docId, attachmentId, rev,
callback) {
var self = this;
self.get(docId, function (err, obj) {
if (err) {
callback(err);
return;
}
if (obj._rev !== rev) {
callback(errors.error(errors.REV_CONFLICT));
return;
}
if (!obj._attachments) {
return callback();
}
delete obj._attachments[attachmentId];
if (Object.keys(obj._attachments).length === 0) {
delete obj._attachments;
}
self.put(obj, callback);
});
});
AbstractPouchDB.prototype.remove =
utils.adapterFun('remove', function (docOrId, optsOrRev, opts, callback) {
var doc;
if (typeof optsOrRev === 'string') {
// id, rev, opts, callback style
doc = {
_id: docOrId,
_rev: optsOrRev
};
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
} else {
// doc, opts, callback style
doc = docOrId;
if (typeof optsOrRev === 'function') {
callback = optsOrRev;
opts = {};
} else {
callback = opts;
opts = optsOrRev;
}
}
opts = utils.clone(opts || {});
opts.was_delete = true;
var newDoc = {_id: doc._id, _rev: (doc._rev || opts.rev)};
newDoc._deleted = true;
if (utils.isLocalId(newDoc._id) && typeof this._removeLocal === 'function') {
return this._removeLocal(doc, callback);
}
this.bulkDocs({docs: [newDoc]}, opts, yankError(callback));
});
AbstractPouchDB.prototype.revsDiff =
utils.adapterFun('revsDiff', function (req, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = utils.clone(opts);
var ids = Object.keys(req);
if (!ids.length) {
return callback(null, {});
}
var count = 0;
var missing = new utils.Map();
function addToMissing(id, revId) {
if (!missing.has(id)) {
missing.set(id, {missing: []});
}
missing.get(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) {
this._getRevisionTree(id, function (err, rev_tree) {
if (err && err.status === 404 && err.message === 'missing') {
missing.set(id, {missing: req[id]});
} else if (err) {
return callback(err);
} else {
processDoc(id, rev_tree);
}
if (++count === ids.length) {
// convert LazyMap to object
var missingObj = {};
missing.forEach(function (value, key) {
missingObj[key] = value;
});
return callback(null, missingObj);
}
});
}, this);
});
// 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
AbstractPouchDB.prototype.compactDocument =
utils.adapterFun('compactDocument', function (docId, maxHeight, callback) {
var self = this;
this._getRevisionTree(docId, function (err, revTree) {
if (err) {
return callback(err);
}
var height = computeHeight(revTree);
var candidates = [];
var revs = [];
Object.keys(height).forEach(function (rev) {
if (height[rev] > maxHeight) {
candidates.push(rev);
}
});
merge.traverseRevTree(revTree, function (isLeaf, pos, revHash, ctx, opts) {
var rev = pos + '-' + revHash;
if (opts.status === 'available' && candidates.indexOf(rev) !== -1) {
revs.push(rev);
}
});
self._doCompaction(docId, revs, callback);
});
});
// compact the whole database using single document
// compaction
AbstractPouchDB.prototype.compact =
utils.adapterFun('compact', function (opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
var self = this;
opts = utils.clone(opts || {});
self.get('_local/compaction').catch(function () {
return false;
}).then(function (doc) {
if (typeof self._compact === 'function') {
if (doc && doc.last_seq) {
opts.last_seq = doc.last_seq;
}
return self._compact(opts, callback);
}
});
});
AbstractPouchDB.prototype._compact = function (opts, callback) {
var self = this;
var changesOpts = {
returnDocs: false,
last_seq: opts.last_seq || 0
};
var promises = [];
function onChange(row) {
promises.push(self.compactDocument(row.id, 0));
}
function onComplete(resp) {
var lastSeq = resp.last_seq;
Promise.all(promises).then(function () {
return upsert(self, '_local/compaction', function deltaFunc(doc) {
if (!doc.last_seq || doc.last_seq < lastSeq) {
doc.last_seq = lastSeq;
return doc;
}
return false; // somebody else got here first, don't update
});
}).then(function () {
callback(null, {ok: true});
}).catch(callback);
}
self.changes(changesOpts)
.on('change', onChange)
.on('complete', onComplete)
.on('error', callback);
};
/* Begin api wrappers. Specific functionality to storage belongs in the
_[method] */
AbstractPouchDB.prototype.get =
utils.adapterFun('get', function (id, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof id !== 'string') {
return callback(errors.error(errors.INVALID_ID));
}
if (utils.isLocalId(id) && typeof this._getLocal === 'function') {
return this._getLocal(id, callback);
}
var leaves = [], self = this;
function finishOpenRevs() {
var result = [];
var count = leaves.length;
if (!count) {
return callback(null, result);
}
// order with open_revs is unspecified
leaves.forEach(function (leaf) {
self.get(id, {
rev: leaf,
revs: opts.revs,
attachments: opts.attachments
}, function (err, doc) {
if (!err) {
result.push({ok: doc});
} else {
result.push({missing: leaf});
}
count--;
if (!count) {
callback(null, result);
}
});
});
}
if (opts.open_revs) {
if (opts.open_revs === "all") {
this._getRevisionTree(id, function (err, rev_tree) {
if (err) {
return callback(err);
}
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 callback(errors.error(errors.INVALID_REV));
}
}
finishOpenRevs();
} else {
return callback(errors.error(errors.UNKNOWN_ERROR,
'function_clause'));
}
}
return; // open_revs does not like other options
}
return this._get(id, opts, function (err, result) {
opts = utils.clone(opts);
if (err) {
return 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 (utils.isDeleted(metadata, doc._rev)) {
doc._deleted = true;
}
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;
});
var indexOfRev = path.ids.map(function (x) {return x.id; })
.indexOf(doc._rev.split('-')[1]) + 1;
var howMany = path.ids.length - indexOfRev;
path.ids.splice(indexOfRev, howMany);
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) {
utils.info('The "local_seq" option is deprecated and will be removed');
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 callback(null, doc);
}
Object.keys(attachments).forEach(function (key) {
this._getAttachment(attachments[key],
{encode: true, ctx: ctx}, function (err, data) {
var att = doc._attachments[key];
att.data = data;
delete att.stub;
delete att.length;
if (!--count) {
callback(null, doc);
}
});
}, self);
} else {
if (doc._attachments) {
for (var key in doc._attachments) {
if (doc._attachments.hasOwnProperty(key)) {
doc._attachments[key].stub = true;
}
}
}
callback(null, doc);
}
});
});
AbstractPouchDB.prototype.getAttachment =
utils.adapterFun('getAttachment', function (docId, attachmentId, opts,
callback) {
var self = this;
if (opts instanceof Function) {
callback = opts;
opts = {};
}
opts = utils.clone(opts);
this._get(docId, opts, function (err, res) {
if (err) {
return callback(err);
}
if (res.doc._attachments && res.doc._attachments[attachmentId]) {
opts.ctx = res.ctx;
self._getAttachment(res.doc._attachments[attachmentId], opts, callback);
} else {
return callback(errors.error(errors.MISSING_DOC));
}
});
});
AbstractPouchDB.prototype.allDocs =
utils.adapterFun('allDocs', function (opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = utils.clone(opts);
opts.skip = typeof opts.skip !== 'undefined' ? opts.skip : 0;
if ('keys' in opts) {
if (!Array.isArray(opts.keys)) {
return callback(new TypeError('options.keys must be an array'));
}
var incompatibleOpt =
['startkey', 'endkey', 'key'].filter(function (incompatibleOpt) {
return incompatibleOpt in opts;
})[0];
if (incompatibleOpt) {
callback(errors.error(errors.QUERY_PARSE_ERROR,
'Query parameter `' + incompatibleOpt +
'` is not compatible with multi-get'
));
return;
}
if (this.type() !== 'http') {
return allDocsKeysQuery(this, opts, callback);
}
}
return this._allDocs(opts, callback);
});
AbstractPouchDB.prototype.changes = function (opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
return new Changes(this, opts, callback);
};
AbstractPouchDB.prototype.close =
utils.adapterFun('close', function (callback) {
this._closed = true;
return this._close(callback);
});
AbstractPouchDB.prototype.info = utils.adapterFun('info', function (callback) {
var self = this;
this._info(function (err, info) {
if (err) {
return callback(err);
}
// assume we know better than the adapter, unless it informs us
info.db_name = info.db_name || self._db_name;
info.auto_compaction = !!(self.auto_compaction && self.type() !== 'http');
callback(null, info);
});
});
AbstractPouchDB.prototype.id = utils.adapterFun('id', function (callback) {
return this._id(callback);
});
AbstractPouchDB.prototype.type = function () {
return (typeof this._type === 'function') ? this._type() : this.adapter;
};
AbstractPouchDB.prototype.bulkDocs =
utils.adapterFun('bulkDocs', function (req, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
opts = utils.clone(opts);
if (Array.isArray(req)) {
req = {
docs: req
};
}
if (!req || !req.docs || !Array.isArray(req.docs)) {
return callback(errors.error(errors.MISSING_BULK_DOCS));
}
for (var i = 0; i < req.docs.length; ++i) {
if (typeof req.docs[i] !== 'object' || Array.isArray(req.docs[i])) {
return callback(errors.error(errors.NOT_AN_OBJECT));
}
}
req = utils.clone(req);
if (!('new_edits' in opts)) {
if ('new_edits' in req) {
opts.new_edits = req.new_edits;
} else {
opts.new_edits = true;
}
}
if (!opts.new_edits && this.type() !== 'http') {
// ensure revisions of the same doc are sorted, so that
// the local adapter processes them correctly (#2935)
req.docs.sort(function (a, b) {
var idCompare = utils.compare(a._id, b._id);
if (idCompare !== 0) {
return idCompare;
}
var aStart = a._revisions ? a._revisions.start : 0;
var bStart = b._revisions ? b._revisions.start : 0;
return utils.compare(aStart, bStart);
});
}
req.docs.forEach(function (doc) {
if (doc._deleted) {
delete doc._attachments; // ignore atts for deleted docs
}
});
return this._bulkDocs(req, opts, function (err, res) {
if (err) {
return callback(err);
}
if (!opts.new_edits) {
// this is what couch does when new_edits is false
res = res.filter(function (x) {
return x.error;
});
}
callback(null, res);
});
});
AbstractPouchDB.prototype.registerDependentDatabase =
utils.adapterFun('registerDependentDatabase', function (dependentDb,
callback) {
var depDB = new this.constructor(dependentDb, this.__opts || {});
function diffFun(doc) {
doc.dependentDbs = doc.dependentDbs || {};
if (doc.dependentDbs[dependentDb]) {
return false; // no update required
}
doc.dependentDbs[dependentDb] = true;
return doc;
}
upsert(this, '_local/_pouch_dependentDbs', diffFun, function (err) {
if (err) {
return callback(err);
}
return callback(null, {db: depDB});
});
});