Skip to content

Commit

Permalink
Initial commit of _update handler. Thanks to Paul Davis, Jason Davies…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
jchris committed Aug 11, 2009
1 parent ea95901 commit 36bbc72
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 23 deletions.
1 change: 1 addition & 0 deletions etc/couchdb/default.ini.tpl.in
Expand Up @@ -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}
1 change: 1 addition & 0 deletions share/Makefile.am
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions share/server/loop.js
Expand Up @@ -41,6 +41,7 @@ var dispatch = {
"rereduce" : Views.rereduce,
"validate" : Validate.validate,
"show" : Render.show,
"update" : Render.update,
"list" : Render.list,
"filter" : Filter.filter
};
Expand Down
20 changes: 20 additions & 0 deletions share/server/render.js
Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions share/www/script/couch_tests.js
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion src/couchdb/couch_httpd.erl
Expand Up @@ -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).
Expand Down
6 changes: 2 additions & 4 deletions src/couchdb/couch_httpd_external.erl
Expand Up @@ -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();
Expand All @@ -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),
Expand All @@ -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, []).
Expand Down
93 changes: 77 additions & 16 deletions src/couchdb/couch_httpd_show.erl
Expand Up @@ -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,
Expand All @@ -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, []),
Expand Down Expand Up @@ -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
Expand All @@ -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]}
Expand Down
20 changes: 19 additions & 1 deletion src/couchdb/couch_query_servers.erl
Expand Up @@ -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]).
Expand Down Expand Up @@ -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].

Expand All @@ -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]),
Expand Down
24 changes: 23 additions & 1 deletion test/query_server_spec.rb
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 36bbc72

Please sign in to comment.