| var CHANGES_BATCH_SIZE = 25; |
| var MAX_SIMULTANEOUS_REVS = 50; |
| |
| var supportsBulkGetMap = {}; |
| |
| import { nextTick } from 'pouchdb-utils'; |
| import ajaxCore from 'pouchdb-ajax'; |
| import getArguments from 'argsarray'; |
| |
| import { |
| pick, |
| filterChange, |
| adapterFun as coreAdapterFun, |
| explainError, |
| clone, |
| parseUri, |
| bulkGetShim, |
| flatten |
| } from 'pouchdb-utils'; |
| |
| import { |
| atob, |
| btoa, |
| binaryStringToBlobOrBuffer as binStringToBluffer, |
| base64StringToBlobOrBuffer as b64StringToBluffer, |
| blobOrBufferToBase64 as blufferToBase64 |
| } from 'pouchdb-binary-utils'; |
| |
| import pool from './promise-pool'; |
| import { createError, BAD_ARG } from 'pouchdb-errors'; |
| import debug from 'debug'; |
| |
| var log = debug('pouchdb:http'); |
| |
| function readAttachmentsAsBlobOrBuffer(row) { |
| var atts = row.doc && row.doc._attachments; |
| if (!atts) { |
| return; |
| } |
| Object.keys(atts).forEach(function (filename) { |
| var att = atts[filename]; |
| att.data = b64StringToBluffer(att.data, att.content_type); |
| }); |
| } |
| |
| function encodeDocId(id) { |
| if (/^_design/.test(id)) { |
| return '_design/' + encodeURIComponent(id.slice(8)); |
| } |
| if (/^_local/.test(id)) { |
| return '_local/' + encodeURIComponent(id.slice(7)); |
| } |
| return encodeURIComponent(id); |
| } |
| |
| function preprocessAttachments(doc) { |
| if (!doc._attachments || !Object.keys(doc._attachments)) { |
| return Promise.resolve(); |
| } |
| |
| return Promise.all(Object.keys(doc._attachments).map(function (key) { |
| var attachment = doc._attachments[key]; |
| if (attachment.data && typeof attachment.data !== 'string') { |
| return new Promise(function (resolve) { |
| blufferToBase64(attachment.data, resolve); |
| }).then(function (b64) { |
| attachment.data = b64; |
| }); |
| } |
| })); |
| } |
| |
| function hasUrlPrefix(opts) { |
| if (!opts.prefix) { |
| return false; |
| } |
| |
| var protocol = parseUri(opts.prefix).protocol; |
| |
| return protocol === 'http' || protocol === 'https'; |
| } |
| |
| // Get all the information you possibly can about the URI given by name and |
| // return it as a suitable object. |
| function getHost(name, opts) { |
| |
| // encode db name if opts.prefix is a url (#5574) |
| if (hasUrlPrefix(opts)) { |
| var dbName = opts.name.substr(opts.prefix.length); |
| name = opts.prefix + encodeURIComponent(dbName); |
| } |
| |
| // Prase the URI into all its little bits |
| var uri = parseUri(name); |
| |
| // 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(); |
| // Prevent double encoding of URI component |
| if (uri.db.indexOf('%') === -1) { |
| uri.db = encodeURIComponent(uri.db); |
| } |
| |
| // Restore the path by joining all the remaining parts (all the parts |
| // except for the database name) with '/'s |
| uri.path = parts.join('/'); |
| |
| return uri; |
| } |
| |
| // 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 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.port) : '') + |
| '/' + opts.path + pathDel + path; |
| } |
| |
| function paramsToStr(params) { |
| return '?' + Object.keys(params).map(function (k) { |
| return k + '=' + encodeURIComponent(params[k]); |
| }).join('&'); |
| } |
| |
| // 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; |
| |
| var host = getHost(opts.name, opts); |
| var dbUrl = genDBUrl(host, ''); |
| |
| opts = clone(opts); |
| var ajaxOpts = opts.ajax || {}; |
| |
| if (opts.auth || host.auth) { |
| var nAuth = opts.auth || host.auth; |
| var str = nAuth.username + ':' + nAuth.password; |
| var token = btoa(unescape(encodeURIComponent(str))); |
| ajaxOpts.headers = ajaxOpts.headers || {}; |
| ajaxOpts.headers.Authorization = 'Basic ' + token; |
| } |
| |
| // Not strictly necessary, but we do this because numerous tests |
| // rely on swapping ajax in and out. |
| api._ajax = ajaxCore; |
| |
| function ajax(userOpts, options, callback) { |
| var reqAjax = userOpts.ajax || {}; |
| var reqOpts = Object.assign(clone(ajaxOpts), reqAjax, options); |
| var defaultHeaders = clone(ajaxOpts.headers || {}); |
| reqOpts.headers = Object.assign(defaultHeaders, reqAjax.headers, |
| options.headers || {}); |
| log(reqOpts.method + ' ' + reqOpts.url); |
| return api._ajax(reqOpts, callback); |
| } |
| |
| function ajaxPromise(userOpts, opts) { |
| return new Promise(function (resolve, reject) { |
| ajax(userOpts, opts, function (err, res) { |
| /* istanbul ignore if */ |
| if (err) { |
| return reject(err); |
| } |
| resolve(res); |
| }); |
| }); |
| } |
| |
| function adapterFun(name, fun) { |
| return coreAdapterFun(name, getArguments(function (args) { |
| setup().then(function () { |
| return fun.apply(this, args); |
| }).catch(function (e) { |
| var callback = args.pop(); |
| callback(e); |
| }); |
| })); |
| } |
| |
| var setupPromise; |
| |
| function setup() { |
| // TODO: Remove `skipSetup` in favor of `skip_setup` in a future release |
| if (opts.skipSetup || opts.skip_setup) { |
| return Promise.resolve(); |
| } |
| |
| // If there is a setup in process or previous successful setup |
| // done then we will use that |
| // If previous setups have been rejected we will try again |
| if (setupPromise) { |
| return setupPromise; |
| } |
| |
| var checkExists = {method: 'GET', url: dbUrl}; |
| setupPromise = ajaxPromise({}, checkExists).catch(function (err) { |
| if (err && err.status && err.status === 404) { |
| // Doesnt exist, create it |
| explainError(404, 'PouchDB is just detecting if the remote exists.'); |
| return ajaxPromise({}, {method: 'PUT', url: dbUrl}); |
| } else { |
| return Promise.reject(err); |
| } |
| }).catch(function (err) { |
| // If we try to create a database that already exists, skipped in |
| // istanbul since its catching a race condition. |
| /* istanbul ignore if */ |
| if (err && err.status && err.status === 412) { |
| return true; |
| } |
| return Promise.reject(err); |
| }); |
| |
| setupPromise.catch(function () { |
| setupPromise = null; |
| }); |
| |
| return setupPromise; |
| } |
| |
| nextTick(function () { |
| callback(null, api); |
| }); |
| |
| api.type = function () { |
| return 'http'; |
| }; |
| |
| api.id = adapterFun('id', function (callback) { |
| ajax({}, {method: 'GET', url: genUrl(host, '')}, function (err, result) { |
| var uuid = (result && result.uuid) ? |
| (result.uuid + host.db) : genDBUrl(host, ''); |
| callback(null, uuid); |
| }); |
| }); |
| |
| api.request = adapterFun('request', function (options, callback) { |
| 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 = adapterFun('compact', function (opts, callback) { |
| if (typeof opts === 'function') { |
| callback = opts; |
| opts = {}; |
| } |
| opts = clone(opts); |
| ajax(opts, { |
| url: genDBUrl(host, '_compact'), |
| method: 'POST' |
| }, function () { |
| function ping() { |
| api.info(function (err, res) { |
| if (res && !res.compact_running) { |
| callback(null, {ok: true}); |
| } else { |
| setTimeout(ping, opts.interval || 200); |
| } |
| }); |
| } |
| // Ping the http if it's finished compaction |
| ping(); |
| }); |
| }); |
| |
| api.bulkGet = coreAdapterFun('bulkGet', function (opts, callback) { |
| var self = this; |
| |
| function doBulkGet(cb) { |
| var params = {}; |
| if (opts.revs) { |
| params.revs = true; |
| } |
| if (opts.attachments) { |
| /* istanbul ignore next */ |
| params.attachments = true; |
| } |
| if (opts.latest) { |
| params.latest = true; |
| } |
| ajax(opts, { |
| url: genDBUrl(host, '_bulk_get' + paramsToStr(params)), |
| method: 'POST', |
| body: { docs: opts.docs} |
| }, cb); |
| } |
| |
| function doBulkGetShim() { |
| // avoid "url too long error" by splitting up into multiple requests |
| var batchSize = MAX_SIMULTANEOUS_REVS; |
| var numBatches = Math.ceil(opts.docs.length / batchSize); |
| var numDone = 0; |
| var results = new Array(numBatches); |
| |
| function onResult(batchNum) { |
| return function (err, res) { |
| // err is impossible because shim returns a list of errs in that case |
| results[batchNum] = res.results; |
| if (++numDone === numBatches) { |
| callback(null, {results: flatten(results)}); |
| } |
| }; |
| } |
| |
| for (var i = 0; i < numBatches; i++) { |
| var subOpts = pick(opts, ['revs', 'attachments', 'latest']); |
| subOpts.ajax = ajaxOpts; |
| subOpts.docs = opts.docs.slice(i * batchSize, |
| Math.min(opts.docs.length, (i + 1) * batchSize)); |
| bulkGetShim(self, subOpts, onResult(i)); |
| } |
| } |
| |
| // mark the whole database as either supporting or not supporting _bulk_get |
| var dbUrl = genUrl(host, ''); |
| var supportsBulkGet = supportsBulkGetMap[dbUrl]; |
| |
| if (typeof supportsBulkGet !== 'boolean') { |
| // check if this database supports _bulk_get |
| doBulkGet(function (err, res) { |
| /* istanbul ignore else */ |
| if (err) { |
| supportsBulkGetMap[dbUrl] = false; |
| explainError( |
| err.status, |
| 'PouchDB is just detecting if the remote ' + |
| 'supports the _bulk_get API.' |
| ); |
| doBulkGetShim(); |
| } else { |
| supportsBulkGetMap[dbUrl] = true; |
| callback(null, res); |
| } |
| }); |
| } else if (supportsBulkGet) { |
| /* istanbul ignore next */ |
| doBulkGet(callback); |
| } else { |
| doBulkGetShim(); |
| } |
| }); |
| |
| // 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) { |
| setup().then(function () { |
| ajax({}, { |
| method: 'GET', |
| url: genDBUrl(host, '') |
| }, function (err, res) { |
| /* istanbul ignore next */ |
| if (err) { |
| return callback(err); |
| } |
| res.host = genDBUrl(host, ''); |
| callback(null, res); |
| }); |
| }).catch(callback); |
| }; |
| |
| // Get the document with the given id from the database given by host. |
| // The id could be solely the _id in the database, or it may be a |
| // _design/ID or _local/ID path |
| api.get = 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 = clone(opts); |
| |
| // List of parameters to add to the GET request |
| var params = {}; |
| |
| if (opts.revs) { |
| params.revs = true; |
| } |
| |
| if (opts.revs_info) { |
| params.revs_info = true; |
| } |
| |
| if (opts.latest) { |
| params.latest = true; |
| } |
| |
| if (opts.open_revs) { |
| if (opts.open_revs !== "all") { |
| opts.open_revs = JSON.stringify(opts.open_revs); |
| } |
| params.open_revs = opts.open_revs; |
| } |
| |
| if (opts.rev) { |
| params.rev = opts.rev; |
| } |
| |
| if (opts.conflicts) { |
| params.conflicts = opts.conflicts; |
| } |
| |
| id = encodeDocId(id); |
| |
| // Set the options for the ajax call |
| var options = { |
| method: 'GET', |
| url: genDBUrl(host, id + paramsToStr(params)) |
| }; |
| |
| function fetchAttachments(doc) { |
| var atts = doc._attachments; |
| var filenames = atts && Object.keys(atts); |
| if (!atts || !filenames.length) { |
| return; |
| } |
| // we fetch these manually in separate XHRs, because |
| // Sync Gateway would normally send it back as multipart/mixed, |
| // which we cannot parse. Also, this is more efficient than |
| // receiving attachments as base64-encoded strings. |
| function fetch(filename) { |
| var att = atts[filename]; |
| var path = encodeDocId(doc._id) + '/' + encodeAttachmentId(filename) + |
| '?rev=' + doc._rev; |
| return ajaxPromise(opts, { |
| method: 'GET', |
| url: genDBUrl(host, path), |
| binary: true |
| }).then(function (blob) { |
| if (opts.binary) { |
| return blob; |
| } |
| return new Promise(function (resolve) { |
| blufferToBase64(blob, resolve); |
| }); |
| }).then(function (data) { |
| delete att.stub; |
| delete att.length; |
| att.data = data; |
| }); |
| } |
| |
| var promiseFactories = filenames.map(function (filename) { |
| return function () { |
| return fetch(filename); |
| }; |
| }); |
| |
| // This limits the number of parallel xhr requests to 5 any time |
| // to avoid issues with maximum browser request limits |
| return pool(promiseFactories, 5); |
| } |
| |
| function fetchAllAttachments(docOrDocs) { |
| if (Array.isArray(docOrDocs)) { |
| return Promise.all(docOrDocs.map(function (doc) { |
| if (doc.ok) { |
| return fetchAttachments(doc.ok); |
| } |
| })); |
| } |
| return fetchAttachments(docOrDocs); |
| } |
| |
| ajaxPromise(opts, options).then(function (res) { |
| return Promise.resolve().then(function () { |
| if (opts.attachments) { |
| return fetchAllAttachments(res); |
| } |
| }).then(function () { |
| callback(null, res); |
| }); |
| }).catch(callback); |
| }); |
| |
| // Delete the document given by doc from the database given by host. |
| api.remove = 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(opts, { |
| 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 = |
| adapterFun('getAttachment', function (docId, attachmentId, opts, |
| callback) { |
| if (typeof opts === 'function') { |
| callback = opts; |
| opts = {}; |
| } |
| var params = opts.rev ? ('?rev=' + opts.rev) : ''; |
| var url = genDBUrl(host, encodeDocId(docId)) + '/' + |
| encodeAttachmentId(attachmentId) + params; |
| ajax(opts, { |
| method: 'GET', |
| url: url, |
| binary: true |
| }, callback); |
| }); |
| |
| // Remove the attachment given by the id and rev |
| api.removeAttachment = |
| adapterFun('removeAttachment', function (docId, attachmentId, rev, |
| callback) { |
| |
| var url = genDBUrl(host, encodeDocId(docId) + '/' + |
| encodeAttachmentId(attachmentId)) + '?rev=' + rev; |
| |
| ajax({}, { |
| 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 = |
| adapterFun('putAttachment', function (docId, attachmentId, rev, blob, |
| type, callback) { |
| if (typeof type === 'function') { |
| callback = type; |
| 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') { |
| // input is assumed to be a base64 string |
| var binary; |
| try { |
| binary = atob(blob); |
| } catch (err) { |
| return callback(createError(BAD_ARG, |
| 'Attachment is not a valid base64 string')); |
| } |
| blob = binary ? binStringToBluffer(binary, type) : ''; |
| } |
| |
| var opts = { |
| headers: {'Content-Type': type}, |
| method: 'PUT', |
| url: url, |
| processData: false, |
| body: blob, |
| timeout: ajaxOpts.timeout || 60000 |
| }; |
| // Add the attachment |
| ajax({}, opts, callback); |
| }); |
| |
| // Update/create multiple documents given by req in the database |
| // given by host. |
| api._bulkDocs = function (req, opts, callback) { |
| // 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. |
| req.new_edits = opts.new_edits; |
| |
| setup().then(function () { |
| return Promise.all(req.docs.map(preprocessAttachments)); |
| }).then(function () { |
| // Update/create the documents |
| ajax(opts, { |
| method: 'POST', |
| url: genDBUrl(host, '_bulk_docs'), |
| timeout: opts.timeout, |
| 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); |
| }; |
| |
| |
| // Update/create document |
| api._put = function (doc, opts, callback) { |
| setup().then(function () { |
| return preprocessAttachments(doc); |
| }).then(function () { |
| // Update/create the document |
| ajax(opts, { |
| method: 'PUT', |
| url: genDBUrl(host, encodeDocId(doc._id)), |
| body: doc |
| }, function (err, result) { |
| if (err) { |
| return callback(err); |
| } |
| callback(null, result); |
| }); |
| }).catch(callback); |
| }; |
| |
| |
| // Get a listing of the documents in the database given |
| // by host and ordered by increasing id. |
| api.allDocs = adapterFun('allDocs', function (opts, callback) { |
| if (typeof opts === 'function') { |
| callback = opts; |
| opts = {}; |
| } |
| opts = clone(opts); |
| |
| // List of parameters to add to the GET request |
| var params = {}; |
| var body; |
| var method = 'GET'; |
| |
| if (opts.conflicts) { |
| params.conflicts = true; |
| } |
| |
| if (opts.descending) { |
| params.descending = true; |
| } |
| |
| if (opts.include_docs) { |
| params.include_docs = true; |
| } |
| |
| // added in CouchDB 1.6.0 |
| if (opts.attachments) { |
| params.attachments = true; |
| } |
| |
| if (opts.key) { |
| params.key = JSON.stringify(opts.key); |
| } |
| |
| if (opts.start_key) { |
| opts.startkey = opts.start_key; |
| } |
| |
| if (opts.startkey) { |
| params.startkey = JSON.stringify(opts.startkey); |
| } |
| |
| if (opts.end_key) { |
| opts.endkey = opts.end_key; |
| } |
| |
| if (opts.endkey) { |
| params.endkey = JSON.stringify(opts.endkey); |
| } |
| |
| if (typeof opts.inclusive_end !== 'undefined') { |
| params.inclusive_end = !!opts.inclusive_end; |
| } |
| |
| if (typeof opts.limit !== 'undefined') { |
| params.limit = opts.limit; |
| } |
| |
| if (typeof opts.skip !== 'undefined') { |
| params.skip = opts.skip; |
| } |
| |
| var paramStr = paramsToStr(params); |
| |
| if (typeof opts.keys !== 'undefined') { |
| method = 'POST'; |
| body = {keys: opts.keys}; |
| } |
| |
| // Get the document listing |
| ajaxPromise(opts, { |
| method: method, |
| url: genDBUrl(host, '_all_docs' + paramStr), |
| body: body |
| }).then(function (res) { |
| if (opts.include_docs && opts.attachments && opts.binary) { |
| res.rows.forEach(readAttachmentsAsBlobOrBuffer); |
| } |
| callback(null, res); |
| }).catch(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 = clone(opts); |
| opts.timeout = ('timeout' in opts) ? opts.timeout : |
| ('timeout' in ajaxOpts) ? ajaxOpts.timeout : |
| 30 * 1000; |
| |
| // We give a 5 second buffer for CouchDB changes to respond with |
| // an ok timeout (if a timeout it set) |
| var params = opts.timeout ? {timeout: opts.timeout - (5 * 1000)} : {}; |
| var limit = (typeof opts.limit !== 'undefined') ? opts.limit : false; |
| var returnDocs; |
| if ('return_docs' in opts) { |
| returnDocs = opts.return_docs; |
| } else if ('returnDocs' in opts) { |
| // TODO: Remove 'returnDocs' in favor of 'return_docs' in a future release |
| 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 ('heartbeat' in opts) { |
| // If the heartbeat value is false, it disables the default heartbeat |
| if (opts.heartbeat) { |
| params.heartbeat = opts.heartbeat; |
| } |
| } else if (opts.continuous) { |
| // Default heartbeat to 10 seconds |
| params.heartbeat = 10000; |
| } |
| |
| if (opts.filter && typeof opts.filter === 'string') { |
| params.filter = opts.filter; |
| } |
| |
| if (opts.view && typeof opts.view === 'string') { |
| params.filter = '_view'; |
| 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) { |
| /* istanbul ignore else */ |
| 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'; |
| method = 'POST'; |
| body = {doc_ids: opts.doc_ids }; |
| } |
| |
| 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; |
| // "since" can be any kind of json object in Coudant/CouchDB 2.x |
| /* istanbul ignore next */ |
| 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; |
| } |
| |
| // Set the options for the ajax call |
| var xhrOpts = { |
| method: method, |
| url: genDBUrl(host, '_changes' + paramsToStr(params)), |
| timeout: opts.timeout, |
| body: body |
| }; |
| lastFetchedSeq = since; |
| |
| /* istanbul ignore if */ |
| if (opts.aborted) { |
| return; |
| } |
| |
| // Get the changes |
| setup().then(function () { |
| xhr = ajax(opts, xhrOpts, callback); |
| }).catch(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 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 = filterChange(opts)(c); |
| if (ret) { |
| if (opts.include_docs && opts.attachments && opts.binary) { |
| readAttachmentsAsBlobOrBuffer(c); |
| } |
| if (returnDocs) { |
| results.results.push(c); |
| } |
| opts.onChange(c); |
| } |
| return ret; |
| }); |
| } else if (err) { |
| // In case of an error, stop listening for changes and call |
| // opts.complete |
| opts.aborted = true; |
| 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) { |
| // Queue a call to fetch again with the newest sequence number |
| nextTick(function () { fetch(lastFetchedSeq, fetched); }); |
| } else { |
| // We're done, call the callback |
| opts.complete(null, results); |
| } |
| }; |
| |
| fetch(opts.since || 0, fetched); |
| |
| // Return a method to cancel this method from processing any more |
| return { |
| cancel: function () { |
| opts.aborted = true; |
| if (xhr) { |
| xhr.abort(); |
| } |
| } |
| }; |
| }; |
| |
| // Given a set of document/revision IDs (given by req), tets the subset of |
| // those that do NOT correspond to revisions stored in the database. |
| // See http://wiki.apache.org/couchdb/HttpPostRevsDiff |
| api.revsDiff = adapterFun('revsDiff', function (req, opts, callback) { |
| // If no options were given, set the callback to be the second parameter |
| if (typeof opts === 'function') { |
| callback = opts; |
| opts = {}; |
| } |
| |
| // Get the missing document/revision IDs |
| ajax(opts, { |
| method: 'POST', |
| url: genDBUrl(host, '_revs_diff'), |
| body: req |
| }, callback); |
| }); |
| |
| api._close = function (callback) { |
| callback(); |
| }; |
| |
| api._destroy = function (options, callback) { |
| ajax(options, { |
| url: genDBUrl(host, ''), |
| method: 'DELETE' |
| }, function (err, resp) { |
| if (err && err.status && err.status !== 404) { |
| return callback(err); |
| } |
| callback(null, resp); |
| }); |
| }; |
| } |
| |
| // HttpPouch is a valid adapter. |
| HttpPouch.valid = function () { |
| return true; |
| }; |
| |
| export default function (PouchDB) { |
| PouchDB.adapter('http', HttpPouch, false); |
| PouchDB.adapter('https', HttpPouch, false); |
| } |