Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/couch/src/couch_query_servers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -477,18 +477,18 @@ builtin_cmp_last(A, B) ->
validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
JsonDiskDoc = json_doc(DiskDoc),
Resp = ddoc_prompt(
Db,
DDoc,
[<<"validate_doc_update">>],
[JsonEditDoc, JsonDiskDoc, Ctx, SecObj]
),
if
Resp == 1 -> ok;
true -> couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1)
end,
Args = [JsonEditDoc, JsonDiskDoc, Ctx, SecObj],

Resp =
case ddoc_prompt(Db, DDoc, [<<"validate_doc_update">>], Args) of
Code when Code =:= 1; Code =:= ok; Code =:= true ->
ok;
Other ->
couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1),
Other
end,
case Resp of
RespCode when RespCode =:= 1; RespCode =:= ok; RespCode =:= true ->
ok ->
ok;
{[{<<"forbidden">>, Message}]} ->
throw({forbidden, Message});
Expand Down
2 changes: 1 addition & 1 deletion src/couch_mrview/src/couch_mrview.erl
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ validate_ddoc_fields(DDoc) ->
[{<<"rewrites">>, [string, array]}],
[{<<"shows">>, object}, {any, [object, string]}],
[{<<"updates">>, object}, {any, [object, string]}],
[{<<"validate_doc_update">>, string}],
[{<<"validate_doc_update">>, [string, object]}],
[{<<"views">>, object}, {<<"lib">>, object}],
[{<<"views">>, object}, {any, object}, {<<"map">>, MapFuncType}],
[{<<"views">>, object}, {any, object}, {<<"reduce">>, string}]
Expand Down
5 changes: 3 additions & 2 deletions src/docs/src/api/ddoc/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@
* **rewrites** (*array* or *string*): Rewrite rules definition. *Deprecated.*
* **shows** (*object*): :ref:`Show functions <showfun>` definition. *Deprecated.*
* **updates** (*object*): :ref:`Update functions <updatefun>` definition
* **validate_doc_update** (*string*): :ref:`Validate document update
<vdufun>` function source
* **validate_doc_update** (*string* or *object*): :ref:`Validate document
update <vdufun>` JavaScript function source, or :ref:`Mango selector
<find/selectors>`
* **views** (*object*): :ref:`View functions <viewfun>` definition.
* **autoupdate** (*boolean*): Indicates whether to automatically build
indexes defined in this design document. Default is ``true``.
Expand Down
75 changes: 75 additions & 0 deletions src/docs/src/ddocs/ddocs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -937,3 +937,78 @@ modified by a user with the ``_admin`` role:
CouchDB Guide:
- `Validation Functions
<http://guide.couchdb.org/editions/1/en/validation.html>`_

Validation using Mango selectors
--------------------------------

The ``validate_doc_update`` field may be written as a :ref:`Mango selector
<find/selectors>`, instead of as a JavaScript function. This provides greater
performance since documents do not need to be sent to an external process for
validation, but is more restrictive in terms of what kinds of validation rules
can be expressed. Mango selectors can express declarative rules about the
strucure of the existing document stored on disk, and the new version of the
document; the document must match the given selector in order for the update to
be accepted.

To use Mango selectors for validation, the design document must have the
``language`` field set to ``query``. The selector is applied to a JSON structure
containing the following fields:

* ``newDoc``: New version of document that will be stored.
* ``oldDoc``: Previous version of document that is already stored.

For example, to check that all docs contain a ``title`` which is a string, and a
``year`` which is a number:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"newDoc": {
"title": { "$type": "string" },
"year": { "$type": "number" }
}
}
}

All the features of Mango selectors are supported here, so any condition that
can be expressed as a selector can be implemented in this way. Operators like
``$lt`` and ``$gt`` can be used to restrict values to a given range,
``$allMatch`` can be used to check all the items in an array match some schema,
and it is even possible to implement conditional checks using logical
combinators.

For example, say we want documents with ``"type": "movie"`` to have a ``title``
and ``year`` as above, and documents with ``"type": "actor"`` to have a ``name``
and a non-empty list of strings under ``movies``. This can be achieved using
this design document:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"newDoc": {
"type": { "$in": ["movie", "actor"] },
"$or": [
{
"type": "movie",
"title": { "$type": "string" },
"year": { "$type": "number" }
},
{
"type": "actor",
"name": { "$type": "string" },
"movies": {
"$type": "array",
"$not": { "$size": 0 },
"$allMatch": { "$type": "string" }
}
}
}
}
}
}
27 changes: 27 additions & 0 deletions src/mango/src/mango_native_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

-record(st, {
indexes = [],
validators = [],
timeout = 5000
}).

Expand Down Expand Up @@ -94,6 +95,32 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
Else
end,
{reply, Vals, St};
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
NewSt =
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
undefined ->
St;
Selector0 ->
Selector = mango_selector:normalize(Selector0),
Validators = couch_util:set_value(DDocId, St#st.validators, Selector),
St#st{validators = Validators}
end,
{reply, true, NewSt};
handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _From, St) ->
case couch_util:get_value(DDocId, St#st.validators) of
undefined ->
Msg = [<<"validate_doc_update">>, DDocId],
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
Selector ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
Reply =
case mango_selector:match(Selector, Struct) of
true -> true;
_ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
end,
{reply, Reply, St}
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

Expand Down
12 changes: 12 additions & 0 deletions test/elixir/test/config/suite.elixir
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,18 @@
"serial execution is not spuriously counted as loop on test_rewrite_suite_db",
"serial execution is not spuriously counted as loop on test_rewrite_suite_db%2Fwith_slashes"
],
"ValidateDocUpdateTest": [
"JavaScript VDU accepts a valid document",
"JavaScript VDU rejects an invalid document",
"JavaScript VDU accepts a valid change",
"JavaScript VDU rejects an invalid change",
"Mango VDU accepts a valid document",
"Mango VDU rejects an invalid document",
"updating a Mango VDU updates its effects",
"converting a Mango VDU to JavaScript updates its effects",
"deleting a Mango VDU removes its effects",
"Mango VDU rejects a doc if any existing ddoc fails to match",
],
"SecurityValidationTest": [
"Author presence and user security",
"Author presence and user security when replicated",
Expand Down
Loading