From bc7e328854f335040e74d56237af87e208f099a1 Mon Sep 17 00:00:00 2001 From: Tim Baumann Date: Wed, 16 May 2012 18:55:58 +0200 Subject: [PATCH] Update CouchDB drivers --- public/vendor/backbone.couchdb.js | 4 +- public/vendor/jquery.couch.js | 533 ++++++++++++++++++++++++++---- 2 files changed, 469 insertions(+), 68 deletions(-) diff --git a/public/vendor/backbone.couchdb.js b/public/vendor/backbone.couchdb.js index 5d2dc17..0c0e514 100644 --- a/public/vendor/backbone.couchdb.js +++ b/public/vendor/backbone.couchdb.js @@ -4,7 +4,7 @@ * Copyright(c) 2011 Thomas Rampelberg */ -(function($) { +(function($, _, Backbone) { Backbone.couch = {}; Backbone.couch.options = { @@ -178,4 +178,4 @@ } }); -})(jQuery); +})(jQuery, _, Backbone); diff --git a/public/vendor/jquery.couch.js b/public/vendor/jquery.couch.js index a606c87..4ae3d40 100644 --- a/public/vendor/jquery.couch.js +++ b/public/vendor/jquery.couch.js @@ -10,9 +10,38 @@ // License for the specific language governing permissions and limitations under // the License. +/** + * @namespace + * $.couch is used to communicate with a CouchDB server, the server methods can + * be called directly without creating an instance. Typically all methods are + * passed an options object which defines a success callback which + * is called with the data returned from the http request to CouchDB, you can + * find the other settings that can be used in the options object + * from + * jQuery.ajax settings + *
$.couch.activeTasks({
+ *   success: function (data) {
+ *     console.log(data);
+ *   }
+ * });
+ * Outputs (for example): + *
[
+ *  {
+ *   "pid" : "<0.11599.0>",
+ *   "status" : "Copied 0 of 18369 changes (0%)",
+ *   "task" : "recipes",
+ *   "type" : "Database Compaction"
+ *  }
+ *]
+ */ (function($) { + $.couch = $.couch || {}; + /** @lends $.couch */ + /** + * @private + */ function encodeDocId(docID) { var parts = docID.split("/"); if (parts[0] == "_design") { @@ -20,31 +49,27 @@ return "_design/" + encodeURIComponent(parts.join('/')); } return encodeURIComponent(docID); - }; + } - function prepareUserDoc(user_doc, new_password) { - if (typeof hex_sha1 == "undefined") { - alert("creating a user doc requires sha1.js to be loaded in the page"); - return; - } - var user_prefix = "org.couchdb.user:"; - user_doc._id = user_doc._id || user_prefix + user_doc.name; - if (new_password) { - // handle the password crypto - user_doc.salt = $.couch.newUUID(); - user_doc.password_sha = hex_sha1(new_password + user_doc.salt); - } - user_doc.type = "user"; - if (!user_doc.roles) { - user_doc.roles = []; - } - return user_doc; - }; + /** + * @private + */ var uuidCache = []; $.extend($.couch, { urlPrefix: '', + + /** + * You can obtain a list of active tasks by using the /_active_tasks URL. + * The result is a JSON array of the currently running tasks, with each task + * being described with a single object. + * @see docs for /_active_tasks + * @param {ajaxSettings} options jQuery ajax settings + */ activeTasks: function(options) { ajax( {url: this.urlPrefix + "/_active_tasks"}, @@ -53,6 +78,14 @@ ); }, + /** + * Returns a list of all the databases in the CouchDB instance + * @see docs for /_all_dbs + * @param {ajaxSettings} options jQuery ajax settings + */ allDbs: function(options) { ajax( {url: this.urlPrefix + "/_all_dbs"}, @@ -61,6 +94,21 @@ ); }, + /** + * View and edit the CouchDB configuration, called with just the options + * parameter the entire config is returned, you can be more specific by + * passing the section and option parameters, if you specify a value that + * value will be stored in the configuration. + * @see docs for /_config + * @param {ajaxSettings} options + * + * jQuery ajax settings + * @param {String} [section] the section of the config + * @param {String} [option] the particular config option + * @param {String} [value] value to be set + */ config: function(options, section, option, value) { var req = {url: this.urlPrefix + "/_config/"}; if (section) { @@ -83,6 +131,12 @@ ); }, + /** + * Returns the session information for the currently logged in user. + * @param {ajaxSettings} options + * + * jQuery ajax settings + */ session: function(options) { options = options || {}; $.ajax({ @@ -91,7 +145,7 @@ xhr.setRequestHeader('Accept', 'application/json'); }, complete: function(req) { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { @@ -103,6 +157,9 @@ }); }, + /** + * @private + */ userDb : function(callback) { $.couch.session({ success : function(resp) { @@ -112,15 +169,57 @@ }); }, + /** + * Create a new user on the CouchDB server, user_doc is an + * object with a name field and other information you want + * to store relating to that user, for example + * {"name": "daleharvey"} + * @param {Object} user_doc Users details + * @param {String} password Users password + * @param {ajaxSettings} options + * + * jQuery ajax settings + */ signup: function(user_doc, password, options) { options = options || {}; // prepare user doc based on name and password - user_doc = prepareUserDoc(user_doc, password); + user_doc = this.prepareUserDoc(user_doc, password); $.couch.userDb(function(db) { db.saveDoc(user_doc, options); }); }, - + + /** + * Populates a user doc with a new password. + * @param {Object} user_doc User details + * @param {String} new_password New Password + */ + prepareUserDoc: function(user_doc, new_password) { + if (typeof hex_sha1 == "undefined") { + alert("creating a user doc requires sha1.js to be loaded in the page"); + return; + } + var user_prefix = "org.couchdb.user:"; + user_doc._id = user_doc._id || user_prefix + user_doc.name; + if (new_password) { + // handle the password crypto + user_doc.salt = $.couch.newUUID(); + user_doc.password_sha = hex_sha1(new_password + user_doc.salt); + } + user_doc.type = "user"; + if (!user_doc.roles) { + user_doc.roles = []; + } + return user_doc; + }, + + /** + * Authenticate against CouchDB, the options parameter is + *expected to have name and password fields. + * @param {ajaxSettings} options + * + * jQuery ajax settings + */ login: function(options) { options = options || {}; $.ajax({ @@ -130,7 +229,7 @@ xhr.setRequestHeader('Accept', 'application/json'); }, complete: function(req) { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { @@ -141,6 +240,14 @@ } }); }, + + + /** + * Delete your current CouchDB user session + * @param {ajaxSettings} options + * + * jQuery ajax settings + */ logout: function(options) { options = options || {}; $.ajax({ @@ -150,7 +257,7 @@ xhr.setRequestHeader('Accept', 'application/json'); }, complete: function(req) { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { @@ -162,14 +269,27 @@ }); }, + /** + * @namespace + * $.couch.db is used to communicate with a specific CouchDB database + *
var $db = $.couch.db("mydatabase");
+     *$db.allApps({
+     *  success: function (data) {
+     *    ... process data ...
+     *  }
+     *});
+     * 
+ */ db: function(name, db_opts) { db_opts = db_opts || {}; var rawDocs = {}; function maybeApplyVersion(doc) { - if (doc._id && doc._rev && rawDocs[doc._id] && rawDocs[doc._id].rev == doc._rev) { + if (doc._id && doc._rev && rawDocs[doc._id] && + rawDocs[doc._id].rev == doc._rev) { // todo: can we use commonjs require here? if (typeof Base64 == "undefined") { - alert("please include /_utils/script/base64.js in the page for base64 support"); + alert("please include /_utils/script/base64.js in the page for " + + "base64 support"); return false; } else { doc._attachments = doc._attachments || {}; @@ -181,10 +301,19 @@ } } }; - return { + return /** @lends $.couch.db */{ name: name, uri: this.urlPrefix + "/" + encodeURIComponent(name) + "/", + /** + * Request compaction of the specified database. + * @see docs for /db/_compact + * @param {ajaxSettings} options + * + * jQuery ajax settings + */ compact: function(options) { $.extend(options, {successStatus: 202}); ajax({ @@ -195,6 +324,15 @@ "The database could not be compacted" ); }, + + /** + * Cleans up the cached view output on disk for a given view. + * @see docs for /db/_compact + * @param {ajaxSettings} options jQuery ajax settings + */ viewCleanup: function(options) { $.extend(options, {successStatus: 202}); ajax({ @@ -205,6 +343,19 @@ "The views could not be cleaned up" ); }, + + /** + * Compacts the view indexes associated with the specified design + * document. You can use this in place of the full database compaction + * if you know a specific set of view indexes have been affected by a + * recent database change. + * @see docs for /db/_compact/design-doc + * @param {String} groupname Name of design-doc to compact + * @param {ajaxSettings} options jQuery ajax settings + */ compactView: function(groupname, options) { $.extend(options, {successStatus: 202}); ajax({ @@ -215,6 +366,15 @@ "The view could not be compacted" ); }, + + /** + * Create a new database + * @see docs for PUT /db/ + * @param {ajaxSettings} options jQuery ajax settings + */ create: function(options) { $.extend(options, {successStatus: 201}); ajax({ @@ -225,6 +385,16 @@ "The database could not be created" ); }, + + /** + * Deletes the specified database, and all the documents and + * attachments contained within it. + * @see docs for DELETE /db/ + * @param {ajaxSettings} options jQuery ajax settings + */ drop: function(options) { ajax( {type: "DELETE", url: this.uri}, @@ -232,6 +402,15 @@ "The database could not be deleted" ); }, + + /** + * Gets information about the specified database. + * @see docs for GET /db/ + * @param {ajaxSettings} options jQuery ajax settings + */ info: function(options) { ajax( {url: this.uri}, @@ -239,15 +418,39 @@ "Database information could not be retrieved" ); }, + + /** + * @namespace + * $.couch.db.changes provides an API for subscribing to the changes + * feed + *
var $changes = $.couch.db("mydatabase").changes();
+         *$changes.onChange = function (data) {
+         *    ... process data ...
+         * }
+         * $changes.stop();
+         * 
+ */ changes: function(since, options) { + options = options || {}; // set up the promise object within a closure for this handler var timeout = 100, db = this, active = true, listeners = [], - promise = { + promise = /** @lends $.couch.db.changes */ { + /** + * Add a listener callback + * @see docs for /db/_changes + * @param {Function} fun Callback function to run when + * notified of changes. + */ onChange : function(fun) { listeners.push(fun); }, + /** + * Stop subscribing to the changes feed + */ stop : function() { active = false; } @@ -258,7 +461,8 @@ this(resp); }); }; - // when there is a change, call any listeners, then check for another change + // when there is a change, call any listeners, then check for + // another change options.success = function(resp) { timeout = 100; if (active) { @@ -298,6 +502,18 @@ } return promise; }, + + /** + * Fetch all the docs in this db, you can specify an array of keys to + * fetch by passing the keys field in the + * options + * parameter. + * @see docs for /db/all_docs/ + * @param {ajaxSettings} options jQuery ajax settings + */ allDocs: function(options) { var type = "GET"; var data = null; @@ -316,9 +532,24 @@ "An error occurred retrieving a list of all documents" ); }, + + /** + * Fetch all the design docs in this db + * @param {ajaxSettings} options jQuery ajax settings + */ allDesignDocs: function(options) { - this.allDocs($.extend({startkey:"_design", endkey:"_design0"}, options)); + this.allDocs($.extend( + {startkey:"_design", endkey:"_design0"}, options)); }, + + /** + * Fetch all the design docs with an index.html, options + * parameter expects an eachApp field which is a callback + * called on each app found. + * @param {ajaxSettings} options jQuery ajax settings + */ allApps: function(options) { options = options || {}; var self = this; @@ -334,7 +565,8 @@ index = ddoc.couchapp && ddoc.couchapp.index; if (index) { appPath = ['', name, ddoc._id, index].join('/'); - } else if (ddoc._attachments && ddoc._attachments["index.html"]) { + } else if (ddoc._attachments && + ddoc._attachments["index.html"]) { appPath = ['', name, ddoc._id, "index.html"].join('/'); } if (appPath) options.eachApp(appName, appPath, ddoc); @@ -347,6 +579,18 @@ alert("Please provide an eachApp function for allApps()"); } }, + + /** + * Returns the specified doc from the specified db. + * @see docs for GET /db/doc + * @param {String} docId id of document to fetch + * @param {ajaxSettings} options jQuery ajax settings + * @param {ajaxSettings} ajaxOptions jQuery ajax settings + */ openDoc: function(docId, options, ajaxOptions) { options = options || {}; if (db_opts.attachPrevRev || options.attachPrevRev) { @@ -376,6 +620,20 @@ ajaxOptions ); }, + + /** + * Create a new document in the specified database, using the supplied + * JSON document structure. If the JSON structure includes the _id + * field, then the document will be created with the specified document + * ID. If the _id field is not specified, a new unique ID will be + * generated. + * @see docs for GET /db/doc + * @param {String} doc document to save + * @param {ajaxSettings} options jQuery ajax settings + */ saveDoc: function(doc, options) { options = options || {}; var db = this; @@ -394,7 +652,7 @@ dataType: "json", data: toJSON(doc), beforeSend : beforeSend, complete: function(req) { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); if (req.status == 200 || req.status == 201 || req.status == 202) { doc._id = resp.id; doc._rev = resp.rev; @@ -417,6 +675,16 @@ } }); }, + + /** + * Save a list of documents + * @see docs for /db/_bulk_docs + * @param {Object[]} docs List of documents to save + * @param {ajaxSettings} options jQuery ajax settings + */ bulkSave: function(docs, options) { var beforeSend = fullCommit(options); $.extend(options, {successStatus: 201, beforeSend : beforeSend}); @@ -429,6 +697,18 @@ "The documents could not be saved" ); }, + + /** + * Deletes the specified document from the database. You must supply + * the current (latest) revision and id of the document + * to delete eg removeDoc({_id:"mydoc", _rev: "1-2345"}) + * @see docs for DELETE /db/doc + * @param {Object} doc Document to delete + * @param {ajaxSettings} options jQuery ajax settings + */ removeDoc: function(doc, options) { ajax({ type: "DELETE", @@ -440,6 +720,16 @@ "The document could not be deleted" ); }, + + /** + * Remove a set of documents + * @see docs for /db/_bulk_docs + * @param {String[]} docs List of document id's to remove + * @param {ajaxSettings} options jQuery ajax settings + */ bulkRemove: function(docs, options){ docs.docs = $.each( docs.docs, function(i, doc){ @@ -456,10 +746,23 @@ "The documents could not be deleted" ); }, + + /** + * The COPY command (which is non-standard HTTP) copies an existing + * document to a new or existing document. + * @see docs for COPY /db/doc + * @param {String[]} docId document id to copy + * @param {ajaxSettings} options jQuery ajax settings + * @param {ajaxSettings} options jQuery ajax settings + */ copyDoc: function(docId, options, ajaxOptions) { ajaxOptions = $.extend(ajaxOptions, { complete: function(req) { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); if (req.status == 201) { if (options.success) options.success(resp); } else if (options.error) { @@ -478,15 +781,31 @@ ajaxOptions ); }, + + /** + * Creates (and executes) a temporary view based on the view function + * supplied in the JSON request. + * @see docs for /db/_temp_view + * @param {Function} mapFun Map function + * @param {Function} reduceFun Reduce function + * @param {Function} language Language the map / reduce funs are + * implemented in + * @param {ajaxSettings} options jQuery ajax settings + */ query: function(mapFun, reduceFun, language, options) { language = language || "javascript"; if (typeof(mapFun) !== "string") { - mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")"; + mapFun = mapFun.toSource ? mapFun.toSource() + : "(" + mapFun.toString() + ")"; } var body = {language: language, map: mapFun}; if (reduceFun != null) { if (typeof(reduceFun) !== "string") - reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")"; + reduceFun = reduceFun.toSource ? reduceFun.toSource() + : "(" + reduceFun.toString() + ")"; body.reduce = reduceFun; } ajax({ @@ -498,7 +817,22 @@ "An error occurred querying the database" ); }, - list: function(list, view, options) { + + /** + * Fetch a _list view output, you can specify a list of + * keys in the options object to recieve only those keys. + * @see + * docs for /db/_design/design-doc/_list/l1/v1 + * @param {String} list Listname in the form of ddoc/listname + * @param {String} view View to run list against + * @param {options} CouchDB View Options + * @param {ajaxSettings} options jQuery ajax settings + */ + list: function(list, view, options, ajaxOptions) { var list = list.split('/'); var options = options || {}; var type = 'GET'; @@ -515,9 +849,22 @@ url: this.uri + '_design/' + list[0] + '/_list/' + list[1] + '/' + view + encodeOptions(options) }, - options, 'An error occured accessing the list' + ajaxOptions, 'An error occured accessing the list' ); }, + + /** + * Executes the specified view-name from the specified design-doc + * design document, you can specify a list of keys + * in the options object to recieve only those keys. + * @see docs for /db/ + * _design/design-doc/_list/l1/v1 + * @param {String} name View to run list against + * @param {ajaxSettings} options jQuery ajax settings + */ view: function(name, options) { var name = name.split('/'); var options = options || {}; @@ -538,6 +885,17 @@ options, "An error occurred accessing the view" ); }, + + /** + * Fetch an arbitrary CouchDB database property + * @see docs for /db/_prop + * @param {String} propName Propery name to fetch + * @param {ajaxSettings} options jQuery ajax settings + * @param {ajaxSettings} ajaxOptions jQuery ajax settings + */ getDbProperty: function(propName, options, ajaxOptions) { ajax({url: this.uri + propName + encodeOptions(options)}, options, @@ -546,6 +904,17 @@ ); }, + /** + * Set an arbitrary CouchDB database property + * @see docs for /db/_prop + * @param {String} propName Propery name to fetch + * @param {String} propValue Propery value to set + * @param {ajaxSettings} options jQuery ajax settings + * @param {ajaxSettings} ajaxOptions jQuery ajax settings + */ setDbProperty: function(propName, propValue, options, ajaxOptions) { ajax({ type: "PUT", @@ -562,6 +931,17 @@ encodeDocId: encodeDocId, + /** + * Accessing the root of a CouchDB instance returns meta information about + * the instance. The response is a JSON structure containing information + * about the server, including a welcome message and the version of the + * server. + * @see + * docs for GET / + * @param {ajaxSettings} options jQuery ajax settings + */ info: function(options) { ajax( {url: this.urlPrefix + "/"}, @@ -570,9 +950,20 @@ ); }, + /** + * Request, configure, or stop, a replication operation. + * @see docs for POST /_replicate + * @param {String} source Path or url to source database + * @param {String} target Path or url to target database + * @param {ajaxSettings} ajaxOptions jQuery ajax settings + * @param {Object} repOpts Additional replication options + */ replicate: function(source, target, ajaxOptions, repOpts) { repOpts = $.extend({source: source, target: target}, repOpts); - if (repOpts.continuous) { + if (repOpts.continuous && !repOpts.cancel) { ajaxOptions.successStatus = 202; } ajax({ @@ -585,12 +976,20 @@ ); }, + /** + * Fetch a new UUID + * @see docs for /_uuids + * @param {Int} cacheNum Number of uuids to keep cached for future use + */ newUUID: function(cacheNum) { if (cacheNum === undefined) { cacheNum = 1; } if (!uuidCache.length) { - ajax({url: this.urlPrefix + "/_uuids", data: {count: cacheNum}, async: false}, { + ajax({url: this.urlPrefix + "/_uuids", data: {count: cacheNum}, async: + false}, { success: function(resp) { uuidCache = resp.uuids; } @@ -602,31 +1001,20 @@ } }); - var httpData = $.httpData || function( xhr, type, s ) { // lifted from jq1.4.4 - var ct = xhr.getResponseHeader("content-type") || "", - xml = type === "xml" || !type && ct.indexOf("xml") >= 0, - data = xml ? xhr.responseXML : xhr.responseText; - - if ( xml && data.documentElement.nodeName === "parsererror" ) { - $.error( "parsererror" ); - } - if ( s && s.dataFilter ) { - data = s.dataFilter( data, type ); - } - if ( typeof data === "string" ) { - if ( type === "json" || !type && ct.indexOf("json") >= 0 ) { - data = $.parseJSON( data ); - } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) { - $.globalEval( data ); - } - } - return data; - }; - + /** + * @private + */ function ajax(obj, options, errorMessage, ajaxOptions) { + var timeStart; + var defaultAjaxOpts = { + contentType: "application/json", + headers:{"Accept": "application/json"} + }; + options = $.extend({successStatus: 200}, options); - ajaxOptions = $.extend({contentType: "application/json"}, ajaxOptions); + ajaxOptions = $.extend(defaultAjaxOpts, ajaxOptions); errorMessage = errorMessage || "Unknown error"; + timeStart = (new Date()).getTime(); $.ajax($.extend($.extend({ type: "GET", dataType: "json", cache : !$.browser.msie, beforeSend: function(xhr){ @@ -637,8 +1025,9 @@ } }, complete: function(req) { + var reqDuration = (new Date()).getTime() - timeStart; try { - var resp = httpData(req, "json"); + var resp = $.parseJSON(req.responseText); } catch(e) { if (options.error) { options.error(req.status, req, e); @@ -651,10 +1040,12 @@ options.ajaxStart(resp); } if (req.status == options.successStatus) { - if (options.beforeSuccess) options.beforeSuccess(req, resp); - if (options.success) options.success(resp); + if (options.beforeSuccess) options.beforeSuccess(req, resp, reqDuration); + if (options.success) options.success(resp, reqDuration); } else if (options.error) { - options.error(req.status, resp && resp.error || errorMessage, resp && resp.reason || "no response"); + options.error(req.status, resp && resp.error || + errorMessage, resp && resp.reason || "no response", + reqDuration); } else { alert(errorMessage + ": " + resp.reason); } @@ -662,6 +1053,9 @@ }, obj), ajaxOptions)); } + /** + * @private + */ function fullCommit(options) { var options = options || {}; if (typeof options.ensure_full_commit !== "undefined") { @@ -674,13 +1068,17 @@ } }; + /** + * @private + */ // Convert a options object to an url query string. // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"' function encodeOptions(options) { var buf = []; if (typeof(options) === "object" && options !== null) { for (var name in options) { - if ($.inArray(name, ["error", "success", "beforeSuccess", "ajaxStart"]) >= 0) + if ($.inArray(name, + ["error", "success", "beforeSuccess", "ajaxStart"]) >= 0) continue; var value = options[name]; if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) { @@ -692,6 +1090,9 @@ return buf.length ? "?" + buf.join("&") : ""; } + /** + * @private + */ function toJSON(obj) { return obj !== null ? JSON.stringify(obj) : null; }