From 287dce73d2ededa4abb5f8c80599032dace46268 Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Tue, 16 Dec 2008 20:48:00 +0000 Subject: action.js and tests git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@727141 13f79535-47bb-0310-9956-ffa450edef68 --- share/server/action.js | 587 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 587 insertions(+) create mode 100644 share/server/action.js (limited to 'share/server') diff --git a/share/server/action.js b/share/server/action.js new file mode 100644 index 00000000..0c1bddcd --- /dev/null +++ b/share/server/action.js @@ -0,0 +1,587 @@ +// 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. + +// JSON from json2.js +/* + json2.js + 2008-03-14 + + Public Domain + + No warranty expressed or implied. Use at your own risk. + + See http://www.JSON.org/js.html + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + Use your own copy. It is extremely unwise to load third party + code into your pages. +*/ + +var JSON = (function () { + + function f(n) { // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + Date.prototype.toJSON = function () { + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + + var m = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + + function stringify(value, whitelist) { + var a, // The array holding the partial texts. + i, // The loop counter. + k, // The member key. + l, // Length. + r = /["\\\x00-\x1f\x7f-\x9f]/g, + v; // The member value. + + switch (typeof value) { + case 'string': + + return r.test(value) ? + '"' + value.replace(r, function (a) { + var c = m[a]; + if (c) { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"' : + '"' + value + '"'; + + case 'number': + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + return String(value); + + case 'object': + + if (!value) { + return 'null'; + } + + if (typeof value.toJSON === 'function') { + return stringify(value.toJSON()); + } + a = []; + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length'))) { + + l = value.length; + for (i = 0; i < l; i += 1) { + a.push(stringify(value[i], whitelist) || 'null'); + } + + return '[' + a.join(',') + ']'; + } + if (whitelist) { + l = whitelist.length; + for (i = 0; i < l; i += 1) { + k = whitelist[i]; + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } else { + + for (k in value) { + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } + + return '{' + a.join(',') + '}'; + } + } + + return { + stringify: stringify, + parse: function (text, filter) { + var j; + + function walk(k, v) { + var i, n; + if (v && typeof v === 'object') { + for (i in v) { + if (Object.prototype.hasOwnProperty.apply(v, [i])) { + n = walk(i, v[i]); + if (n !== undefined) { + v[i] = n; + } else { + delete v[i]; + } + } + } + } + return filter(k, v); + } + + if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + j = eval('(' + text + ')'); + + return typeof filter === 'function' ? walk('', j) : j; + } + + throw new SyntaxError('parseJSON'); + } + }; +})(); + +// HTTP object provides access to curl via couchjs. + +var HTTP = (function() { + function parseCurl(string) { + var parts = string.split(/\r\n\r\n/); + var body = parts.pop(); + var header = parts.pop(); + var headers = header.split(/\n/); + + var status = /HTTP\/1.\d (\d*)/.exec(header)[1]; + return { + responseText: body, + status: parseInt(status), + getResponseHeader: function(key) { + var keymatcher = new RegExp(RegExp.escape(key), "i"); + for (var i in headers) { + var h = headers[i]; + if (keymatcher.test(h)) { + var value = h.substr(key.length+2); + value = value.slice(0, value.length-1); + return value; + } + } + return ""; + } + } + }; + return { + GET : function(url, body, headers) { + var st, urx = url, hx = (headers || null); + st = gethttp(urx, hx); + return parseCurl(st); + }, + HEAD : function(url, body, headers) { + var st, urx = url, hx = (headers || null); + st = headhttp(urx, hx); + return parseCurl(st); + }, + DELETE : function(url, body, headers) { + var st, urx = url, hx = (headers || null); + st = delhttp(urx, hx); + return parseCurl(st); + }, + MOVE : function(url, body, headers) { + var st, urx = url, hx = (headers || null); + st = movehttp(urx, hx); + return parseCurl(st); + }, + COPY : function(url, body, headers) { + var st, urx = url, hx = (headers || null); + st = copyhttp(urx, hx); + return parseCurl(st); + }, + POST : function(url, body, headers) { + var st, urx = url, bx = (body || ""), hx = (headers || {}); + hx['Content-Type'] = hx['Content-Type'] || "application/json"; + st = posthttp(urx, bx, hx); + return parseCurl(st); + }, + PUT : function(url, body, headers) { + var st, urx = url, bx = (body || ""), hx = (headers || {}); + hx['Content-Type'] = hx['Content-Type'] || "application/json"; + st = puthttp(urx, bx, hx); + return parseCurl(st); + } + }; +})(); + + +// couch.js + +// A simple class to represent a database. Uses XMLHttpRequest to interface with +// the CouchDB server. + +function CouchDB(name) { + this.name = name + this.uri = "/" + encodeURIComponent(name) + "/"; + request = CouchDB.request; + + // Creates the database on the server + this.createDb = function() { + var req = request("PUT", this.uri); + var result = JSON.parse(req.responseText); + if (req.status != 201) + throw result; + return result; + } + + // Deletes the database on the server + this.deleteDb = function() { + var req = request("DELETE", this.uri); + if (req.status == 404) + return false; + var result = JSON.parse(req.responseText); + if (req.status != 200) + throw result; + return result; + } + + // Save a document to the database + this.save = function(doc, options) { + var req; + if (doc._id == undefined) + doc._id = CouchDB.newUuids(1)[0]; + + req = request("PUT", this.uri + encodeURIComponent(doc._id) + encodeOptions(options), { + body: JSON.stringify(doc) + }); + var result = JSON.parse(req.responseText); + if (req.status != 201) + throw result; + doc._rev = result.rev; + return result; + } + + // Open a document from the database + this.open = function(docId, options) { + var req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options)); + if (req.status == 404) + return null; + var result = JSON.parse(req.responseText); + if (req.status != 200) + throw result; + return result; + } + + // Deletes a document from the database + this.deleteDoc = function(doc) { + var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev); + var result = JSON.parse(req.responseText); + if (req.status != 200) + throw result; + doc._rev = result.rev; //record rev in input document + doc._deleted = true; + return result; + } + + // Deletes an attachment from a document + this.deleteDocAttachment = function(doc, attachment_name) { + var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev); + var result = JSON.parse(req.responseText); + if (req.status != 200) + throw result; + doc._rev = result.rev; //record rev in input document + return result; + } + + this.bulkSave = function(docs, options) { + // first prepoulate the UUIDs for new documents + var newCount = 0 + for (var i=0; i= n) { + var uuids = CouchDB.uuids_cache.slice(CouchDB.uuids_cache.length - n); + if(CouchDB.uuids_cache.length - n == 0) { + CouchDB.uuids_cache = []; + } else { + CouchDB.uuids_cache = + CouchDB.uuids_cache.slice(0, CouchDB.uuids_cache.length - n); + } + return uuids; + } else { + var req = CouchDB.request("POST", "/_uuids?count=" + (100 + n)); + var result = JSON.parse(req.responseText); + if (req.status != 200) + throw result; + CouchDB.uuids_cache = + CouchDB.uuids_cache.concat(result.uuids.slice(0, 100)); + return result.uuids.slice(100); + } +} + +CouchDB.host = (typeof window == 'undefined' || !window) ? "127.0.0.1:5984" : window; + +CouchDB.request = function(method, uri, options) { + var full_uri = "http://" + CouchDB.host + uri; + options = options || {}; + var response = HTTP[method](full_uri, options.body, options.headers); + return response; +} + +log = function(message) { + print(toJSON({log: toJSON(message)})); +} + +var sandbox = null; + +try { + // if possible, use evalcx (not always available) + sandbox = evalcx(''); + sandbox.JSON = JSON; + sandbox.HTTP = HTTP; + sandbox.CouchDB = CouchDB; + sandbox.log = log; +} catch (e) {} + +function error(httpcode, type, msg) { + return {code: httpcode , json:{error: type, reason: msg}}; +} + +function process_request(req) { + // Req is an object that contains information about the HTTP + // request made to the action server. + // Something like: {"verb": "GET", "path": ["foo", "bar"], "query": ..., "post":...} + // Here we use "path" to fetch the action, compile it, and run it. + if (req.path.length != 2) { + return error(404, "action_error", "Invalid path: \"" + req.path.join("/") + "\"."); + } + + var desname = "_design%2F" + req.path[0]; + + // todo: make this work with other ports and addresses... + // maybe couchdb can send config information over to the _external server on boot + + var desdoc = HTTP.GET("http://127.0.0.1:5984/" + req.info.db_name + "/" + desname); + + if (desdoc.status != 200) { + return error(404, "action_error", "Design document '" + desname + "' not found."); + } + + var desjson = JSON.parse(desdoc.responseText); + + if (!desjson.actions) { + return error(500, "action_error", "No actions found in design doc '" + desname + "'."); + } + + if (!desjson.actions[req.path[1]]) { + return error(500, "action_error", "No action '" + req.path[1] + "' in design doc '" + desname + "'"); + } + + var func = null; + try { + // todo - use compileFunction from main.js? + func = eval(desjson.actions[req.path[1]]); + } catch(exception) { + return error(500, "action_error", "Failed to compile action: " + JSON.stringify(exception)); + } + + var ret = null; + var db = new CouchDB(req.info.db_name); + return func(req, db); +} + +function respond(obj) { + print(JSON.stringify(obj)); +}; + +var req; +var resp; +while (req = JSON.parse(readline())) { + try { + resp = process_request(req); + } catch(exception) { + if (exception.code) { + resp = exception; + } else if (exception.error && exception.reason) { + resp = error(500, exception.error, exception.reason); + } else { + resp = error(500, "action_error", JSON.stringify(exception)); + } + } + + try { + respond(resp); + } catch(e) { + respond(error(500, "json_error", "Error encoding JSON response. Exception: "+e.toString())); + } +} \ No newline at end of file -- cgit v1.2.3