3.1. 设计文件

在本节中,我们将展示如何使用内置的 JavaScript Query Server .

但是,在开始编写第一个文档之前,让我们先看看代码过程中使用的常见对象列表—我们将在每个函数中广泛使用它们:

3.1.1. 创作与结构

设计文档包含查看和更新功能等功能。这些函数在请求时执行。

设计文档由id字段表示,格式为 _design/{{name}} 。它们的结构遵循下面的示例。

例子

{
    "_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"
}

如您所见,设计文档可以包含多个相同类型的函数。该示例定义了两个视图,这两个视图都具有map函数,其中一个视图具有Reduce函数。它还定义了两个更新函数和一个过滤函数。验证文档更新功能是一个特例,因为每个设计文档不能包含其中的一个以上。

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);
    });
  }
}

在本例中,为 tags 具有 type “职务”。请注意 emit() 对于一个文档可以多次调用,因此同一个文档可以通过多个不同的键来使用。

还要记住,每个文档 密封的 防止一个映射函数更改文档状态,而另一个映射函数收到修改后的版本。

为了提高效率,文档被传递到一组map函数中,每个文档都由一组map函数从相关设计文档的所有视图中进行处理。这意味着,如果为设计文档中的一个视图触发索引更新,则所有其他视图也将更新。

自版本 1.1.0map 支架 CommonJS 模块和 require() 功能。

3.1.2.2. 约化和重排函数

redfun(keys, values[, rereduce])
参数:
  • keys -- 相关映射函数结果的键docid对数组。总是 null 如果rereduce正在运行(有 true 价值)。
  • values -- 映射函数结果值的数组。
  • rereduce -- 布尔标志,用于指示重新导出运行。
返回:

减少 values

Reduce函数接受键和值列表的两个必需参数(相关映射函数的结果)和一个可选的第三个值,该值指示 rereduce 模式是否激活。 Rereduce 用于附加的reduce值列表,因此 true 没有相关信息 keys (第一个论点是 null

注意,如果 reduce 函数的长度大于初始值列表,则将引发查询服务器错误。但是,可以通过设置来禁用此行为 reduce_limit 配置选项到 false

[query_server_config]
reduce_limit = false

禁用时 reduce_limit 可能对调试建议有用,记住reduce函数的主要任务是 减少 映射结果,而不是使其更大。一般来说,reduce函数应该快速收敛到一个值,这个值可以是数组或类似的对象。

3.1.2.2.1. 内置Reduce函数

另外,CouchDB有一组内置的reduce函数。它们是在Erlang中实现的,并在CouchDB中运行,因此它们比等效的JavaScript函数快得多。

_approx_count_distinct

2.2 新版功能.

使用 HyperLogLog 算法。该算法能够使用固定内存资源进行有效的、可并行化的基数计算。CouchDB已将底层数据结构配置为具有~2%的相对误差。

由于这个reducer完全忽略了发出的值,因此调用 group=true 将为视图中的每个不同键返回值1。对于数组键,使用 group_level 指定将返回每行中共享公共组前缀的不同键的数目。算法也认识到 startkeyendkey 边界,并将返回指定键范围内不同键的数目。

关于Unicode排序规则的最后一点注意:这个reduce函数直接使用索引中每个键的二进制表示作为超日志过滤器的输入。因此,它将(错误地)将字节不相同但根据Unicode排序规则比较相等的键视为不同的键,因此如果存在大量这样的键,则有可能高估键空间的基数。

_count

使用给定键计算索引中的值数。这可以在JavaScript中实现为:

// could be replaced by _count
function(keys, values, rereduce) {
    if (rereduce) {
        return sum(values);
    } else {
        return values.length;
    }
}
_stats

计算与每个键关联的数值的以下数量: summinmaxcountsumsqr . 的行为 _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;
            })(),
        }
    }
}

这个 _stats 函数还将处理映射阶段的“预聚合”值。发出包含 summinmaxcountsumsqr 每个键和数值都可以使用 _stats 函数将这些结果与其他文档中的数据相结合。发出的对象可能包含其他键(reducer会忽略这些键),还可以在单个视图中混合原始数值和预聚集对象,从而获得正确的聚合统计信息。

最后, _stats 可以对键值对进行操作,其中每个值都是由数字或预聚合对象组成的数组。在这种情况下 每一个 从map函数发出的值必须是一个数组,并且数组的长度必须与 _stats 将计算上述统计数量 独立地 对于数组中的每个元素。想要从单个文档计算多个值的统计信息的用户应该 emit 将每个值分别放入索引中,或者使用上面的JavaScript示例计算值集的统计信息并发出一个预聚合对象。

_sum

在最简单的变化中, _sum 将与每个键关联的数值求和,如以下JavaScript所示:

// could be replaced by _sum
function(keys, values) {
    return sum(values);
}

和一样 _stats , the _sum 函数提供了许多扩展功能。这个 _sum 函数要求映射值是数字、数字数组或对象。当显示映射函数的数组输出时, _sum 将计算数组中每个元素的和。一个纯数值将被视为具有单个元素的数组,而元素较少的数组将被视为在最长发出的数组中每个附加元素都包含零。作为示例,请考虑以下映射输出:

{"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}
]}

这个 _sum 对于这个没有任何分组的输出将是:

{"rows": [
    {"key":null, "value": [9,5,7,42]}
]}

而分组输出将是

{"rows": [
    {"key":"abc", "value": [5,5,7]},
    {"key":"def", "value": [0,0,0,42]},
    {"key":"ghi", "value": 4
]}

这与 _stats 函数,如果发出任何数组,则要求所有发出的值都是相同长度的数组。

也有可能 _sum 递归地向下遍历发射的对象并计算对象中每个字段的和。物体 不能 与其他数据结构混合。对象可以任意嵌套,前提是所有字段的值本身都是数字、数字数组或对象。

注解

reduce函数为什么不支持CommonJS模块?

同时 map 函数只能通过 require() ,没有此功能 reduce 功能。原因就在这条路的深处 mapreduce 函数由查询服务器处理。我们来看看 map 功能优先:

  1. CouchDB发送所有 map 将已处理的设计文档中的函数传递给查询服务器。
  2. 查询服务器逐个处理它们,将它们编译并放入内部堆栈中。
  3. 毕竟 map 函数已处理完毕,CouchDB将发送剩余文档进行索引,一个接一个。
  4. 查询服务器接收document对象并将其应用于堆栈中的每个函数。然后,将发出的结果连接到一个数组中并发送回CouchDB。

现在让我们看看怎么做 reduce 处理函数:

  1. CouchDB发送 作为单一命令 可用列表 reduce 函数中返回的键值对的结果列表 map 功能。
  2. 查询服务器编译reduce函数并将它们应用于键值列表。减少后的结果被发送回CouchDB。

你可能注意到了, reduce 函数将一次性应用于地图结果,而 map 函数逐个应用于文档。这意味着有可能 map 函数来预编译CommonJS库并在整个视图处理期间使用它们,但是 reduce 函数它们将被反复编译以减少每个视图的结果,这将导致性能下降。

3.1.3. 显示函数

警告

Show函数在couchdb3.0中被弃用,并将在couchdb4.0中被删除。

showfun(doc, req)
参数:
  • doc -- 正在处理的文件;可以省略。
  • req -- Request object .
返回:

Response object

返回类型:

对象或字符串

Show函数用于以各种格式表示文档,通常是具有良好格式的HTML页面。它们还可以用于运行服务器端函数,而不需要预先存在的文档。

show函数的基本示例可以是:

function(doc, req){
    if (doc) {
        return "Hello from " + doc._id + "!";
    } else {
        return "Hello, world!";
    }
}

此外,还有更简单的方法返回json编码的数据:

function(doc, req){
    return {
        'json': {
            'id': doc['_id'],
            'rev': doc['_rev']
        }
    }
}

甚至文件(这个是CouchDB徽标):

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=')
    }
}

但是如果需要通过一个函数以不同的格式表示数据呢?功能 registerType()provides() 在这个问题上你最好的朋友是:

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(/&quot;/g, '"')
                                .replace(/&gt;/g, '>')
                                .replace(/&lt;/g, '<')
                                .replace(/&amp;/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);
    })
}

此函数可能返回 htmljsonxml 或者我们的习惯 text json 具有相同处理规则的同一文档对象的格式表示。可能是 xml 我们函数中的提供者需要更加小心地正确处理嵌套对象和带有无效字符的键,但是你已经知道了!

参见

CouchDB指南:

3.1.4. 列表函数

警告

列表函数在couchdb3.0中被弃用,并将在couchdb4.0中被删除。

listfun(head, req)
参数:
返回:

最后一块。

返回类型:

一串

同时 显示函数 用于自定义文档演示文稿, 列表函数 用于相同的目的,但是 查看函数 结果。

以下列表函数格式化视图并将其表示为一个非常简单的HTML页面:

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>');
}

显然可以使用模板和样式以更好的方式显示数据,但这是一个很好的起点。请注意,您也可以使用 registerType()provides() 功能与 显示函数 ! 但是,请注意 provides() 在列表函数中使用时,返回值应为字符串,因此需要使用 start() 设置任何自定义头并在返回JSON之前对其进行stringify。

参见

CouchDB指南:

3.1.5. 更新函数

updatefun(doc, req)
参数:
返回:

双元素数组:第一个元素是提交给数据库的(更新的或新的)文档。如果第一个元素是 null 不会向数据库提交任何文档。如果您正在更新现有文档,它应该已经具有 _id 如果要创建新文档,请确保将其 _id 根据输入或 req.uuid 提供。第二个元素是将被发送回调用者的响应。

更新处理程序是客户端可以请求调用将创建或更新文档的服务器端逻辑的函数。这个特性允许使用一系列用例,例如提供服务器端最后修改的时间戳、在不首先获得最新修订版的情况下更新文档中的各个字段等等。

当对更新处理程序的请求在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)
参数:
返回:

布尔值: true 意思是说 doc 通过过滤规则, false 意味着它没有。

过滤函数的作用是 显示函数列表函数 :它们格式化,或 滤波器 这个 changes feed .

3.1.6.1. 经典过滤器

默认情况下,changes提要会发出所有数据库文档更改。但如果您在等待一些特殊的更改,则处理所有文档的效率都很低。

过滤器是一种特殊的设计文档函数,它允许更改提要只发出通过过滤规则的特定文档。

假设我们的数据库是一个邮箱,我们只需要处理新的邮件事件(状态为 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 如果一个文件通过了所有的规则。现在,如果您将此函数应用于changes feed,它将只发出关于“new mail”的更改:

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_seq10-.. ,但我们只收到两张唱片。似乎其他的更改都是针对未通过筛选的文档。

我们可能需要用不止一个状态值来过滤邮箱的更改提要。我们还对诸如“垃圾邮件”这样的状态感兴趣,以更新垃圾邮件过滤器的启发式规则,“传出”以允许邮件守护程序实际发送邮件,等等。创建许多实际执行类似工作的类似函数并不是一个好主意,因此我们需要一个动态过滤器。

您可能已经注意到,过滤器函数采用了名为 request . 允许基于此动态参数创建查询, user context 还有更多。

过滤器的动态版本如下所示:

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 feed允许创建功能强大的事件驱动系统。

3.1.6.2. 查看筛选器

视图过滤器与上面的经典过滤器相同,只是有一点不同:它们使用 map 而不是 filter 一个视图的函数,用于筛选更改提要。每对密钥从每个时间发出一个值 map 函数,则返回更改。这样可以避免过滤函数的工作与视图基本相同。

要用它们就过去 filter=_viewview=designdoc/viewname 作为请求参数 changes feed ::

GET /somedatabase/_changes?filter=_view&view=dname/viewname  HTTP/1.1

注解

因为视图过滤器使用 map 函数作为过滤器,它们不能显示任何动态行为,因为 request object 不可用。

参见

CouchDB指南:

3.1.7. 验证文档更新功能

validatefun(newDoc, oldDoc, userCtx, secObj)
参数:
投掷:

forbidden 正常阻止文档存储时出错。

投掷:

unauthorized 阻止存储并允许用户重新验证的错误。

设计文档可以包含一个名为 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.' });

文档验证是可选的,数据库中的每个设计文档最多可以有一个验证功能。当收到给定数据库的写入请求时,将以未指定的顺序调用该数据库中每个设计文档中的验证函数。如果任何验证函数抛出错误,则写入将不会成功。

例子_design/_auth 来自ddoc _users database uses a validation function to ensure that documents contain some required fields and are only modified by a user with the ``_ 管理员角色:

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.'});
        }
    }
}

注解

这个 return 语句只用于函数,它对验证过程没有影响。

参见

CouchDB指南: