blob: e0001da6727317aa9a6c43959440b9a6ce0f0f13 [file] [log] [blame]
% 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(chttpd_view).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_views/include/couch_views.hrl").
-export([
handle_view_req/3,
validate_args/2,
parse_queries/4,
view_cb/2
]).
-define(DEFAULT_ALL_DOCS_PAGE_SIZE, 2000).
-define(DEFAULT_VIEWS_PAGE_SIZE, 2000).
multi_query_view(Req, Db, DDoc, ViewName, Queries) ->
Args = couch_views_http:parse_params(Req, undefined),
case couch_views_util:is_paginated(Args) of
false ->
stream_multi_query_view(Req, Db, DDoc, ViewName, Args, Queries);
true ->
paginate_multi_query_view(Req, Db, DDoc, ViewName, Args, Queries)
end.
stream_multi_query_view(Req, Db, DDoc, ViewName, Args0, Queries) ->
{ok, #mrst{views=Views}} = couch_views_util:ddoc_to_mrst(Db, DDoc),
Args1 = couch_views_util:set_view_type(Args0, ViewName, Views),
ArgQueries = parse_queries(Req, Args1, Queries, fun(QueryArg) ->
couch_views_util:set_view_type(QueryArg, ViewName, Views)
end),
VAcc0 = #vacc{db=Db, req=Req, prepend="\r\n"},
FirstChunk = "{\"results\":[",
{ok, Resp0} = chttpd:start_delayed_json_response(VAcc0#vacc.req, 200, [], FirstChunk),
VAcc1 = VAcc0#vacc{resp=Resp0},
VAcc2 = lists:foldl(fun(Args, Acc0) ->
Fun = fun view_cb/2,
{ok, Acc1} = couch_views:query(Db, DDoc, ViewName, Fun, Acc0, Args),
Acc1
end, VAcc1, ArgQueries),
{ok, Resp1} = chttpd:send_delayed_chunk(VAcc2#vacc.resp, "\r\n]}"),
chttpd:end_delayed_json_response(Resp1).
paginate_multi_query_view(Req, Db, DDoc, ViewName, Args0, Queries) ->
{ok, #mrst{views=Views}} = couch_views_util:ddoc_to_mrst(Db, DDoc),
ArgQueries = parse_queries(Req, Args0, Queries, fun(QueryArg) ->
couch_views_util:set_view_type(QueryArg, ViewName, Views)
end),
KeyFun = fun({Props}) ->
{couch_util:get_value(id, Props), couch_util:get_value(key, Props)}
end,
#mrargs{page_size = PageSize} = Args0,
#httpd{path_parts = Parts} = Req,
UpdateSeq = fabric2_db:get_update_seq(Db),
EtagTerm = {Parts, UpdateSeq, Args0},
Response = couch_views_http:paginated(
Req, EtagTerm, PageSize, ArgQueries, KeyFun,
fun(Args) ->
{ok, #vacc{meta=MetaMap, buffer=Items}} = couch_views:query(
Db, DDoc, ViewName, fun view_cb/2, #vacc{paginated=true}, Args),
{MetaMap, Items}
end),
chttpd:send_json(Req, Response).
design_doc_post_view(Req, Props, Db, DDoc, ViewName, Keys) ->
Args = couch_views_http_util:parse_body_and_query(Req, Props, Keys),
fabric_query_view(Db, Req, DDoc, ViewName, Args).
design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
Args = couch_views_http:parse_params(Req, Keys),
fabric_query_view(Db, Req, DDoc, ViewName, Args).
fabric_query_view(Db, Req, DDoc, ViewName, Args) ->
case couch_views_util:is_paginated(Args) of
false ->
stream_fabric_query_view(Db, Req, DDoc, ViewName, Args);
true ->
paginate_fabric_query_view(Db, Req, DDoc, ViewName, Args)
end.
stream_fabric_query_view(Db, Req, DDoc, ViewName, Args) ->
Max = chttpd:chunked_response_buffer_size(),
Fun = fun view_cb/2,
VAcc = #vacc{db=Db, req=Req, threshold=Max},
{ok, Resp} = couch_views:query(Db, DDoc, ViewName, Fun, VAcc, Args),
{ok, Resp#vacc.resp}.
paginate_fabric_query_view(Db, Req, DDoc, ViewName, Args0) ->
KeyFun = fun({Props}) ->
{couch_util:get_value(id, Props), couch_util:get_value(key, Props)}
end,
#httpd{path_parts = Parts} = Req,
UpdateSeq = fabric2_db:get_update_seq(Db),
ETagTerm = {Parts, UpdateSeq, Args0},
Response = couch_views_http:paginated(
Req, ETagTerm, Args0, KeyFun,
fun(Args) ->
VAcc0 = #vacc{paginated=true},
{ok, VAcc1} = couch_views:query(Db, DDoc, ViewName, fun view_cb/2, VAcc0, Args),
#vacc{meta=Meta, buffer=Items} = VAcc1,
{Meta, Items}
end),
chttpd:send_json(Req, Response).
view_cb({row, Row} = Msg, Acc) ->
case lists:keymember(doc, 1, Row) of
true -> chttpd_stats:incr_reads();
false -> ok
end,
chttpd_stats:incr_rows(),
couch_views_http:view_cb(Msg, Acc);
view_cb(Msg, Acc) ->
couch_views_http:view_cb(Msg, Acc).
handle_view_req(#httpd{method='POST',
path_parts=[_, _, _, _, ViewName, <<"queries">>]}=Req, Db, DDoc) ->
chttpd:validate_ctype(Req, "application/json"),
Props = couch_httpd:json_body_obj(Req),
case couch_views_util:get_view_queries(Props) of
undefined ->
throw({bad_request,
<<"POST body must include `queries` parameter.">>});
Queries ->
multi_query_view(Req, Db, DDoc, ViewName, Queries)
end;
handle_view_req(#httpd{path_parts=[_, _, _, _, _, <<"queries">>]}=Req,
_Db, _DDoc) ->
chttpd:send_method_not_allowed(Req, "POST");
handle_view_req(#httpd{method='GET',
path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) ->
couch_stats:increment_counter([couchdb, httpd, view_reads]),
Keys = chttpd:qs_json_value(Req, "keys", undefined),
design_doc_view(Req, Db, DDoc, ViewName, Keys);
handle_view_req(#httpd{method='POST',
path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) ->
chttpd:validate_ctype(Req, "application/json"),
Props = couch_httpd:json_body_obj(Req),
assert_no_queries_param(couch_views_util:get_view_queries(Props)),
Keys = couch_views_util:get_view_keys(Props),
couch_stats:increment_counter([couchdb, httpd, view_reads]),
design_doc_post_view(Req, Props, Db, DDoc, ViewName, Keys);
handle_view_req(Req, _Db, _DDoc) ->
chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").
% See https://github.com/apache/couchdb/issues/2168
assert_no_queries_param(undefined) ->
ok;
assert_no_queries_param(_) ->
throw({
bad_request,
"The `queries` parameter is no longer supported at this endpoint"
}).
validate_args(Req, #mrargs{page_size = PageSize} = Args) when is_integer(PageSize) ->
MaxPageSize = max_page_size(Req),
couch_views_util:validate_args(Args, [{page_size, MaxPageSize}]);
validate_args(_Req, #mrargs{} = Args) ->
couch_views_util:validate_args(Args, []).
max_page_size(#httpd{path_parts=[_Db, <<"_all_docs">>, <<"queries">>]}) ->
config:get_integer(
"request_limits", "_all_docs/queries", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[_Db, <<"_all_docs">>]}) ->
config:get_integer(
"request_limits", "_all_docs", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[_Db, <<"_local_docs">>, <<"queries">>]}) ->
config:get_integer(
"request_limits", "_all_docs/queries", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[_Db, <<"_local_docs">>]}) ->
config:get_integer(
"request_limits", "_all_docs", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[_Db, <<"_design_docs">>, <<"queries">>]}) ->
config:get_integer(
"request_limits", "_all_docs/queries", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[_Db, <<"_design_docs">>]}) ->
config:get_integer(
"request_limits", "_all_docs", ?DEFAULT_ALL_DOCS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[
_Db, <<"_design">>, _DDocName, <<"_view">>, _View, <<"queries">>]}) ->
config:get_integer(
"request_limits", "_view/queries", ?DEFAULT_VIEWS_PAGE_SIZE);
max_page_size(#httpd{path_parts=[
_Db, <<"_design">>, _DDocName, <<"_view">>, _View]}) ->
config:get_integer(
"request_limits", "_view", ?DEFAULT_VIEWS_PAGE_SIZE).
parse_queries(Req, #mrargs{page_size = PageSize} = Args0, Queries, Fun)
when is_integer(PageSize) ->
MaxPageSize = max_page_size(Req),
if length(Queries) < PageSize -> ok; true ->
throw({
query_parse_error,
<<"Provided number of queries is more than given page_size">>
})
end,
couch_views_util:validate_args(Fun(Args0), [{page_size, MaxPageSize}]),
Args = Args0#mrargs{page_size = undefined},
lists:map(fun({Query}) ->
Args1 = couch_views_http:parse_params(Query, undefined, Args, [decoded]),
if not is_integer(Args1#mrargs.page_size) -> ok; true ->
throw({
query_parse_error,
<<"You cannot specify `page_size` inside the query">>
})
end,
Args2 = maybe_set_page_size(Args1, MaxPageSize),
couch_views_util:validate_args(Fun(Args2), [{page_size, MaxPageSize}])
end, Queries);
parse_queries(_Req, #mrargs{} = Args, Queries, Fun) ->
lists:map(fun({Query}) ->
Args1 = couch_views_http:parse_params(Query, undefined, Args, [decoded]),
couch_views_util:validate_args(Fun(Args1))
end, Queries).
maybe_set_page_size(#mrargs{page_size = undefined} = Args, MaxPageSize) ->
Args#mrargs{page_size = MaxPageSize};
maybe_set_page_size(#mrargs{} = Args, _MaxPageSize) ->
Args.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
check_multi_query_reduce_view_overrides_test_() ->
{
setup,
fun setup_all/0,
fun teardown_all/1,
{
foreach,
fun setup/0,
fun teardown/1,
[
t_check_include_docs_throw_validation_error(),
t_check_user_can_override_individual_query_type()
]
}
}.
t_check_include_docs_throw_validation_error() ->
?_test(begin
Req = #httpd{qs = []},
Db = #{name => <<"foo">>},
Query = {[{<<"include_docs">>, true}]},
Throw = {query_parse_error, <<"`include_docs` is invalid for reduce">>},
?assertThrow(Throw, multi_query_view(Req, Db, ddoc, <<"v">>, [Query]))
end).
t_check_user_can_override_individual_query_type() ->
?_test(begin
Req = #httpd{qs = []},
Db = #{name => <<"foo">>},
Query = {[{<<"include_docs">>, true}, {<<"reduce">>, false}]},
multi_query_view(Req, Db, ddoc, <<"v">>, [Query]),
?assertEqual(1, meck:num_calls(chttpd, start_delayed_json_response, '_'))
end).
setup_all() ->
Views = [#mrview{reduce_funs = [{<<"v">>, <<"_count">>}]}],
meck:expect(couch_views_util, ddoc_to_mrst, 2, {ok, #mrst{views = Views}}),
meck:expect(chttpd, start_delayed_json_response, 4, {ok, resp}),
meck:expect(couch_views, query, 6, {ok, #vacc{}}),
meck:expect(chttpd, send_delayed_chunk, 2, {ok, resp}),
meck:expect(chttpd, end_delayed_json_response, 1, ok).
teardown_all(_) ->
meck:unload().
setup() ->
meck:reset([
chttpd,
couch_views,
couch_views_util
]).
teardown(_) ->
ok.
-endif.