blob: fe8017bdfcf4a2fbc012f40c37c6fe4e7f6a63be [file] [log] [blame]
"use strict";
var utils = require('./utils');
var pick = require('./deps/pick');
var toPromise = require('./deps/toPromise');
var collections = require('pouchdb-collections');
var inherits = require('inherits');
var getArguments = require('argsarray');
var adapterFun = require('./deps/adapterFun');
var errors = require('./deps/errors');
var EventEmitter = require('events').EventEmitter;
var upsert = require('./deps/upsert');
var Changes = require('./changes');
var bulkGetShim = require('./deps/bulkGetShim');
var Promise = utils.Promise;
var isDeleted = require('./deps/docs/isDeleted');
var isLocalId = require('./deps/docs/isLocalId');
var traverseRevTree = require('./deps/merge/traverseRevTree');
var collectLeaves = require('./deps/merge/collectLeaves');
var rootToLeaf = require('./deps/merge/rootToLeaf');
var collectConflicts = require('./deps/merge/collectConflicts');
var parseDoc = require('./deps/docs/parseDoc');
/*
* A generic pouch adapter
*/
function compare(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
// 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];
}
}
}
// 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);
}
};
}
// clean docs given to us by the user
function cleanDocs(docs) {
for (var i = 0; i < docs.length; i++) {
var doc = docs[i];
if (doc._deleted) {
delete doc._attachments; // ignore atts for deleted docs
} else if (doc._attachments) {
// filter out extraneous keys from _attachments
var atts = Object.keys(doc._attachments);
for (var j = 0; j < atts.length; j++) {
var att = atts[j];
doc._attachments[att] = pick(doc._attachments[att],
['data', 'digest', 'content_type', 'length', 'revpos', 'stub']);
}
}
}
}
// compare two docs, first by _id then by _rev
function compareByIdThenRev(a, b) {
var idCompare = 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 compare(aStart, bStart);
}
// for every node in a revision tree computes its distance from the closest
// leaf
function computeHeight(revs) {
var height = {};
var edges = [];
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({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) {
/* istanbul ignore if */
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;
});
}
// all compaction is done in a queue, to avoid attaching
// too many listeners at once
function doNextCompaction(self) {
var task = self._compactionQueue[0];
var opts = task.opts;
var callback = task.callback;
self.get('_local/compaction').catch(function () {
return false;
}).then(function (doc) {
if (doc && doc.last_seq) {
opts.last_seq = doc.last_seq;
}
self._compact(opts, function (err, res) {
/* istanbul ignore if */
if (err) {
callback(err);
} else {
callback(null, res);
}
process.nextTick(function () {
self._compactionQueue.shift();
if (self._compactionQueue.length) {
doNextCompaction(self);
}
});
});
});
}
function attachmentNameError(name) {
if (name.charAt(0) === '_') {
return name + 'is not a valid attachment name, attachment ' +
'names cannot start with \'_\'';
}
return false;
}
inherits(AbstractPouchDB, EventEmitter);
module.exports = AbstractPouchDB;
function AbstractPouchDB() {
EventEmitter.call(this);
}
AbstractPouchDB.prototype.post =
adapterFun('post', function (doc, opts, callback) {
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 =
adapterFun('put', function(doc, opts, callback) {
if (typeof doc !== 'object' || Array.isArray(doc)) {
return callback(errors.error(errors.NOT_AN_OBJECT));
}
parseDoc.invalidIdError(doc._id);
if (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 =
adapterFun('putAttachment', function (docId, attachmentId, rev,
blob, type, opts, callback) {
var api = this;
if (typeof opts === 'function') {
callback = opts;
opts = type;
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
/* istanbul ignore else */
if (err.reason === errors.MISSING_DOC.message) {
return createAttachment({_id: docId});
} else {
throw err;
}
});
});
AbstractPouchDB.prototype.removeAttachment =
adapterFun('removeAttachment', function (docId, attachmentId, rev,
opts, callback) {
var self = this;
self.get(docId, function (err, obj) {
/* istanbul ignore if */
if (err) {
callback(err);
return;
}
if (obj._rev !== rev) {
callback(errors.error(errors.REV_CONFLICT));
return;
}
/* istanbul ignore if */
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 =
adapterFun('remove', function (docOrId, optsOrRev, opts, callback) {
var doc;
if (typeof optsOrRev === 'string') {
// id, rev, opts, callback style
doc = {
_id: docOrId,
_rev: optsOrRev
};
} else {
// doc, opts, callback style
doc = docOrId;
callback = opts;
opts = optsOrRev;
}
opts = opts || {};
opts.was_delete = true;
var newDoc = {_id: doc._id, _rev: (doc._rev || opts.rev)};
newDoc._deleted = true;
if (isLocalId(newDoc._id) && typeof this._removeLocal === 'function') {
return this._removeLocal(doc, callback);
}
this.bulkDocs({docs: [newDoc]}, opts, yankError(callback));
});
AbstractPouchDB.prototype.revsDiff =
adapterFun('revsDiff', function (req, opts, callback) {
var ids = Object.keys(req);
if (!ids.length) {
return callback(null, {});
}
var count = 0;
var missing = new collections.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);
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);
/* istanbul ignore if */
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) {
/* istanbul ignore next */
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);
});
// _bulk_get API for faster replication, as described in
// https://github.com/apache/couchdb-chttpd/pull/33
// At the "abstract" level, it will just run multiple get()s in
// parallel, because this isn't much of a performance cost
// for local databases (except the cost of multiple transactions, which is
// small). The http adapter overrides this in order
// to do a more efficient single HTTP request.
AbstractPouchDB.prototype.bulkGet =
adapterFun('bulkGet', function (opts, callback) {
bulkGetShim(this, opts, callback);
});
// 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
var compactDocument = toPromise(function(self, docId, maxHeight, callback) {
self._getRevisionTree(docId, function (err, revTree) {
var height = computeHeight(revTree);
var candidates = [];
var revs = [];
Object.keys(height).forEach(function (rev) {
if (height[rev] > maxHeight) {
candidates.push(rev);
}
});
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 =
adapterFun('compact', function (opts, callback) {
var self = this;
self._compactionQueue = self._compactionQueue || [];
self._compactionQueue.push({opts: opts, callback: callback});
if (self._compactionQueue.length === 1) {
doNextCompaction(self);
}
});
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(compactDocument(self, 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 =
adapterFun('get', function (id, opts, callback) {
if (typeof id !== 'string') {
return callback(errors.error(errors.INVALID_ID));
}
if (isLocalId(id) && typeof this._getLocal === 'function') {
return this._getLocal(id, callback);
}
var leaves = [], self = this;
function finishOpenRevs() {
var result = [];
var count = leaves.length;
/* istanbul ignore if */
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 = 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) {
if (err) {
return callback(err);
}
var doc = result.doc;
var metadata = result.metadata;
var ctx = result.ctx;
if (opts.conflicts) {
var conflicts = collectConflicts(metadata);
if (conflicts.length) {
doc._conflicts = conflicts;
}
}
if (isDeleted(metadata, doc._rev)) {
doc._deleted = true;
}
if (opts.revs || opts.revs_info) {
var paths = 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.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], {
binary: opts.binary,
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) {
/* istanbul ignore else */
if (doc._attachments.hasOwnProperty(key)) {
doc._attachments[key].stub = true;
}
}
}
callback(null, doc);
}
});
});
AbstractPouchDB.prototype.getAttachment =
adapterFun('getAttachment', function (docId, attachmentId, opts,
callback) {
var self = this;
this._get(docId, opts, function (err, res) {
if (err) {
return callback(err);
}
if (res.doc._attachments && res.doc._attachments[attachmentId]) {
opts.ctx = res.ctx;
opts.binary = true;
self._getAttachment(res.doc._attachments[attachmentId], opts, callback);
} else {
return callback(errors.error(errors.MISSING_DOC));
}
});
});
AbstractPouchDB.prototype.allDocs =
adapterFun('allDocs', function (opts, callback) {
opts.skip = typeof opts.skip !== 'undefined' ? opts.skip : 0;
if (opts.start_key) {
opts.startkey = opts.start_key;
}
if (opts.end_key) {
opts.endkey = opts.end_key;
}
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 =
adapterFun('close', function (opts, callback) {
this._closed = true;
return this._close(callback);
});
AbstractPouchDB.prototype.info = adapterFun('info', function (opts, 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');
info.adapter = self.type();
callback(null, info);
});
});
AbstractPouchDB.prototype.id = adapterFun('id', function (opts, callback) {
return this._id(callback);
});
AbstractPouchDB.prototype.type = function () {
/* istanbul ignore next */
return (typeof this._type === 'function') ? this._type() : this.adapter;
};
AbstractPouchDB.prototype.bulkDocs =
adapterFun('bulkDocs', function (req, opts, callback) {
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));
}
}
var attachmentError;
req.docs.forEach(function(doc) {
if (doc._attachments) {
Object.keys(doc._attachments).forEach(function (name) {
attachmentError = attachmentError || attachmentNameError(name);
});
}
});
if (attachmentError) {
return callback(errors.error(errors.BAD_REQUEST, attachmentError));
}
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(compareByIdThenRev);
}
cleanDocs(req.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 =
adapterFun('registerDependentDatabase', function (dependentDb,
opts, 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)
.then(function () {
callback(null, {db: depDB});
}).catch(callback);
});
AbstractPouchDB.prototype.destroy =
adapterFun('destroy', function (opts, callback) {
var self = this;
var usePrefix = 'use_prefix' in self ? self.use_prefix : true;
function destroyDb() {
// call destroy method of the particular adaptor
self._destroy(opts, function (err, resp) {
if (err) {
return callback(err);
}
self.emit('destroyed');
callback(null, resp || { 'ok': true });
});
}
if (self.type() === 'http') {
// no need to check for dependent DBs if it's a remote DB
return destroyDb();
}
self.get('_local/_pouch_dependentDbs', function (err, localDoc) {
if (err) {
/* istanbul ignore if */
if (err.status !== 404) {
return callback(err);
} else { // no dependencies
return destroyDb();
}
}
var dependentDbs = localDoc.dependentDbs;
var PouchDB = self.constructor;
var deletedMap = Object.keys(dependentDbs).map(function (name) {
var trueName = usePrefix ?
name.replace(new RegExp('^' + PouchDB.prefix), '') : name;
return new PouchDB(trueName, self.__opts).destroy();
});
Promise.all(deletedMap).then(destroyDb, function (error) {
/* istanbul ignore next */
callback(error);
});
});
});