Skip to content

Commit cd883a2

Browse files
committed
feat: Add the ability for VDUs to be written as Mango selectors
1 parent ede5be2 commit cd883a2

4 files changed

Lines changed: 167 additions & 1 deletion

File tree

src/couch_mrview/src/couch_mrview.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ validate_ddoc_fields(DDoc) ->
6262
[{<<"rewrites">>, [string, array]}],
6363
[{<<"shows">>, object}, {any, [object, string]}],
6464
[{<<"updates">>, object}, {any, [object, string]}],
65-
[{<<"validate_doc_update">>, string}],
65+
[{<<"validate_doc_update">>, [string, object]}],
6666
[{<<"views">>, object}, {<<"lib">>, object}],
6767
[{<<"views">>, object}, {any, object}, {<<"map">>, MapFuncType}],
6868
[{<<"views">>, object}, {any, object}, {<<"reduce">>, string}]

src/mango/src/mango_native_proc.erl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
-record(st, {
3131
indexes = [],
32+
validators = [],
3233
timeout = 5000
3334
}).
3435

@@ -94,6 +95,32 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
9495
Else
9596
end,
9697
{reply, Vals, St};
98+
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
99+
NewSt =
100+
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
101+
undefined ->
102+
St;
103+
Selector0 ->
104+
Selector = mango_selector:normalize(Selector0),
105+
Validators = couch_util:set_value(DDocId, St#st.validators, Selector),
106+
St#st{validators = Validators}
107+
end,
108+
{reply, true, NewSt};
109+
handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _From, St) ->
110+
case couch_util:get_value(DDocId, St#st.validators) of
111+
undefined ->
112+
Msg = [<<"validate_doc_update">>, DDocId],
113+
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
114+
Selector ->
115+
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
116+
Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
117+
Reply =
118+
case mango_selector:match(Selector, Struct) of
119+
true -> true;
120+
_ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
121+
end,
122+
{reply, Reply, St}
123+
end;
97124
handle_call(Msg, _From, St) ->
98125
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
99126

test/elixir/test/config/suite.elixir

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@
518518
"JavaScript VDU rejects an invalid document",
519519
"JavaScript VDU accepts a valid change",
520520
"JavaScript VDU rejects an invalid change",
521+
"Mango VDU accepts a valid document",
522+
"Mango VDU rejects an invalid document",
523+
"updating a Mango VDU updates its effects",
524+
"converting a Mango VDU to JavaScript updates its effects",
525+
"deleting a Mango VDU removes its effects",
526+
"Mango VDU rejects a doc if any existing ddoc fails to match",
521527
],
522528
"SecurityValidationTest": [
523529
"Author presence and user security",

test/elixir/test/validate_doc_update_test.exs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,137 @@ defmodule ValidateDocUpdateTest do
7676

7777
assert resp.status_code == 403
7878
end
79+
80+
@mango_type_check %{
81+
language: "query",
82+
83+
validate_doc_update: %{
84+
"newDoc" => %{"type" => %{"$exists" => true}}
85+
}
86+
}
87+
88+
@tag :with_db
89+
test "Mango VDU accepts a valid document", context do
90+
db = context[:db_name]
91+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
92+
assert resp.status_code == 201
93+
94+
resp = Couch.put("/#{db}/doc", body: %{"type" => "movie"})
95+
assert resp.status_code == 201
96+
assert resp.body["ok"] == true
97+
end
98+
99+
@tag :with_db
100+
test "Mango VDU rejects an invalid document", context do
101+
db = context[:db_name]
102+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
103+
assert resp.status_code == 201
104+
105+
resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
106+
assert resp.status_code == 403
107+
assert resp.body["error"] == "forbidden"
108+
end
109+
110+
@tag :with_db
111+
test "updating a Mango VDU updates its effects", context do
112+
db = context[:db_name]
113+
114+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
115+
assert resp.status_code == 201
116+
117+
ddoc = %{
118+
language: "query",
119+
120+
validate_doc_update: %{
121+
"newDoc" => %{
122+
"type" => %{"$type" => "string"},
123+
"year" => %{"$lt" => 2026}
124+
}
125+
}
126+
}
127+
resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]})
128+
assert resp.status_code == 201
129+
130+
resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994})
131+
assert resp.status_code == 201
132+
133+
resp = Couch.put("/#{db}/doc2", body: %{"type" => 42, "year" => 1994})
134+
assert resp.status_code == 403
135+
assert resp.body["error"] == "forbidden"
136+
137+
resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094})
138+
assert resp.status_code == 403
139+
assert resp.body["error"] == "forbidden"
140+
end
141+
142+
@tag :with_db
143+
test "converting a Mango VDU to JavaScript updates its effects", context do
144+
db = context[:db_name]
145+
146+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
147+
assert resp.status_code == 201
148+
149+
ddoc = %{
150+
language: "javascript",
151+
152+
validate_doc_update: ~s"""
153+
function (newDoc) {
154+
if (typeof newDoc.year !== 'number') {
155+
throw {forbidden: 'Documents must have a valid year field'};
156+
}
157+
}
158+
"""
159+
}
160+
resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]})
161+
assert resp.status_code == 201
162+
163+
resp = Couch.put("/#{db}/doc1", body: %{"year" => 1994})
164+
assert resp.status_code == 201
165+
166+
resp = Couch.put("/#{db}/doc2", body: %{"year" => "1994"})
167+
assert resp.status_code == 403
168+
assert resp.body["error"] == "forbidden"
169+
end
170+
171+
@tag :with_db
172+
test "deleting a Mango VDU removes its effects", context do
173+
db = context[:db_name]
174+
175+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
176+
assert resp.status_code == 201
177+
178+
resp = Couch.delete("/#{db}/_design/mango-test", query: %{rev: resp.body["rev"]})
179+
assert resp.status_code == 200
180+
181+
resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
182+
assert resp.status_code == 201
183+
end
184+
185+
@tag :with_db
186+
test "Mango VDU rejects a doc if any existing ddoc fails to match", context do
187+
db = context[:db_name]
188+
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
189+
assert resp.status_code == 201
190+
191+
ddoc = %{
192+
language: "query",
193+
194+
validate_doc_update: %{
195+
"newDoc" => %{"year" => %{"$lt" => 2026}}
196+
}
197+
}
198+
resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
199+
assert resp.status_code == 201
200+
201+
resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994})
202+
assert resp.status_code == 201
203+
204+
resp = Couch.put("/#{db}/doc2", body: %{"year" => 1994})
205+
assert resp.status_code == 403
206+
assert resp.body["error"] == "forbidden"
207+
208+
resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094})
209+
assert resp.status_code == 403
210+
assert resp.body["error"] == "forbidden"
211+
end
79212
end

0 commit comments

Comments
 (0)