UNPKG

mam-project-templates

Version:
1,646 lines (1,472 loc) 171 kB
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.PouchDB=e():"undefined"!=typeof global?global.PouchDB=e():"undefined"!=typeof self&&(self.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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.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(require,module,exports){ /*globals cordova */ "use strict"; var utils = require('./utils'); var merge = require('./merge'); var errors = require('./deps/errors'); var call = utils.call; /* * 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].error) { call(callback, err || results[0]); } else { call(callback, null, results[0]); } }; } // 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; } module.exports = function (Pouch) { return function (opts, callback) { var api = {}; var customApi = Pouch.adapters[opts.adapter](opts, function (err, db) { if (err) { if (callback) { callback(err); } return; } for (var j in api) { if (!db.hasOwnProperty(j)) { db[j] = api[j]; } } // Don't call Pouch.open for ALL_DBS // Pouch.open saves the db's name into ALL_DBS if (opts.name === Pouch.prefix + Pouch.ALL_DBS) { callback(err, db); } else { Pouch.open(opts, function (err) { callback(err, db); }); } }); var auto_compaction = (opts.auto_compaction === true); // wraps a callback with a function that runs compaction after each edit function autoCompact(callback) { if (!auto_compaction) { return callback; } return function (err, res) { if (err) { call(callback, err); } else { var count = res.length; var decCount = function () { count--; if (!count) { call(callback, null, res); } }; res.forEach(function (doc) { if (doc.ok) { // TODO: we need better error handling compactDocument(doc.id, 1, decCount); } else { decCount(); } }); } }; } api.post = function (doc, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } if (typeof doc !== 'object' || Array.isArray(doc)) { return call(callback, errors.NOT_AN_OBJECT); } return customApi.bulkDocs({docs: [doc]}, opts, autoCompact(yankError(callback))); }; api.put = function (doc, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } if (typeof doc !== 'object') { return call(callback, errors.NOT_AN_OBJECT); } if (!utils.isValidId(doc._id)) { return call(callback, errors.MISSING_ID); } return customApi.bulkDocs({docs: [doc]}, opts, autoCompact(yankError(callback))); }; api.putAttachment = function (docId, attachmentId, rev, blob, type, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('putAttachment', arguments); return; } 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 }; api.put(doc, callback); } api.get(docId, function (err, doc) { // create new doc if (err && err.error === errors.MISSING_DOC.error) { createAttachment({_id: docId}); return; } if (err) { call(callback, err); return; } if (doc._rev !== rev) { call(callback, errors.REV_CONFLICT); return; } createAttachment(doc); }); }; api.removeAttachment = function (docId, attachmentId, rev, callback) { api.get(docId, function (err, obj) { if (err) { call(callback, err); return; } if (obj._rev !== rev) { call(callback, errors.REV_CONFLICT); return; } if (!obj._attachments) { return call(callback, null); } delete obj._attachments[attachmentId]; if (Object.keys(obj._attachments).length === 0) { delete obj._attachments; } api.put(obj, callback); }); }; api.remove = function (doc, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } if (opts === undefined) { opts = {}; } opts.was_delete = true; var newDoc = {_id: doc._id, _rev: doc._rev}; newDoc._deleted = true; return customApi.bulkDocs({docs: [newDoc]}, opts, yankError(callback)); }; api.revsDiff = function (req, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } var ids = Object.keys(req); var count = 0; var missing = {}; function addToMissing(id, revId) { if (!missing[id]) { missing[id] = {missing: []}; } missing[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) { customApi._getRevisionTree(id, function (err, rev_tree) { if (err && err.name === 'not_found' && err.message === 'missing') { missing[id] = {missing: req[id]}; } else if (err) { return call(callback, err); } else { processDoc(id, rev_tree); } if (++count === ids.length) { return call(callback, null, missing); } }); }); }; // 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 function compactDocument(docId, max_height, callback) { customApi._getRevisionTree(docId, function (err, rev_tree) { if (err) { return call(callback); } var height = computeHeight(rev_tree); var candidates = []; var revs = []; Object.keys(height).forEach(function (rev) { if (height[rev] > max_height) { candidates.push(rev); } }); merge.traverseRevTree(rev_tree, function (isLeaf, pos, revHash, ctx, opts) { var rev = pos + '-' + revHash; if (opts.status === 'available' && candidates.indexOf(rev) !== -1) { opts.status = 'missing'; revs.push(rev); } }); customApi._doCompaction(docId, rev_tree, revs, callback); }); } // compact the whole database using single document // compaction api.compact = function (opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } api.changes({complete: function (err, res) { if (err) { call(callback); // TODO: silently fail return; } var count = res.results.length; if (!count) { call(callback); return; } res.results.forEach(function (row) { compactDocument(row.id, 0, function () { count--; if (!count) { call(callback); } }); }); }}); }; /* Begin api wrappers. Specific functionality to storage belongs in the _[method] */ api.get = function (id, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('get', arguments); return; } if (typeof opts === 'function') { callback = opts; opts = {}; } var leaves = []; function finishOpenRevs() { var result = []; var count = leaves.length; if (!count) { return call(callback, null, result); } // order with open_revs is unspecified leaves.forEach(function (leaf) { api.get(id, {rev: leaf, revs: opts.revs}, function (err, doc) { if (!err) { result.push({ok: doc}); } else { result.push({missing: leaf}); } count--; if (!count) { call(callback, null, result); } }); }); } if (opts.open_revs) { if (opts.open_revs === "all") { customApi._getRevisionTree(id, function (err, rev_tree) { if (err) { // if there's no such document we should treat this // situation the same way as if revision tree was empty rev_tree = []; } 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 call(callback, errors.error(errors.BAD_REQUEST, "Invalid rev format")); } } finishOpenRevs(); } else { return call(callback, errors.error(errors.UNKNOWN_ERROR, 'function_clause')); } } return; // open_revs does not like other options } return customApi._get(id, opts, function (err, result) { if (err) { return call(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 (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; }); path.ids.splice(path.ids.map(function (x) {return x.id; }) .indexOf(doc._rev.split('-')[1]) + 1); 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) { 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 call(callback, null, doc); } Object.keys(attachments).forEach(function (key) { customApi._getAttachment(attachments[key], {encode: true, ctx: ctx}, function (err, data) { doc._attachments[key].data = data; if (!--count) { call(callback, null, doc); } }); }); } else { if (doc._attachments) { for (var key in doc._attachments) { doc._attachments[key].stub = true; } } call(callback, null, doc); } }); }; api.getAttachment = function (docId, attachmentId, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('getAttachment', arguments); return; } if (opts instanceof Function) { callback = opts; opts = {}; } customApi._get(docId, opts, function (err, res) { if (err) { return call(callback, err); } if (res.doc._attachments && res.doc._attachments[attachmentId]) { opts.ctx = res.ctx; customApi._getAttachment(res.doc._attachments[attachmentId], opts, callback); } else { return call(callback, errors.MISSING_DOC); } }); }; api.allDocs = function (opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('allDocs', arguments); return; } if (typeof opts === 'function') { callback = opts; opts = {}; } if ('keys' in opts) { if ('startkey' in opts) { call(callback, errors.error(errors.QUERY_PARSE_ERROR, 'Query parameter `start_key` is not compatible with multi-get' )); return; } if ('endkey' in opts) { call(callback, errors.error(errors.QUERY_PARSE_ERROR, 'Query parameter `end_key` is not compatible with multi-get' )); return; } } if (typeof opts.skip === 'undefined') { opts.skip = 0; } return customApi._allDocs(opts, callback); }; function processChange(doc, metadata, opts) { var changeList = [{rev: doc._rev}]; if (opts.style === 'all_docs') { changeList = merge.collectLeaves(metadata.rev_tree) .map(function (x) { return {rev: x.rev}; }); } var change = { id: metadata.id, changes: changeList, doc: doc }; if (utils.isDeleted(metadata, doc._rev)) { change.deleted = true; } if (opts.conflicts) { change.doc._conflicts = merge.collectConflicts(metadata); if (!change.doc._conflicts.length) { delete change.doc._conflicts; } } return change; } api.changes = function (opts) { if (!api.taskqueue.ready()) { var task = api.taskqueue.addTask('changes', arguments); return { cancel: function () { if (task.task) { return task.task.cancel(); } if (Pouch.DEBUG) { //console.log('Cancel Changes Feed'); } task.parameters[0].aborted = true; } }; } opts = utils.extend(true, {}, opts); opts.processChange = processChange; if (!opts.since) { opts.since = 0; } if (opts.since === 'latest') { var changes; api.info(function (err, info) { if (!opts.aborted) { opts.since = info.update_seq - 1; api.changes(opts); } }); // Return a method to cancel this method from processing any more return { cancel: function () { if (changes) { return changes.cancel(); } if (Pouch.DEBUG) { //console.log('Cancel Changes Feed'); } opts.aborted = true; } }; } if (opts.filter && typeof opts.filter === 'string') { if (opts.filter === '_view') { if (opts.view && typeof opts.view === 'string') { // fetch a view from a design doc, make it behave like a filter var viewName = opts.view.split('/'); api.get('_design/' + viewName[0], function (err, ddoc) { if (ddoc && ddoc.views && ddoc.views[viewName[1]]) { /*jshint evil: true */ var filter = eval('(function () {' + ' return function (doc) {' + ' var emitted = false;' + ' var emit = function (a, b) {' + ' emitted = true;' + ' };' + ' var view = ' + ddoc.views[viewName[1]].map + ';' + ' view(doc);' + ' if (emitted) {' + ' return true;' + ' }' + ' }' + '})()'); if (!opts.aborted) { opts.filter = filter; api.changes(opts); } } else { var msg = ddoc.views ? 'missing json key: ' + viewName[1] : 'missing json key: views'; err = err || errors.error(errors.MISSING_DOC, msg); utils.call(opts.complete, err); } }); } else { var err = errors.error(errors.BAD_REQUEST, '`view` filter parameter is not provided.'); utils.call(opts.complete, err); } } else { // fetch a filter from a design doc var filterName = opts.filter.split('/'); api.get('_design/' + filterName[0], function (err, ddoc) { if (ddoc && ddoc.filters && ddoc.filters[filterName[1]]) { /*jshint evil: true */ var filter = eval('(function () { return ' + ddoc.filters[filterName[1]] + ' })()'); if (!opts.aborted) { opts.filter = filter; api.changes(opts); } } else { var msg = (ddoc && ddoc.filters) ? 'missing json key: ' + filterName[1] : 'missing json key: filters'; err = err || errors.error(errors.MISSING_DOC, msg); utils.call(opts.complete, err); } }); } // Return a method to cancel this method from processing any more return { cancel: function () { if (Pouch.DEBUG) { console.log('Cancel Changes Feed'); } opts.aborted = true; } }; } if (!('descending' in opts)) { opts.descending = false; } // 0 and 1 should return 1 document opts.limit = opts.limit === 0 ? 1 : opts.limit; return customApi._changes(opts); }; api.close = function (callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('close', arguments); return; } return customApi._close(callback); }; api.info = function (callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('info', arguments); return; } return customApi._info(callback); }; api.id = function () { return customApi._id(); }; api.type = function () { return (typeof customApi._type === 'function') ? customApi._type() : opts.adapter; }; api.bulkDocs = function (req, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('bulkDocs', arguments); return; } if (typeof opts === 'function') { callback = opts; opts = {}; } if (!opts) { opts = {}; } else { opts = utils.extend(true, {}, opts); } if (!req || !req.docs || req.docs.length < 1) { return call(callback, errors.MISSING_BULK_DOCS); } if (!Array.isArray(req.docs)) { return call(callback, errors.QUERY_PARSE_ERROR); } for (var i = 0; i < req.docs.length; ++i) { if (typeof req.docs[i] !== 'object' || Array.isArray(req.docs[i])) { return call(callback, errors.NOT_AN_OBJECT); } } req = utils.extend(true, {}, req); if (!('new_edits' in opts)) { opts.new_edits = true; } return customApi._bulkDocs(req, opts, autoCompact(callback)); }; /* End Wrappers */ var taskqueue = {}; taskqueue.ready = false; taskqueue.queue = []; api.taskqueue = {}; api.taskqueue.execute = function (db) { if (taskqueue.ready) { taskqueue.queue.forEach(function (d) { d.task = db[d.name].apply(null, d.parameters); }); } }; api.taskqueue.ready = function () { if (arguments.length === 0) { return taskqueue.ready; } taskqueue.ready = arguments[0]; }; api.taskqueue.addTask = function (name, parameters) { var task = { name: name, parameters: parameters }; taskqueue.queue.push(task); return task; }; api.replicate = {}; api.replicate.from = function (url, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } return Pouch.replicate(url, customApi, opts, callback); }; api.replicate.to = function (dbName, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } return Pouch.replicate(customApi, dbName, opts, callback); }; for (var j in api) { if (!customApi.hasOwnProperty(j)) { customApi[j] = api[j]; } } // Http adapter can skip setup so we force the db to be ready and execute any jobs if (opts.skipSetup) { api.taskqueue.ready(true); api.taskqueue.execute(api); } if (utils.isCordova()) { //to inform websql adapter that we can use api cordova.fireWindowEvent(opts.name + "_pouch", {}); } return customApi; }; }; },{"./deps/errors":8,"./merge":13,"./utils":16}],2:[function(require,module,exports){ "use strict"; var utils = require('../utils'); var errors = require('../deps/errors'); // parseUri 1.2.2 // (c) Steven Levithan <stevenlevithan.com> // MIT License function parseUri(str) { var o = parseUri.options; var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str); var uri = {}; var i = 14; while (i--) { uri[o.key[i]] = m[i] || ""; } uri[o.q.name] = {}; uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { if ($1) { uri[o.q.name][$1] = $2; } }); return uri; } function encodeDocId(id) { if (/^_(design|local)/.test(id)) { return id; } return encodeURIComponent(id); } parseUri.options = { strictMode: false, key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], q: { name: "queryKey", parser: /(?:^|&)([^&=]*)=?([^&]*)/g }, parser: { strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ } }; // 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 = 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 || {}; uri.headers = opts.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) { // If the host is remote 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 ? '' : '/'; // Return the URL made up of all the host's information and the given path return opts.protocol + '://' + opts.host + ':' + opts.port + '/' + opts.path + pathDel + opts.db + '/' + path; } // If the host is not remote, then return the URL made up of just the // database name and the given path return '/' + 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) { // Parse the URI given by opts.name into an easy-to-use object var host = getHost(opts.name, opts); // Generate the database URL based on the host var db_url = genDBUrl(host, ''); // The functions that will be publically available for HttpPouch var api = {}; var ajaxOpts = opts.ajax || {}; function ajax(options, callback) { return utils.ajax(utils.extend({}, ajaxOpts, options), callback); } var uuids = { list: [], get: function (opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {count: 10}; } var cb = function (err, body) { if (err || !('uuids' in body)) { utils.call(callback, err || errors.UNKNOWN_ERROR); } else { uuids.list = uuids.list.concat(body.uuids); utils.call(callback, null, "OK"); } }; var params = '?count=' + opts.count; ajax({ headers: host.headers, method: 'GET', url: genUrl(host, '_uuids') + params }, cb); } }; // Create a new CouchDB database based on the given opts var createDB = function () { ajax({headers: host.headers, method: 'PUT', url: db_url}, function (err, ret) { // If we get an "Unauthorized" error if (err && err.status === 401) { // Test if the database already exists ajax({headers: host.headers, method: 'HEAD', url: db_url}, function (err, ret) { // If there is still an error if (err) { // Give the error to the callback to deal with utils.call(callback, err); } else { // Continue as if there had been no errors utils.call(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 utils.call(callback, null, api); } else { utils.call(callback, errors.UNKNOWN_ERROR); } }); }; if (!opts.skipSetup) { ajax({headers: host.headers, method: 'GET', url: db_url}, function (err, ret) { //check if the db exists if (err) { if (err.status === 404) { //if it doesn't, create it createDB(); } else { utils.call(callback, err); } } else { //go do stuff with the db utils.call(callback, null, api); } }); } api.type = function () { return 'http'; }; // The HttpPouch's ID is its URL api.id = function () { return genDBUrl(host, ''); }; api.request = function (options, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('request', arguments); return; } 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 = function (opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('compact', arguments); return; } if (typeof opts === 'function') { callback = opts; opts = {}; } ajax({ headers: host.headers, url: genDBUrl(host, '_compact'), method: 'POST' }, function () { function ping() { api.info(function (err, res) { if (!res.compact_running) { utils.call(callback, null); } 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) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('info', arguments); return; } ajax({ headers: host.headers, method: 'GET', url: genDBUrl(host, '') }, 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 = function (id, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('get', arguments); return; } // If no options were given, set the callback to the second parameter if (typeof opts === 'function') { callback = opts; 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 utils.call(callback, err); } // Send the document to the callback utils.call(callback, null, doc, xhr); }); }; // Delete the document given by doc from the database given by host. api.remove = function (doc, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('remove', arguments); return; } // If no options were given, set the callback to be the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } // Delete the document ajax({ headers: host.headers, method: 'DELETE', url: genDBUrl(host, encodeDocId(doc._id)) + '?rev=' + doc._rev }, callback); }; // Get the attachment api.getAttachment = function (docId, attachmentId, opts, callback) { if (typeof opts === 'function') { callback = opts; opts = {}; } if (opts.auto_encode === undefined) { opts.auto_encode = true; } if (opts.auto_encode) { docId = encodeDocId(docId); } opts.auto_encode = false; api.get(docId + '/' + attachmentId, opts, callback); }; // Remove the attachment given by the id and rev api.removeAttachment = function (docId, attachmentId, rev, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('removeAttachment', arguments); return; } ajax({ headers: host.headers, method: 'DELETE', url: genDBUrl(host, encodeDocId(docId) + '/' + attachmentId) + '?rev=' + rev }, 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 = function (docId, attachmentId, rev, blob, type, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('putAttachment', arguments); return; } 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) + '/' + attachmentId; var url = genDBUrl(host, id); if (rev) { url += '?rev=' + rev; } var opts = { headers: 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 = function (doc, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('put', arguments); return; } // If no options were given, set the callback to be the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } if (typeof doc !== 'object') { return utils.call(callback, errors.NOT_AN_OBJECT); } if (!utils.isValidId(doc._id)) { return utils.call(callback, errors.MISSING_ID); } // 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 }, 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 = function (doc, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('post', arguments); return; } // If no options were given, set the callback to be the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } if (typeof doc !== 'object') { return utils.call(callback, errors.NOT_AN_OBJECT); } if (! ("_id" in doc)) { if (uuids.list.length > 0) { doc._id = uuids.list.pop(); api.put(doc, opts, callback); } else { uuids.get(function (err, resp) { if (err) { return utils.call(callback, errors.UNKNOWN_ERROR); } doc._id = uuids.list.pop(); api.put(doc, opts, callback); }); } } else { api.put(doc, opts, callback); } }; // Update/create multiple documents given by req in the database // given by host. api.bulkDocs = function (req, opts, callback) { if (!api.taskqueue.ready()) { api.taskqueue.addTask('bulkDocs', arguments); return; } // If no options were given, set the callback to be the second parameter if (typeof opts === 'function') { callback = opts; opts = {}; } if (!opts) { opts = {}; } // 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; } // Update/create the documents ajax({ headers: host.headers, method: 'POST', url: genDBUrl(host, '_bulk_docs'), body: req }, callback); }; // Get a listing of the documents in the database given // by host and ordered by increasing id. api.allDocs = function (opts, callback) { // If no options were given, set the callback to be the second parameter if (!api.taskqueue.ready()) { api.taskqueue.addTask('allDocs', arguments); return; } if (typeof opts === 'function') { callback = opts; opts = {}; } // List of parameters to add to the GET request var params = []; var body; var method = 'GET'; // TODO I don't see conflicts as a valid parameter for a // _all_docs request (see http://wiki.apache.org/couchdb/HTTP_Document_API#all_docs) 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.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 opts.limit exists, add the limit value to the parameter list. if (opts.limit) { 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 keys are supplied, issue a POST request to circumvent GET query string limits // see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options if (typeof opts.keys !== 'undefined') { 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 CHANGES_LIMIT = 25; if (!api.taskqueue.ready()) { var task = api.taskqueue.addTask('changes', arguments); return { cancel: function () { if (task.task) { return task.task.cancel(); } //console.log(db_url + ': Cancel Changes Feed'); task.parameters[0].aborted = true; } }; } if (opts.since === 'latest') { var changes; api.info(function (err, info) { if (!opts.aborted) { opts.since = info.update_seq; changes = api.changes(opts); } }); // Return a method to cancel this method from processing any more return { cancel: function () { if (changes) { return changes.cancel(); } //console.log(db_url + ': Cancel Changes Feed'); opts.aborted = true; } }; } //console.log(db_url + ': Start Changes Feed: continuous=' + opts.continuous); var params = {}; var limit = (typeof opts.limit !== 'undefined') ? opts.limit : false; if (limit === 0) { limit = 1; } // 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.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 xhr; var lastFetchedSeq; var remoteLastSeq; var pagingCount; // Get all the changes starting wtih the one immediately after the // sequence number given by since. var fetch = function (since, callback) { params.since = since; if (!opts.continuous && !pagingCount) { pagingCount = remoteLastSeq; } params.limit = (!limit || leftToFetch > CHANGES_LIMIT) ? CHANGES_LIMIT : 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: 'GET', url: genDBUrl(host, '_changes' + paramStr), // _changes can take a long time to generate, especially when filtered timeout: null }; 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 the result of the ajax call (res) contains changes (res.results) if (res && res.results) { 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) { 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, null); } // The changes feed may have timed out with no results // if so reuse last update sequence if (res && res.last_seq) {