| "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}); |
| }); |
| }); |