summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2009-08-11 18:50:08 +0000
committerJohn Christopher Anderson <jchris@apache.org>2009-08-11 18:50:08 +0000
commit36bbc72b6b0992639a0ba7a2077d75ec9f7cf03d (patch)
tree3e4e0d1e46a7fe04a9b185455a025e8ff5fa3e62
parentea95901fe52df11338134bd86f9bc8f028c5444b (diff)
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, "<h1>added something to your doc</h1>"]; } 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: https://svn.apache.org/repos/asf/couchdb/trunk@803245 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--etc/couchdb/default.ini.tpl.in1
-rw-r--r--share/Makefile.am1
-rw-r--r--share/server/loop.js1
-rw-r--r--share/server/render.js20
-rw-r--r--share/www/script/couch_tests.js1
-rw-r--r--src/couchdb/couch_httpd.erl2
-rw-r--r--src/couchdb/couch_httpd_external.erl6
-rw-r--r--src/couchdb/couch_httpd_show.erl93
-rw-r--r--src/couchdb/couch_query_servers.erl20
-rw-r--r--test/query_server_spec.rb24
10 files changed, 146 insertions, 23 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 68373691..28c4e294 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 89ca9eb5..0fbf602f 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 34354769..ff241eed 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 1a9fc5a5..82ceb3ab 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 91e95b11..86c65bb7 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 fee5004e..fde02317 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 d6fa945f..0a222311 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 1428e612..01bf0055 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 bb0cc853..fca7c85a 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 dfc57a5b..c9fb6942 100644
--- a/test/query_server_spec.rb
+++ b/test/query_server_spec.rb
@@ -242,12 +242,21 @@ functions = {
},
"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 @@ describe "query server normal case" do
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