From 9ccb235a2d58d6b7caf406952f18ca13d9889f3e Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Fri, 10 Jul 2009 00:59:36 +0000 Subject: Apply patch from Benoit Chesneau's COUCHDB-404 Restores 0.8-style /db/_view view urls and adds an option to render views and documents as other formats like: /db/docid?show=blog/post /db/_view/blog/posts?list=index We're retaining the longer _design/appname paths as well because that resource is valuable for reverse proxies and rewriters. git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@792771 13f79535-47bb-0310-9956-ffa450edef68 --- etc/couchdb/default.ini.tpl.in | 1 + share/www/script/test/list_views.js | 31 +++++++++++- share/www/script/test/show_documents.js | 13 ++++- src/couchdb/couch_db.hrl | 3 +- src/couchdb/couch_httpd_db.erl | 89 ++++++++++++++++++++------------- src/couchdb/couch_httpd_show.erl | 56 +++++++++++---------- src/couchdb/couch_httpd_view.erl | 49 +++++++++++++++++- 7 files changed, 174 insertions(+), 68 deletions(-) diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index c4a87363..77a09fcb 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -68,6 +68,7 @@ _whoami = {couch_httpd_misc_handlers, handle_whoami_req} _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} diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index f9268479..67043ac8 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -176,6 +176,10 @@ couchTests.list_views = function(debug) { var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicBasic/basicView"); 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"); @@ -183,6 +187,12 @@ couchTests.list_views = function(debug) { 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); // test the richness of the arguments xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/basicJSON/basicView"); @@ -208,16 +218,27 @@ couchTests.list_views = function(debug) { T(resp.req.cookie); // get with query params - var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=3"); + xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/simpleForm/basicView?startkey=3"); 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: 9/.test(xhr.responseText)); - + + var xhr = CouchDB.request("GET", "/test_suite_db/_view/lists/basicView?list=simpleForm&startkey=3"); + 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: 9/.test(xhr.responseText)); + // 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)); //too many Get Rows @@ -231,6 +252,12 @@ couchTests.list_views = function(debug) { 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"); diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index 7201ae81..6f41f6e8 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -147,6 +147,14 @@ couchTests.show_documents = function(debug) { // Fix for COUCHDB-379 T(equals(xhr.getResponseHeader("Server").substr(0,7), "CouchDB")); + + + xhr = CouchDB.request("GET", "/test_suite_db/"+docid+"?show=template/hello"); + T(xhr.responseText == "Hello World"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + + // Fix for COUCHDB-379 + T(equals(xhr.getResponseHeader("Server").substr(0,7), "CouchDB")); // // error stacktraces // xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); @@ -163,7 +171,10 @@ couchTests.show_documents = function(debug) { // show with doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/"+docid); T(xhr.responseText == "Just Rusty"); - + + xhr = CouchDB.request("GET", "/test_suite_db/"+docid+"?show=template/just-name"); + T(xhr.responseText == "Just Rusty"); + // show with missing doc xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/just-name/missingdoc"); diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index abb301eb..f74d0ae0 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -167,7 +167,8 @@ include_docs = false, stale = false, multi_get = false, - callback = nil + callback = nil, + list = nil }). -record(view_fold_helper_funs, { diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index f5bebc20..fcbdc4c6 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -26,7 +26,8 @@ -record(doc_query_args, { options = [], rev = nil, - open_revs = [] + open_revs = [], + show = nil }). % Database request handlers @@ -192,7 +193,6 @@ handle_design_info_req(#httpd{ handle_design_info_req(Req, _Db) -> send_method_not_allowed(Req, "GET"). - create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> ok = couch_httpd:verify_is_server_admin(Req), case couch_server:create(DbName, [{user_ctx, UserCtx}]) of @@ -558,8 +558,6 @@ all_docs_view(Req, Db, Keys) -> end end). - - db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> % check for the existence of the doc to handle the 404 case. couch_doc_open(Db, DocId, nil, []), @@ -572,43 +570,50 @@ db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> #doc_query_args{ + show = Format, rev = Rev, open_revs = Revs, options = Options } = parse_doc_query(Req), - case Revs of - [] -> - Doc = couch_doc_open(Db, DocId, Rev, Options), - DiskEtag = couch_httpd:doc_etag(Doc), - couch_httpd:etag_respond(Req, DiskEtag, fun() -> - Headers = case Doc#doc.meta of - [] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta - _ -> [] - end, - send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options)) - end); - _ -> - {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options), - {ok, Resp} = start_json_response(Req, 200), - send_chunk(Resp, "["), - % We loop through the docs. The first time through the separator - % is whitespace, then a comma on subsequent iterations. - lists:foldl( - fun(Result, AccSeparator) -> - case Result of - {ok, Doc} -> - JsonDoc = couch_doc:to_json_obj(Doc, Options), - Json = ?JSON_ENCODE({[{ok, JsonDoc}]}), - send_chunk(Resp, AccSeparator ++ Json); - {{not_found, missing}, RevId} -> - Json = ?JSON_ENCODE({[{"missing", RevId}]}), - send_chunk(Resp, AccSeparator ++ Json) + case Format of + nil -> + case Revs of + [] -> + Doc = couch_doc_open(Db, DocId, Rev, Options), + DiskEtag = couch_httpd:doc_etag(Doc), + couch_httpd:etag_respond(Req, DiskEtag, fun() -> + Headers = case Doc#doc.meta of + [] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta + _ -> [] end, - "," % AccSeparator now has a comma - end, - "", Results), - send_chunk(Resp, "]"), - end_json_response(Resp) + send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options)) + end); + _ -> + {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options), + {ok, Resp} = start_json_response(Req, 200), + send_chunk(Resp, "["), + % We loop through the docs. The first time through the separator + % is whitespace, then a comma on subsequent iterations. + lists:foldl( + fun(Result, AccSeparator) -> + case Result of + {ok, Doc} -> + JsonDoc = couch_doc:to_json_obj(Doc, Options), + Json = ?JSON_ENCODE({[{ok, JsonDoc}]}), + send_chunk(Resp, AccSeparator ++ Json); + {{not_found, missing}, RevId} -> + Json = ?JSON_ENCODE({[{"missing", RevId}]}), + send_chunk(Resp, AccSeparator ++ Json) + end, + "," % AccSeparator now has a comma + end, + "", Results), + send_chunk(Resp, "]"), + end_json_response(Resp) + end; + _ -> + {DesignName, ShowName} = Format, + couch_httpd_show:handle_doc_show(Req, DesignName, ShowName, DocId, Db) end; db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> @@ -843,6 +848,16 @@ db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts) db_attachment_req(Req, _Db, _DocId, _FileNameParts) -> send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT"). +parse_doc_format(FormatStr) when is_binary(FormatStr) -> + parse_doc_format(?b2l(FormatStr)); +parse_doc_format(FormatStr) when is_list(FormatStr) -> + SplitFormat = lists:splitwith(fun($/) -> false; (_) -> true end, FormatStr), + case SplitFormat of + {DesignName, [$/ | ShowName]} -> {?l2b(DesignName), ?l2b(ShowName)}; + _Else -> throw({bad_request, <<"Invalid doc format">>}) + end; +parse_doc_format(_BadFormatStr) -> + throw({bad_request, <<"Invalid doc format">>}). parse_doc_query(Req) -> lists:foldl(fun({Key,Value}, Args) -> @@ -875,6 +890,8 @@ parse_doc_query(Req) -> {"open_revs", RevsJsonStr} -> JsonArray = ?JSON_DECODE(RevsJsonStr), Args#doc_query_args{open_revs=[couch_doc:parse_rev(Rev) || Rev <- JsonArray]}; + {"show", FormatStr} -> + Args#doc_query_args{show=parse_doc_format(FormatStr)}; _Else -> % unknown key value pair, ignore. Args end diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index c29d89c5..9c873e8f 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -12,7 +12,8 @@ -module(couch_httpd_show). --export([handle_doc_show_req/2, handle_view_list_req/2]). +-export([handle_doc_show_req/2, handle_view_list_req/2, + handle_doc_show/5, handle_view_list/6]). -include("couch_db.hrl"). @@ -26,58 +27,59 @@ handle_doc_show_req(#httpd{ method='GET', path_parts=[_DbName, _Design, DesignName, _Show, ShowName, 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">>), - ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]), - Doc = try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of - FoundDoc -> FoundDoc - catch - _ -> nil - end, - send_doc_show_response(Lang, ShowSrc, DocId, Doc, Req, Db); - + handle_doc_show(Req, DesignName, ShowName, DocId, Db); + handle_doc_show_req(#httpd{ method='GET', path_parts=[_DbName, _Design, DesignName, _Show, ShowName] }=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">>), - ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]), - send_doc_show_response(Lang, ShowSrc, nil, nil, Req, Db); + handle_doc_show(Req, DesignName, ShowName, nil, Db); 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,HEAD"). - -handle_view_list_req(#httpd{method='GET', - path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> + +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">>), - ListSrc = get_nested_json_value({Props}, [<<"lists">>, ListName]), - send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, nil); + ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]), + Doc = case DocId of + nil -> nil; + _ -> + try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of + FoundDoc -> FoundDoc + catch + _ -> nil + end + end, + send_doc_show_response(Lang, ShowSrc, DocId, Doc, Req, Db). + +handle_view_list_req(#httpd{method='GET', + path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> + handle_view_list(Req, DesignName, ListName, ViewName, Db, nil); handle_view_list_req(#httpd{method='GET'}=Req, _Db) -> 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) -> - DesignId = <<"_design/", DesignName/binary>>, - #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - ListSrc = get_nested_json_value({Props}, [<<"lists">>, ListName]), ReqBody = couch_httpd:body(Req), {Props2} = ?JSON_DECODE(ReqBody), Keys = proplists:get_value(<<"keys">>, Props2, nil), - send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req#httpd{req_body=ReqBody}, Db, Keys); + handle_view_list(Req#httpd{req_body=ReqBody}, DesignName, ListName, ViewName, Db, Keys); handle_view_list_req(Req, _Db) -> send_method_not_allowed(Req, "GET,POST,HEAD"). +handle_view_list(Req, DesignName, ListName, ViewName, Db, Keys) -> + DesignId = <<"_design/", DesignName/binary>>, + #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), + Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), + ListSrc = get_nested_json_value({Props}, [<<"lists">>, ListName]), + send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys). get_nested_json_value({Props}, [Key|Keys]) -> case proplists:get_value(Key, Props, nil) of diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl index 2028840c..0150582a 100644 --- a/src/couchdb/couch_httpd_view.erl +++ b/src/couchdb/couch_httpd_view.erl @@ -13,7 +13,7 @@ -module(couch_httpd_view). -include("couch_db.hrl"). --export([handle_view_req/2,handle_temp_view_req/2]). +-export([handle_view_req/2,handle_temp_view_req/2,handle_db_view_req/2]). -export([get_stale_type/1, get_reduce_type/1, parse_view_params/3]). -export([make_view_fold_fun/6, finish_view_fold/3, view_row_obj/3]). @@ -73,6 +73,49 @@ handle_view_req(#httpd{method='POST', handle_view_req(Req, _Db) -> send_method_not_allowed(Req, "GET,POST,HEAD"). +handle_db_view_req(#httpd{method='GET', + path_parts=[_Db, _View, DName, ViewName]}=Req, Db) -> + QueryArgs = couch_httpd_view:parse_view_params(Req, nil, nil), + #view_query_args{ + list = ListName + } = QueryArgs, + ?LOG_DEBUG("ici ~p", [ListName]), + case ListName of + nil -> couch_httpd_view:design_doc_view(Req, Db, DName, ViewName, nil); + _ -> + couch_httpd_show:handle_view_list(Req, DName, ListName, ViewName, Db, nil) + end; + +handle_db_view_req(#httpd{method='POST', + path_parts=[_Db, _View, DName, ViewName]}=Req, Db) -> + QueryArgs = couch_httpd_view:parse_view_params(Req, nil, nil), + #view_query_args{ + list = ListName + } = QueryArgs, + case ListName of + nil -> + {Fields} = couch_httpd:json_body_obj(Req), + case proplists:get_value(<<"keys">>, Fields, nil) of + nil -> + Fmt = "POST to view ~p/~p in database ~p with no keys member.", + ?LOG_DEBUG(Fmt, [DName, ViewName, Db]), + couch_httpd_view:design_doc_view(Req, Db, DName, ViewName, nil); + Keys when is_list(Keys) -> + couch_httpd_view:design_doc_view(Req, Db, DName, ViewName, Keys); + _ -> + throw({bad_request, "`keys` member must be a array."}) + end; + _ -> + ReqBody = couch_httpd:body(Req), + {Props2} = ?JSON_DECODE(ReqBody), + Keys = proplists:get_value(<<"keys">>, Props2, nil), + couch_httpd_show:handle_view_list(Req#httpd{req_body=ReqBody}, + DName, ListName, ViewName, Db, Keys) + end; + +handle_db_view_req(Req, _Db) -> + send_method_not_allowed(Req, "GET,POST,HEAD"). + handle_temp_view_req(#httpd{method='POST'}=Req, Db) -> couch_stats_collector:increment({httpd, temporary_view_reads}), {Props} = couch_httpd:json_body_obj(Req), @@ -269,6 +312,8 @@ parse_view_param("reduce", Value) -> [{reduce, parse_bool_param(Value)}]; parse_view_param("include_docs", Value) -> [{include_docs, parse_bool_param(Value)}]; +parse_view_param("list", Value) -> + [{list, ?l2b(Value)}]; parse_view_param("callback", _) -> []; % Verified in the JSON response functions parse_view_param(Key, Value) -> @@ -298,6 +343,8 @@ validate_view_query(end_docid, Value, Args) -> Args#view_query_args{end_docid=Value}; validate_view_query(limit, Value, Args) -> Args#view_query_args{limit=Value}; +validate_view_query(list, Value, Args) -> + Args#view_query_args{list=Value}; validate_view_query(stale, _, Args) -> Args; validate_view_query(descending, true, Args) -> -- cgit v1.2.3