| % Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| % use this file except in compliance with the License. You may obtain a copy of |
| % the License at |
| % |
| % http://www.apache.org/licenses/LICENSE-2.0 |
| % |
| % Unless required by applicable law or agreed to in writing, software |
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| % License for the specific language governing permissions and limitations under |
| % the License. |
| |
| -module(couch_httpd_view). |
| -include("couch_db.hrl"). |
| |
| -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]). |
| -export([view_group_etag/2, view_group_etag/3, make_reduce_fold_funs/5]). |
| -export([design_doc_view/5, parse_bool_param/1]). |
| |
| -import(couch_httpd, |
| [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,send_chunk/2, |
| start_json_response/2, start_json_response/3, end_json_response/1, |
| send_chunked_error/2]). |
| |
| design_doc_view(Req, Db, Id, ViewName, Keys) -> |
| DesignId = <<"_design/", Id/binary>>, |
| Stale = get_stale_type(Req), |
| Reduce = get_reduce_type(Req), |
| Result = case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of |
| {ok, View, Group} -> |
| QueryArgs = parse_view_params(Req, Keys, map), |
| output_map_view(Req, 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 = parse_view_params(Req, Keys, red_map), |
| MapView = couch_view:extract_map_view(ReduceView), |
| output_map_view(Req, MapView, Group, Db, QueryArgs, Keys); |
| _ -> |
| QueryArgs = parse_view_params(Req, Keys, reduce), |
| output_reduce_view(Req, Db, ReduceView, Group, QueryArgs, Keys) |
| end; |
| _ -> |
| throw({not_found, Reason}) |
| end |
| end, |
| couch_stats_collector:increment({httpd, view_reads}), |
| Result. |
| |
| handle_view_req(#httpd{method='GET', |
| path_parts=[_Db, _Design, DName, _View, ViewName]}=Req, Db) -> |
| design_doc_view(Req, Db, DName, ViewName, nil); |
| |
| handle_view_req(#httpd{method='POST', |
| path_parts=[_Db, _Design, DName, _View, ViewName]}=Req, Db) -> |
| {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]), |
| design_doc_view(Req, Db, DName, ViewName, nil); |
| Keys when is_list(Keys) -> |
| design_doc_view(Req, Db, DName, ViewName, Keys); |
| _ -> |
| throw({bad_request, "`keys` member must be a array."}) |
| end; |
| |
| 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, DName, 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, DName, 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), |
| Language = proplists:get_value(<<"language">>, Props, <<"javascript">>), |
| {DesignOptions} = proplists:get_value(<<"options">>, Props, {[]}), |
| MapSrc = proplists:get_value(<<"map">>, Props), |
| Keys = proplists:get_value(<<"keys">>, Props, nil), |
| case proplists:get_value(<<"reduce">>, Props, null) of |
| null -> |
| QueryArgs = parse_view_params(Req, Keys, map), |
| {ok, View, Group} = couch_view:get_temp_map_view(Db, Language, |
| DesignOptions, MapSrc), |
| output_map_view(Req, View, Group, Db, QueryArgs, Keys); |
| RedSrc -> |
| QueryArgs = parse_view_params(Req, Keys, reduce), |
| {ok, View, Group} = couch_view:get_temp_reduce_view(Db, Language, |
| DesignOptions, MapSrc, RedSrc), |
| output_reduce_view(Req, Db, View, Group, QueryArgs, Keys) |
| end; |
| |
| handle_temp_view_req(Req, _Db) -> |
| send_method_not_allowed(Req, "POST"). |
| |
| output_map_view(Req, View, Group, Db, QueryArgs, nil) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_key = StartKey, |
| start_docid = StartDocId |
| } = QueryArgs, |
| CurrentEtag = view_group_etag(Group, Db), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| {ok, RowCount} = couch_view:get_row_count(View), |
| Start = {StartKey, StartDocId}, |
| FoldlFun = make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, RowCount, #view_fold_helper_funs{reduce_count=fun couch_view:reduce_to_count/1}), |
| FoldAccInit = {Limit, SkipCount, undefined, [], nil}, |
| FoldResult = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit), |
| finish_view_fold(Req, RowCount, FoldResult) |
| end); |
| |
| output_map_view(Req, View, Group, Db, QueryArgs, Keys) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_docid = StartDocId |
| } = QueryArgs, |
| CurrentEtag = view_group_etag(Group, Db, Keys), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| {ok, RowCount} = couch_view:get_row_count(View), |
| FoldAccInit = {Limit, SkipCount, undefined, [], nil}, |
| FoldResult = lists:foldl( |
| fun(Key, {ok, FoldAcc}) -> |
| Start = {Key, StartDocId}, |
| FoldlFun = make_view_fold_fun(Req, |
| QueryArgs#view_query_args{ |
| start_key = Key, |
| end_key = Key |
| }, CurrentEtag, Db, RowCount, |
| #view_fold_helper_funs{ |
| reduce_count = fun couch_view:reduce_to_count/1 |
| }), |
| couch_view:fold(View, Start, Dir, FoldlFun, FoldAcc) |
| end, {ok, FoldAccInit}, Keys), |
| finish_view_fold(Req, RowCount, FoldResult) |
| end). |
| |
| output_reduce_view(Req, Db, View, Group, QueryArgs, nil) -> |
| #view_query_args{ |
| start_key = StartKey, |
| end_key = EndKey, |
| limit = Limit, |
| skip = Skip, |
| direction = Dir, |
| start_docid = StartDocId, |
| end_docid = EndDocId, |
| group_level = GroupLevel |
| } = QueryArgs, |
| CurrentEtag = view_group_etag(Group, Db), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| {ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel, QueryArgs, CurrentEtag, #reduce_fold_helper_funs{}), |
| FoldAccInit = {Limit, Skip, undefined, []}, |
| {ok, {_, _, Resp, _}} = couch_view:fold_reduce(View, Dir, {StartKey, StartDocId}, |
| {EndKey, EndDocId}, GroupRowsFun, RespFun, FoldAccInit), |
| finish_reduce_fold(Req, Resp) |
| end); |
| |
| output_reduce_view(Req, Db, View, Group, QueryArgs, Keys) -> |
| #view_query_args{ |
| limit = Limit, |
| skip = Skip, |
| direction = Dir, |
| start_docid = StartDocId, |
| end_docid = EndDocId, |
| group_level = GroupLevel |
| } = QueryArgs, |
| CurrentEtag = view_group_etag(Group, Db), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| {ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel, QueryArgs, CurrentEtag, #reduce_fold_helper_funs{}), |
| {Resp, _RedAcc3} = lists:foldl( |
| fun(Key, {Resp, RedAcc}) -> |
| % run the reduce once for each key in keys, with limit etc reapplied for each key |
| FoldAccInit = {Limit, Skip, Resp, RedAcc}, |
| {_, {_, _, Resp2, RedAcc2}} = couch_view:fold_reduce(View, Dir, {Key, StartDocId}, |
| {Key, EndDocId}, GroupRowsFun, RespFun, FoldAccInit), |
| % Switch to comma |
| {Resp2, RedAcc2} |
| end, |
| {undefined, []}, Keys), % Start with no comma |
| finish_reduce_fold(Req, Resp) |
| end). |
| |
| reverse_key_default(nil) -> {}; |
| reverse_key_default({}) -> nil; |
| reverse_key_default(Key) -> Key. |
| |
| get_stale_type(Req) -> |
| list_to_atom(couch_httpd:qs_value(Req, "stale", "nil")). |
| |
| get_reduce_type(Req) -> |
| list_to_atom(couch_httpd:qs_value(Req, "reduce", "true")). |
| |
| parse_view_params(Req, Keys, ViewType) -> |
| QueryList = couch_httpd:qs(Req), |
| QueryParams = |
| lists:foldl(fun({K, V}, Acc) -> |
| parse_view_param(K, V) ++ Acc |
| end, [], QueryList), |
| IsMultiGet = case Keys of |
| nil -> false; |
| _ -> true |
| end, |
| Args = #view_query_args{ |
| view_type=ViewType, |
| multi_get=IsMultiGet |
| }, |
| QueryArgs = lists:foldl(fun({K, V}, Args2) -> |
| validate_view_query(K, V, Args2) |
| end, Args, lists:reverse(QueryParams)), % Reverse to match QS order. |
| |
| GroupLevel = QueryArgs#view_query_args.group_level, |
| case {ViewType, GroupLevel, IsMultiGet} of |
| {reduce, exact, true} -> |
| QueryArgs; |
| {reduce, _, false} -> |
| QueryArgs; |
| {reduce, _, _} -> |
| Msg = <<"Multi-key fetchs for reduce " |
| "view must include `group=true`">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| QueryArgs |
| end, |
| QueryArgs. |
| |
| parse_view_param("", _) -> |
| []; |
| parse_view_param("key", Value) -> |
| JsonKey = ?JSON_DECODE(Value), |
| [{start_key, JsonKey}, {end_key, JsonKey}]; |
| parse_view_param("startkey_docid", Value) -> |
| [{start_docid, ?l2b(Value)}]; |
| parse_view_param("endkey_docid", Value) -> |
| [{end_docid, ?l2b(Value)}]; |
| parse_view_param("startkey", Value) -> |
| [{start_key, ?JSON_DECODE(Value)}]; |
| parse_view_param("endkey", Value) -> |
| [{end_key, ?JSON_DECODE(Value)}]; |
| parse_view_param("limit", Value) -> |
| [{limit, parse_positive_int_param(Value)}]; |
| parse_view_param("count", _Value) -> |
| throw({query_parse_error, <<"Query parameter 'count' is now 'limit'.">>}); |
| parse_view_param("stale", "ok") -> |
| [{stale, ok}]; |
| parse_view_param("stale", _Value) -> |
| throw({query_parse_error, <<"stale only available as stale=ok">>}); |
| parse_view_param("update", _Value) -> |
| throw({query_parse_error, <<"update=false is now stale=ok">>}); |
| parse_view_param("descending", Value) -> |
| [{descending, parse_bool_param(Value)}]; |
| parse_view_param("skip", Value) -> |
| [{skip, parse_int_param(Value)}]; |
| parse_view_param("group", Value) -> |
| case parse_bool_param(Value) of |
| true -> [{group_level, exact}]; |
| false -> [{group_level, 0}] |
| end; |
| parse_view_param("group_level", Value) -> |
| [{group_level, parse_positive_int_param(Value)}]; |
| parse_view_param("inclusive_end", Value) -> |
| [{inclusive_end, parse_bool_param(Value)}]; |
| 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) -> |
| [{extra, {Key, Value}}]. |
| |
| validate_view_query(start_key, Value, Args) -> |
| case Args#view_query_args.multi_get of |
| true -> |
| Msg = <<"Query parameter `start_key` is " |
| "not compatiible with multi-get">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#view_query_args{start_key=Value} |
| end; |
| validate_view_query(start_docid, Value, Args) -> |
| Args#view_query_args{start_docid=Value}; |
| validate_view_query(end_key, Value, Args) -> |
| case Args#view_query_args.multi_get of |
| true-> |
| Msg = <<"Query paramter `end_key` is " |
| "not compatibile with multi-get">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#view_query_args{end_key=Value} |
| end; |
| 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) -> |
| case Args#view_query_args.direction of |
| rev -> Args; % Already reversed |
| fwd -> |
| Args#view_query_args{ |
| direction = rev, |
| start_key = |
| reverse_key_default(Args#view_query_args.start_key), |
| start_docid = |
| reverse_key_default(Args#view_query_args.start_docid), |
| end_key = |
| reverse_key_default(Args#view_query_args.end_key), |
| end_docid = |
| reverse_key_default(Args#view_query_args.end_docid) |
| } |
| end; |
| validate_view_query(descending, false, Args) -> |
| Args; % Ignore default condition |
| validate_view_query(skip, Value, Args) -> |
| Args#view_query_args{skip=Value}; |
| validate_view_query(group_level, Value, Args) -> |
| case Args#view_query_args.view_type of |
| reduce -> |
| Args#view_query_args{group_level=Value}; |
| _ -> |
| Msg = <<"Invalid URL parameter 'group' or " |
| " 'group_level' for non-reduce view.">>, |
| throw({query_parse_error, Msg}) |
| end; |
| validate_view_query(inclusive_end, Value, Args) -> |
| Args#view_query_args{inclusive_end=Value}; |
| validate_view_query(reduce, _, Args) -> |
| case Args#view_query_args.view_type of |
| map -> |
| Msg = <<"Invalid URL parameter `reduce` for map view.">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args |
| end; |
| validate_view_query(include_docs, true, Args) -> |
| case Args#view_query_args.view_type of |
| reduce -> |
| Msg = <<"Query paramter `include_docs` " |
| "is invalid for reduce views.">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#view_query_args{include_docs=true} |
| end; |
| validate_view_query(include_docs, _Value, Args) -> |
| Args; |
| validate_view_query(extra, _Value, Args) -> |
| Args. |
| |
| make_view_fold_fun(Req, QueryArgs, Etag, Db, TotalViewCount, HelperFuns) -> |
| #view_query_args{ |
| end_key = EndKey, |
| end_docid = EndDocId, |
| inclusive_end = InclusiveEnd, |
| direction = Dir |
| } = QueryArgs, |
| |
| #view_fold_helper_funs{ |
| passed_end = PassedEndFun, |
| start_response = StartRespFun, |
| send_row = SendRowFun, |
| reduce_count = ReduceCountFun |
| } = apply_default_helper_funs(HelperFuns, |
| {Dir, EndKey, EndDocId, InclusiveEnd}), |
| |
| #view_query_args{ |
| include_docs = IncludeDocs |
| } = QueryArgs, |
| |
| fun({{Key, DocId}, Value}, OffsetReds, {AccLimit, AccSkip, Resp, RowFunAcc, |
| OffsetAcc}) -> |
| PassedEnd = PassedEndFun(Key, DocId), |
| case {PassedEnd, AccLimit, AccSkip, Resp} of |
| {true, _, _, _} -> |
| % The stop key has been passed, stop looping. |
| % We may need offset so calcluate it here. |
| % Checking Resp is an optimization that tells |
| % us its already been calculated (and sent). |
| NewOffset = case Resp of |
| undefined -> ReduceCountFun(OffsetReds); |
| _ -> nil |
| end, |
| {stop, {AccLimit, AccSkip, Resp, RowFunAcc, NewOffset}}; |
| {_, 0, _, _} -> |
| % we've done "limit" rows, stop foldling |
| {stop, {0, 0, Resp, RowFunAcc, OffsetAcc}}; |
| {_, _, AccSkip, _} when AccSkip > 0 -> |
| % just keep skipping |
| {ok, {AccLimit, AccSkip - 1, Resp, RowFunAcc, OffsetAcc}}; |
| {_, _, _, undefined} -> |
| % rendering the first row, first we start the response |
| Offset = ReduceCountFun(OffsetReds), |
| {ok, Resp2, RowFunAcc0} = StartRespFun(Req, Etag, |
| TotalViewCount, Offset, RowFunAcc), |
| {Go, RowFunAcc2} = SendRowFun(Resp2, Db, {{Key, DocId}, Value}, |
| IncludeDocs, RowFunAcc0), |
| {Go, {AccLimit - 1, 0, Resp2, RowFunAcc2, Offset}}; |
| {_, AccLimit, _, Resp} when (AccLimit > 0) -> |
| % rendering all other rows |
| {Go, RowFunAcc2} = SendRowFun(Resp, Db, {{Key, DocId}, Value}, |
| IncludeDocs, RowFunAcc), |
| {Go, {AccLimit - 1, 0, Resp, RowFunAcc2, OffsetAcc}} |
| end |
| end. |
| |
| make_reduce_fold_funs(Req, GroupLevel, _QueryArgs, Etag, HelperFuns) -> |
| #reduce_fold_helper_funs{ |
| start_response = StartRespFun, |
| send_row = SendRowFun |
| } = apply_default_helper_funs(HelperFuns), |
| |
| GroupRowsFun = |
| fun({_Key1,_}, {_Key2,_}) when GroupLevel == 0 -> |
| true; |
| ({Key1,_}, {Key2,_}) |
| when is_integer(GroupLevel) and is_list(Key1) and is_list(Key2) -> |
| lists:sublist(Key1, GroupLevel) == lists:sublist(Key2, GroupLevel); |
| ({Key1,_}, {Key2,_}) -> |
| Key1 == Key2 |
| end, |
| |
| RespFun = fun |
| (_Key, _Red, {AccLimit, AccSkip, Resp, RowAcc}) when AccSkip > 0 -> |
| % keep skipping |
| {ok, {AccLimit, AccSkip - 1, Resp, RowAcc}}; |
| (_Key, _Red, {0, _AccSkip, Resp, RowAcc}) -> |
| % we've exhausted limit rows, stop |
| {stop, {0, _AccSkip, Resp, RowAcc}}; |
| |
| (_Key, Red, {AccLimit, 0, undefined, RowAcc0}) when GroupLevel == 0 -> |
| % we haven't started responding yet and group=false |
| {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0), |
| {Go, RowAcc2} = SendRowFun(Resp2, {null, Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp2, RowAcc2}}; |
| (_Key, Red, {AccLimit, 0, Resp, RowAcc}) when GroupLevel == 0 -> |
| % group=false but we've already started the response |
| {Go, RowAcc2} = SendRowFun(Resp, {null, Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp, RowAcc2}}; |
| |
| (Key, Red, {AccLimit, 0, undefined, RowAcc0}) |
| when is_integer(GroupLevel), is_list(Key) -> |
| % group_level and we haven't responded yet |
| {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0), |
| {Go, RowAcc2} = SendRowFun(Resp2, {lists:sublist(Key, GroupLevel), Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp2, RowAcc2}}; |
| (Key, Red, {AccLimit, 0, Resp, RowAcc}) |
| when is_integer(GroupLevel), is_list(Key) -> |
| % group_level and we've already started the response |
| {Go, RowAcc2} = SendRowFun(Resp, {lists:sublist(Key, GroupLevel), Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp, RowAcc2}}; |
| |
| (Key, Red, {AccLimit, 0, undefined, RowAcc0}) -> |
| % group=true and we haven't responded yet |
| {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0), |
| {Go, RowAcc2} = SendRowFun(Resp2, {Key, Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp2, RowAcc2}}; |
| (Key, Red, {AccLimit, 0, Resp, RowAcc}) -> |
| % group=true and we've already started the response |
| {Go, RowAcc2} = SendRowFun(Resp, {Key, Red}, RowAcc), |
| {Go, {AccLimit - 1, 0, Resp, RowAcc2}} |
| end, |
| {ok, GroupRowsFun, RespFun}. |
| |
| apply_default_helper_funs(#view_fold_helper_funs{ |
| passed_end = PassedEnd, |
| start_response = StartResp, |
| send_row = SendRow |
| }=Helpers, {Dir, EndKey, EndDocId, InclusiveEnd}) -> |
| PassedEnd2 = case PassedEnd of |
| undefined -> make_passed_end_fun(Dir, EndKey, EndDocId, InclusiveEnd); |
| _ -> PassedEnd |
| end, |
| |
| StartResp2 = case StartResp of |
| undefined -> fun json_view_start_resp/5; |
| _ -> StartResp |
| end, |
| |
| SendRow2 = case SendRow of |
| undefined -> fun send_json_view_row/5; |
| _ -> SendRow |
| end, |
| |
| Helpers#view_fold_helper_funs{ |
| passed_end = PassedEnd2, |
| start_response = StartResp2, |
| send_row = SendRow2 |
| }. |
| |
| apply_default_helper_funs(#reduce_fold_helper_funs{ |
| start_response = StartResp, |
| send_row = SendRow |
| }=Helpers) -> |
| StartResp2 = case StartResp of |
| undefined -> fun json_reduce_start_resp/3; |
| _ -> StartResp |
| end, |
| |
| SendRow2 = case SendRow of |
| undefined -> fun send_json_reduce_row/3; |
| _ -> SendRow |
| end, |
| |
| Helpers#reduce_fold_helper_funs{ |
| start_response = StartResp2, |
| send_row = SendRow2 |
| }. |
| |
| make_passed_end_fun(fwd, EndKey, EndDocId, InclusiveEnd) -> |
| case InclusiveEnd of |
| true -> |
| fun(ViewKey, ViewId) -> |
| couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId]) |
| end; |
| false -> |
| fun |
| (ViewKey, _ViewId) when ViewKey == EndKey -> |
| true; |
| (ViewKey, ViewId) -> |
| couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId]) |
| end |
| end; |
| |
| make_passed_end_fun(rev, EndKey, EndDocId, InclusiveEnd) -> |
| case InclusiveEnd of |
| true -> |
| fun(ViewKey, ViewId) -> |
| couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId]) |
| end; |
| false-> |
| fun |
| (ViewKey, _ViewId) when ViewKey == EndKey -> |
| true; |
| (ViewKey, ViewId) -> |
| couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId]) |
| end |
| end. |
| |
| json_view_start_resp(Req, Etag, TotalViewCount, Offset, _Acc) -> |
| {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]), |
| BeginBody = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n", |
| [TotalViewCount, Offset]), |
| {ok, Resp, BeginBody}. |
| |
| send_json_view_row(Resp, Db, {{Key, DocId}, Value}, IncludeDocs, RowFront) -> |
| JsonObj = view_row_obj(Db, {{Key, DocId}, Value}, IncludeDocs), |
| send_chunk(Resp, RowFront ++ ?JSON_ENCODE(JsonObj)), |
| {ok, ",\r\n"}. |
| |
| json_reduce_start_resp(Req, Etag, _Acc0) -> |
| {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]), |
| {ok, Resp, "{\"rows\":[\r\n"}. |
| |
| send_json_reduce_row(Resp, {Key, Value}, RowFront) -> |
| send_chunk(Resp, RowFront ++ ?JSON_ENCODE({[{key, Key}, {value, Value}]})), |
| {ok, ",\r\n"}. |
| |
| view_group_etag(Group, Db) -> |
| view_group_etag(Group, Db, nil). |
| |
| view_group_etag(#group{sig=Sig,current_seq=CurrentSeq}, _Db, Extra) -> |
| % ?LOG_ERROR("Group ~p",[Group]), |
| % This is not as granular as it could be. |
| % If there are updates to the db that do not effect the view index, |
| % they will change the Etag. For more granular Etags we'd need to keep |
| % track of the last Db seq that caused an index change. |
| couch_httpd:make_etag({Sig, CurrentSeq, Extra}). |
| |
| % the view row has an error |
| view_row_obj(_Db, {{Key, error}, Value}, _IncludeDocs) -> |
| {[{key, Key}, {error, Value}]}; |
| % include docs in the view output |
| view_row_obj(Db, {{Key, DocId}, {Props}}, true) -> |
| Rev = case proplists:get_value(<<"_rev">>, Props) of |
| undefined -> |
| nil; |
| Rev0 -> |
| couch_doc:parse_rev(Rev0) |
| end, |
| view_row_with_doc(Db, {{Key, DocId}, {Props}}, Rev); |
| view_row_obj(Db, {{Key, DocId}, Value}, true) -> |
| view_row_with_doc(Db, {{Key, DocId}, Value}, nil); |
| % the normal case for rendering a view row |
| view_row_obj(_Db, {{Key, DocId}, Value}, _IncludeDocs) -> |
| {[{id, DocId}, {key, Key}, {value, Value}]}. |
| |
| view_row_with_doc(Db, {{Key, DocId}, Value}, Rev) -> |
| ?LOG_DEBUG("Include Doc: ~p ~p", [DocId, Rev]), |
| case (catch couch_httpd_db:couch_doc_open(Db, DocId, Rev, [])) of |
| {{not_found, missing}, _RevId} -> |
| {[{id, DocId}, {key, Key}, {value, Value}, {error, missing}]}; |
| {not_found, missing} -> |
| {[{id, DocId}, {key, Key}, {value, Value}, {error, missing}]}; |
| {not_found, deleted} -> |
| {[{id, DocId}, {key, Key}, {value, Value}]}; |
| Doc -> |
| JsonDoc = couch_doc:to_json_obj(Doc, []), |
| {[{id, DocId}, {key, Key}, {value, Value}, {doc, JsonDoc}]} |
| end. |
| |
| finish_view_fold(Req, TotalRows, FoldResult) -> |
| case FoldResult of |
| {ok, {_, _, undefined, _, Offset}} -> |
| % nothing found in the view, nothing has been returned |
| % send empty view |
| NewOffset = case Offset of |
| nil -> TotalRows; |
| _ -> Offset |
| end, |
| send_json(Req, 200, {[ |
| {total_rows, TotalRows}, |
| {offset, NewOffset}, |
| {rows, []} |
| ]}); |
| {ok, {_, _, Resp, _, _}} -> |
| % end the view |
| send_chunk(Resp, "\r\n]}"), |
| end_json_response(Resp); |
| Error -> |
| throw(Error) |
| end. |
| |
| finish_reduce_fold(Req, Resp) -> |
| case Resp of |
| undefined -> |
| send_json(Req, 200, {[ |
| {rows, []} |
| ]}); |
| Resp -> |
| send_chunk(Resp, "\r\n]}"), |
| end_json_response(Resp) |
| end. |
| |
| parse_bool_param("true") -> true; |
| parse_bool_param("false") -> false; |
| parse_bool_param(Val) -> |
| Msg = io_lib:format("Invalid value for boolean paramter: ~p", [Val]), |
| throw({query_parse_error, ?l2b(Msg)}). |
| |
| parse_int_param(Val) -> |
| case (catch list_to_integer(Val)) of |
| IntVal when is_integer(IntVal) -> |
| IntVal; |
| _ -> |
| Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]), |
| throw({query_parse_error, ?l2b(Msg)}) |
| end. |
| |
| parse_positive_int_param(Val) -> |
| case parse_int_param(Val) of |
| IntVal when IntVal >= 0 -> |
| IntVal; |
| _ -> |
| Fmt = "Invalid value for positive integer parameter: ~p", |
| Msg = io_lib:format(Fmt, [Val]), |
| throw({query_parse_error, ?l2b(Msg)}) |
| end. |
| |