summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_httpd_db.erl
diff options
context:
space:
mode:
authorDamien F. Katz <damien@apache.org>2008-10-02 16:14:45 +0000
committerDamien F. Katz <damien@apache.org>2008-10-02 16:14:45 +0000
commiteb5264f55d5b594d7082918e9e94b64bf75d25a1 (patch)
treeb21d6d06dffcaabcd777c8859c33168c4aa362d6 /src/couchdb/couch_httpd_db.erl
parent82d31aa2671ac3ffc7b1dbf4c6c9b6c38f0d9f2e (diff)
Added files forgotten in the httpd refactoring checkin.
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@701174 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/couchdb/couch_httpd_db.erl')
-rw-r--r--src/couchdb/couch_httpd_db.erl577
1 files changed, 577 insertions, 0 deletions
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
new file mode 100644
index 00000000..ff3d8125
--- /dev/null
+++ b/src/couchdb/couch_httpd_db.erl
@@ -0,0 +1,577 @@
+% 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_db).
+-include("couch_db.hrl").
+
+-export([handle_request/1, db_req/2]).
+
+-import(couch_httpd,
+ [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+ start_json_response/2,send_chunk/2,end_json_response/1,
+ start_chunked_response/3]).
+
+-record(doc_query_args, {
+ options = [],
+ rev = "",
+ open_revs = ""
+}).
+
+% Database request handlers
+handle_request(#httpd{path_parts=[DbName|RestParts],method=Method,
+ db_url_handlers=DbUrlHandlers}=Req)->
+ case {Method, RestParts} of
+ {'PUT', []} ->
+ create_db_req(Req, DbName);
+ {'DELETE', []} ->
+ delete_db_req(Req, DbName);
+ {_, []} ->
+ do_db_req(Req, fun db_req/2);
+ {_, [SecondPart|_]} ->
+ Handler = couch_util:dict_find(SecondPart, DbUrlHandlers, fun db_req/2),
+ do_db_req(Req, Handler)
+ end.
+
+create_db_req(Req, DbName) ->
+ case couch_server:create(DbName, []) of
+ {ok, Db} ->
+ couch_db:close(Db),
+ send_json(Req, 201, {[{ok, true}]});
+ Error ->
+ throw(Error)
+ end.
+
+delete_db_req(Req, DbName) ->
+ case couch_server:delete(DbName) of
+ ok ->
+ send_json(Req, 200, {[{ok, true}]});
+ Error ->
+ throw(Error)
+ end.
+
+do_db_req(#httpd{path_parts=[DbName|_]}=Req, Fun) ->
+ case couch_db:open(DbName, []) of
+ {ok, Db} ->
+ try
+ Fun(Req, Db)
+ after
+ couch_db:close(Db)
+ end;
+ Error ->
+ throw(Error)
+ end.
+
+db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
+ {ok, DbInfo} = couch_db:get_db_info(Db),
+ send_json(Req, {DbInfo});
+
+db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
+ Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
+ DocId = couch_util:new_uuid(),
+ {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=[]}, []),
+ send_json(Req, 201, {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_req(#httpd{path_parts=[_DbName]}=Req, _Db) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
+ {JsonProps} = couch_httpd:json_body(Req),
+ DocsArray = proplists:get_value(<<"docs">>, JsonProps),
+ % convert all the doc elements to native docs
+ case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+ true ->
+ Docs = lists:map(
+ fun({ObjProps} = JsonObj) ->
+ Doc = couch_doc:from_json_obj(JsonObj),
+ Id = case Doc#doc.id of
+ <<>> -> couch_util:new_uuid();
+ Id0 -> Id0
+ end,
+ Revs = case proplists:get_value(<<"_rev">>, ObjProps) of
+ undefined -> [];
+ Rev -> [Rev]
+ end,
+ Doc#doc{id=Id,revs=Revs}
+ end,
+ DocsArray),
+ {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+ % output the results
+ DocResults = lists:zipwith(
+ fun(Doc, NewRev) ->
+ {[{"id", Doc#doc.id}, {"rev", NewRev}]}
+ end,
+ Docs, ResultRevs),
+ send_json(Req, 201, {[
+ {ok, true},
+ {new_revs, DocResults}
+ ]});
+
+ false ->
+ Options =
+ case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+ true -> [new_edits];
+ _ -> []
+ end,
+ Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray],
+ ok = couch_db:save_docs(Db, Docs, Options),
+ send_json(Req, 201, {[
+ {ok, true}
+ ]})
+ end;
+db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_compact">>]}=Req, Db) ->
+ ok = couch_db:start_compact(Db),
+ send_json(Req, 202, {[{ok, true}]});
+
+db_req(#httpd{path_parts=[_,<<"_compact">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
+ {IdsRevs} = couch_httpd:json_body(Req),
+ % validate the json input
+ [{_Id, [_|_]=_Revs} = IdRevs || IdRevs <- IdsRevs],
+
+ case couch_db:purge_docs(Db, IdsRevs) of
+ {ok, PurgeSeq, PurgedIdsRevs} ->
+ send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs}}]});
+ Error ->
+ throw(Error)
+ end;
+
+db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs">>]}=Req, Db) ->
+ #view_query_args{
+ start_key = StartKey,
+ start_docid = StartDocId,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir
+ } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+ {ok, Info} = couch_db:get_db_info(Db),
+ TotalRowCount = proplists:get_value(doc_count, Info),
+
+ StartId = if is_binary(StartKey) -> StartKey;
+ true -> StartDocId
+ end,
+
+ FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+ fun couch_db:enum_docs_reduce_to_count/1),
+ AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) ->
+ case couch_doc:to_doc_info(FullDocInfo) of
+ #doc_info{deleted=false, rev=Rev} ->
+ FoldlFun({{Id, Id}, {[{rev, Rev}]}}, Offset, Acc);
+ #doc_info{deleted=true} ->
+ {ok, Acc}
+ end
+ end,
+ {ok, FoldResult} = couch_db:enum_docs(Db, StartId, Dir, AdapterFun,
+ {Count, SkipCount, undefined, []}),
+ couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+db_req(#httpd{path_parts=[_,<<"_all_docs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "GET,HEAD");
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) ->
+ #view_query_args{
+ start_key = StartKey,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir
+ } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+
+ {ok, Info} = couch_db:get_db_info(Db),
+ TotalRowCount = proplists:get_value(doc_count, Info),
+
+ FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+ fun couch_db:enum_docs_since_reduce_to_count/1),
+ StartKey2 = case StartKey of
+ nil -> 0;
+ <<>> -> 100000000000;
+ StartKey when is_integer(StartKey) -> StartKey
+ end,
+ {ok, FoldResult} = couch_db:enum_docs_since(Db, StartKey2, Dir,
+ fun(DocInfo, Offset, Acc) ->
+ #doc_info{
+ id=Id,
+ rev=Rev,
+ update_seq=UpdateSeq,
+ deleted=Deleted,
+ conflict_revs=ConflictRevs,
+ deleted_conflict_revs=DelConflictRevs
+ } = DocInfo,
+ Json = {
+ [{"rev", Rev}] ++
+ case ConflictRevs of
+ [] -> [];
+ _ -> [{"conflicts", ConflictRevs}]
+ end ++
+ case DelConflictRevs of
+ [] -> [];
+ _ -> [{"deleted_conflicts", DelConflictRevs}]
+ end ++
+ case Deleted of
+ true -> [{"deleted", true}];
+ false -> []
+ end
+ },
+ FoldlFun({{UpdateSeq, Id}, Json}, Offset, Acc)
+ end, {Count, SkipCount, undefined, []}),
+ couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+db_req(#httpd{path_parts=[_,<<"_all_docs_by_seq">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "GET,HEAD");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->
+ {JsonDocIdRevs} = couch_httpd:json_body(Req),
+ {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs),
+ send_json(Req, {[
+ {missing_revs, {Results}}
+ ]});
+
+db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[DbName,<<"_design">>,Name|Rest]}=Req,
+ Db) ->
+ % Special case to enable using an unencoded in the URL of design docs, as
+ % slashes in document IDs must otherwise be URL encoded
+ db_req(Req#httpd{path_parts=[DbName,<<"_design/",Name/binary>>|Rest]}, Db);
+
+db_req(#httpd{path_parts=[_, DocId]}=Req, Db) ->
+ db_doc_req(Req, Db, DocId);
+
+db_req(#httpd{path_parts=[_, DocId, FileName]}=Req, Db) ->
+ db_attachment_req(Req, Db, DocId, FileName).
+
+
+
+db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev ->
+ couch_httpd:send_error(Req, 412, <<"missing_rev">>,
+ <<"Document rev/etag must be specified to delete">>);
+ RevToDelete ->
+ {ok, NewRev} = couch_db:delete_doc(Db, DocId, [RevToDelete]),
+ send_json(Req, 200, {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]})
+ end;
+
+db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
+ #doc_query_args{
+ 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),
+ EtagsToMatch = string:tokens(
+ couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+ case lists:member(DiskEtag, EtagsToMatch) of
+ true ->
+ % the client has this in their cache.
+ couch_httpd:send_response(Req, 304, [{"Etag", DiskEtag}], <<>>);
+ false ->
+ 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)
+ end,
+ "," % AccSeparator now has a comma
+ end,
+ "", Results),
+ send_chunk(Resp, "]"),
+ end_json_response(Resp)
+ end;
+
+db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
+ Form = couch_httpd:parse_form(Req),
+ Rev = list_to_binary(proplists:get_value("_rev", Form)),
+ Doc = case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]};
+ {ok, [Error]} -> throw(Error)
+ end,
+
+ NewAttachments = [
+ {list_to_binary(Name), {list_to_binary(ContentType), Content}} ||
+ {Name, {ContentType, _}, Content} <-
+ proplists:get_all_values("_attachments", Form)
+ ],
+ #doc{attachments=Attachments} = Doc,
+ NewDoc = Doc#doc{
+ attachments = Attachments ++ NewAttachments
+ },
+ {ok, NewRev} = couch_db:update_doc(Db, NewDoc, []),
+
+ send_json(Req, 201, [{"Etag", "\"" ++ NewRev ++ "\""}], {obj, [
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
+ Json = couch_httpd:json_body(Req),
+ Doc = couch_doc:from_json_obj(Json),
+ ExplicitRev =
+ case Doc#doc.revs of
+ [Rev0|_] -> Rev0;
+ [] -> undefined
+ end,
+ case extract_header_rev(Req, ExplicitRev) of
+ missing_rev ->
+ Revs = [];
+ Rev ->
+ Revs = [Rev]
+ end,
+ {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=Revs}, []),
+ send_json(Req, 201, [{"Etag", <<"\"", NewRev/binary, "\"">>}], {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
+ SourceRev =
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev -> [];
+ Rev -> Rev
+ end,
+
+ {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+
+ % open revision Rev or Current
+ Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+ % save new doc
+ {ok, NewTargetRev} = couch_db:update_doc(Db, Doc#doc{id=TargetDocId, revs=TargetRev}, []),
+
+ send_json(Req, 201, [{"Etag", "\"" ++ binary_to_list(NewTargetRev) ++ "\""}], {[
+ {ok, true},
+ {id, TargetDocId},
+ {rev, NewTargetRev}
+ ]});
+
+db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) ->
+ SourceRev =
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev ->
+ throw({bad_request, "MOVE requires a specified rev parameter"
+ "for the origin resource."});
+ Rev -> Rev
+ end,
+
+ {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+ % open revision Rev or Current
+ Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+ % save new doc & delete old doc in one operation
+ Docs = [
+ Doc#doc{id=TargetDocId, revs=TargetRev},
+ #doc{id=SourceDocId, revs=[SourceRev], deleted=true}
+ ],
+
+ {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+ DocResults = lists:zipwith(
+ fun(FDoc, NewRev) ->
+ {[{id, FDoc#doc.id}, {rev, NewRev}]}
+ end,
+ Docs, ResultRevs),
+ send_json(Req, 201, {[
+ {ok, true},
+ {new_revs, DocResults}
+ ]});
+
+db_doc_req(Req, _Db, _DocId) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY,MOVE").
+
+% Useful for debugging
+% couch_doc_open(Db, DocId) ->
+% couch_doc_open(Db, DocId, [], []).
+
+couch_doc_open(Db, DocId, Rev, Options) ->
+ case Rev of
+ "" -> % open most recent rev
+ case couch_db:open_doc(Db, DocId, Options) of
+ {ok, Doc} ->
+ Doc;
+ Error ->
+ throw(Error)
+ end;
+ _ -> % open a specific rev (deletions come back as stubs)
+ case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of
+ {ok, [{ok, Doc}]} ->
+ Doc;
+ {ok, [Else]} ->
+ throw(Else)
+ end
+ end.
+
+% Attachment request handlers
+
+db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileName) ->
+ case couch_db:open_doc(Db, DocId, []) of
+ {ok, #doc{attachments=Attachments}} ->
+ case proplists:get_value(FileName, Attachments) of
+ undefined ->
+ throw({not_found, "Document is missing attachment"});
+ {Type, Bin} ->
+ {ok, Resp} = start_chunked_response(Req, 200, [
+ {"Cache-Control", "must-revalidate"},
+ {"Content-Type", binary_to_list(Type)},
+ {"Content-Length", integer_to_list(couch_doc:bin_size(Bin))}]),
+ couch_doc:bin_foldl(Bin,
+ fun(BinSegment, []) ->
+ send_chunk(Resp, BinSegment),
+ {ok, []}
+ end,
+ []
+ ),
+ send_chunk(Resp, "")
+ end;
+ Error ->
+ throw(Error)
+ end;
+
+db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileName)
+ when (Method == 'PUT') or (Method == 'DELETE') ->
+
+ NewAttachment = case Method of
+ 'DELETE' ->
+ [];
+ _ ->
+ [{FileName, {
+ list_to_binary(couch_httpd:header_value(Req,"Content-Type")),
+ couch_httpd:body(Req)
+ }}]
+ end,
+
+ Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev -> % make the new doc
+ #doc{id=DocId};
+ Rev ->
+ case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]};
+ {ok, [Error]} -> throw(Error)
+ end
+ end,
+
+ #doc{attachments=Attachments} = Doc,
+ DocEdited = Doc#doc{
+ attachments = NewAttachment ++ proplists:delete(FileName, Attachments)
+ },
+ {ok, UpdatedRev} = couch_db:update_doc(Db, DocEdited, []),
+ send_json(Req, case Method of 'DELETE' -> 200; _ -> 201 end, {[
+ {ok, true},
+ {id, DocId},
+ {rev, UpdatedRev}
+ ]});
+
+db_attachment_req(Req, _Db, _DocId, _FileName) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT").
+
+
+parse_doc_query(Req) ->
+ lists:foldl(fun({Key,Value}, Args) ->
+ case {Key, Value} of
+ {"attachments", "true"} ->
+ Options = [attachments | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"meta", "true"} ->
+ Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"revs", "true"} ->
+ Options = [revs | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"revs_info", "true"} ->
+ Options = [revs_info | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"conflicts", "true"} ->
+ Options = [conflicts | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"deleted_conflicts", "true"} ->
+ Options = [deleted_conflicts | Args#doc_query_args.options],
+ Args#doc_query_args{options=Options};
+ {"rev", Rev} ->
+ Args#doc_query_args{rev=list_to_binary(Rev)};
+ {"open_revs", "all"} ->
+ Args#doc_query_args{open_revs=all};
+ {"open_revs", RevsJsonStr} ->
+ JsonArray = ?JSON_DECODE(RevsJsonStr),
+ Args#doc_query_args{open_revs=JsonArray};
+ _Else -> % unknown key value pair, ignore.
+ Args
+ end
+ end, #doc_query_args{}, couch_httpd:qs(Req)).
+
+
+
+extract_header_rev(Req, ExplictRev) when is_list(ExplictRev)->
+ extract_header_rev(Req, list_to_binary(ExplictRev));
+extract_header_rev(Req, ExplictRev) ->
+ Etag = case couch_httpd:header_value(Req, "If-Match") of
+ undefined -> undefined;
+ Tag -> string:strip(Tag, both, $")
+ end,
+ case {ExplictRev, Etag} of
+ {undefined, undefined} -> missing_rev;
+ {_, undefined} -> ExplictRev;
+ {undefined, _} -> list_to_binary(Etag);
+ _ when ExplictRev == Etag -> list_to_binary(Etag);
+ _ ->
+ throw({bad_request, "Document rev and etag have different values"})
+ end.
+
+
+parse_copy_destination_header(Req) ->
+ Destination = couch_httpd:header_value(Req, "Destination"),
+ case regexp:match(Destination, "\\?") of
+ nomatch ->
+ {list_to_binary(Destination), []};
+ {match, _, _} ->
+ {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"),
+ {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="),
+ {list_to_binary(DocId), [list_to_binary(Rev)]}
+ end.
+