3.1. 设计文档¶
在本节中,我们将展示如何使用内置的 JavaScript 查询服务器 来编写设计文档。
但在开始编写第一个文档之前,让我们先看一下在我们代码旅程中将使用到的常见对象列表 - 我们将在每个函数中广泛使用它们。
3.1.1. 创建和结构¶
设计文档包含视图函数和更新函数等函数。这些函数在请求时执行。
设计文档由格式为 _design/{name}
的 id 字段表示。它们的结构遵循以下示例。
示例:
{
"_id": "_design/example",
"views": {
"view-number-one": {
"map": "function (doc) {/* function code here - see below */}"
},
"view-number-two": {
"map": "function (doc) {/* function code here - see below */}",
"reduce": "function (keys, values, rereduce) {/* function code here - see below */}"
}
},
"updates": {
"updatefun1": "function(doc,req) {/* function code here - see below */}",
"updatefun2": "function(doc,req) {/* function code here - see below */}"
},
"filters": {
"filterfunction1": "function(doc, req){ /* function code here - see below */ }"
},
"validate_doc_update": "function(newDoc, oldDoc, userCtx, secObj) { /* function code here - see below */ }",
"language": "javascript"
}
如您所见,设计文档可以包含多种类型的多个函数。该示例定义了两个视图,它们都具有映射函数,其中一个具有归约函数。它还定义了两个更新函数和一个过滤器函数。验证文档更新函数是一个特殊情况,因为每个设计文档不能包含多个此类函数。
3.1.2. 视图函数¶
视图是用于查询和报告 CouchDB 数据库的主要工具。
3.1.2.1. 映射函数¶
- mapfun(doc)¶
- 参数:
doc – 正在处理的文档
映射函数接受单个文档作为参数,并(可选) emit()
存储在视图中的键值对。
function (doc) {
if (doc.type === 'post' && doc.tags && Array.isArray(doc.tags)) {
doc.tags.forEach(function (tag) {
emit(tag.toLowerCase(), 1);
});
}
}
在此示例中,对于每个具有 type 为“post”的文档的 tags 数组中的每个值,都会发出一个键值对。请注意,emit()
可以在单个文档中多次调用,因此同一个文档可以通过多个不同的键获得。
还要记住,每个文档都被密封,以防止一个映射函数更改文档状态而另一个映射函数接收修改后的版本的情况。
出于效率原因,文档被传递给一组映射函数 - 每个文档都由来自相关设计文档的所有视图的一组映射函数处理。这意味着,如果您触发了设计文档中一个视图的索引更新,那么所有其他视图也会被更新。
3.1.2.2. 归约和重新归约函数¶
- redfun(keys, values[, rereduce])¶
- 参数:
keys – 相关映射函数结果的键-docid 对数组。如果正在运行重新归约(具有
true
值),则始终为null
。values – 映射函数结果值数组。
rereduce – 布尔标志,指示重新归约运行。
- 返回值:
归约 values
归约函数接受两个必需的参数,即键和值列表 - 相关映射函数的结果 - 以及一个可选的第三个值,该值指示 rereduce 模式是否处于活动状态。 Rereduce 用于额外的归约值列表,因此当它为 true
时,没有关于相关 keys 的信息(第一个参数为 null
)。
请注意,如果归约函数的结果长于初始值列表,则会引发查询服务器错误。但是,可以通过将 reduce_limit
配置选项设置为 false
来禁用此行为。
[query_server_config]
reduce_limit = false
虽然禁用 reduce_limit
可能对调试有帮助,但请记住,归约函数的主要任务是归约映射结果,而不是使其更大。通常,您的归约函数应该快速收敛到单个值 - 它可以是数组或类似对象。
3.1.2.2.1. 内置归约函数¶
此外,CouchDB 有一组内置的归约函数。这些函数是用 Erlang 实现的,并在 CouchDB 内部运行,因此它们比等效的 JavaScript 函数快得多。
- _approx_count_distinct¶
版本 2.2 中的新增功能。
使用 HyperLogLog 算法的变体来近似视图索引中不同键的数量。该算法允许使用固定内存资源对基数进行高效、可并行化的计算。CouchDB 已将底层数据结构配置为具有约 2% 的相对误差。
由于此归约器完全忽略了发出的值,因此使用 group=true
的调用将简单地为视图中的每个不同键返回一个值为 1 的值。在数组键的情况下,使用指定的 group_level
查询视图将返回共享每个行中公共组前缀的不同键的数量。该算法还认识到 startkey
和 endkey
边界,并将返回指定键范围内的不同键的数量。
关于 Unicode 排序的最后说明:此归约函数直接使用索引中每个键的二进制表示作为 HyperLogLog 过滤器的输入。因此,它将(错误地)将根据 Unicode 排序规则比较相等但字节不相同的键视为不同键,因此如果存在大量此类键,则有可能高估键空间的基数。
- _count¶
计算索引中具有给定键的值的数量。这可以在 JavaScript 中实现为
// could be replaced by _count
function(keys, values, rereduce) {
if (rereduce) {
return sum(values);
} else {
return values.length;
}
}
- _stats¶
计算与每个键关联的数值的以下数量:sum
、min
、max
、count
和 sumsqr
。 _stats
函数的行为取决于映射函数的输出。最简单的情况是,当映射阶段为每个键发出单个数值时。在这种情况下,_stats
函数等效于以下 JavaScript
// could be replaced by _stats
function(keys, values, rereduce) {
if (rereduce) {
return {
'sum': values.reduce(function(a, b) { return a + b.sum }, 0),
'min': values.reduce(function(a, b) { return Math.min(a, b.min) }, Infinity),
'max': values.reduce(function(a, b) { return Math.max(a, b.max) }, -Infinity),
'count': values.reduce(function(a, b) { return a + b.count }, 0),
'sumsqr': values.reduce(function(a, b) { return a + b.sumsqr }, 0)
}
} else {
return {
'sum': sum(values),
'min': Math.min.apply(null, values),
'max': Math.max.apply(null, values),
'count': values.length,
'sumsqr': (function() {
var sumsqr = 0;
values.forEach(function (value) {
sumsqr += value * value;
});
return sumsqr;
})(),
}
}
}
The _stats
function will also work with “pre-aggregated” values from a map phase. A map function that emits an object containing sum
, min
, max
, count
, and sumsqr
keys and numeric values for each can use the _stats
function to combine these results with the data from other documents. The emitted object may contain other keys (these are ignored by the reducer), and it is also possible to mix raw numeric values and pre-aggregated objects in a single view and obtain the correct aggregated statistics.
Finally, _stats
can operate on key-value pairs where each value is an array comprised of numbers or pre-aggregated objects. In this case every value emitted from the map function must be an array, and the arrays must all be the same length, as _stats
will compute the statistical quantities above independently for each element in the array. Users who want to compute statistics on multiple values from a single document should either emit
each value into the index separately, or compute the statistics for the set of values using the JavaScript example above and emit a pre-aggregated object.
- _sum¶
In its simplest variation, _sum
sums the numeric values associated with each key, as in the following JavaScript
// could be replaced by _sum
function(keys, values) {
return sum(values);
}
As with _stats
, the _sum
function offers a number of extended capabilities. The _sum
function requires that map values be numbers, arrays of numbers, or objects. When presented with array output from a map function, _sum
will compute the sum for every element of the array. A bare numeric value will be treated as an array with a single element, and arrays with fewer elements will be treated as if they contained zeroes for every additional element in the longest emitted array. As an example, consider the following map output
{"total_rows":5, "offset":0, "rows": [
{"id":"id1", "key":"abc", "value": 2},
{"id":"id2", "key":"abc", "value": [3,5,7]},
{"id":"id2", "key":"def", "value": [0,0,0,42]},
{"id":"id2", "key":"ghi", "value": 1},
{"id":"id1", "key":"ghi", "value": 3}
]}
The _sum
for this output without any grouping would be
{"rows": [
{"key":null, "value": [9,5,7,42]}
]}
while the grouped output would be
{"rows": [
{"key":"abc", "value": [5,5,7]},
{"key":"def", "value": [0,0,0,42]},
{"key":"ghi", "value": 4
]}
This is in contrast to the behavior of the _stats
function which requires that all emitted values be arrays of identical length if any array is emitted.
It is also possible to have _sum
recursively descend through an emitted object and compute the sums for every field in the object. Objects cannot be mixed with other data structures. Objects can be arbitrarily nested, provided that the values for all fields are themselves numbers, arrays of numbers, or objects.
Note
Why don’t reduce functions support CommonJS modules?
While map functions have limited access to stored modules through require()
, there is no such feature for reduce functions. The reason lies deep inside the way map and reduce functions are processed by the Query Server. Let’s take a look at map functions first
CouchDB sends all map functions in a processed design document to the Query Server.
the Query Server handles them one by one, compiles and puts them onto an internal stack.
after all map functions have been processed, CouchDB will send the remaining documents for indexing, one by one.
the Query Server receives the document object and applies it to every function from the stack. The emitted results are then joined into a single array and sent back to CouchDB.
Now let’s see how reduce functions are handled
CouchDB sends as a single command the list of available reduce functions with the result list of key-value pairs that were previously returned from the map functions.
the Query Server compiles the reduce functions and applies them to the key-value lists. The reduced result is sent back to CouchDB.
As you may note, reduce functions are applied in a single shot to the map results while map functions are applied to documents one by one. This means that it’s possible for map functions to precompile CommonJS libraries and use them during the entire view processing, but for reduce functions they would be compiled again and again for each view result reduction, which would lead to performance degradation.
3.1.3. Show Functions¶
Warning
Show functions are deprecated in CouchDB 3.0, and will be removed in CouchDB 4.0.
- showfun(doc, req)¶
- 参数:
doc – The document that is being processed; may be omitted.
req – Request object.
- 返回值:
- Return type:
object or string
Show functions are used to represent documents in various formats, commonly as HTML pages with nice formatting. They can also be used to run server-side functions without requiring a pre-existing document.
Basic example of show function could be
function(doc, req){
if (doc) {
return "Hello from " + doc._id + "!";
} else {
return "Hello, world!";
}
}
Also, there is more simple way to return json encoded data
function(doc, req){
return {
'json': {
'id': doc['_id'],
'rev': doc['_rev']
}
}
}
and even files (this one is CouchDB logo)
function(doc, req){
return {
'headers': {
'Content-Type' : 'image/png',
},
'base64': ''.concat(
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAsV',
'BMVEUAAAD////////////////////////5ur3rEBn////////////////wDBL/',
'AADuBAe9EB3IEBz/7+//X1/qBQn2AgP/f3/ilpzsDxfpChDtDhXeCA76AQH/v7',
'/84eLyWV/uc3bJPEf/Dw/uw8bRWmP1h4zxSlD6YGHuQ0f6g4XyQkXvCA36MDH6',
'wMH/z8/yAwX64ODeh47BHiv/Ly/20dLQLTj98PDXWmP/Pz//39/wGyJ7Iy9JAA',
'AADHRSTlMAbw8vf08/bz+Pv19jK/W3AAAAg0lEQVR4Xp3LRQ4DQRBD0QqTm4Y5',
'zMxw/4OleiJlHeUtv2X6RbNO1Uqj9g0RMCuQO0vBIg4vMFeOpCWIWmDOw82fZx',
'vaND1c8OG4vrdOqD8YwgpDYDxRgkSm5rwu0nQVBJuMg++pLXZyr5jnc1BaH4GT',
'LvEliY253nA3pVhQqdPt0f/erJkMGMB8xucAAAAASUVORK5CYII=')
}
}
But what if you need to represent data in different formats via a single function? Functions registerType()
and provides()
are your the best friends in that question
function(doc, req){
provides('json', function(){
return {'json': doc}
});
provides('html', function(){
return '<pre>' + toJSON(doc) + '</pre>'
})
provides('xml', function(){
return {
'headers': {'Content-Type': 'application/xml'},
'body' : ''.concat(
'<?xml version="1.0" encoding="utf-8"?>\n',
'<doc>',
(function(){
escape = function(s){
return s.replace(/"/g, '"')
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/&/g, '&');
};
var content = '';
for(var key in doc){
if(!doc.hasOwnProperty(key)) continue;
var value = escape(toJSON(doc[key]));
var key = escape(key);
content += ''.concat(
'<' + key + '>',
value
'</' + key + '>'
)
}
return content;
})(),
'</doc>'
)
}
})
registerType('text-json', 'text/json')
provides('text-json', function(){
return toJSON(doc);
})
}
This function may return html, json , xml or our custom text json format representation of same document object with same processing rules. Probably, the xml provider in our function needs more care to handle nested objects correctly, and keys with invalid characters, but you’ve got the idea!
See also
- CouchDB Guide
3.1.4. List Functions¶
Warning
List functions are deprecated in CouchDB 3.0, and will be removed in CouchDB 4.0.
- listfun(head, req)¶
- 参数:
head – View Head Information
req – Request object.
- 返回值:
Last chunk.
- Return type:
string
While Show Functions are used to customize document presentation, List Functions are used for the same purpose, but on View Functions results.
The following list function formats the view and represents it as a very simple HTML page
function(head, req){
start({
'headers': {
'Content-Type': 'text/html'
}
});
send('<html><body><table>');
send('<tr><th>ID</th><th>Key</th><th>Value</th></tr>');
while(row = getRow()){
send(''.concat(
'<tr>',
'<td>' + toJSON(row.id) + '</td>',
'<td>' + toJSON(row.key) + '</td>',
'<td>' + toJSON(row.value) + '</td>',
'</tr>'
));
}
send('</table></body></html>');
}
Templates and styles could obviously be used to present data in a nicer fashion, but this is an excellent starting point. Note that you may also use registerType()
and provides()
functions in a similar way as for Show Functions! However, note that provides()
expects the return value to be a string when used inside a list function, so you’ll need to use start()
to set any custom headers and stringify your JSON before returning it.
See also
- CouchDB Guide
3.1.5. Update Functions¶
- updatefun(doc, req)¶
- 参数:
doc – The document that is being processed.
req – Request object
- 返回值:
Two-element array: the first element is the (updated or new) document, which is committed to the database. If the first element is
null
no document will be committed to the database. If you are updating an existing document, it should already have an_id
set, and if you are creating a new document, make sure to set its_id
to something, either generated based on the input or thereq.uuid
provided. The second element is the response that will be sent back to the caller.
Update handlers are functions that clients can request to invoke server-side logic that will create or update a document. This feature allows a range of use cases such as providing a server-side last modified timestamp, updating individual fields in a document without first getting the latest revision, etc.
当更新处理程序的请求在 URL 中包含文档 ID 时,服务器将向该函数提供该文档的最新版本。您可以通过 POST
/PUT
实体主体或请求的查询字符串参数,提供更新处理程序函数所需的任何其他值。
一个演示更新处理程序所有用例的基本示例
function(doc, req){
if (!doc){
if ('id' in req && req['id']){
// create new document
return [{'_id': req['id']}, 'New World']
}
// change nothing in database
return [null, 'Empty World']
}
doc['world'] = 'hello';
doc['edited_by'] = req['userCtx']['name']
return [doc, 'Edited World!']
}
3.1.6. 过滤器函数¶
- filterfun(doc, req)¶
- 参数:
doc – 正在处理的文档
req – Request object
- 返回值:
布尔值:
true
表示 doc 通过了过滤器规则,false
表示它没有通过。
过滤器函数在很大程度上类似于 显示函数 和 列表函数:它们格式化或过滤 更改馈送。
3.1.6.1. 经典过滤器¶
默认情况下,更改馈送会发出所有数据库文档更改。但是,如果您正在等待一些特殊的更改,处理所有文档效率低下。
过滤器是特殊的“设计文档”函数,允许更改馈送仅发出通过过滤器规则的特定文档。
假设我们的数据库是一个邮箱,我们只需要处理新的邮件事件(状态为 new 的文档)。我们的过滤器函数将如下所示
function(doc, req){
// we need only `mail` documents
if (doc.type != 'mail'){
return false;
}
// we're interested only in `new` ones
if (doc.status != 'new'){
return false;
}
return true; // passed!
}
如果文档通过了所有规则,则过滤器函数必须返回 true
。现在,如果您将此函数应用于更改馈送,它将仅发出有关“新邮件”的更改
GET /somedatabase/_changes?filter=mailbox/new_mail HTTP/1.1
{"results":[
{"seq":"1-g1AAAAF9eJzLYWBg4MhgTmHgz8tPSTV0MDQy1zMAQsMcoARTIkOS_P___7MymBMZc4EC7MmJKSmJqWaYynEakaQAJJPsoaYwgE1JM0o1TjQ3T2HgLM1LSU3LzEtNwa3fAaQ_HqQ_kQG3qgSQqnoCqvJYgCRDA5ACKpxPWOUCiMr9hFUegKi8T1jlA4hKkDuzAC2yZRo","id":"df8eca9da37dade42ee4d7aa3401f1dd","changes":[{"rev":"1-c2e0085a21d34fa1cecb6dc26a4ae657"}]},
{"seq":"9-g1AAAAIreJyVkEsKwjAURUMrqCOXoCuQ5MU0OrI70XyppcaRY92J7kR3ojupaSPUUgqWwAu85By4t0AITbJYo5k7aUNSAnyJ_SGFf4gEkvOyLPMsFtHRL8ZKaC1M0v3eq5ALP-X2a0G1xYKhgnONpmenjT04o_v5tOJ3LV5itTES_uP3FX9ppcAACaVsQAo38hNd_eVFt8ZklVljPqSPYLoH06PJhG0Cxq7-yhQcz-B4_fQCjFuqBjjewVF3E9cORoExSrpU_gHBTo5m","id":"df8eca9da37dade42ee4d7aa34024714","changes":[{"rev":"1-29d748a6e87b43db967fe338bcb08d74"}]},
],
"last_seq":"10-g1AAAAIreJyVkEsKwjAURR9tQR25BF2B5GMaHdmdaNIk1FLjyLHuRHeiO9Gd1LQRaimFlsALvOQcuLcAgGkWKpjbs9I4wYSvkDu4cA-BALkoyzLPQhGc3GKSCqWEjrvfexVy6abc_SxQWwzRVHCuYHaxSpuj1aqfTyp-3-IlSrdakmH8oeKvrRSIkJhSNiKFjdyEm7uc6N6YTKo3iI_pw5se3vRsMiETE23WgzJ5x8s73n-9EMYNTUc4Pt5RdxPVDkYJYxR3qfwLwW6OZw"}
请注意,last_seq
的值为 10-..,但我们只收到了两条记录。似乎任何其他更改都是针对未通过我们过滤器的文档。
我们可能需要通过单个状态值以外的其他内容来过滤邮箱的更改馈送。我们还对诸如“垃圾邮件”之类的状态感兴趣,以更新垃圾邮件过滤器启发式规则,“外出”以让邮件守护程序实际发送邮件,等等。创建许多实际上执行类似工作的类似函数不是一个好主意 - 所以我们需要一个动态过滤器。
您可能已经注意到,过滤器函数接受一个名为 请求 的第二个参数。这允许根据查询参数、用户上下文 等创建动态过滤器。
我们过滤器的动态版本如下所示
function(doc, req){
// we need only `mail` documents
if (doc.type != 'mail'){
return false;
}
// we're interested only in requested status
if (doc.status != req.query.status){
return false;
}
return true; // passed!
}
现在,我们在请求中传递了 status 查询参数,以使我们的过滤器仅匹配所需的文档
GET /somedatabase/_changes?filter=mailbox/by_status&status=new HTTP/1.1
{"results":[
{"seq":"1-g1AAAAF9eJzLYWBg4MhgTmHgz8tPSTV0MDQy1zMAQsMcoARTIkOS_P___7MymBMZc4EC7MmJKSmJqWaYynEakaQAJJPsoaYwgE1JM0o1TjQ3T2HgLM1LSU3LzEtNwa3fAaQ_HqQ_kQG3qgSQqnoCqvJYgCRDA5ACKpxPWOUCiMr9hFUegKi8T1jlA4hKkDuzAC2yZRo","id":"df8eca9da37dade42ee4d7aa3401f1dd","changes":[{"rev":"1-c2e0085a21d34fa1cecb6dc26a4ae657"}]},
{"seq":"9-g1AAAAIreJyVkEsKwjAURUMrqCOXoCuQ5MU0OrI70XyppcaRY92J7kR3ojupaSPUUgqWwAu85By4t0AITbJYo5k7aUNSAnyJ_SGFf4gEkvOyLPMsFtHRL8ZKaC1M0v3eq5ALP-X2a0G1xYKhgnONpmenjT04o_v5tOJ3LV5itTES_uP3FX9ppcAACaVsQAo38hNd_eVFt8ZklVljPqSPYLoH06PJhG0Cxq7-yhQcz-B4_fQCjFuqBjjewVF3E9cORoExSrpU_gHBTo5m","id":"df8eca9da37dade42ee4d7aa34024714","changes":[{"rev":"1-29d748a6e87b43db967fe338bcb08d74"}]},
],
"last_seq":"10-g1AAAAIreJyVkEsKwjAURR9tQR25BF2B5GMaHdmdaNIk1FLjyLHuRHeiO9Gd1LQRaimFlsALvOQcuLcAgGkWKpjbs9I4wYSvkDu4cA-BALkoyzLPQhGc3GKSCqWEjrvfexVy6abc_SxQWwzRVHCuYHaxSpuj1aqfTyp-3-IlSrdakmH8oeKvrRSIkJhSNiKFjdyEm7uc6N6YTKo3iI_pw5se3vRsMiETE23WgzJ5x8s73n-9EMYNTUc4Pt5RdxPVDkYJYxR3qfwLwW6OZw"}
我们可以轻松地使用以下方法更改过滤器行为
GET /somedatabase/_changes?filter=mailbox/by_status&status=spam HTTP/1.1
{"results":[
{"seq":"6-g1AAAAIreJyVkM0JwjAYQD9bQT05gk4gaWIaPdlNNL_UUuPJs26im-gmuklMjVClFFoCXyDJe_BSAsA4jxVM7VHpJEswWyC_ktJfRBzEzDlX5DGPDv5gJLlSXKfN560KMfdTbL4W-FgM1oQzpmByskqbvdWqnc8qfvvHCyTXWuBu_K7iz38VCOOUENqjwg79hIvfvOhamQahROoVYn3-I5huwXSvm5BJsTbLTk3B8QiO58-_YMoMkT0cr-BwdRElmFKSNKniDcAcjmM","id":"8960e91220798fc9f9d29d24ed612e0d","changes":[{"rev":"3-cc6ff71af716ddc2ba114967025c0ee0"}]},
],
"last_seq":"10-g1AAAAIreJyVkEsKwjAURR9tQR25BF2B5GMaHdmdaNIk1FLjyLHuRHeiO9Gd1LQRaimFlsALvOQcuLcAgGkWKpjbs9I4wYSvkDu4cA-BALkoyzLPQhGc3GKSCqWEjrvfexVy6abc_SxQWwzRVHCuYHaxSpuj1aqfTyp-3-IlSrdakmH8oeKvrRSIkJhSNiKFjdyEm7uc6N6YTKo3iI_pw5se3vRsMiETE23WgzJ5x8s73n-9EMYNTUc4Pt5RdxPVDkYJYxR3qfwLwW6OZw"}
将过滤器与 continuous 馈送结合使用可以创建强大的事件驱动系统。
3.1.6.2. 视图过滤器¶
视图过滤器与上面的经典过滤器相同,只有一个细微的差别:它们使用视图的 map 而不是 filter 函数来过滤更改馈送。每次从 map 函数发出键值对时,都会返回一个更改。这允许避免过滤器函数,这些函数在很大程度上与视图执行相同的操作。
要使用它们,只需将 filter=_view 和 view=designdoc/viewname 作为请求参数传递给 更改馈送
GET /somedatabase/_changes?filter=_view&view=dname/viewname HTTP/1.1
Note
由于视图过滤器使用 map 函数作为过滤器,因此它们无法显示任何动态行为,因为 请求对象 不可用。
See also
- CouchDB Guide
3.1.7. 验证文档更新函数¶
- validatefun(newDoc, oldDoc, userCtx, secObj)¶
设计文档可能包含一个名为 validate_doc_update 的函数,该函数可用于阻止存储无效或未经授权的文档更新请求。该函数传递了更新请求中的新文档、存储在数据库中的当前文档、包含有关写入文档的用户的信息的 用户上下文对象(如果存在)以及包含数据库安全角色列表的 安全对象。
验证函数通常检查新文档的结构,以确保存在必需的字段,并验证请求用户是否被允许更改文档属性。例如,应用程序可能要求用户必须经过身份验证才能创建新文档,或者在更新文档时必须存在特定文档字段。验证函数可以通过抛出两个错误对象之一来中止挂起的文档写入
// user is not authorized to make the change but may re-authenticate
throw({ unauthorized: 'Error message here.' });
// change is not allowed
throw({ forbidden: 'Error message here.' });
文档验证是可选的,每个数据库中的每个设计文档最多可以有一个验证函数。当收到针对给定数据库的写入请求时,将以未指定的顺序调用该数据库中每个设计文档中的验证函数。如果任何验证函数抛出错误,则写入将不会成功。
示例:来自 _users 数据库的 _design/_auth
ddoc 使用验证函数来确保文档包含一些必需的字段,并且只能由具有 _admin
角色的用户修改
function(newDoc, oldDoc, userCtx, secObj) {
if (newDoc._deleted === true) {
// allow deletes by admins and matching users
// without checking the other fields
if ((userCtx.roles.indexOf('_admin') !== -1) ||
(userCtx.name == oldDoc.name)) {
return;
} else {
throw({forbidden: 'Only admins may delete other user docs.'});
}
}
if ((oldDoc && oldDoc.type !== 'user') || newDoc.type !== 'user') {
throw({forbidden : 'doc.type must be user'});
} // we only allow user docs for now
if (!newDoc.name) {
throw({forbidden: 'doc.name is required'});
}
if (!newDoc.roles) {
throw({forbidden: 'doc.roles must exist'});
}
if (!isArray(newDoc.roles)) {
throw({forbidden: 'doc.roles must be an array'});
}
if (newDoc._id !== ('org.couchdb.user:' + newDoc.name)) {
throw({
forbidden: 'Doc ID must be of the form org.couchdb.user:name'
});
}
if (oldDoc) { // validate all updates
if (oldDoc.name !== newDoc.name) {
throw({forbidden: 'Usernames can not be changed.'});
}
}
if (newDoc.password_sha && !newDoc.salt) {
throw({
forbidden: 'Users with password_sha must have a salt.' +
'See /_utils/script/couch.js for example code.'
});
}
var is_server_or_database_admin = function(userCtx, secObj) {
// see if the user is a server admin
if(userCtx.roles.indexOf('_admin') !== -1) {
return true; // a server admin
}
// see if the user a database admin specified by name
if(secObj && secObj.admins && secObj.admins.names) {
if(secObj.admins.names.indexOf(userCtx.name) !== -1) {
return true; // database admin
}
}
// see if the user a database admin specified by role
if(secObj && secObj.admins && secObj.admins.roles) {
var db_roles = secObj.admins.roles;
for(var idx = 0; idx < userCtx.roles.length; idx++) {
var user_role = userCtx.roles[idx];
if(db_roles.indexOf(user_role) !== -1) {
return true; // role matches!
}
}
}
return false; // default to no admin
}
if (!is_server_or_database_admin(userCtx, secObj)) {
if (oldDoc) { // validate non-admin updates
if (userCtx.name !== newDoc.name) {
throw({
forbidden: 'You may only update your own user document.'
});
}
// validate role updates
var oldRoles = oldDoc.roles.sort();
var newRoles = newDoc.roles.sort();
if (oldRoles.length !== newRoles.length) {
throw({forbidden: 'Only _admin may edit roles'});
}
for (var i = 0; i < oldRoles.length; i++) {
if (oldRoles[i] !== newRoles[i]) {
throw({forbidden: 'Only _admin may edit roles'});
}
}
} else if (newDoc.roles.length > 0) {
throw({forbidden: 'Only _admin may set roles'});
}
}
// no system roles in users db
for (var i = 0; i < newDoc.roles.length; i++) {
if (newDoc.roles[i][0] === '_') {
throw({
forbidden:
'No system roles (starting with underscore) in users db.'
});
}
}
// no system names as names
if (newDoc.name[0] === '_') {
throw({forbidden: 'Username may not start with underscore.'});
}
var badUserNameChars = [':'];
for (var i = 0; i < badUserNameChars.length; i++) {
if (newDoc.name.indexOf(badUserNameChars[i]) >= 0) {
throw({forbidden: 'Character `' + badUserNameChars[i] +
'` is not allowed in usernames.'});
}
}
}
Note
return
语句仅用于函数,它对验证过程没有影响。
See also
- CouchDB Guide