% 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/3,handle_temp_view_req/2]). -export([parse_view_params/3]). -export([make_view_fold_fun/7, finish_view_fold/4, finish_view_fold/5, view_row_obj/3]). -export([view_group_etag/2, view_group_etag/3, make_reduce_fold_funs/6]). -export([design_doc_view/5, parse_bool_param/1, doc_member/2]). -export([make_key_options/1, load_view/4]). -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]). -import(couch_db,[get_update_seq/1]). design_doc_view(Req, Db, DName, ViewName, Keys) -> DesignId = <<"_design/", DName/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=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) -> design_doc_view(Req, Db, DName, ViewName, nil); handle_view_req(#httpd{method='POST', path_parts=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) -> couch_httpd:validate_ctype(Req, "application/json"), {Fields} = couch_httpd:json_body_obj(Req), case couch_util: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, _DDoc) -> send_method_not_allowed(Req, "GET,POST,HEAD"). handle_temp_view_req(#httpd{method='POST'}=Req, Db) -> couch_httpd:validate_ctype(Req, "application/json"), ok = couch_db:check_is_admin(Db), couch_stats_collector:increment({httpd, temporary_view_reads}), {Props} = couch_httpd:json_body_obj(Req), Language = couch_util:get_value(<<"language">>, Props, <<"javascript">>), {DesignOptions} = couch_util:get_value(<<"options">>, Props, {[]}), MapSrc = couch_util:get_value(<<"map">>, Props), Keys = couch_util:get_value(<<"keys">>, Props, nil), Reduce = get_reduce_type(Req), case couch_util: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); _ when Reduce =:= false -> QueryArgs = parse_view_params(Req, Keys, red_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, skip = SkipCount } = QueryArgs, CurrentEtag = view_group_etag(Group, Db), couch_httpd:etag_respond(Req, CurrentEtag, fun() -> {ok, RowCount} = couch_view:get_row_count(View), FoldlFun = make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, Group#group.current_seq, RowCount, #view_fold_helper_funs{reduce_count=fun couch_view:reduce_to_count/1}), FoldAccInit = {Limit, SkipCount, undefined, []}, {ok, LastReduce, FoldResult} = couch_view:fold(View, FoldlFun, FoldAccInit, make_key_options(QueryArgs)), finish_view_fold(Req, RowCount, couch_view:reduce_to_count(LastReduce), FoldResult) end); output_map_view(Req, View, Group, Db, QueryArgs, Keys) -> #view_query_args{ limit = Limit, skip = SkipCount } = 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, []}, {LastReduce, FoldResult} = lists:foldl(fun(Key, {_, FoldAcc}) -> FoldlFun = make_view_fold_fun(Req, QueryArgs#view_query_args{}, CurrentEtag, Db, Group#group.current_seq, RowCount, #view_fold_helper_funs{ reduce_count = fun couch_view:reduce_to_count/1 }), {ok, LastReduce, FoldResult} = couch_view:fold(View, FoldlFun, FoldAcc, make_key_options( QueryArgs#view_query_args{start_key=Key, end_key=Key})), {LastReduce, FoldResult} end, {{[],[]}, FoldAccInit}, Keys), finish_view_fold(Req, RowCount, couch_view:reduce_to_count(LastReduce), FoldResult, [{update_seq,Group#group.current_seq}]) end). output_reduce_view(Req, Db, View, Group, QueryArgs, nil) -> #view_query_args{ limit = Limit, skip = Skip, 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, Group#group.current_seq, #reduce_fold_helper_funs{}), FoldAccInit = {Limit, Skip, undefined, []}, {ok, {_, _, Resp, _}} = couch_view:fold_reduce(View, RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} | make_key_options(QueryArgs)]), finish_reduce_fold(Req, Resp) end); output_reduce_view(Req, Db, View, Group, QueryArgs, Keys) -> #view_query_args{ limit = Limit, skip = Skip, group_level = GroupLevel } = QueryArgs, CurrentEtag = view_group_etag(Group, Db, Keys), couch_httpd:etag_respond(Req, CurrentEtag, fun() -> {ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel, QueryArgs, CurrentEtag, Group#group.current_seq, #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, RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} | make_key_options(QueryArgs#view_query_args{ start_key=Key, end_key=Key})]), % Switch to comma {Resp2, RedAcc2} end, {undefined, []}, Keys), % Start with no comma finish_reduce_fold(Req, Resp, [{update_seq,Group#group.current_seq}]) end). reverse_key_default(?MIN_STR) -> ?MAX_STR; reverse_key_default(?MAX_STR) -> ?MIN_STR; reverse_key_default(Key) -> Key. get_stale_type(Req) -> list_to_existing_atom(couch_httpd:qs_value(Req, "stale", "nil")). get_reduce_type(Req) -> list_to_existing_atom(couch_httpd:qs_value(Req, "reduce", "true")). load_view(Req, Db, {ViewDesignId, ViewName}, Keys) -> Stale = get_stale_type(Req), Reduce = get_reduce_type(Req), case couch_view:get_map_view(Db, ViewDesignId, ViewName, Stale) of {ok, View, Group} -> QueryArgs = parse_view_params(Req, Keys, map), {map, View, Group, QueryArgs}; {not_found, _Reason} -> case couch_view:get_reduce_view(Db, ViewDesignId, ViewName, Stale) of {ok, ReduceView, Group} -> case Reduce of false -> QueryArgs = parse_view_params(Req, Keys, map_red), MapView = couch_view:extract_map_view(ReduceView), {map, MapView, Group, QueryArgs}; _ -> QueryArgs = parse_view_params(Req, Keys, reduce), {reduce, ReduceView, Group, QueryArgs} end; {not_found, Reason} -> throw({not_found, Reason}) end end. % query_parse_error could be removed % we wouldn't need to pass the view type, it'd just parse params. % I'm not sure what to do about the error handling, but % it might simplify things to have a parse_view_params function % that doesn't throw(). 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 = (Keys =/= nil), 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. warn_on_empty_key_range(QueryArgs), GroupLevel = QueryArgs#view_query_args.group_level, case {ViewType, GroupLevel, IsMultiGet} of {reduce, exact, true} -> QueryArgs; {reduce, _, false} -> QueryArgs; {reduce, _, _} -> % we can simplify code if we just drop this error message. 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", "update_after") -> [{stale, update_after}]; parse_view_param("stale", _Value) -> throw({query_parse_error, <<"stale only available as stale=ok or as stale=update_after">>}); 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}}]. warn_on_empty_key_range(#view_query_args{start_key=undefined}) -> ok; warn_on_empty_key_range(#view_query_args{end_key=undefined}) -> ok; warn_on_empty_key_range(#view_query_args{start_key=A, end_key=A}) -> ok; warn_on_empty_key_range(#view_query_args{ start_key=StartKey, end_key=EndKey, direction=Dir}) -> ?LOG_ERROR("view_query_args ~p", [{StartKey, EndKey, Dir}]), case {Dir, couch_view:less_json(StartKey, EndKey)} of {fwd, false} -> throw({query_parse_error, <<"No rows can match your key range, reverse your startkey and endkey or set descending=true">>}); {rev, true} -> throw({query_parse_error, <<"No rows can match your key range, reverse your startkey and endkey or set descending=false">>}); _ -> ok end. validate_view_query(start_key, Value, Args) -> case Args#view_query_args.multi_get of true -> Msg = <<"Query parameter `start_key` is " "not compatible 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 parameter `end_key` is " "not compatible 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_docid = reverse_key_default(Args#view_query_args.start_docid), 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 parameter `include_docs` " "is invalid for reduce views.">>, throw({query_parse_error, Msg}); _ -> Args#view_query_args{include_docs=true} end; % Use the view_query_args record's default value validate_view_query(include_docs, _Value, Args) -> Args; validate_view_query(extra, _Value, Args) -> Args. make_view_fold_fun(Req, QueryArgs, Etag, Db, UpdateSeq, TotalViewCount, HelperFuns) -> #view_fold_helper_funs{ start_response = StartRespFun, send_row = SendRowFun, reduce_count = ReduceCountFun } = apply_default_helper_funs(HelperFuns), #view_query_args{ include_docs = IncludeDocs } = QueryArgs, fun({{Key, DocId}, Value}, OffsetReds, {AccLimit, AccSkip, Resp, RowFunAcc}) -> case {AccLimit, AccSkip, Resp} of {0, _, _} -> % we've done "limit" rows, stop foldling {stop, {0, 0, Resp, RowFunAcc}}; {_, AccSkip, _} when AccSkip > 0 -> % just keep skipping {ok, {AccLimit, AccSkip - 1, Resp, RowFunAcc}}; {_, _, undefined} -> % rendering the first row, first we start the response Offset = ReduceCountFun(OffsetReds), {ok, Resp2, RowFunAcc0} = StartRespFun(Req, Etag, TotalViewCount, Offset, RowFunAcc, UpdateSeq), {Go, RowFunAcc2} = SendRowFun(Resp2, Db, {{Key, DocId}, Value}, IncludeDocs, RowFunAcc0), {Go, {AccLimit - 1, 0, Resp2, RowFunAcc2}}; {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}} end end. make_reduce_fold_funs(Req, GroupLevel, _QueryArgs, Etag, UpdateSeq, 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, UpdateSeq), {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, UpdateSeq), {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, UpdateSeq), {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{ start_response = StartResp, send_row = SendRow }=Helpers) -> StartResp2 = case StartResp of undefined -> fun json_view_start_resp/6; _ -> StartResp end, SendRow2 = case SendRow of undefined -> fun send_json_view_row/5; _ -> SendRow end, Helpers#view_fold_helper_funs{ 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/4; _ -> 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_key_options(#view_query_args{direction = Dir}=QueryArgs) -> [{dir,Dir} | make_start_key_option(QueryArgs) ++ make_end_key_option(QueryArgs)]. make_start_key_option( #view_query_args{ start_key = StartKey, start_docid = StartDocId}) -> if StartKey == undefined -> []; true -> [{start_key, {StartKey, StartDocId}}] end. make_end_key_option(#view_query_args{end_key = undefined}) -> []; make_end_key_option( #view_query_args{end_key = EndKey, end_docid = EndDocId, inclusive_end = true}) -> [{end_key, {EndKey, EndDocId}}]; make_end_key_option( #view_query_args{ end_key = EndKey, end_docid = EndDocId, inclusive_end = false}) -> [{end_key_gt, {EndKey,reverse_key_default(EndDocId)}}]. json_view_start_resp(Req, Etag, TotalViewCount, Offset, _Acc, UpdateSeq) -> {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]), BeginBody = case couch_httpd:qs_value(Req, "update_seq") of "true" -> io_lib:format( "{\"total_rows\":~w,\"update_seq\":~w," "\"offset\":~w,\"rows\":[\r\n", [TotalViewCount, UpdateSeq, Offset]); _Else -> io_lib:format( "{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n", [TotalViewCount, Offset]) end, {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, UpdateSeq) -> {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]), case couch_httpd:qs_value(Req, "update_seq") of "true" -> {ok, Resp, io_lib:format("{\"update_seq\":~w,\"rows\":[\r\n",[UpdateSeq])}; _Else -> {ok, Resp, "{\"rows\":[\r\n"} end. 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 couch_util:get_value(<<"_rev">>, Props) of undefined -> nil; Rev0 -> couch_doc:parse_rev(Rev0) end, IncludeId = couch_util:get_value(<<"_id">>, Props, DocId), view_row_with_doc(Db, {{Key, DocId}, {Props}}, {IncludeId, Rev}); view_row_obj(Db, {{Key, DocId}, Value}, true) -> view_row_with_doc(Db, {{Key, DocId}, Value}, {DocId, 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}, IdRev) -> {[{id, DocId}, {key, Key}, {value, Value}] ++ doc_member(Db, IdRev)}. doc_member(Db, {DocId, Rev}) -> ?LOG_DEBUG("Include Doc: ~p ~p", [DocId, Rev]), case (catch couch_httpd_db:couch_doc_open(Db, DocId, Rev, [])) of #doc{} = Doc -> JsonDoc = couch_doc:to_json_obj(Doc, []), [{doc, JsonDoc}]; _Else -> [{doc, null}] end. finish_view_fold(Req, TotalRows, Offset, FoldResult) -> finish_view_fold(Req, TotalRows, Offset, FoldResult, []). finish_view_fold(Req, TotalRows, Offset, FoldResult, Fields) -> case FoldResult of {_, _, undefined, _} -> % nothing found in the view or keys, nothing has been returned % send empty view send_json(Req, 200, {[ {total_rows, TotalRows}, {offset, Offset}, {rows, []} ] ++ Fields}); {_, _, Resp, _} -> % end the view send_chunk(Resp, "\r\n]}"), end_json_response(Resp) end. finish_reduce_fold(Req, Resp) -> finish_reduce_fold(Req, Resp, []). finish_reduce_fold(Req, Resp, Fields) -> case Resp of undefined -> send_json(Req, 200, {[ {rows, []} ] ++ Fields}); Resp -> send_chunk(Resp, "\r\n]}"), end_json_response(Resp) end. parse_bool_param(Val) -> case string:to_lower(Val) of "true" -> true; "false" -> false; _ -> Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]), throw({query_parse_error, ?l2b(Msg)}) end. 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.