3deb4410237a5d256ccb7ac52272edcb81991802
[couchdb.git] / share / www / script / couch.js
1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
2 // use this file except in compliance with the License. You may obtain a copy of
3 // the License at
4 //
5 //   http://www.apache.org/licenses/LICENSE-2.0
6 //
7 // Unless required by applicable law or agreed to in writing, software
8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 // License for the specific language governing permissions and limitations under
11 // the License.
12
13 // A simple class to represent a database. Uses XMLHttpRequest to interface with
14 // the CouchDB server.
15
16 function CouchDB(name, httpHeaders) {
17   this.name = name;
18   this.uri = "/" + encodeURIComponent(name) + "/";
19
20   // The XMLHttpRequest object from the most recent request. Callers can
21   // use this to check result http status and headers.
22   this.last_req = null;
23
24   this.request = function(method, uri, requestOptions) {
25     requestOptions = requestOptions || {};
26     requestOptions.headers = combine(requestOptions.headers, httpHeaders);
27     return CouchDB.request(method, uri, requestOptions);
28   };
29
30   // Creates the database on the server
31   this.createDb = function() {
32     this.last_req = this.request("PUT", this.uri);
33     CouchDB.maybeThrowError(this.last_req);
34     return JSON.parse(this.last_req.responseText);
35   };
36
37   // Deletes the database on the server
38   this.deleteDb = function() {
39     this.last_req = this.request("DELETE", this.uri);
40     if (this.last_req.status == 404) {
41       return false;
42     }
43     CouchDB.maybeThrowError(this.last_req);
44     return JSON.parse(this.last_req.responseText);
45   };
46
47   // Save a document to the database
48   this.save = function(doc, options, http_headers) {
49     if (doc._id == undefined) {
50       doc._id = CouchDB.newUuids(1)[0];
51     }
52     http_headers = http_headers || {};
53     this.last_req = this.request("PUT", this.uri  +
54         encodeURIComponent(doc._id) + encodeOptions(options),
55         {body: JSON.stringify(doc), headers: http_headers});
56     CouchDB.maybeThrowError(this.last_req);
57     var result = JSON.parse(this.last_req.responseText);
58     doc._rev = result.rev;
59     return result;
60   };
61
62   // Open a document from the database
63   this.open = function(docId, url_params, http_headers) {
64     this.last_req = this.request("GET", this.uri + encodeURIComponent(docId)
65       + encodeOptions(url_params), {headers:http_headers});
66     if (this.last_req.status == 404) {
67       return null;
68     }
69     CouchDB.maybeThrowError(this.last_req);
70     return JSON.parse(this.last_req.responseText);
71   };
72
73   // Deletes a document from the database
74   this.deleteDoc = function(doc) {
75     this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id)
76       + "?rev=" + doc._rev);
77     CouchDB.maybeThrowError(this.last_req);
78     var result = JSON.parse(this.last_req.responseText);
79     doc._rev = result.rev; //record rev in input document
80     doc._deleted = true;
81     return result;
82   };
83
84   // Deletes an attachment from a document
85   this.deleteDocAttachment = function(doc, attachment_name) {
86     this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id)
87       + "/" + attachment_name + "?rev=" + doc._rev);
88     CouchDB.maybeThrowError(this.last_req);
89     var result = JSON.parse(this.last_req.responseText);
90     doc._rev = result.rev; //record rev in input document
91     return result;
92   };
93
94   this.bulkSave = function(docs, options) {
95     // first prepoulate the UUIDs for new documents
96     var newCount = 0;
97     for (var i=0; i<docs.length; i++) {
98       if (docs[i]._id == undefined) {
99         newCount++;
100       }
101     }
102     var newUuids = CouchDB.newUuids(newCount);
103     var newCount = 0;
104     for (var i=0; i<docs.length; i++) {
105       if (docs[i]._id == undefined) {
106         docs[i]._id = newUuids.pop();
107       }
108     }
109     var json = {"docs": docs};
110     // put any options in the json
111     for (var option in options) {
112       json[option] = options[option];
113     }
114     this.last_req = this.request("POST", this.uri + "_bulk_docs", {
115       body: JSON.stringify(json)
116     });
117     if (this.last_req.status == 417) {
118       return {errors: JSON.parse(this.last_req.responseText)};
119     }
120     else {
121       CouchDB.maybeThrowError(this.last_req);
122       var results = JSON.parse(this.last_req.responseText);
123       for (var i = 0; i < docs.length; i++) {
124         if(results[i] && results[i].rev && results[i].ok) {
125           docs[i]._rev = results[i].rev;
126         }
127       }
128       return results;
129     }
130   };
131
132   this.ensureFullCommit = function() {
133     this.last_req = this.request("POST", this.uri + "_ensure_full_commit");
134     CouchDB.maybeThrowError(this.last_req);
135     return JSON.parse(this.last_req.responseText);
136   };
137
138   // Applies the map function to the contents of database and returns the results.
139   this.query = function(mapFun, reduceFun, options, keys, language) {
140     var body = {language: language || "javascript"};
141     if(keys) {
142       body.keys = keys ;
143     }
144     if (typeof(mapFun) != "string") {
145       mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")";
146     }
147     body.map = mapFun;
148     if (reduceFun != null) {
149       if (typeof(reduceFun) != "string") {
150         reduceFun = reduceFun.toSource ?
151           reduceFun.toSource() : "(" + reduceFun.toString() + ")";
152       }
153       body.reduce = reduceFun;
154     }
155     if (options && options.options != undefined) {
156         body.options = options.options;
157         delete options.options;
158     }
159     this.last_req = this.request("POST", this.uri + "_temp_view"
160       + encodeOptions(options), {
161       headers: {"Content-Type": "application/json"},
162       body: JSON.stringify(body)
163     });
164     CouchDB.maybeThrowError(this.last_req);
165     return JSON.parse(this.last_req.responseText);
166   };
167
168   this.view = function(viewname, options, keys) {
169     var viewParts = viewname.split('/');
170     var viewPath = this.uri + "_design/" + viewParts[0] + "/_view/"
171         + viewParts[1] + encodeOptions(options);
172     if(!keys) {
173       this.last_req = this.request("GET", viewPath);
174     } else {
175       this.last_req = this.request("POST", viewPath, {
176         headers: {"Content-Type": "application/json"},
177         body: JSON.stringify({keys:keys})
178       });
179     }
180     if (this.last_req.status == 404) {
181       return null;
182     }
183     CouchDB.maybeThrowError(this.last_req);
184     return JSON.parse(this.last_req.responseText);
185   };
186
187   // gets information about the database
188   this.info = function() {
189     this.last_req = this.request("GET", this.uri);
190     CouchDB.maybeThrowError(this.last_req);
191     return JSON.parse(this.last_req.responseText);
192   };
193
194   // gets information about a design doc
195   this.designInfo = function(docid) {
196     this.last_req = this.request("GET", this.uri + docid + "/_info");
197     CouchDB.maybeThrowError(this.last_req);
198     return JSON.parse(this.last_req.responseText);
199   };
200
201   this.allDocs = function(options,keys) {
202     if(!keys) {
203       this.last_req = this.request("GET", this.uri + "_all_docs"
204         + encodeOptions(options));
205     } else {
206       this.last_req = this.request("POST", this.uri + "_all_docs"
207         + encodeOptions(options), {
208         headers: {"Content-Type": "application/json"},
209         body: JSON.stringify({keys:keys})
210       });
211     }
212     CouchDB.maybeThrowError(this.last_req);
213     return JSON.parse(this.last_req.responseText);
214   };
215
216   this.designDocs = function() {
217     return this.allDocs({startkey:"_design", endkey:"_design0"});
218   };
219
220   this.changes = function(options) {
221     this.last_req = this.request("GET", this.uri + "_changes"
222       + encodeOptions(options));
223     CouchDB.maybeThrowError(this.last_req);
224     return JSON.parse(this.last_req.responseText);
225   };
226
227   this.compact = function() {
228     this.last_req = this.request("POST", this.uri + "_compact");
229     CouchDB.maybeThrowError(this.last_req);
230     return JSON.parse(this.last_req.responseText);
231   };
232
233   this.viewCleanup = function() {
234     this.last_req = this.request("POST", this.uri + "_view_cleanup");
235     CouchDB.maybeThrowError(this.last_req);
236     return JSON.parse(this.last_req.responseText);
237   };
238
239   this.setDbProperty = function(propId, propValue) {
240     this.last_req = this.request("PUT", this.uri + propId,{
241       body:JSON.stringify(propValue)
242     });
243     CouchDB.maybeThrowError(this.last_req);
244     return JSON.parse(this.last_req.responseText);
245   };
246
247   this.getDbProperty = function(propId) {
248     this.last_req = this.request("GET", this.uri + propId);
249     CouchDB.maybeThrowError(this.last_req);
250     return JSON.parse(this.last_req.responseText);
251   };
252
253   this.setSecObj = function(secObj) {
254     this.last_req = this.request("PUT", this.uri + "_security",{
255       body:JSON.stringify(secObj)
256     });
257     CouchDB.maybeThrowError(this.last_req);
258     return JSON.parse(this.last_req.responseText);
259   };
260
261   this.getSecObj = function() {
262     this.last_req = this.request("GET", this.uri + "_security");
263     CouchDB.maybeThrowError(this.last_req);
264     return JSON.parse(this.last_req.responseText);
265   };
266
267   // Convert a options object to an url query string.
268   // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
269   function encodeOptions(options) {
270     var buf = [];
271     if (typeof(options) == "object" && options !== null) {
272       for (var name in options) {
273         if (!options.hasOwnProperty(name)) { continue; };
274         var value = options[name];
275         if (name == "key" || name == "keys" || name == "startkey" || name == "endkey" || (name == "open_revs" && value !== "all")) {
276           value = toJSON(value);
277         }
278         buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
279       }
280     }
281     if (!buf.length) {
282       return "";
283     }
284     return "?" + buf.join("&");
285   }
286
287   function toJSON(obj) {
288     return obj !== null ? JSON.stringify(obj) : null;
289   }
290
291   function combine(object1, object2) {
292     if (!object2) {
293       return object1;
294     }
295     if (!object1) {
296       return object2;
297     }
298
299     for (var name in object2) {
300       object1[name] = object2[name];
301     }
302     return object1;
303   }
304
305 }
306
307 // this is the XMLHttpRequest object from last request made by the following
308 // CouchDB.* functions (except for calls to request itself).
309 // Use this from callers to check HTTP status or header values of requests.
310 CouchDB.last_req = null;
311 CouchDB.urlPrefix = '';
312
313 CouchDB.login = function(name, password) {
314   CouchDB.last_req = CouchDB.request("POST", "/_session", {
315     headers: {"Content-Type": "application/x-www-form-urlencoded",
316       "X-CouchDB-WWW-Authenticate": "Cookie"},
317     body: "name=" + encodeURIComponent(name) + "&password="
318       + encodeURIComponent(password)
319   });
320   return JSON.parse(CouchDB.last_req.responseText);
321 }
322
323 CouchDB.logout = function() {
324   CouchDB.last_req = CouchDB.request("DELETE", "/_session", {
325     headers: {"Content-Type": "application/x-www-form-urlencoded",
326       "X-CouchDB-WWW-Authenticate": "Cookie"}
327   });
328   return JSON.parse(CouchDB.last_req.responseText);
329 };
330
331 CouchDB.session = function(options) {
332   options = options || {};
333   CouchDB.last_req = CouchDB.request("GET", "/_session", options);
334   CouchDB.maybeThrowError(CouchDB.last_req);
335   return JSON.parse(CouchDB.last_req.responseText);
336 };
337
338 CouchDB.allDbs = function() {
339   CouchDB.last_req = CouchDB.request("GET", "/_all_dbs");
340   CouchDB.maybeThrowError(CouchDB.last_req);
341   return JSON.parse(CouchDB.last_req.responseText);
342 };
343
344 CouchDB.allDesignDocs = function() {
345   var ddocs = {}, dbs = CouchDB.allDbs();
346   for (var i=0; i < dbs.length; i++) {
347     var db = new CouchDB(dbs[i]);
348     ddocs[dbs[i]] = db.designDocs();
349   };
350   return ddocs;
351 };
352
353 CouchDB.getVersion = function() {
354   CouchDB.last_req = CouchDB.request("GET", "/");
355   CouchDB.maybeThrowError(CouchDB.last_req);
356   return JSON.parse(CouchDB.last_req.responseText).version;
357 };
358
359 CouchDB.replicate = function(source, target, rep_options) {
360   rep_options = rep_options || {};
361   var headers = rep_options.headers || {};
362   var body = rep_options.body || {};
363   body.source = source;
364   body.target = target;
365   CouchDB.last_req = CouchDB.request("POST", "/_replicate", {
366     headers: headers,
367     body: JSON.stringify(body)
368   });
369   CouchDB.maybeThrowError(CouchDB.last_req);
370   return JSON.parse(CouchDB.last_req.responseText);
371 };
372
373 CouchDB.newXhr = function() {
374   if (typeof(XMLHttpRequest) != "undefined") {
375     return new XMLHttpRequest();
376   } else if (typeof(ActiveXObject) != "undefined") {
377     return new ActiveXObject("Microsoft.XMLHTTP");
378   } else {
379     throw new Error("No XMLHTTPRequest support detected");
380   }
381 };
382
383 CouchDB.xhrbody = function(xhr) {
384   if (xhr.responseText) {
385     return xhr.responseText;
386   } else if (xhr.body) {
387     return xhr.body
388   } else {
389     throw new Error("No XMLHTTPRequest support detected");
390   }
391 }
392
393 CouchDB.xhrheader = function(xhr, header) {
394   if(xhr.getResponseHeader) {
395     return xhr.getResponseHeader(header);
396   } else if(xhr.headers) {
397     return xhr.headers[header] || null;
398   } else {
399     throw new Error("No XMLHTTPRequest support detected");
400   }
401 }
402
403 CouchDB.proxyUrl = function(uri) {
404   if(uri.substr(0, CouchDB.protocol.length) != CouchDB.protocol) {
405     uri = CouchDB.urlPrefix + uri;
406   }
407   return uri;
408 }
409
410 CouchDB.request = function(method, uri, options) {
411   options = typeof(options) == 'object' ? options : {};
412   options.headers = typeof(options.headers) == 'object' ? options.headers : {};
413   options.headers["Content-Type"] = options.headers["Content-Type"] || options.headers["content-type"] || "application/json";
414   options.headers["Accept"] = options.headers["Accept"] || options.headers["accept"] || "application/json";
415   var req = CouchDB.newXhr();
416   uri = CouchDB.proxyUrl(uri);
417   req.open(method, uri, false);
418   if (options.headers) {
419     var headers = options.headers;
420     for (var headerName in headers) {
421       if (!headers.hasOwnProperty(headerName)) { continue; }
422       req.setRequestHeader(headerName, headers[headerName]);
423     }
424   }
425   req.send(options.body || "");
426   return req;
427 };
428
429 CouchDB.requestStats = function(module, key, test) {
430   var query_arg = "";
431   if(test !== null) {
432     query_arg = "?flush=true";
433   }
434
435   var url = "/_stats/" + module + "/" + key + query_arg;
436   var stat = CouchDB.request("GET", url).responseText;
437   return JSON.parse(stat)[module][key];
438 };
439
440 CouchDB.uuids_cache = [];
441
442 CouchDB.newUuids = function(n, buf) {
443   buf = buf || 100;
444   if (CouchDB.uuids_cache.length >= n) {
445     var uuids = CouchDB.uuids_cache.slice(CouchDB.uuids_cache.length - n);
446     if(CouchDB.uuids_cache.length - n == 0) {
447       CouchDB.uuids_cache = [];
448     } else {
449       CouchDB.uuids_cache =
450           CouchDB.uuids_cache.slice(0, CouchDB.uuids_cache.length - n);
451     }
452     return uuids;
453   } else {
454     CouchDB.last_req = CouchDB.request("GET", "/_uuids?count=" + (buf + n));
455     CouchDB.maybeThrowError(CouchDB.last_req);
456     var result = JSON.parse(CouchDB.last_req.responseText);
457     CouchDB.uuids_cache =
458         CouchDB.uuids_cache.concat(result.uuids.slice(0, buf));
459     return result.uuids.slice(buf);
460   }
461 };
462
463 CouchDB.maybeThrowError = function(req) {
464   if (req.status >= 400) {
465     try {
466       var result = JSON.parse(req.responseText);
467     } catch (ParseError) {
468       var result = {error:"unknown", reason:req.responseText};
469     }
470
471     throw (new CouchError(result));
472   }
473 }
474
475 CouchDB.params = function(options) {
476   options = options || {};
477   var returnArray = [];
478   for(var key in options) {
479     var value = options[key];
480     returnArray.push(key + "=" + value);
481   }
482   return returnArray.join("&");
483 };
484 // Used by replication test
485 if (typeof window == 'undefined' || !window) {
486   var hostRE = RegExp("https?://([^\/]+)");
487   var getter = function () {
488     return (new CouchHTTP).base_url.match(hostRE)[1];
489   };
490   if(Object.defineProperty) {
491     Object.defineProperty(CouchDB, "host", {
492       get : getter,
493       enumerable : true
494     });
495   } else {
496     CouchDB.__defineGetter__("host", getter);
497   }
498   CouchDB.protocol = "http://";
499   CouchDB.inBrowser = false;
500 } else {
501   CouchDB.host = window.location.host;
502   CouchDB.inBrowser = true;
503   CouchDB.protocol = window.location.protocol + "//";
504 }
505
506 // Turns an {error: ..., reason: ...} response into an Error instance
507 function CouchError(error) {
508   var inst = new Error(error.reason);
509   inst.name = 'CouchError';
510   inst.error = error.error;
511   inst.reason = error.reason;
512   return inst;
513 }
514 CouchError.prototype.constructor = CouchError;