// Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. (function($) { $.couch = $.couch || {}; function encodeDocId(docID) { var parts = docID.split("/"); if (parts[0] == "_design") { parts.shift(); 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; }; var uuidCache = []; $.extend($.couch, { urlPrefix: '', activeTasks: function(options) { ajax( {url: this.urlPrefix + "/_active_tasks"}, options, "Active task status could not be retrieved" ); }, allDbs: function(options) { ajax( {url: this.urlPrefix + "/_all_dbs"}, options, "An error occurred retrieving the list of all databases" ); }, config: function(options, section, option, value) { var req = {url: this.urlPrefix + "/_config/"}; if (section) { req.url += encodeURIComponent(section) + "/"; if (option) { req.url += encodeURIComponent(option); } } if (value === null) { req.type = "DELETE"; } else if (value !== undefined) { req.type = "PUT"; req.data = toJSON(value); req.contentType = "application/json"; req.processData = false } ajax(req, options, "An error occurred retrieving/updating the server configuration" ); }, session: function(options) { options = options || {}; $.ajax({ type: "GET", url: this.urlPrefix + "/_session", complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { alert("An error occurred getting session info: " + resp.reason); } } }); }, userDb : function(callback) { $.couch.session({ success : function(resp) { var userDb = $.couch.db(resp.info.authentication_db); callback(userDb); } }); }, signup: function(user_doc, password, options) { options = options || {}; // prepare user doc based on name and password user_doc = prepareUserDoc(user_doc, password); $.couch.userDb(function(db) { db.saveDoc(user_doc, options); }) }, login: function(options) { options = options || {}; $.ajax({ type: "POST", url: this.urlPrefix + "/_session", dataType: "json", data: {name: options.name, password: options.password}, complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { alert("An error occurred logging in: " + resp.reason); } } }); }, logout: function(options) { options = options || {}; $.ajax({ type: "DELETE", url: this.urlPrefix + "/_session", dataType: "json", username : "_", password : "_", complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200) { if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { alert("An error occurred logging out: " + resp.reason); } } }); }, 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) { // todo: can we use commonjs require here? if (typeof Base64 == "undefined") { alert("please include /_utils/script/base64.js in the page for base64 support"); return false; } else { doc._attachments = doc._attachments || {}; doc._attachments["rev-"+doc._rev.split("-")[0]] = { content_type :"application/json", data : Base64.encode(rawDocs[doc._id].raw) } return true; } } }; return { name: name, uri: this.urlPrefix + "/" + encodeURIComponent(name) + "/", compact: function(options) { $.extend(options, {successStatus: 202}); ajax({ type: "POST", url: this.uri + "_compact", data: "", processData: false }, options, "The database could not be compacted" ); }, viewCleanup: function(options) { $.extend(options, {successStatus: 202}); ajax({ type: "POST", url: this.uri + "_view_cleanup", data: "", processData: false }, options, "The views could not be cleaned up" ); }, compactView: function(groupname, options) { $.extend(options, {successStatus: 202}); ajax({ type: "POST", url: this.uri + "_compact/" + groupname, data: "", processData: false }, options, "The view could not be compacted" ); }, create: function(options) { $.extend(options, {successStatus: 201}); ajax({ type: "PUT", url: this.uri, contentType: "application/json", data: "", processData: false }, options, "The database could not be created" ); }, drop: function(options) { ajax( {type: "DELETE", url: this.uri}, options, "The database could not be deleted" ); }, info: function(options) { ajax( {url: this.uri}, options, "Database information could not be retrieved" ); }, 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 = { onChange : function(fun) { listeners.push(fun); }, stop : function() { active = false; } }; // call each listener when there is a change function triggerListeners(resp) { $.each(listeners, function() { this(resp); }); }; // when there is a change, call any listeners, then check for another change options.success = function(resp) { timeout = 100; if (active) { since = resp.last_seq; triggerListeners(resp); getChangesSince(); }; }; options.error = function() { if (active) { setTimeout(getChangesSince, timeout); timeout = timeout * 2; } }; // actually make the changes request function getChangesSince() { var opts = $.extend({heartbeat : 10 * 1000}, options, { feed : "longpoll", since : since }); ajax( {url: db.uri + "_changes"+encodeOptions(opts)}, options, "Error connecting to "+db.uri+"/_changes." ); } // start the first request if (since) { getChangesSince(); } else { db.info({ success : function(info) { since = info.update_seq; getChangesSince(); } }); } return promise; }, allDocs: function(options) { var type = "GET"; var data = null; if (options["keys"]) { type = "POST"; var keys = options["keys"]; delete options["keys"]; data = toJSON({ "keys": keys }); } ajax({ type: type, data: data, url: this.uri + "_all_docs" + encodeOptions(options) }, options, "An error occurred retrieving a list of all documents" ); }, allDesignDocs: function(options) { this.allDocs($.extend({startkey:"_design", endkey:"_design0"}, options)); }, allApps: function(options) { options = options || {}; var self = this; if (options.eachApp) { this.allDesignDocs({ success: function(resp) { $.each(resp.rows, function() { self.openDoc(this.id, { success: function(ddoc) { var index, appPath, appName = ddoc._id.split('/'); appName.shift(); appName = appName.join('/'); index = ddoc.couchapp && ddoc.couchapp.index; if (index) { appPath = ['', name, ddoc._id, index].join('/'); } else if (ddoc._attachments && ddoc._attachments["index.html"]) { appPath = ['', name, ddoc._id, "index.html"].join('/'); } if (appPath) options.eachApp(appName, appPath, ddoc); } }); }); } }); } else { alert("Please provide an eachApp function for allApps()"); } }, openDoc: function(docId, options, ajaxOptions) { options = options || {}; if (db_opts.attachPrevRev || options.attachPrevRev) { $.extend(options, { beforeSuccess : function(req, doc) { rawDocs[doc._id] = { rev : doc._rev, raw : req.responseText }; } }); } else { $.extend(options, { beforeSuccess : function(req, doc) { if (doc["jquery.couch.attachPrevRev"]) { rawDocs[doc._id] = { rev : doc._rev, raw : req.responseText }; } } }); } ajax({url: this.uri + encodeDocId(docId) + encodeOptions(options)}, options, "The document could not be retrieved", ajaxOptions ); }, saveDoc: function(doc, options) { options = options || {}; var db = this; var beforeSend = fullCommit(options); if (doc._id === undefined) { var method = "POST"; var uri = this.uri; } else { var method = "PUT"; var uri = this.uri + encodeDocId(doc._id); } var versioned = maybeApplyVersion(doc); $.ajax({ type: method, url: uri + encodeOptions(options), contentType: "application/json", dataType: "json", data: toJSON(doc), beforeSend : beforeSend, complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 200 || req.status == 201 || req.status == 202) { doc._id = resp.id; doc._rev = resp.rev; if (versioned) { db.openDoc(doc._id, { attachPrevRev : true, success : function(d) { doc._attachments = d._attachments; if (options.success) options.success(resp); } }); } else { if (options.success) options.success(resp); } } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { alert("The document could not be saved: " + resp.reason); } } }); }, bulkSave: function(docs, options) { var beforeSend = fullCommit(options); $.extend(options, {successStatus: 201, beforeSend : beforeSend}); ajax({ type: "POST", url: this.uri + "_bulk_docs" + encodeOptions(options), contentType: "application/json", data: toJSON(docs) }, options, "The documents could not be saved" ); }, removeDoc: function(doc, options) { ajax({ type: "DELETE", url: this.uri + encodeDocId(doc._id) + encodeOptions({rev: doc._rev}) }, options, "The document could not be deleted" ); }, bulkRemove: function(docs, options){ docs.docs = $.each( docs.docs, function(i, doc){ doc._deleted = true; } ); $.extend(options, {successStatus: 201}); ajax({ type: "POST", url: this.uri + "_bulk_docs" + encodeOptions(options), data: toJSON(docs) }, options, "The documents could not be deleted" ); }, copyDoc: function(docId, options, ajaxOptions) { ajaxOptions = $.extend(ajaxOptions, { complete: function(req) { var resp = $.httpData(req, "json"); if (req.status == 201) { if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp.error, resp.reason); } else { alert("The document could not be copied: " + resp.reason); } } }); ajax({ type: "COPY", url: this.uri + encodeDocId(docId) }, options, "The document could not be copied", ajaxOptions ); }, query: function(mapFun, reduceFun, language, options) { language = language || "javascript"; if (typeof(mapFun) !== "string") { 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() + ")"; body.reduce = reduceFun; } ajax({ type: "POST", url: this.uri + "_temp_view" + encodeOptions(options), contentType: "application/json", data: toJSON(body) }, options, "An error occurred querying the database" ); }, list: function(list, view, options) { var list = list.split('/'); var options = options || {}; var type = 'GET'; var data = null; if (options['keys']) { type = 'POST'; var keys = options['keys']; delete options['keys']; data = toJSON({'keys': keys }); } ajax({ type: type, data: data, url: this.uri + '_design/' + list[0] + '/_list/' + list[1] + '/' + view + encodeOptions(options) }, options, 'An error occured accessing the list' ); }, view: function(name, options) { var name = name.split('/'); var options = options || {}; var type = "GET"; var data= null; if (options["keys"]) { type = "POST"; var keys = options["keys"]; delete options["keys"]; data = toJSON({ "keys": keys }); } ajax({ type: type, data: data, url: this.uri + "_design/" + name[0] + "/_view/" + name[1] + encodeOptions(options) }, options, "An error occurred accessing the view" ); }, getDbProperty: function(propName, options, ajaxOptions) { ajax({url: this.uri + propName + encodeOptions(options)}, options, "The property could not be retrieved", ajaxOptions ); }, setDbProperty: function(propName, propValue, options, ajaxOptions) { ajax({ type: "PUT", url: this.uri + propName + encodeOptions(options), data : JSON.stringify(propValue) }, options, "The property could not be updated", ajaxOptions ); } }; }, encodeDocId: encodeDocId, info: function(options) { ajax( {url: this.urlPrefix + "/"}, options, "Server information could not be retrieved" ); }, replicate: function(source, target, ajaxOptions, repOpts) { repOpts = $.extend({source: source, target: target}, repOpts); if (repOpts.continuous) { ajaxOptions.successStatus = 202; } ajax({ type: "POST", url: this.urlPrefix + "/_replicate", data: JSON.stringify(repOpts), contentType: "application/json" }, ajaxOptions, "Replication failed" ); }, newUUID: function(cacheNum) { if (cacheNum === undefined) { cacheNum = 1; } if (!uuidCache.length) { ajax({url: this.urlPrefix + "/_uuids", data: {count: cacheNum}, async: false}, { success: function(resp) { uuidCache = resp.uuids } }, "Failed to retrieve UUID batch." ); } return uuidCache.shift(); } }); function ajax(obj, options, errorMessage, ajaxOptions) { options = $.extend({successStatus: 200}, options); ajaxOptions = $.extend({contentType: "application/json"}, ajaxOptions); errorMessage = errorMessage || "Unknown error"; $.ajax($.extend($.extend({ type: "GET", dataType: "json", cache : !$.browser.msie, beforeSend: function(xhr){ if(ajaxOptions && ajaxOptions.headers){ for (var header in ajaxOptions.headers){ xhr.setRequestHeader(header, ajaxOptions.headers[header]); } } }, complete: function(req) { try { var resp = $.httpData(req, "json"); } catch(e) { if (options.error) { options.error(req.status, req, e); } else { alert(errorMessage + ": " + e); } return; } if (options.ajaxStart) { options.ajaxStart(resp); } if (req.status == options.successStatus) { if (options.beforeSuccess) options.beforeSuccess(req, resp); if (options.success) options.success(resp); } else if (options.error) { options.error(req.status, resp && resp.error || errorMessage, resp && resp.reason || "no response"); } else { alert(errorMessage + ": " + resp.reason); } } }, obj), ajaxOptions)); } function fullCommit(options) { var options = options || {}; if (typeof options.ensure_full_commit !== "undefined") { var commit = options.ensure_full_commit; delete options.ensure_full_commit; return function(xhr) { xhr.setRequestHeader("X-Couch-Full-Commit", commit.toString()); }; } }; // 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) continue; var value = options[name]; if ($.inArray(name, ["key", "startkey", "endkey"]) >= 0) { value = toJSON(value); } buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value)); } } return buf.length ? "?" + buf.join("&") : ""; } function toJSON(obj) { return obj !== null ? JSON.stringify(obj) : null; } })(jQuery);