From 7339dba0eb0904846ba9c04a1e485bf97cd475ae Mon Sep 17 00:00:00 2001 From: jchris Date: Tue, 11 Aug 2009 18:50:08 +0000 Subject: [PATCH] Initial commit of _update handler. Thanks to Paul Davis, Jason Davies for code and others for discussion. The _update handler accepts POSTs to paths like: /db/_design/foo/_update/bar and PUTs which include docids, like: /db/_design/foo/_update/bar/docid The function signature: function(doc, req) { doc.a_new_field = req.query.something; return [doc, "

added something to your doc

"]; } The tests in update_documents.js are fairly complete and include examples of bumping a counter, changing only a single field, parsing from (and returning) XML, and creating new documents. git-svn-id: http://svn.apache.org/repos/asf/couchdb/trunk@803245 13f79535-47bb-0310-9956-ffa450edef68 --- etc/couchdb/default.ini.tpl.in | 1 + share/Makefile.am | 1 + share/server/loop.js | 1 + share/server/render.js | 20 ++++++ share/www/script/couch_tests.js | 1 + src/couchdb/couch_httpd.erl | 2 +- src/couchdb/couch_httpd_external.erl | 6 +- src/couchdb/couch_httpd_show.erl | 93 +++++++++++++++++++++++----- src/couchdb/couch_query_servers.erl | 20 +++++- test/query_server_spec.rb | 24 ++++++- 10 files changed, 146 insertions(+), 23 deletions(-) diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index 6837369..28c4e29 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -90,3 +90,4 @@ _view = {couch_httpd_view, handle_view_req} _show = {couch_httpd_show, handle_doc_show_req} _list = {couch_httpd_show, handle_view_list_req} _info = {couch_httpd_db, handle_design_info_req} +_update = {couch_httpd_show, handle_doc_update_req} diff --git a/share/Makefile.am b/share/Makefile.am index 89ca9eb..0fbf602 100644 --- a/share/Makefile.am +++ b/share/Makefile.am @@ -137,6 +137,7 @@ nobase_dist_localdata_DATA = \ www/script/test/etags_head.js \ www/script/test/etags_views.js \ www/script/test/show_documents.js \ + www/script/test/update_documents.js \ www/script/test/list_views.js \ www/script/test/compact.js \ www/script/test/purge.js \ diff --git a/share/server/loop.js b/share/server/loop.js index 3435476..ff241ee 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -41,6 +41,7 @@ var dispatch = { "rereduce" : Views.rereduce, "validate" : Validate.validate, "show" : Render.show, + "update" : Render.update, "list" : Render.list, "filter" : Filter.filter }; diff --git a/share/server/render.js b/share/server/render.js index 1a9fc5a..82ceb3a 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -166,11 +166,16 @@ function runProvides(req) { //// //// //// + 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]); } @@ -212,6 +217,21 @@ function runShow(showFun, doc, req, funSrc) { } }; +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"); + } + } catch(e) { + respondError(e, funSrc, true); + } +}; + function resetList() { gotRow = false; lastRow = false; diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index 91e95b1..86c65bb 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -67,6 +67,7 @@ loadTest("replication.js"); loadTest("etags_head.js"); loadTest("etags_views.js"); loadTest("show_documents.js"); +loadTest("update_documents.js"); loadTest("list_views.js"); loadTest("compact.js"); loadTest("purge.js"); diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index fee5004..fde0231 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -353,7 +353,7 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}. send_method_not_allowed(Req, Methods) -> - send_response(Req, 405, [{"Allow", Methods}], <<>>). + send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). send_json(Req, Value) -> send_json(Req, 200, Value). diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl index d6fa945..0a22231 100644 --- a/src/couchdb/couch_httpd_external.erl +++ b/src/couchdb/couch_httpd_external.erl @@ -57,8 +57,7 @@ process_external_req(HttpReq, Db, Name) -> json_req_obj(#httpd{mochi_req=Req, method=Verb, path_parts=Path, - req_body=ReqBody, - user_ctx=#user_ctx{name=UserName, roles=UserRoles} + req_body=ReqBody }, Db) -> Body = case ReqBody of undefined -> Req:recv_body(); @@ -70,7 +69,6 @@ json_req_obj(#httpd{mochi_req=Req, _ -> [] end, - UserCtx = {[{<<"name">>, UserName}, {<<"roles">>, UserRoles}]}, Headers = Req:get(headers), Hlist = mochiweb_headers:to_list(Headers), {ok, Info} = couch_db:get_db_info(Db), @@ -83,7 +81,7 @@ json_req_obj(#httpd{mochi_req=Req, {<<"body">>, Body}, {<<"form">>, to_json_terms(ParsedForm)}, {<<"cookie">>, to_json_terms(Req:parse_cookie())}, - {<<"userCtx">>, UserCtx}]}. + {<<"userCtx">>, couch_util:json_user_ctx(Db)}]}. to_json_terms(Data) -> to_json_terms(Data, []). diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index 1428e61..01bf005 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -12,10 +12,9 @@ -module(couch_httpd_show). --export([handle_doc_show_req/2, handle_view_list_req/2, +-export([handle_doc_show_req/2, handle_doc_update_req/2, handle_view_list_req/2, handle_doc_show/5, handle_view_list/6]). - -include("couch_db.hrl"). -import(couch_httpd, @@ -40,6 +39,47 @@ handle_doc_show_req(#httpd{method='GET'}=Req, _Db) -> handle_doc_show_req(Req, _Db) -> send_method_not_allowed(Req, "GET,POST,HEAD"). +handle_doc_update_req(#httpd{ + method = 'PUT', + 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]), + Doc = try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of + FoundDoc -> FoundDoc + catch + _ -> nil + end, + send_doc_update_response(Lang, UpdateSrc, DocId, Doc, Req, Db); + +handle_doc_update_req(#httpd{ + method = 'POST', + 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); + +handle_doc_update_req(#httpd{ + path_parts=[_DbName, _Design, DesignName, _Update, UpdateName, DocId] + }=Req, Db) -> + send_method_not_allowed(Req, "PUT"); + +handle_doc_update_req(#httpd{ + path_parts=[_DbName, _Design, DesignName, _Update, UpdateName] + }=Req, Db) -> + send_method_not_allowed(Req, "POST"); + +handle_doc_update_req(Req, _Db) -> + 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, []), @@ -364,18 +404,39 @@ send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_r couch_httpd_external:send_external_response(Req, JsonResp) end). -set_or_replace_header(H, L) -> - set_or_replace_header(H, L, []). - -set_or_replace_header({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) -> +send_doc_update_response(Lang, UpdateSrc, DocId, Doc, #httpd{mochi_req=MReq}=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, + {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 +json_apply_field(H, {L}) -> + json_apply_field(H, L, []). +json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) -> % drop matching keys - set_or_replace_header({Key, NewValue}, Headers, Acc); -set_or_replace_header({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) -> + json_apply_field({Key, NewValue}, Headers, Acc); +json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) -> % something else is next, leave it alone. - set_or_replace_header({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]); -set_or_replace_header({Key, NewValue}, [], Acc) -> + json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]); +json_apply_field({Key, NewValue}, [], Acc) -> % end of list, add ours - [{Key, NewValue}|Acc]. + {[{Key, NewValue}|Acc]}. apply_etag({ExternalResponse}, CurrentEtag) -> % Here we embark on the delicate task of replacing or creating the @@ -387,12 +448,12 @@ apply_etag({ExternalResponse}, CurrentEtag) -> % no JSON headers % add our Etag and Vary headers to the response {[{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | ExternalResponse]}; - {JsonHeaders} -> + JsonHeaders -> {[case Field of - {<<"headers">>, {JsonHeaders}} -> % add our headers - JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders), - JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged), - {<<"headers">>, {JsonHeadersVaried}}; + {<<"headers">>, JsonHeaders} -> % add our headers + JsonHeadersEtagged = json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders), + JsonHeadersVaried = json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged), + {<<"headers">>, JsonHeadersVaried}; _ -> % skip non-header fields Field end || Field <- ExternalResponse]} diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl index bb0cc85..fca7c85 100644 --- a/src/couchdb/couch_query_servers.erl +++ b/src/couchdb/couch_query_servers.erl @@ -18,7 +18,7 @@ -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]). -export([start_doc_map/2, map_docs/2, stop_doc_map/1]). -export([reduce/3, rereduce/3,validate_doc_update/5]). --export([render_doc_show/6, start_view_list/2, +-export([render_doc_show/6, render_doc_update/6, start_view_list/2, render_list_head/4, render_list_row/4, render_list_tail/1]). -export([start_filter/2, filter_doc/4, end_filter/1]). % -export([test/0]). @@ -170,6 +170,7 @@ validate_doc_update(Lang, FunSrc, EditDoc, DiskDoc, Ctx) -> after ok = ret_os_process(Lang, Pid) end. +% todo use json_apply_field append_docid(DocId, JsonReqIn) -> [{<<"docId">>, DocId} | JsonReqIn]. @@ -190,6 +191,23 @@ render_doc_show(Lang, ShowSrc, DocId, Doc, Req, Db) -> ok = ret_os_process(Lang, Pid) end. +render_doc_update(Lang, UpdateSrc, DocId, Doc, Req, Db) -> + Pid = get_os_process(Lang), + {JsonReqIn} = couch_httpd_external:json_req_obj(Req, Db), + + {JsonReq, JsonDoc} = case {DocId, Doc} of + {nil, nil} -> {{JsonReqIn}, null}; + {DocId, nil} -> {{append_docid(DocId, JsonReqIn)}, null}; + _ -> {{append_docid(DocId, JsonReqIn)}, couch_doc:to_json_obj(Doc, [revs])} + end, + try couch_os_process:prompt(Pid, + [<<"update">>, UpdateSrc, JsonDoc, JsonReq]) of + FormResp -> + FormResp + after + ok = ret_os_process(Lang, Pid) + end. + start_view_list(Lang, ListSrc) -> Pid = get_os_process(Lang), true = couch_os_process:prompt(Pid, [<<"add_fun">>, ListSrc]), diff --git a/test/query_server_spec.rb b/test/query_server_spec.rb index dfc57a5..c9fb694 100644 --- a/test/query_server_spec.rb +++ b/test/query_server_spec.rb @@ -242,12 +242,21 @@ def self.run_command }, "filter-basic" => { "js" => <<-JS - function(doc, req, userCtx) { + function(doc, req) { if (doc.good) { return true; } } JS + }, + "update-basic" => { + "js" => <<-JS + function(doc, req) { + doc.world = "hello"; + var resp = [doc, "hello doc"]; + return resp; + } + JS } } @@ -441,6 +450,19 @@ def self.run_command should == [true, [true, false, true]] end end + + describe "update" do + before(:all) do + @fun = functions["update-basic"][LANGUAGE] + @qs.reset! + end + it "should return a doc and a resp body" do + up, doc, resp = @qs.run(["update", @fun, {"foo" => "gnarly"}, {"verb" => "POST"}]) + up.should == "up" + doc.should == {"foo" => "gnarly", "world" => "hello"} + resp["body"].should == "hello doc" + end + end end def should_have_exited qs