Return-Path: Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: (qmail 42504 invoked from network); 22 Dec 2009 18:04:24 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.3) by minotaur.apache.org with SMTP; 22 Dec 2009 18:04:24 -0000 Received: (qmail 99534 invoked by uid 500); 22 Dec 2009 18:04:24 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 99453 invoked by uid 500); 22 Dec 2009 18:04:24 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 99444 invoked by uid 99); 22 Dec 2009 18:04:24 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 22 Dec 2009 18:04:24 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 22 Dec 2009 18:04:10 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 744A623889D5; Tue, 22 Dec 2009 18:03:47 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r893249 [1/2] - in /couchdb/trunk: etc/couchdb/ share/server/ share/www/script/test/ src/couchdb/ test/view_server/ Date: Tue, 22 Dec 2009 18:03:46 -0000 To: commits@couchdb.apache.org From: jchris@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20091222180347.744A623889D5@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Author: jchris Date: Tue Dec 22 18:03:44 2009 New Revision: 893249 URL: http://svn.apache.org/viewvc?rev=893249&view=rev Log: move query server to a design-doc based protocol, closes COUCHDB-589 Modified: couchdb/trunk/etc/couchdb/default.ini.tpl.in couchdb/trunk/share/server/filter.js couchdb/trunk/share/server/loop.js couchdb/trunk/share/server/render.js couchdb/trunk/share/server/state.js couchdb/trunk/share/server/util.js couchdb/trunk/share/server/validate.js couchdb/trunk/share/server/views.js couchdb/trunk/share/www/script/test/changes.js couchdb/trunk/share/www/script/test/design_docs.js couchdb/trunk/share/www/script/test/list_views.js couchdb/trunk/share/www/script/test/show_documents.js couchdb/trunk/share/www/script/test/update_documents.js couchdb/trunk/src/couchdb/couch_doc.erl couchdb/trunk/src/couchdb/couch_httpd.erl couchdb/trunk/src/couchdb/couch_httpd_db.erl couchdb/trunk/src/couchdb/couch_httpd_external.erl couchdb/trunk/src/couchdb/couch_httpd_show.erl couchdb/trunk/src/couchdb/couch_httpd_view.erl couchdb/trunk/src/couchdb/couch_native_process.erl couchdb/trunk/src/couchdb/couch_os_process.erl couchdb/trunk/src/couchdb/couch_query_servers.erl couchdb/trunk/test/view_server/query_server_spec.rb couchdb/trunk/test/view_server/run_native_process.es Modified: couchdb/trunk/etc/couchdb/default.ini.tpl.in URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/default.ini.tpl.in?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/etc/couchdb/default.ini.tpl.in (original) +++ couchdb/trunk/etc/couchdb/default.ini.tpl.in Tue Dec 22 18:03:44 2009 @@ -76,7 +76,6 @@ _view_cleanup = {couch_httpd_db, handle_view_cleanup_req} _compact = {couch_httpd_db, handle_compact_req} _design = {couch_httpd_db, handle_design_req} -_view = {couch_httpd_view, handle_db_view_req} _temp_view = {couch_httpd_view, handle_temp_view_req} _changes = {couch_httpd_db, handle_changes_req} Modified: couchdb/trunk/share/server/filter.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/filter.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/filter.js (original) +++ couchdb/trunk/share/server/filter.js Tue Dec 22 18:03:44 2009 @@ -11,17 +11,14 @@ // the License. var Filter = { - filter : function(funSrc, docs, req, userCtx) { - var filterFun = compileFunction(funSrc); - + filter : function(fun, ddoc, args) { var results = []; - try { - for (var i=0; i < docs.length; i++) { - results.push((filterFun(docs[i], req, userCtx) && true) || false); - }; - respond([true, results]); - } catch (error) { - respond(error); - } + var docs = args[0]; + var req = args[1]; + var userCtx = args[2]; + for (var i=0; i < docs.length; i++) { + results.push((fun.apply(ddoc, [docs[i], req, userCtx]) && true) || false); + }; + respond([true, results]); } }; Modified: couchdb/trunk/share/server/loop.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/loop.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/loop.js (original) +++ couchdb/trunk/share/server/loop.js Tue Dec 22 18:03:44 2009 @@ -12,21 +12,21 @@ var sandbox = null; -var init_sandbox = function() { +function init_sandbox() { try { // if possible, use evalcx (not always available) sandbox = evalcx(''); - sandbox.emit = emit; - sandbox.sum = sum; + sandbox.emit = Views.emit; + sandbox.sum = Views.sum; sandbox.log = log; - sandbox.toJSON = toJSON; - sandbox.provides = provides; - sandbox.registerType = registerType; - sandbox.start = start; - sandbox.send = send; - sandbox.getRow = getRow; + sandbox.toJSON = Couch.toJSON; + sandbox.provides = Mime.provides; + sandbox.registerType = Mime.registerType; + sandbox.start = Render.start; + sandbox.send = Render.send; + sandbox.getRow = Render.getRow; } catch (e) { - log(toJSON(e)); + log(e.toSource()); } }; init_sandbox(); @@ -36,37 +36,104 @@ // // Responses are json values followed by a new line ("\n") -var line, cmd, cmdkey; - -var dispatch = { - "reset" : State.reset, - "add_fun" : State.addFun, - "map_doc" : Views.mapDoc, - "reduce" : Views.reduce, - "rereduce" : Views.rereduce, - "validate" : Validate.validate, - "show" : Render.show, - "update" : Render.update, - "list" : Render.list, - "filter" : Filter.filter -}; +var DDoc = (function() { + var ddoc_dispatch = { + "lists" : Render.list, + "shows" : Render.show, + "filters" : Filter.filter, + "updates" : Render.update, + "validate_doc_update" : Validate.validate + }; + var ddocs = {}; + return { + ddoc : function() { + var args = []; + for (var i=0; i < arguments.length; i++) { + args.push(arguments[i]); + }; + var ddocId = args.shift(); + if (ddocId == "new") { + // get the real ddocId. + ddocId = args.shift(); + // store the ddoc, functions are lazily compiled. + ddocs[ddocId] = args.shift(); + print("true"); + } else { + // Couch makes sure we know this ddoc already. + var ddoc = ddocs[ddocId]; + if (!ddoc) throw(["fatal", "query_protocol_error", "uncached design doc: "+ddocId]); + var funPath = args.shift(); + var cmd = funPath[0]; + // the first member of the fun path determines the type of operation + var funArgs = args.shift(); + if (ddoc_dispatch[cmd]) { + // get the function, call the command with it + var point = ddoc; + for (var i=0; i < funPath.length; i++) { + if (i+1 == funPath.length) { + fun = point[funPath[i]] + if (typeof fun != "function") { + fun = Couch.compileFunction(fun); + // cache the compiled fun on the ddoc + point[funPath[i]] = fun + }; + } else { + point = point[funPath[i]] + } + }; + + // run the correct responder with the cmd body + ddoc_dispatch[cmd].apply(null, [fun, ddoc, funArgs]); + } else { + // unknown command, quit and hope the restarted version is better + throw(["fatal", "unknown_command", "unknown ddoc command '" + cmd + "'"]); + } + } + } + }; +})(); -while (line = eval(readline())) { - cmd = eval(line); - line_length = line.length; - try { - cmdkey = cmd.shift(); - if (dispatch[cmdkey]) { - // run the correct responder with the cmd body - dispatch[cmdkey].apply(this, cmd); +var Loop = function() { + var line, cmd, cmdkey, dispatch = { + "ddoc" : DDoc.ddoc, + // "view" : Views.handler, + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce + }; + function handleError(e) { + var type = e[0]; + if (type == "fatal") { + e[0] = "error"; // we tell the client it was a fatal error by dying + respond(e); + quit(-1); + } else if (type == "error") { + respond(e); + } else if (e.error && e.reason) { + // compatibility with old error format + respond(["error", e.error, e.reason]); } else { - // unknown command, quit and hope the restarted version is better - respond({ - error: "query_server_error", - reason: "unknown command '" + cmdkey + "'"}); - quit(); + respond(["error","unnamed_error",e.toSource()]); } - } catch(e) { - respond(e); - } + }; + while (line = readline()) { + cmd = eval('('+line+')'); + State.line_length = line.length; + try { + cmdkey = cmd.shift(); + if (dispatch[cmdkey]) { + // run the correct responder with the cmd body + dispatch[cmdkey].apply(null, cmd); + } else { + // unknown command, quit and hope the restarted version is better + throw(["fatal", "unknown_command", "unknown command '" + cmdkey + "'"]); + } + } catch(e) { + handleError(e); + } + }; }; + +Loop(); Modified: couchdb/trunk/share/server/render.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/render.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/render.js (original) +++ couchdb/trunk/share/server/render.js Tue Dec 22 18:03:44 2009 @@ -11,152 +11,115 @@ // the License. -// registerType(name, mime-type, mime-type, ...) -// -// Available in query server sandbox. TODO: The list is cleared on reset. -// This registers a particular name with the set of mimetypes it can handle. -// Whoever registers last wins. -// -// Example: -// registerType("html", "text/html; charset=utf-8"); - -mimesByKey = {}; -keysByMime = {}; -registerType = function() { - var mimes = [], key = arguments[0]; - for (var i=1; i < arguments.length; i++) { - mimes.push(arguments[i]); - }; - mimesByKey[key] = mimes; - for (var i=0; i < mimes.length; i++) { - keysByMime[mimes[i]] = key; - }; -}; - -// Some default types -// Ported from Ruby on Rails -// Build list of Mime types for HTTP responses -// http://www.iana.org/assignments/media-types/ -// http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb - -registerType("all", "*/*"); -registerType("text", "text/plain; charset=utf-8", "txt"); -registerType("html", "text/html; charset=utf-8"); -registerType("xhtml", "application/xhtml+xml", "xhtml"); -registerType("xml", "application/xml", "text/xml", "application/x-xml"); -registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); -registerType("css", "text/css"); -registerType("ics", "text/calendar"); -registerType("csv", "text/csv"); -registerType("rss", "application/rss+xml"); -registerType("atom", "application/atom+xml"); -registerType("yaml", "application/x-yaml", "text/yaml"); -// just like Rails -registerType("multipart_form", "multipart/form-data"); -registerType("url_encoded_form", "application/x-www-form-urlencoded"); -// http://www.ietf.org/rfc/rfc4627.txt -registerType("json", "application/json", "text/x-json"); - -// Start chunks -var startResp = {}; -function start(resp) { - startResp = resp || {}; -}; - -function sendStart() { - startResp = applyContentType((startResp || {}), responseContentType); - respond(["start", chunks, startResp]); - chunks = []; - startResp = {}; -} - -function applyContentType(resp, responseContentType) { - resp["headers"] = resp["headers"] || {}; - if (responseContentType) { - resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; - } - return resp; -} - -// Send chunk -var chunks = []; -function send(chunk) { - chunks.push(chunk.toString()); -}; - -function blowChunks(label) { - respond([label||"chunks", chunks]); - chunks = []; -}; - -var gotRow = false, lastRow = false; -function getRow() { - if (lastRow) return null; - if (!gotRow) { - gotRow = true; - sendStart(); - } else { - blowChunks(); - } - var line = readline(); - var json = eval(line); - if (json[0] == "list_end") { - lastRow = true; - return null; - } - if (json[0] != "list_row") { - respond({ - error: "query_server_error", - reason: "not a row '" + json[0] + "'"}); - quit(); +var Mime = (function() { + // registerType(name, mime-type, mime-type, ...) + // + // Available in query server sandbox. TODO: The list is cleared on reset. + // This registers a particular name with the set of mimetypes it can handle. + // Whoever registers last wins. + // + // Example: + // registerType("html", "text/html; charset=utf-8"); + + var mimesByKey = {}; + var keysByMime = {}; + function registerType() { + var mimes = [], key = arguments[0]; + for (var i=1; i < arguments.length; i++) { + mimes.push(arguments[i]); + }; + mimesByKey[key] = mimes; + for (var i=0; i < mimes.length; i++) { + keysByMime[mimes[i]] = key; + }; } - return json[1]; -}; -var mimeFuns = [], providesUsed, responseContentType; -function provides(type, fun) { - providesUsed = true; - mimeFuns.push([type, fun]); -}; - -function runProvides(req) { - var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; - if (req.query && req.query.format) { - bestKey = req.query.format; - responseContentType = mimesByKey[bestKey][0]; - } else if (accept) { - // log("using accept header: "+accept); - mimeFuns.reverse().forEach(function(mimeFun) { - var mimeKey = mimeFun[0]; - if (mimesByKey[mimeKey]) { - supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); - } - }); - responseContentType = Mimeparse.bestMatch(supportedMimes, accept); - bestKey = keysByMime[responseContentType]; - } else { - // just do the first one - bestKey = mimeFuns[0][0]; - responseContentType = mimesByKey[bestKey][0]; - } + // Some default types + // Ported from Ruby on Rails + // Build list of Mime types for HTTP responses + // http://www.iana.org/assignments/media-types/ + // http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb + + registerType("all", "*/*"); + registerType("text", "text/plain; charset=utf-8", "txt"); + registerType("html", "text/html; charset=utf-8"); + registerType("xhtml", "application/xhtml+xml", "xhtml"); + registerType("xml", "application/xml", "text/xml", "application/x-xml"); + registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); + registerType("css", "text/css"); + registerType("ics", "text/calendar"); + registerType("csv", "text/csv"); + registerType("rss", "application/rss+xml"); + registerType("atom", "application/atom+xml"); + registerType("yaml", "application/x-yaml", "text/yaml"); + // just like Rails + registerType("multipart_form", "multipart/form-data"); + registerType("url_encoded_form", "application/x-www-form-urlencoded"); + // http://www.ietf.org/rfc/rfc4627.txt + registerType("json", "application/json", "text/x-json"); - if (bestKey) { - for (var i=0; i < mimeFuns.length; i++) { - if (mimeFuns[i][0] == bestKey) { - bestFun = mimeFuns[i][1]; - break; - } + + var mimeFuns = []; + function provides(type, fun) { + Mime.providesUsed = true; + mimeFuns.push([type, fun]); + }; + + function resetProvides() { + // set globals + Mime.providesUsed = false; + mimeFuns = []; + Mime.responseContentType = null; + }; + + function runProvides(req) { + var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; + if (req.query && req.query.format) { + bestKey = req.query.format; + Mime.responseContentType = mimesByKey[bestKey][0]; + } else if (accept) { + // log("using accept header: "+accept); + mimeFuns.reverse().forEach(function(mimeFun) { + var mimeKey = mimeFun[0]; + if (mimesByKey[mimeKey]) { + supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); + } + }); + Mime.responseContentType = Mimeparse.bestMatch(supportedMimes, accept); + bestKey = keysByMime[Mime.responseContentType]; + } else { + // just do the first one + bestKey = mimeFuns[0][0]; + Mime.responseContentType = mimesByKey[bestKey][0]; + } + + if (bestKey) { + for (var i=0; i < mimeFuns.length; i++) { + if (mimeFuns[i][0] == bestKey) { + bestFun = mimeFuns[i][1]; + break; + } + }; }; + + if (bestFun) { + return bestFun(); + } else { + var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); + throw(["error","not_acceptable", + "Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')]); + } }; + - if (bestFun) { - // log("responding with: "+bestKey); - return bestFun(); - } else { - var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); - throw({error:"not_acceptable", reason:"Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')}); - } -}; + return { + registerType : registerType, + provides : provides, + resetProvides : resetProvides, + runProvides : runProvides + } +})(); + @@ -167,151 +130,202 @@ //// //// -var Render = { - show : function(funSrc, doc, req) { - var showFun = compileFunction(funSrc); - runShow(showFun, doc, req, funSrc); - }, - update : function(funSrc, doc, req) { - var upFun = compileFunction(funSrc); - runUpdate(upFun, doc, req, funSrc); - }, - list : function(head, req) { - runList(funs[0], head, req, funsrc[0]); +var Render = (function() { + var chunks = []; + + + // Start chunks + var startResp = {}; + function start(resp) { + startResp = resp || {}; + }; + + function sendStart() { + startResp = applyContentType((startResp || {}), Mime.responseContentType); + respond(["start", chunks, startResp]); + chunks = []; + startResp = {}; } -}; -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { + function applyContentType(resp, responseContentType) { + resp["headers"] = resp["headers"] || {}; + if (responseContentType) { + resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; + } return resp; } -}; -function resetProvides() { - // set globals - providesUsed = false; - mimeFuns = []; - responseContentType = null; -}; + function send(chunk) { + chunks.push(chunk.toString()); + }; + + function blowChunks(label) { + respond([label||"chunks", chunks]); + chunks = []; + }; -// from http://javascript.crockford.com/remedial.html -function typeOf(value) { + var gotRow = false, lastRow = false; + function getRow() { + if (lastRow) return null; + if (!gotRow) { + gotRow = true; + sendStart(); + } else { + blowChunks(); + } + var line = readline(); + var json = eval('('+line+')'); + if (json[0] == "list_end") { + lastRow = true; + return null; + } + if (json[0] != "list_row") { + throw(["fatal", "list_error", "not a row '" + json[0] + "'"]); + } + return json[1]; + }; + + + function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } + }; + + // from http://javascript.crockford.com/remedial.html + function typeOf(value) { var s = typeof value; if (s === 'object') { - if (value) { - if (value instanceof Array) { - s = 'array'; - } - } else { - s = 'null'; + if (value) { + if (value instanceof Array) { + s = 'array'; } + } else { + s = 'null'; + } } return s; -}; + }; -function runShow(showFun, doc, req, funSrc) { - try { - resetProvides(); - var resp = showFun.apply(null, [doc, req]); - - if (providesUsed) { - resp = runProvides(req); - resp = applyContentType(maybeWrapResponse(resp), responseContentType); - } + function runShow(fun, ddoc, args) { + try { + Mime.resetProvides(); + var resp = fun.apply(ddoc, args); + + if (Mime.providesUsed) { + resp = Mime.runProvides(args[1]); + resp = applyContentType(maybeWrapResponse(resp), Mime.responseContentType); + } - var type = typeOf(resp); - if (type == 'object' || type == 'string') { - respond(["resp", maybeWrapResponse(resp)]); - } else { - renderError("undefined response from show function"); + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["resp", maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from show function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - } catch(e) { - respondError(e, funSrc, true); - } -}; + }; -function runUpdate(renderFun, doc, req, funSrc) { - try { - var result = renderFun.apply(null, [doc, req]); - var doc = result[0]; - var resp = result[1]; - if (resp) { - respond(["up", doc, maybeWrapResponse(resp)]); - } else { - renderError("undefined response from update function"); + function runUpdate(fun, ddoc, args) { + try { + var verb = args[1].verb; + // for analytics logging applications you might want to remove the next line + if (verb == "GET") throw(["error","method_not_allowed","Update functions do not allow GET"]); + var result = fun.apply(ddoc, args); + var doc = result[0]; + var resp = result[1]; + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["up", doc, maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from update function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - } catch(e) { - respondError(e, funSrc, true); - } -}; + }; -function resetList() { - gotRow = false; - lastRow = false; - chunks = []; - startResp = {}; -}; - -function runList(listFun, head, req, funSrc) { - try { - if (listFun.arity > 2) { - throw("the list API has changed for CouchDB 0.10, please upgrade your code"); - } - - resetProvides(); - resetList(); - - var tail = listFun.apply(null, [head, req]); - - if (providesUsed) { - tail = runProvides(req); - } - - if (!gotRow) { - getRow(); + function resetList() { + gotRow = false; + lastRow = false; + chunks = []; + startResp = {}; + }; + + function runList(listFun, ddoc, args) { + try { + Mime.resetProvides(); + resetList(); + head = args[0] + req = args[1] + var tail = listFun.apply(ddoc, args); + + if (Mime.providesUsed) { + tail = Mime.runProvides(req); + } + if (!gotRow) getRow(); + if (typeof tail != "undefined") { + chunks.push(tail); + } + blowChunks("end"); + } catch(e) { + renderError(e, listFun.toSource()); } - if (typeof tail != "undefined") { - chunks.push(tail); + }; + + function renderError(e, funSrc) { + if (e.error && e.reason || e[0] == "error" || e[0] == "fatal") { + throw(e); + } else { + var logMessage = "function raised error: "+e.toSource()+" \nstacktrace: "+e.stack; + log(logMessage); + throw(["error", "render_error", logMessage]); } - blowChunks("end"); - } catch(e) { - respondError(e, funSrc, false); - } -}; + }; -function renderError(m) { - respond({error : "render_error", reason : m}); -} - -function respondError(e, funSrc, htmlErrors) { - if (e.error && e.reason) { - respond(e); - } else { - var logMessage = "function raised error: "+e.toString(); - log(logMessage); - log("stacktrace: "+e.stack); - var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; - renderError(errorMessage); - } -} + function escapeHTML(string) { + return string && string.replace(/&/g, "&") + .replace(//g, ">"); + }; + + + return { + start : start, + send : send, + getRow : getRow, + show : function(fun, ddoc, args) { + // var showFun = Couch.compileFunction(funSrc); + runShow(fun, ddoc, args); + }, + update : function(fun, ddoc, args) { + // var upFun = Couch.compileFunction(funSrc); + runUpdate(fun, ddoc, args); + }, + list : function(fun, ddoc, args) { + runList(fun, ddoc, args); + } + }; +})(); -function escapeHTML(string) { - return string.replace(/&/g, "&") - .replace(//g, ">"); -} - -function htmlRenderError(e, funSrc) { - var msg = ["

Render Error

", - "

JavaScript function raised error: ", - e.toString(), - "

Stacktrace:

",
-    escapeHTML(e.stack),
-    "

Function source:

",
-    escapeHTML(funSrc),
-    "
"].join(''); - return {body:msg}; -}; +// send = Render.send; +// getRow = Render.getRow; +// start = Render.start; + +// unused. this will be handled in the Erlang side of things. +// function htmlRenderError(e, funSrc) { +// var msg = ["

Render Error

", +// "

JavaScript function raised error: ", +// e.toString(), +// "

Stacktrace:

",
+//     escapeHTML(e.stack),
+//     "

Function source:

",
+//     escapeHTML(funSrc),
+//     "
"].join(''); +// return {body:msg}; +// }; Modified: couchdb/trunk/share/server/state.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/state.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/state.js (original) +++ couchdb/trunk/share/server/state.js Tue Dec 22 18:03:44 2009 @@ -10,26 +10,18 @@ // License for the specific language governing permissions and limitations under // the License. -// globals used by other modules and functions -var funs = []; // holds functions used for computation -var funsrc = []; // holds function source for debug info -var query_config = {}; -var State = (function() { - return { - reset : function(config) { - // clear the globals and run gc - funs = []; - funsrc = []; - query_config = config; - init_sandbox(); - gc(); - print("true"); // indicates success - }, - addFun : function(newFun) { - // Compile to a function and add it to funs array - funsrc.push(newFun); - funs.push(compileFunction(newFun)); - print("true"); - } +var State = { + reset : function(config) { + // clear the globals and run gc + State.funs = []; + State.query_config = config || {}; + init_sandbox(); + gc(); + print("true"); // indicates success + }, + addFun : function(newFun) { + // Compile to a function and add it to funs array + State.funs.push(Couch.compileFunction(newFun)); + print("true"); } -})(); +} Modified: couchdb/trunk/share/server/util.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/util.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/util.js (original) +++ couchdb/trunk/share/server/util.js Tue Dec 22 18:03:44 2009 @@ -10,13 +10,50 @@ // License for the specific language governing permissions and limitations under // the License. -toJSON.subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', +var Couch = { + // moving this away from global so we can move to json2.js later + toJSON : function (val) { + if (typeof(val) == "undefined") { + throw "Cannot encode 'undefined' value as JSON"; + } + if (typeof(val) == "xml") { // E4X support + val = val.toXMLString(); + } + if (val === null) { return "null"; } + return (Couch.toJSON.dispatcher[val.constructor.name])(val); + }, + compileFunction : function(source) { + if (!source) throw(["error","not_found","missing function"]); + try { + var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); + } catch (err) { + throw(["error", "compilation_error", err.toSource() + " (" + source + ")"]); + }; + if (typeof(functionObject) == "function") { + return functionObject; + } else { + throw(["error","compilation_error", + "Expression does not eval to a function. (" + source.toSource() + ")"]); + }; + }, + recursivelySeal : function(obj) { + // seal() is broken in current Spidermonkey + seal(obj); + for (var propname in obj) { + if (typeof doc[propname] == "object") { + recursivelySeal(doc[propname]); + } + } + } +} + +Couch.toJSON.subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'}; -toJSON.dispatcher = { +Couch.toJSON.dispatcher = { "Array": function(v) { var buf = []; for (var i = 0; i < v.length; i++) { - buf.push(toJSON(v[i])); + buf.push(Couch.toJSON(v[i])); } return "[" + buf.join(",") + "]"; }, @@ -42,14 +79,14 @@ if (!v.hasOwnProperty(k) || typeof(k) !== "string" || v[k] === undefined) { continue; } - buf.push(toJSON(k) + ": " + toJSON(v[k])); + buf.push(Couch.toJSON(k) + ": " + Couch.toJSON(v[k])); } return "{" + buf.join(",") + "}"; }, "String": function(v) { if (/["\\\x00-\x1f]/.test(v)) { v = v.replace(/([\x00-\x1f\\"])/g, function(a, b) { - var c = toJSON.subs[b]; + var c = Couch.toJSON.subs[b]; if (c) return c; c = b.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); @@ -59,56 +96,22 @@ } }; -function toJSON(val) { - if (typeof(val) == "undefined") { - throw "Cannot encode 'undefined' value as JSON"; - } - if (typeof(val) == "xml") { // E4X support - val = val.toXMLString(); - } - if (val === null) { return "null"; } - return (toJSON.dispatcher[val.constructor.name])(val); -} - -function compileFunction(source) { - try { - var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); - } catch (err) { - throw {error: "compilation_error", - reason: err.toString() + " (" + source + ")"}; - } - if (typeof(functionObject) == "function") { - return functionObject; - } else { - throw {error: "compilation_error", - reason: "expression does not eval to a function. (" + source + ")"}; - } -} - -function recursivelySeal(obj) { - seal(obj); - for (var propname in obj) { - if (typeof doc[propname] == "object") { - recursivelySeal(doc[propname]); - } - } -} - // prints the object as JSON, and rescues and logs any toJSON() related errors function respond(obj) { try { - print(toJSON(obj)); + print(Couch.toJSON(obj)); } catch(e) { log("Error converting object to JSON: " + e.toString()); + log("error on obj: "+ obj.toSource()); } }; -log = function(message) { - // return; +function log(message) { + // return; // idea: query_server_config option for log level if (typeof message == "undefined") { message = "Error: attempting to log message of 'undefined'."; } else if (typeof message != "string") { - message = toJSON(message); + message = Couch.toJSON(message); } respond(["log", message]); }; Modified: couchdb/trunk/share/server/validate.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/validate.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/validate.js (original) +++ couchdb/trunk/share/server/validate.js Tue Dec 22 18:03:44 2009 @@ -11,10 +11,9 @@ // the License. var Validate = { - validate : function(funSrc, newDoc, oldDoc, userCtx) { - var validateFun = compileFunction(funSrc); + validate : function(fun, ddoc, args) { try { - validateFun(newDoc, oldDoc, userCtx); + fun.apply(ddoc, args); print("1"); } catch (error) { respond(error); Modified: couchdb/trunk/share/server/views.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/views.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/server/views.js (original) +++ couchdb/trunk/share/server/views.js Tue Dec 22 18:03:44 2009 @@ -10,58 +10,76 @@ // License for the specific language governing permissions and limitations under // the License. -// globals used by views -var map_results = []; // holds temporary emitted values during doc map -// view helper functions -emit = function(key, value) { - map_results.push([key, value]); -} - -sum = function(values) { - var rv = 0; - for (var i in values) { - rv += values[i]; - } - return rv; -} var Views = (function() { + var map_results = []; // holds temporary emitted values during doc map + function runReduce(reduceFuns, keys, values, rereduce) { for (var i in reduceFuns) { - reduceFuns[i] = compileFunction(reduceFuns[i]); - } + reduceFuns[i] = Couch.compileFunction(reduceFuns[i]); + }; var reductions = new Array(reduceFuns.length); for(var i = 0; i < reduceFuns.length; i++) { try { reductions[i] = reduceFuns[i](keys, values, rereduce); } catch (err) { - if (err == "fatal_error") { - throw { - error: "reduce_runtime_error", - reason: "function raised fatal exception"}; - } - log("function raised exception (" + err + ")"); + handleViewError(err); + // if the error is not fatal, ignore the results and continue reductions[i] = null; } - } - var reduce_line = toJSON(reductions); + }; + var reduce_line = Couch.toJSON(reductions); var reduce_length = reduce_line.length; - if (query_config && query_config.reduce_limit && - reduce_length > 200 && ((reduce_length * 2) > line.length)) { - var reduce_preview = "Current output: '"+(reduce_line.substring(0,100) + "'... (first 100 of "+reduce_length+' bytes)'); - - throw { - error:"reduce_overflow_error", - reason: "Reduce output must shrink more rapidly: "+reduce_preview+"" - }; + // TODO make reduce_limit config into a number + if (State.query_config && State.query_config.reduce_limit && + reduce_length > 200 && ((reduce_length * 2) > State.line_length)) { + var reduce_preview = "Current output: '"+(reduce_line.substring(0,100) + "'... (first 100 of "+reduce_length+" bytes)"); + throw(["error", + "reduce_overflow_error", + "Reduce output must shrink more rapidly: "+reduce_preview]); } else { print("[true," + reduce_line + "]"); } }; + function handleViewError(err, doc) { + if (err == "fatal_error") { + // Only if it's a "fatal_error" do we exit. What's a fatal error? + // That's for the query to decide. + // + // This will make it possible for queries to completely error out, + // by catching their own local exception and rethrowing a + // fatal_error. But by default if they don't do error handling we + // just eat the exception and carry on. + // + // In this case we abort map processing but don't destroy the + // JavaScript process. If you need to destroy the JavaScript + // process, throw the error form matched by the block below. + throw(["error", "map_runtime_error", "function raised 'fatal_error'"]); + } else if (err[0] == "fatal") { + // Throwing errors of the form ["fatal","error_key","reason"] + // will kill the OS process. This is not normally what you want. + throw(err); + } + var message = "function raised exception " + err.toSource(); + if (doc) message += " with doc._id " + doc._id; + log(message); + }; + return { + // view helper functions + emit : function(key, value) { + map_results.push([key, value]); + }, + sum : function(values) { + var rv = 0; + for (var i in values) { + rv += values[i]; + } + return rv; + }, reduce : function(reduceFuns, kvs) { var keys = new Array(kvs.length); var values = new Array(kvs.length); @@ -101,25 +119,15 @@ recursivelySeal(doc); // seal to prevent map functions from changing doc */ var buf = []; - for (var i = 0; i < funs.length; i++) { + for (var i = 0; i < State.funs.length; i++) { map_results = []; try { - funs[i](doc); - buf.push(toJSON(map_results)); + State.funs[i](doc); + buf.push(Couch.toJSON(map_results)); } catch (err) { - if (err == "fatal_error") { - // Only if it's a "fatal_error" do we exit. What's a fatal error? - // That's for the query to decide. - // - // This will make it possible for queries to completely error out, - // by catching their own local exception and rethrowing a - // fatal_error. But by default if they don't do error handling we - // just eat the exception and carry on. - throw { - error: "map_runtime_error", - reason: "function raised fatal exception"}; - } - log("function raised exception (" + err + ") with doc._id " + doc._id); + handleViewError(err, doc); + // If the error is not fatal, we treat the doc as if it + // did not emit anything, by buffering an empty array. buf.push("[]"); } } Modified: couchdb/trunk/share/www/script/test/changes.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/changes.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/www/script/test/changes.js (original) +++ couchdb/trunk/share/www/script/test/changes.js Tue Dec 22 18:03:44 2009 @@ -213,12 +213,12 @@ xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=7&filter=changes_filter/bop", true); xhr.send(""); - db.save({"bop" : ""}); // empty string is falsy - var id = db.save({"bop" : "bingo"}).id; + db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy + db.save({"_id":"bingo","bop" : "bingo"}); sleep(100); var resp = JSON.parse(xhr.responseText); T(resp.last_seq == 9); - T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == id, "filter the correct update"); + T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); // filter with continuous xhr = CouchDB.newXhr(); @@ -226,30 +226,29 @@ xhr.send(""); db.save({"_id":"rusty", "bop" : "plankton"}); T(db.ensureFullCommit().ok); - sleep(200); + sleep(300); var lines = xhr.responseText.split("\n"); - T(JSON.parse(lines[1]).id == id); - T(JSON.parse(lines[2]).id == "rusty"); - T(JSON.parse(lines[3]).last_seq == 10); + T(JSON.parse(lines[1]).id == "bingo", lines[1]); + T(JSON.parse(lines[2]).id == "rusty", lines[2]); + T(JSON.parse(lines[3]).last_seq == 10, lines[3]); } - // error conditions // non-existing design doc var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/bop"); - TEquals(400, req.status, "should return 400 for non existant design doc"); + TEquals(404, req.status, "should return 404 for non existant design doc"); // non-existing filter var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/movealong"); - TEquals(400, req.status, "should return 400 for non existant filter fun"); + TEquals(404, req.status, "should return 404 for non existant filter fun"); // both var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/movealong"); - TEquals(400, req.status, - "should return 400 for non existant design doc and filter fun"); + TEquals(404, req.status, + "should return 404 for non existant design doc and filter fun"); // changes get all_docs style with deleted docs var doc = {a:1}; Modified: couchdb/trunk/share/www/script/test/design_docs.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/design_docs.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/www/script/test/design_docs.js (original) +++ couchdb/trunk/share/www/script/test/design_docs.js Tue Dec 22 18:03:44 2009 @@ -12,8 +12,11 @@ couchTests.design_docs = function(debug) { var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db2 = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); db.deleteDb(); db.createDb(); + db2.deleteDb(); + db2.createDb(); if (debug) debugger; run_on_modified_server( @@ -45,10 +48,32 @@ reduce:"function (keys, values) { return sum(values); };"}, huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}", reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"} + }, + shows: { + simple: "function() {return 'ok'};" } } + var xhr = CouchDB.request("PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)}); + var resp = JSON.parse(xhr.responseText); + + TEquals(resp.rev, db.save(designDoc).rev); + + // test that editing a show fun on the ddoc results in a change in output + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ok"); + + designDoc.shows.simple = "function() {return 'ko'};" T(db.save(designDoc).ok); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ko"); + + var xhr = CouchDB.request("GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster"); + T(xhr.status == 200); + TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); + // test that we get design doc info back var dinfo = db.designInfo("_design/test"); TEquals("test", dinfo.name); Modified: couchdb/trunk/share/www/script/test/list_views.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/list_views.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/www/script/test/list_views.js (original) +++ couchdb/trunk/share/www/script/test/list_views.js Tue Dec 22 18:03:44 2009 @@ -62,12 +62,7 @@ }), simpleForm: stringFun(function(head, req) { log("simpleForm"); - send('

Total Rows: ' - // + head.total_rows - // + ' Offset: ' + head.offset - + '

    '); - - // rows + send('
      '); var row, row_number = 0, prevKey, firstKey = null; while (row = getRow()) { row_number += 1; @@ -77,8 +72,6 @@ +' Value: '+row.value +' LineNo: '+row_number+''); } - - // tail return '

    FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'

    '; }), acceptSwitch: stringFun(function(head, req) { @@ -208,22 +201,12 @@ T(xhr.status == 200, "standard get should be 200"); T(/head0123456789tail/.test(xhr.responseText)); - var xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/basicView?list=basicBasic"); - T(xhr.status == 200, "standard get should be 200"); - T(/head0123456789tail/.test(xhr.responseText)); - // test that etags are available var etag = xhr.getResponseHeader("etag"); xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView", { headers: {"if-none-match": etag} }); T(xhr.status == 304); - - var etag = xhr.getResponseHeader("etag"); - xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/basicView?list=basicBasic", { - headers: {"if-none-match": etag} - }); - T(xhr.status == 304); // confirm ETag changes with different POST bodies xhr = CouchDB.request("POST", "/test_suite_db/_design/lists/_list/basicBasic/basicView", @@ -262,14 +245,6 @@ // get with query params xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=3&endkey=8"); T(xhr.status == 200, "with query params"); - T(/Total Rows/.test(xhr.responseText)); - T(!(/Key: 1/.test(xhr.responseText))); - T(/FirstKey: 3/.test(xhr.responseText)); - T(/LastKey: 8/.test(xhr.responseText)); - - var xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/basicView?list=simpleForm&startkey=3&endkey=8"); - T(xhr.status == 200, "with query params"); - T(/Total Rows/.test(xhr.responseText)); T(!(/Key: 1/.test(xhr.responseText))); T(/FirstKey: 3/.test(xhr.responseText)); T(/LastKey: 8/.test(xhr.responseText)); @@ -277,11 +252,7 @@ // with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=30"); T(xhr.status == 200, "0 rows"); - T(/Total Rows/.test(xhr.responseText)); - - var xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/basicView?list=simpleForm&startkey=30"); - T(xhr.status == 200, "0 rows"); - T(/Total Rows/.test(xhr.responseText)); + T(/<\/ul>/.test(xhr.responseText)); //too many Get Rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/tooManyGetRows/basicView"); @@ -292,19 +263,11 @@ // reduce with 0 rows var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?startkey=30"); T(xhr.status == 200, "reduce 0 rows"); - T(/Total Rows/.test(xhr.responseText)); - T(/LastKey: undefined/.test(xhr.responseText)); - - // reduce with 0 rows - var xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/withReduce?list=simpleForm&startkey=30"); - T(xhr.status == 200, "reduce 0 rows"); - T(/Total Rows/.test(xhr.responseText)); T(/LastKey: undefined/.test(xhr.responseText)); // when there is a reduce present, but not used var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/withReduce?reduce=false"); T(xhr.status == 200, "reduce false"); - T(/Total Rows/.test(xhr.responseText)); T(/Key: 1/.test(xhr.responseText)); @@ -352,7 +315,6 @@ body: '{"keys":[2,4,5,7]}' }); T(xhr.status == 200, "multi key"); - T(/Total Rows/.test(xhr.responseText)); T(!(/Key: 1 /.test(xhr.responseText))); T(/Key: 2/.test(xhr.responseText)); T(/FirstKey: 2/.test(xhr.responseText)); @@ -416,11 +378,22 @@ "?startkey=-3"; xhr = CouchDB.request("GET", url); T(xhr.status == 200, "multiple design docs."); - T(/Total Rows/.test(xhr.responseText)); T(!(/Key: -4/.test(xhr.responseText))); T(/FirstKey: -3/.test(xhr.responseText)); T(/LastKey: 0/.test(xhr.responseText)); + // Test we do multi-key requests on lists and views in separate docs. + var url = "/test_suite_db/_design/lists/_list/simpleForm/views/basicView" + xhr = CouchDB.request("POST", url, { + body: '{"keys":[-2,-4,-5,-7]}' + }); + + T(xhr.status == 200, "multi key separate docs"); + T(!(/Key: -3/.test(xhr.responseText))); + T(/Key: -7/.test(xhr.responseText)); + T(/FirstKey: -2/.test(xhr.responseText)); + T(/LastKey: -7/.test(xhr.responseText)); + var erlViewTest = function() { T(db.save(erlListDoc).ok); var url = "/test_suite_db/_design/erlang/_list/simple/views/basicView" + Modified: couchdb/trunk/share/www/script/test/show_documents.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/show_documents.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/www/script/test/show_documents.js (original) +++ couchdb/trunk/share/www/script/test/show_documents.js Tue Dec 22 18:03:44 2009 @@ -21,14 +21,11 @@ language: "javascript", shows: { "hello" : stringFun(function(doc, req) { + log("hello fun"); if (doc) { return "Hello World"; } else { - if(req.docId) { - return "New World"; - } else { - return "Empty World"; - } + return "Empty World"; } }), "just-name" : stringFun(function(doc, req) { @@ -140,7 +137,7 @@ // hello template world xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid); - T(xhr.responseText == "Hello World"); + T(xhr.responseText == "Hello World", "hello"); T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) // Fix for COUCHDB-379 @@ -168,8 +165,10 @@ // // hello template world (non-existing docid) xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/nonExistingDoc"); - T(xhr.responseText == "New World"); - + T(xhr.status == 404); + var resp = JSON.parse(xhr.responseText); + T(resp.error == "not_found"); + // show with doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid); T(xhr.responseText == "Just Rusty"); @@ -179,9 +178,9 @@ // show with missing doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/missingdoc"); - - T(xhr.status == 404, 'Doc should be missing'); - T(xhr.responseText == "No such doc"); + T(xhr.status == 404); + var resp = JSON.parse(xhr.responseText); + T(resp.error == "not_found"); // show with missing func xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/missing/"+docid); @@ -268,8 +267,8 @@ xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid, { headers: {"if-none-match": etag} }); - // should be 304 - T(xhr.status == 304); + // should not be 304 if we change the doc + T(xhr.status != 304, "changed ddoc"); // update design doc function designDoc.shows["just-name"] = (function(doc, req) { Modified: couchdb/trunk/share/www/script/test/update_documents.js URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/update_documents.js?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/share/www/script/test/update_documents.js (original) +++ couchdb/trunk/share/www/script/test/update_documents.js Tue Dec 22 18:03:44 2009 @@ -22,17 +22,26 @@ language: "javascript", updates: { "hello" : stringFun(function(doc, req) { + log(doc); + log(req); if (!doc) { - if (req.docId) { - return [{ - _id : req.docId - }, "New World"] - } - return [null, "Empty World"]; - } + if (req.id) { + return [ + // Creates a new document with the PUT docid, + { _id : req.id, + reqs : [req] }, + // and returns an HTML response to the client. + "

    New World

    "]; + }; + // + return [null, "

    Empty World

    "]; + }; + // we can update the document inline doc.world = "hello"; + // we can record aspects of the request or use them in application logic. + doc.reqs && doc.reqs.push(req); doc.edited_by = req.userCtx; - return [doc, "hello doc"]; + return [doc, "

    hello doc

    "]; }), "in-place" : stringFun(function(doc, req) { var field = req.query.field; @@ -81,7 +90,7 @@ // hello update world xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/"+docid); T(xhr.status == 201); - T(xhr.responseText == "hello doc"); + T(xhr.responseText == "

    hello doc

    "); T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) doc = db.open(docid); @@ -93,17 +102,17 @@ // hello update world (no docid) xhr = CouchDB.request("POST", "/test_suite_db/_design/update/_update/hello"); T(xhr.status == 200); - T(xhr.responseText == "Empty World"); + T(xhr.responseText == "

    Empty World

    "); // no GET allowed xhr = CouchDB.request("GET", "/test_suite_db/_design/update/_update/hello"); - T(xhr.status == 405); + // T(xhr.status == 405); // TODO allow qs to throw error code as well as error message T(JSON.parse(xhr.responseText).error == "method_not_allowed"); // // hello update world (non-existing docid) xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/hello/nonExistingDoc"); T(xhr.status == 201); - T(xhr.responseText == "New World"); + T(xhr.responseText == "

    New World

    "); // in place update xhr = CouchDB.request("PUT", "/test_suite_db/_design/update/_update/in-place/"+docid+'?field=title&value=test'); Modified: couchdb/trunk/src/couchdb/couch_doc.erl URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_doc.erl?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/src/couchdb/couch_doc.erl (original) +++ couchdb/trunk/src/couchdb/couch_doc.erl Tue Dec 22 18:03:44 2009 @@ -292,15 +292,13 @@ lists:reverse(fold_streamed_data(DataFun, Len, fun(Data, Acc) -> [Data | Acc] end, [])). -get_validate_doc_fun(#doc{body={Props}}) -> - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), +get_validate_doc_fun(#doc{body={Props}}=DDoc) -> case proplists:get_value(<<"validate_doc_update">>, Props) of undefined -> nil; - FunSrc -> + _Else -> fun(EditDoc, DiskDoc, Ctx) -> - couch_query_servers:validate_doc_update( - Lang, FunSrc, EditDoc, DiskDoc, Ctx) + couch_query_servers:validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx) end end. Modified: couchdb/trunk/src/couchdb/couch_httpd.erl URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd.erl?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/src/couchdb/couch_httpd.erl (original) +++ couchdb/trunk/src/couchdb/couch_httpd.erl Tue Dec 22 18:03:44 2009 @@ -51,7 +51,7 @@ DesignUrlHandlersList = lists:map( fun({UrlKey, SpecStr}) -> - {?l2b(UrlKey), make_arity_2_fun(SpecStr)} + {?l2b(UrlKey), make_arity_3_fun(SpecStr)} end, couch_config:get("httpd_design_handlers")), UrlHandlers = dict:from_list(UrlHandlersList), @@ -110,6 +110,14 @@ fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2) end end. +make_arity_3_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3, SpecArg) end; + {ok, {Mod, Fun}} -> + fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3) end + end. + % SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}" make_arity_1_fun_list(SpecStr) -> [make_arity_1_fun(FunSpecStr) || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])]. Modified: couchdb/trunk/src/couchdb/couch_httpd_db.erl URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/src/couchdb/couch_httpd_db.erl (original) +++ couchdb/trunk/src/couchdb/couch_httpd_db.erl Tue Dec 22 18:03:44 2009 @@ -16,7 +16,7 @@ -export([handle_request/1, handle_compact_req/2, handle_design_req/2, db_req/2, couch_doc_open/4,handle_changes_req/2, update_doc_result_to_json/1, update_doc_result_to_json/2, - handle_design_info_req/2, handle_view_cleanup_req/2]). + handle_design_info_req/3, handle_view_cleanup_req/2]). -import(couch_httpd, [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, @@ -232,26 +232,18 @@ end; [DName, FName] -> DesignId = <<"_design/", DName/binary>>, - case couch_db:open_doc(Db, DesignId) of - {ok, #doc{body={Props}}} -> - FilterSrc = try couch_util:get_nested_json_value({Props}, - [<<"filters">>, FName]) - catch - throw:{not_found, _} -> - throw({bad_request, "invalid filter function"}) - end, - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - fun(DocInfos) -> - Docs = [Doc || {ok, Doc} <- [ - {ok, Doc} = couch_db:open_doc(Db, DInfo, [deleted]) - || DInfo <- DocInfos]], - {ok, Passes} = couch_query_servers:filter_docs(Lang, FilterSrc, Docs, Req, Db), - [{[{rev, couch_doc:rev_to_str(Rev)}]} - || #doc_info{revs=[#rev_info{rev=Rev}|_]} <- DocInfos, - Pass <- Passes, Pass == true] - end; - _Error -> - throw({bad_request, "invalid design doc"}) + DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), + % validate that the ddoc has the filter fun + #doc{body={Props}} = DDoc, + couch_util:get_nested_json_value({Props}, [<<"filters">>, FName]), + fun(DocInfos) -> + Docs = [Doc || {ok, Doc} <- [ + {ok, Doc} = couch_db:open_doc(Db, DInfo, [deleted]) + || DInfo <- DocInfos]], + {ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs), + [{[{rev, couch_doc:rev_to_str(Rev)}]} + || #doc_info{revs=[#rev_info{rev=Rev}|_]} <- DocInfos, + Pass <- Passes, Pass == true] end; _Else -> throw({bad_request, @@ -279,11 +271,14 @@ handle_design_req(#httpd{ - path_parts=[_DbName,_Design,_DesName, <<"_",_/binary>> = Action | _Rest], + path_parts=[_DbName, _Design, DesignName, <<"_",_/binary>> = Action | _Rest], design_url_handlers = DesignUrlHandlers }=Req, Db) -> + % load ddoc + DesignId = <<"_design/", DesignName/binary>>, + DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), Handler = couch_util:dict_find(Action, DesignUrlHandlers, fun db_req/2), - Handler(Req, Db); + Handler(Req, Db, DDoc); handle_design_req(Req, Db) -> db_req(Req, Db). @@ -291,7 +286,7 @@ handle_design_info_req(#httpd{ method='GET', path_parts=[_DbName, _Design, DesignName, _] - }=Req, Db) -> + }=Req, Db, _DDoc) -> DesignId = <<"_design/", DesignName/binary>>, {ok, GroupInfoList} = couch_view:get_group_info(Db, DesignId), send_json(Req, 200, {[ @@ -299,7 +294,7 @@ {view_index, {GroupInfoList}} ]}); -handle_design_info_req(Req, _Db) -> +handle_design_info_req(Req, _Db, _DDoc) -> send_method_not_allowed(Req, "GET"). create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> @@ -725,7 +720,12 @@ end; _ -> {DesignName, ShowName} = Format, - couch_httpd_show:handle_doc_show(Req, DesignName, ShowName, DocId, Db) + % load ddoc + DesignId = <<"_design/", DesignName/binary>>, + DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), + % open doc + Doc = couch_doc_open(Db, DocId, Rev, Options), + couch_httpd_show:handle_doc_show(Req, Db, DDoc, ShowName, Doc) end; db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> Modified: couchdb/trunk/src/couchdb/couch_httpd_external.erl URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_external.erl?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/src/couchdb/couch_httpd_external.erl (original) +++ couchdb/trunk/src/couchdb/couch_httpd_external.erl Tue Dec 22 18:03:44 2009 @@ -13,7 +13,7 @@ -module(couch_httpd_external). -export([handle_external_req/2, handle_external_req/3]). --export([send_external_response/2, json_req_obj/2]). +-export([send_external_response/2, json_req_obj/2, json_req_obj/3]). -export([default_or_content_type/2, parse_external_response/1]). -import(couch_httpd,[send_error/4]). @@ -53,12 +53,12 @@ _ -> send_external_response(HttpReq, Response) end. - +json_req_obj(Req, Db) -> json_req_obj(Req, Db, null). json_req_obj(#httpd{mochi_req=Req, method=Verb, path_parts=Path, req_body=ReqBody - }, Db) -> + }, Db, DocId) -> Body = case ReqBody of undefined -> Req:recv_body(); Else -> Else @@ -74,6 +74,7 @@ {ok, Info} = couch_db:get_db_info(Db), % add headers... {[{<<"info">>, {Info}}, + {<<"id">>, DocId}, {<<"verb">>, Verb}, {<<"path">>, Path}, {<<"query">>, to_json_terms(Req:parse_qs())}, Modified: couchdb/trunk/src/couchdb/couch_httpd_show.erl URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_show.erl?rev=893249&r1=893248&r2=893249&view=diff ============================================================================== --- couchdb/trunk/src/couchdb/couch_httpd_show.erl (original) +++ couchdb/trunk/src/couchdb/couch_httpd_show.erl Tue Dec 22 18:03:44 2009 @@ -12,8 +12,8 @@ -module(couch_httpd_show). --export([handle_doc_show_req/2, handle_doc_update_req/2, handle_view_list_req/2, - handle_doc_show/5, handle_view_list/7]). +-export([handle_doc_show_req/3, handle_doc_update_req/3, handle_view_list_req/3, + handle_doc_show/5, handle_view_list/6, get_fun_key/3]). -include("couch_db.hrl"). @@ -22,217 +22,245 @@ start_json_response/2,send_chunk/2,last_chunk/1,send_chunked_error/2, start_chunked_response/3, send_error/4]). +% /db/_design/foo/show/bar/docid +% show converts a json doc to a response of any content-type. +% it looks up the doc an then passes it to the query server. +% then it sends the response from the query server to the http client. handle_doc_show_req(#httpd{ - method='GET', - path_parts=[_DbName, _Design, DesignName, _Show, ShowName, DocId] - }=Req, Db) -> - handle_doc_show(Req, DesignName, ShowName, DocId, Db); + path_parts=[_, _, _, _, ShowName, DocId] + }=Req, Db, DDoc) -> + % open the doc + Doc = couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]), + % we don't handle revs here b/c they are an internal api + % returns 404 if there is no doc with DocId + handle_doc_show(Req, Db, DDoc, ShowName, Doc); handle_doc_show_req(#httpd{ - path_parts=[_DbName, _Design, DesignName, _Show, ShowName] - }=Req, Db) -> - handle_doc_show(Req, DesignName, ShowName, nil, Db); + path_parts=[_, _, _, _, ShowName] + }=Req, Db, DDoc) -> + % with no docid the doc is nil + handle_doc_show(Req, Db, DDoc, ShowName, nil); + +handle_doc_show_req(Req, _Db, _DDoc) -> + send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>). + +handle_doc_show(Req, Db, DDoc, ShowName, Doc) -> + % get responder for ddoc/showname + CurrentEtag = show_etag(Req, Doc, DDoc, []), + couch_httpd:etag_respond(Req, CurrentEtag, fun() -> + JsonReq = couch_httpd_external:json_req_obj(Req, Db), + JsonDoc = couch_query_servers:json_doc(Doc), + [<<"resp">>, ExternalResp] = + couch_query_servers:ddoc_prompt(DDoc, [<<"shows">>, ShowName], [JsonDoc, JsonReq]), + JsonResp = apply_etag(ExternalResp, CurrentEtag), + couch_httpd_external:send_external_response(Req, JsonResp) + end). -handle_doc_show_req(#httpd{method='GET'}=Req, _Db) -> - send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>); -handle_doc_show_req(Req, _Db) -> - send_method_not_allowed(Req, "GET,POST,HEAD"). +show_etag(#httpd{user_ctx=UserCtx}=Req, Doc, DDoc, More) -> + Accept = couch_httpd:header_value(Req, "Accept"), + DocPart = case Doc of + nil -> nil; + Doc -> couch_httpd:doc_etag(Doc) + end, + couch_httpd:make_etag({couch_httpd:doc_etag(DDoc), DocPart, Accept, UserCtx#user_ctx.roles, More}). -handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db) -> - send_method_not_allowed(Req, "POST,PUT,DELETE,ETC"); +get_fun_key(DDoc, Type, Name) -> + #doc{body={Props}} = DDoc, + Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), + Src = couch_util:get_nested_json_value({Props}, [Type, Name]), + {Lang, Src}. + +% /db/_design/foo/update/bar/docid +% updates a doc based on a request +% handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db, _DDoc) -> +% % anything but GET +% send_method_not_allowed(Req, "POST,PUT,DELETE,ETC"); handle_doc_update_req(#httpd{ - path_parts=[_DbName, _Design, DesignName, _Update, UpdateName, DocId] - }=Req, Db) -> - DesignId = <<"_design/", DesignName/binary>>, - #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - UpdateSrc = couch_util:get_nested_json_value({Props}, [<<"updates">>, UpdateName]), + path_parts=[_, _, _, _, UpdateName, DocId] + }=Req, Db, DDoc) -> Doc = try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) - catch - _ -> nil - end, - send_doc_update_response(Lang, UpdateSrc, DocId, Doc, Req, Db); + catch + _ -> nil + end, + send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId); handle_doc_update_req(#httpd{ - path_parts=[_DbName, _Design, DesignName, _Update, UpdateName] - }=Req, Db) -> - DesignId = <<"_design/", DesignName/binary>>, - #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - UpdateSrc = couch_util:get_nested_json_value({Props}, [<<"updates">>, UpdateName]), - send_doc_update_response(Lang, UpdateSrc, nil, nil, Req, Db); + path_parts=[_, _, _, _, UpdateName] + }=Req, Db, DDoc) -> + send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null); -handle_doc_update_req(Req, _Db) -> +handle_doc_update_req(Req, _Db, _DDoc) -> send_error(Req, 404, <<"update_error">>, <<"Invalid path.">>). - - -handle_doc_show(Req, DesignName, ShowName, DocId, Db) -> - DesignId = <<"_design/", DesignName/binary>>, - #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - ShowSrc = couch_util:get_nested_json_value({Props}, [<<"shows">>, ShowName]), - Doc = case DocId of - nil -> nil; - _ -> - try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) - catch - _ -> nil - end +send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) -> + JsonReq = couch_httpd_external:json_req_obj(Req, Db, DocId), + JsonDoc = couch_query_servers:json_doc(Doc), + case couch_query_servers:ddoc_prompt(DDoc, [<<"updates">>, UpdateName], [JsonDoc, JsonReq]) of + [<<"up">>, {NewJsonDoc}, JsonResp] -> + Options = case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of + "true" -> + [full_commit]; + _ -> + [] + end, + NewDoc = couch_doc:from_json_obj({NewJsonDoc}), + Code = 201, + {ok, _NewRev} = couch_db:update_doc(Db, NewDoc, Options); + [<<"up">>, _Other, JsonResp] -> + Code = 200, + ok end, - send_doc_show_response(Lang, ShowSrc, DocId, Doc, Req, Db). + JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp), + % todo set location field + couch_httpd_external:send_external_response(Req, JsonResp2). + % view-list request with view and list from same design doc. handle_view_list_req(#httpd{method='GET', - path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> - handle_view_list(Req, DesignName, ListName, DesignName, ViewName, Db, nil); + path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) -> + handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, nil); % view-list request with view and list from different design docs. handle_view_list_req(#httpd{method='GET', - path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewDesignName, ViewName]}=Req, Db) -> - handle_view_list(Req, DesignName, ListName, ViewDesignName, ViewName, Db, nil); + path_parts=[_, _, _, _, ListName, ViewDesignName, ViewName]}=Req, Db, DDoc) -> + handle_view_list(Req, Db, DDoc, ListName, {ViewDesignName, ViewName}, nil); -handle_view_list_req(#httpd{method='GET'}=Req, _Db) -> +handle_view_list_req(#httpd{method='GET'}=Req, _Db, _DDoc) -> send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>); handle_view_list_req(#httpd{method='POST', - path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> + path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) -> + % {Props2} = couch_httpd:json_body(Req), ReqBody = couch_httpd:body(Req), {Props2} = ?JSON_DECODE(ReqBody), Keys = proplists:get_value(<<"keys">>, Props2, nil), - handle_view_list(Req#httpd{req_body=ReqBody}, DesignName, ListName, DesignName, ViewName, Db, Keys); - -handle_view_list_req(Req, _Db) -> - send_method_not_allowed(Req, "GET,POST,HEAD"). + handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName, {DesignName, ViewName}, Keys); -handle_view_list(Req, ListDesignName, ListName, ViewDesignName, ViewName, Db, Keys) -> - ListDesignId = <<"_design/", ListDesignName/binary>>, - #doc{body={ListProps}} = couch_httpd_db:couch_doc_open(Db, ListDesignId, nil, []), - if - ViewDesignName == ListDesignName -> - ViewDesignId = ListDesignId; - true -> - ViewDesignId = <<"_design/", ViewDesignName/binary>> - end, +handle_view_list_req(#httpd{method='POST', + path_parts=[_, _, _, _, ListName, ViewDesignName, ViewName]}=Req, Db, DDoc) -> + % {Props2} = couch_httpd:json_body(Req), + ReqBody = couch_httpd:body(Req), + {Props2} = ?JSON_DECODE(ReqBody), + Keys = proplists:get_value(<<"keys">>, Props2, nil), + handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName, {ViewDesignName, ViewName}, Keys); - ListLang = proplists:get_value(<<"language">>, ListProps, <<"javascript">>), - ListSrc = couch_util:get_nested_json_value({ListProps}, [<<"lists">>, ListName]), - send_view_list_response(ListLang, ListSrc, ViewName, ViewDesignId, Req, Db, Keys). - - -send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys) -> - Stale = couch_httpd_view:get_stale_type(Req), - Reduce = couch_httpd_view:get_reduce_type(Req), - case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of - {ok, View, Group} -> - QueryArgs = couch_httpd_view:parse_view_params(Req, Keys, map), - output_map_list(Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys); - {not_found, _Reason} -> - case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of - {ok, ReduceView, Group} -> - case Reduce of - false -> - QueryArgs = couch_httpd_view:parse_view_params( - Req, Keys, map_red - ), - MapView = couch_view:extract_map_view(ReduceView), - output_map_list(Req, Lang, ListSrc, MapView, Group, Db, QueryArgs, Keys); - _ -> - QueryArgs = couch_httpd_view:parse_view_params( - Req, Keys, reduce - ), - output_reduce_list(Req, Lang, ListSrc, ReduceView, Group, Db, QueryArgs, Keys) - end; - {not_found, Reason} -> - throw({not_found, Reason}) - end - end. +handle_view_list_req(#httpd{method='POST'}=Req, _Db, _DDoc) -> + send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>); +handle_view_list_req(Req, _Db, _DDoc) -> + send_method_not_allowed(Req, "GET,POST,HEAD"). -output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> +handle_view_list(Req, Db, DDoc, LName, {ViewDesignName, ViewName}, Keys) -> + ViewDesignId = <<"_design/", ViewDesignName/binary>>, + {ViewType, View, Group, QueryArgs} = couch_httpd_view:load_view(Req, Db, {ViewDesignId, ViewName}, Keys), + Etag = list_etag(Req, Db, Group, {couch_httpd:doc_etag(DDoc), Keys}), + couch_httpd:etag_respond(Req, Etag, fun() -> + output_list(ViewType, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys) + end). + +list_etag(#httpd{user_ctx=UserCtx}=Req, Db, Group, More) -> + Accept = couch_httpd:header_value(Req, "Accept"), + couch_httpd_view:view_group_etag(Group, Db, {More, Accept, UserCtx#user_ctx.roles}). + +output_list(map, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys) -> + output_map_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys); +output_list(reduce, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys) -> + output_reduce_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys). + +% next step: +% use with_ddoc_proc/2 to make this simpler +output_map_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys) -> #view_query_args{ limit = Limit, skip = SkipCount } = QueryArgs, + + FoldAccInit = {Limit, SkipCount, undefined, []}, {ok, RowCount} = couch_view:get_row_count(View), - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - % get the os process here - % pass it into the view fold with closures - {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), - - StartListRespFun = make_map_start_resp_fun(QueryServer, Db), - SendListRowFun = make_map_send_row_fun(QueryServer), - - FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, RowCount, - #view_fold_helper_funs{ - reduce_count = fun couch_view:reduce_to_count/1, - start_response = StartListRespFun, - send_row = SendListRowFun - }), - FoldAccInit = {Limit, SkipCount, undefined, []}, - {ok, _, FoldResult} = couch_view:fold(View, FoldlFun, FoldAccInit, - couch_httpd_view:make_key_options(QueryArgs)), - finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) - end); + + + couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) -> + + ListFoldHelpers = #view_fold_helper_funs{ + reduce_count = fun couch_view:reduce_to_count/1, + start_response = StartListRespFun = make_map_start_resp_fun(QServer, Db, LName), + send_row = make_map_send_row_fun(QServer) + }, + + {ok, _, FoldResult} = case Keys of + nil -> + FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Etag, Db, RowCount, ListFoldHelpers), + couch_view:fold(View, FoldlFun, FoldAccInit, + couch_httpd_view:make_key_options(QueryArgs)); + Keys -> + lists:foldl( + fun(Key, {ok, _, FoldAcc}) -> + QueryArgs2 = QueryArgs#view_query_args{ + start_key = Key, + end_key = Key + }, + FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs2, Etag, Db, RowCount, ListFoldHelpers), + couch_view:fold(View, FoldlFun, FoldAcc, + couch_httpd_view:make_key_options(QueryArgs2)) + end, {ok, nil, FoldAccInit}, Keys) + end, + finish_list(Req, QServer, Etag, FoldResult, StartListRespFun, RowCount) + end). -output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> + +output_reduce_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys) -> #view_query_args{ limit = Limit, - skip = SkipCount + skip = SkipCount, + group_level = GroupLevel } = QueryArgs, - {ok, RowCount} = couch_view:get_row_count(View), - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx, Keys}), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - % get the os process here - % pass it into the view fold with closures - {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), - - StartListRespFun = make_map_start_resp_fun(QueryServer, Db), - SendListRowFun = make_map_send_row_fun(QueryServer), + couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) -> + StartListRespFun = make_reduce_start_resp_fun(QServer, Db, LName), + SendListRowFun = make_reduce_send_row_fun(QServer, Db), + {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, + GroupLevel, QueryArgs, Etag, + #reduce_fold_helper_funs{ + start_response = StartListRespFun, + send_row = SendListRowFun + }), FoldAccInit = {Limit, SkipCount, undefined, []}, - {ok, _, FoldResult} = lists:foldl( - fun(Key, {ok, _, FoldAcc}) -> - QueryArgs2 = QueryArgs#view_query_args{ - start_key = Key, - end_key = Key - }, - FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs2, CurrentEtag, Db, RowCount, - #view_fold_helper_funs{ - reduce_count = fun couch_view:reduce_to_count/1, - start_response = StartListRespFun, - send_row = SendListRowFun - }), - couch_view:fold(View, FoldlFun, FoldAcc, - couch_httpd_view:make_key_options(QueryArgs2)) - end, {ok, nil, FoldAccInit}, Keys), - finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) + {ok, FoldResult} = case Keys of + nil -> + couch_view:fold_reduce(View, RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} | + couch_httpd_view:make_key_options(QueryArgs)]); + Keys -> + lists:foldl( + fun(Key, {ok, FoldAcc}) -> + couch_view:fold_reduce(View, RespFun, FoldAcc, + [{key_group_fun, GroupRowsFun} | + couch_httpd_view:make_key_options( + QueryArgs#view_query_args{start_key=Key, end_key=Key})] + ) + end, {ok, FoldAccInit}, Keys) + end, + finish_list(Req, QServer, Etag, FoldResult, StartListRespFun, null) end). -make_map_start_resp_fun(QueryServer, Db) -> + +make_map_start_resp_fun(QueryServer, Db, LName) -> fun(Req, Etag, TotalRows, Offset, _Acc) -> Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}]}, - start_list_resp(QueryServer, Req, Db, Head, Etag) + start_list_resp(QueryServer, LName, Req, Db, Head, Etag) end. -make_reduce_start_resp_fun(QueryServer, _Req, Db, _CurrentEtag) -> +make_reduce_start_resp_fun(QueryServer, Db, LName) -> fun(Req2, Etag, _Acc) -> - start_list_resp(QueryServer, Req2, Db, {[]}, Etag) + start_list_resp(QueryServer, LName, Req2, Db, {[]}, Etag) end. -start_list_resp(QueryServer, Req, Db, Head, Etag) -> - [<<"start">>,Chunks,JsonResp] = couch_query_servers:render_list_head(QueryServer, - Req, Db, Head), +start_list_resp(QServer, LName, Req, Db, Head, Etag) -> + JsonReq = couch_httpd_external:json_req_obj(Req, Db), + [<<"start">>,Chunks,JsonResp] = couch_query_servers:ddoc_proc_prompt(QServer, + [<<"lists">>, LName], [Head, JsonReq]), JsonResp2 = apply_etag(JsonResp, Etag), #extern_resp_args{ code = Code, @@ -255,7 +283,7 @@ send_list_row(Resp, QueryServer, Db, Row, RowFront, IncludeDoc) -> try - [Go,Chunks] = couch_query_servers:render_list_row(QueryServer, Db, Row, IncludeDoc), + [Go,Chunks] = prompt_list_row(QueryServer, Db, Row, IncludeDoc), Chunk = RowFront ++ ?b2l(?l2b(Chunks)), send_non_empty_chunk(Resp, Chunk), case Go of @@ -270,78 +298,22 @@ throw({already_sent, Resp, Error}) end. + +prompt_list_row({Proc, _DDocId}, Db, {{Key, DocId}, Value}, IncludeDoc) -> + JsonRow = couch_httpd_view:view_row_obj(Db, {{Key, DocId}, Value}, IncludeDoc), + couch_query_servers:proc_prompt(Proc, [<<"list_row">>, JsonRow]); + +prompt_list_row({Proc, _DDocId}, _, {Key, Value}, _IncludeDoc) -> + JsonRow = {[{key, Key}, {value, Value}]}, + couch_query_servers:proc_prompt(Proc, [<<"list_row">>, JsonRow]). + send_non_empty_chunk(Resp, Chunk) -> case Chunk of [] -> ok; _ -> send_chunk(Resp, Chunk) end. -output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> - #view_query_args{ - limit = Limit, - skip = SkipCount, - group_level = GroupLevel - } = QueryArgs, - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - % get the os process here - % pass it into the view fold with closures - {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), - StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), - SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), - - {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, - GroupLevel, QueryArgs, CurrentEtag, - #reduce_fold_helper_funs{ - start_response = StartListRespFun, - send_row = SendListRowFun - }), - FoldAccInit = {Limit, SkipCount, undefined, []}, - {ok, FoldResult} = couch_view:fold_reduce(View, RespFun, FoldAccInit, - [{key_group_fun, GroupRowsFun} | - couch_httpd_view:make_key_options(QueryArgs)]), - finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) - end); - -output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> - #view_query_args{ - limit = Limit, - skip = SkipCount, - group_level = GroupLevel - } = QueryArgs, - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx, Keys}), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - % get the os process here - % pass it into the view fold with closures - {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), - StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), - SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), - - {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, - GroupLevel, QueryArgs, CurrentEtag, - #reduce_fold_helper_funs{ - start_response = StartListRespFun, - send_row = SendListRowFun - }), - FoldAccInit = {Limit, SkipCount, undefined, []}, - {ok, FoldResult} = lists:foldl( - fun(Key, {ok, FoldAcc}) -> - couch_view:fold_reduce(View, RespFun, FoldAcc, - [{key_group_fun, GroupRowsFun} | - couch_httpd_view:make_key_options( - QueryArgs#view_query_args{start_key=Key, end_key=Key})] - ) - end, {ok, FoldAccInit}, Keys), - finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) - end). - -finish_list(Req, QueryServer, Etag, FoldResult, StartFun, TotalRows) -> +finish_list(Req, {Proc, _DDocId}, Etag, FoldResult, StartFun, TotalRows) -> FoldResult2 = case FoldResult of {Limit, SkipCount, Response, RowAcc} -> {Limit, SkipCount, Response, RowAcc, nil}; @@ -352,16 +324,15 @@ {_, _, undefined, _, _} -> {ok, Resp, BeginBody} = render_head_for_empty_list(StartFun, Req, Etag, TotalRows), - [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), + [<<"end">>, Chunks] = couch_query_servers:proc_prompt(Proc, [<<"list_end">>]), Chunk = BeginBody ++ ?b2l(?l2b(Chunks)), send_non_empty_chunk(Resp, Chunk); {_, _, Resp, stop, _} -> ok; {_, _, Resp, _, _} -> - [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), + [<<"end">>, Chunks] = couch_query_servers:proc_prompt(Proc, [<<"list_end">>]), send_non_empty_chunk(Resp, ?b2l(?l2b(Chunks))) end, - couch_query_servers:stop_doc_map(QueryServer), last_chunk(Resp). @@ -370,53 +341,6 @@ render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) -> StartListRespFun(Req, Etag, TotalRows, null, []). -send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) -> - % compute etag with no doc - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept, UserCtx}), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, - DocId, nil, Req, Db), - JsonResp = apply_etag(ExternalResp, CurrentEtag), - couch_httpd_external:send_external_response(Req, JsonResp) - end); - -send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) -> - % calculate the etag - Headers = MReq:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - Accept = proplists:get_value('Accept', Hlist), - CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept, UserCtx}), - % We know our etag now - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> - [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, - DocId, Doc, Req, Db), - JsonResp = apply_etag(ExternalResp, CurrentEtag), - couch_httpd_external:send_external_response(Req, JsonResp) - end). - -send_doc_update_response(Lang, UpdateSrc, DocId, Doc, Req, Db) -> - case couch_query_servers:render_doc_update(Lang, UpdateSrc, - DocId, Doc, Req, Db) of - [<<"up">>, {NewJsonDoc}, JsonResp] -> - Options = case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of - "true" -> - [full_commit]; - _ -> - [] - end, - NewDoc = couch_doc:from_json_obj({NewJsonDoc}), - Code = 201, - % todo set location field - {ok, _NewRev} = couch_db:update_doc(Db, NewDoc, Options); - [<<"up">>, _Other, JsonResp] -> - Code = 200, - ok - end, - JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp), - couch_httpd_external:send_external_response(Req, JsonResp2). % Maybe this is in the proplists API % todo move to couch_util