UNPKG

pouchdb-find

Version:
1,705 lines (1,528 loc) 363 kB
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.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);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.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(_dereq_,module,exports){ "use strict"; var utils = _dereq_(32); var merge = _dereq_(27); var errors = _dereq_(19); var EventEmitter = _dereq_(36).EventEmitter; var upsert = _dereq_(23); var Changes = _dereq_(13); 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}); }); }); },{}],2:[function(_dereq_,module,exports){ (function (process){ "use strict"; var CHANGES_BATCH_SIZE = 25; // according to http://stackoverflow.com/a/417184/680742, // the de factor URL length limit is 2000 characters. // but since most of our measurements don't take the full // URL into account, we fudge it a bit. // TODO: we could measure the full URL to enforce exactly 2000 chars var MAX_URL_LENGTH = 1800; var utils = _dereq_(32); var errors = _dereq_(19); var log = _dereq_(38)('pouchdb:http'); var isBrowser = typeof process === 'undefined' || process.browser; var buffer = _dereq_(18); function encodeDocId(id) { if (/^_(design|local)/.test(id)) { return id; } return encodeURIComponent(id); } function preprocessAttachments(doc) { if (!doc._attachments || !Object.keys(doc._attachments)) { return utils.Promise.resolve(); } return utils.Promise.all(Object.keys(doc._attachments).map(function (key) { var attachment = doc._attachments[key]; if (attachment.data && typeof attachment.data !== 'string') { if (isBrowser) { return new utils.Promise(function (resolve) { utils.readAsBinaryString(attachment.data, function (binary) { attachment.data = utils.btoa(binary); resolve(); }); }); } else { attachment.data = attachment.data.toString('base64'); } } })); } // 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 = utils.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 || {}; opts = utils.clone(opts); uri.headers = opts.headers || (opts.ajax && opts.ajax.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) { return genUrl(opts, 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) { // The functions that will be publicly available for HttpPouch var api = this; api.getHost = opts.getHost ? opts.getHost : getHost; // Parse the URI given by opts.name into an easy-to-use object var host = api.getHost(opts.name, opts); // Generate the database URL based on the host var dbUrl = genDBUrl(host, ''); api.getUrl = function () {return dbUrl; }; api.getHeaders = function () {return utils.clone(host.headers); }; var ajaxOpts = opts.ajax || {}; opts = utils.clone(opts); function ajax(options, callback) { var reqOpts = utils.extend({}, ajaxOpts, options); log(reqOpts.method + ' ' + reqOpts.url); return utils.ajax(reqOpts, callback); } // Create a new CouchDB database based on the given opts var createDB = function () { ajax({headers: host.headers, method: 'PUT', url: dbUrl}, function (err) { // If we get an "Unauthorized" error if (err && err.status === 401) { // Test if the database already exists ajax({headers: host.headers, method: 'HEAD', url: dbUrl}, function (err) { // If there is still an error if (err) { // Give the error to the callback to deal with callback(err); } else { // Continue as if there had been no errors 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 callback(null, api); } else { callback(err); } }); }; if (!opts.skipSetup) { ajax({headers: host.headers, method: 'GET', url: dbUrl}, function (err) { //check if the db exists if (err) { if (err.status === 404) { utils.explain404( 'PouchDB is just detecting if the remote DB exists.'); //if it doesn't, create it createDB(); } else { callback(err); } } else { //go do stuff with the db callback(null, api); } }); } api.type = function () { return 'http'; }; api.id = utils.adapterFun('id', function (callback) { ajax({ headers: host.headers, method: 'GET', url: genUrl(host, '') }, function (err, result) { var uuid = (result && result.uuid) ? result.uuid + host.db : genDBUrl(host, ''); callback(null, uuid); }); }); api.request = utils.adapterFun('request', function (options, callback) { 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 = utils.adapterFun('compact', function (opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.clone(opts); ajax({ headers: host.headers, url: genDBUrl(host, '_compact'), method: 'POST' }, function () { function ping() { api.info(function (err, res) { if (!res.compact_running) { callback(null, {ok: true}); } 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) { ajax({ headers: host.headers, method: 'GET', url: genDBUrl(host, '') }, function (err, res) { if (err) { callback(err); } else { res.host = genDBUrl(host, ''); callback(null, res); } }); }; // 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 = utils.adapterFun('get', function (id, opts, callback) { // If no options were given, set the callback to the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.clone(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 callback(err); } // Send the document to the callback callback(null, doc, xhr); }); }); // Delete the document given by doc from the database given by host. api.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; } } var rev = (doc._rev || opts.rev); // Delete the document ajax({ headers: host.headers, method: 'DELETE', url: genDBUrl(host, encodeDocId(doc._id)) + '?rev=' + rev }, callback); }); function encodeAttachmentId(attachmentId) { return attachmentId.split("/").map(encodeURIComponent).join("/"); } // Get the attachment api.getAttachment = utils.adapterFun('getAttachment', function (docId, attachmentId, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.clone(opts); if (opts.auto_encode === undefined) { opts.auto_encode = true; } if (opts.auto_encode) { docId = encodeDocId(docId); } opts.auto_encode = false; api.get(docId + '/' + encodeAttachmentId(attachmentId), opts, callback); }); // Remove the attachment given by the id and rev api.removeAttachment = utils.adapterFun('removeAttachment', function (docId, attachmentId, rev, callback) { var url = genDBUrl(host, encodeDocId(docId) + '/' + encodeAttachmentId(attachmentId)) + '?rev=' + rev; ajax({ headers: host.headers, method: 'DELETE', url: url }, 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 = utils.adapterFun('putAttachment', function (docId, attachmentId, rev, blob, type, callback) { 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) + '/' + encodeAttachmentId(attachmentId); var url = genDBUrl(host, id); if (rev) { url += '?rev=' + rev; } if (typeof blob === 'string') { var binary; try { binary = utils.atob(blob); } catch (err) { // it's not base64-encoded, so throw error return callback(errors.error(errors.BAD_ARG, 'Attachments need to be base64 encoded')); } if (isBrowser) { blob = utils.createBlob([utils.fixBinary(binary)], {type: type}); } else { blob = binary ? new buffer(binary, 'binary') : ''; } } var opts = { headers: utils.clone(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 = utils.adapterFun('put', utils.getArguments(function (args) { var temp, temptype, opts; var doc = args.shift(); var id = '_id' in doc; var callback = args.pop(); if (typeof doc !== 'object' || Array.isArray(doc)) { return callback(errors.error(errors.NOT_AN_OBJECT)); } doc = utils.clone(doc); preprocessAttachments(doc).then(function () { 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 = utils.clone(temp); } if (!args.length) { break; } } opts = opts || {}; var error = utils.invalidIdError(doc._id); if (error) { throw error; } // 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 }, function (err, res) { if (err) { return callback(err); } res.ok = true; callback(null, res); }); })["catch"](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 = utils.adapterFun('post', function (doc, opts, callback) { // If no options were given, set the callback to be the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.clone(opts); if (typeof doc !== 'object') { return callback(errors.error(errors.NOT_AN_OBJECT)); } if (! ("_id" in doc)) { doc._id = utils.uuid(); } api.put(doc, opts, function (err, res) { if (err) { return callback(err); } res.ok = true; callback(null, res); }); }); // Update/create multiple documents given by req in the database // given by host. api._bulkDocs = function (req, opts, callback) { // 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; } utils.Promise.all(req.docs.map(preprocessAttachments)).then(function () { // Update/create the documents ajax({ headers: host.headers, method: 'POST', url: genDBUrl(host, '_bulk_docs'), body: req }, function (err, results) { if (err) { return callback(err); } results.forEach(function (result) { result.ok = true; // smooths out cloudant not adding this }); callback(null, results); }); })["catch"](callback); }; // Get a listing of the documents in the database given // by host and ordered by increasing id. api.allDocs = utils.adapterFun('allDocs', function (opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.clone(opts); // List of parameters to add to the GET request var params = []; var body; var method = 'GET'; 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.attachments) { // added in CouchDB 1.6.0 params.push('attachments=true'); } if (opts.key) { params.push('key=' + encodeURIComponent(JSON.stringify(opts.key))); } // 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 (typeof opts.inclusive_end !== 'undefined') { params.push('inclusive_end=' + !!opts.inclusive_end); } // If opts.limit exists, add the limit value to the parameter list. if (typeof opts.limit !== 'undefined') { 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 (typeof opts.keys !== 'undefined') { var keysAsString = 'keys=' + encodeURIComponent(JSON.stringify(opts.keys)); if (keysAsString.length + params.length + 1 <= MAX_URL_LENGTH) { // If the keys are short enough, do a GET. we do this to work around // Safari not understanding 304s on POSTs (see issue #1239) params += (params.indexOf('?') !== -1 ? '&' : '?') + keysAsString; } else { // If keys are too long, issue a POST request to circumvent GET // query string limits // see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options 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 batchSize = 'batch_size' in opts ? opts.batch_size : CHANGES_BATCH_SIZE; opts = utils.clone(opts); opts.timeout = opts.timeout || 30 * 1000; // We give a 5 second buffer for CouchDB changes to respond with // an ok timeout var params = { timeout: opts.timeout - (5 * 1000) }; var limit = (typeof opts.limit !== 'undefined') ? opts.limit : false; if (limit === 0) { limit = 1; } var returnDocs; if ('returnDocs' in opts) { returnDocs = opts.returnDocs; } else { returnDocs = true; } // 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.attachments) { params.attachments = 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 method = 'GET'; var body; if (opts.doc_ids) { // set this automagically for the user; it's annoying that couchdb // requires both a "filter" and a "doc_ids" param. params.filter = '_doc_ids'; var docIdsJson = JSON.stringify(opts.doc_ids); if (docIdsJson.length < MAX_URL_LENGTH) { params.doc_ids = docIdsJson; } else { // anything greater than ~2000 is unsafe for gets, so // use POST instead method = 'POST'; body = {doc_ids: opts.doc_ids }; } } if (opts.continuous && api._useSSE) { return api.sse(opts, params, returnDocs); } var xhr; var lastFetchedSeq; // Get all the changes starting wtih the one immediately after the // sequence number given by since. var fetch = function (since, callback) { if (opts.aborted) { return; } params.since = since; if (typeof params.since === "object") { params.since = JSON.stringify(params.since); } if (opts.descending) { if (limit) { params.limit = leftToFetch; } } else { params.limit = (!limit || leftToFetch > batchSize) ? batchSize : 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: method, url: genDBUrl(host, '_changes' + paramStr), // _changes can take a long time to generate, especially when filtered timeout: opts.timeout, body: body }; 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 (opts.aborted) { return; } var raw_results_length = 0; // If the result of the ajax call (res) contains changes (res.results) if (res && res.results) { raw_results_length = res.results.length; 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) { if (returnDocs) { 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); return; } // The changes feed may have timed out with no results // if so reuse last update sequence if (res && res.last_seq) { lastFetchedSeq = res.last_seq; } var finished = (limit && leftToFetch <= 0) || (res && raw_results_length < batchSize) || (opts.descending); if ((opts.continuous && !(limit && leftToFetch <= 0)) || !finished) { // Increase retry delay expone