Skip to main content

CouchDB

Tips#

  • CouchDB
  • 默认端口: 5984
  • 集群使用 4369 EPMD 发现其它节点,使用 9100 通信
  • /_utils futon 管理界面
  • 关于 CouchDB 的评论
  • 什么时候用 CouchDB
    • 当你需要解决多端离在线数据同步时 - 其它功能可能都很差,但这一个功能是最好的
    • 当没有后端开发接口,希望直接存取数据时 - BaaS
    • 类似的数据库
      • RealmDB
      • RethinkDB
    • 配合 PouchDB 在 JS 使用 - 如果用不上那考虑不要用 CouchDB
      • 如果只需要 CRUD 考虑使用 PostREST 或 Hasura
  • 注意
    • 不支持单文档读权限控制
    • 不支持默认读取的过滤
    • 同步是 数据库 纬度
    • 数据库 成本很低,可以为每个用户创建一个数据库,实现权限和同步
    • 默认没有 members 和 admins,所有文档都可以读写
    • design doc 能够被直接读取,/db/_design_docs 能够被直接访问
    • /db/_all_docs 能够被直接访问
    • 相关关闭相关接口只能通过反向代理实现
    • 没办法全量修改,写客户端进行操作
    • couch_httpd_auth/public_fields 控制用户的那些字段可见
  • 索引也叫设计文档/design document
    • 一般包含 JS 代码
  • 数据库通过 安全对象/security object 定义安全属性
    • JSON 对象 {admins:[{names:[],roles:[]}],members:[]}
  • 两种角色
    • admins - 索引修改、安全对象修改
    • members - 文档 CRUD
# Docker 启动# 官方镜像有 2.0 https://github.com/apache/couchdb-docker# COUCHDB_USER COUCHDB_PASSWORD# EXPOSE 4369 5984 9100docker run -p 5984:5984 -v $(pwd):/opt/couchdb/data --name couchdb couchdb
db=http://127.0.0.1:5984# 创建 DBcurl -X PUT $db/test# 直接修改curl $db/test/hello | jq '.name="abc"' | curl -X POST $db/test -d @-

安全#

# 创建用户curl -X POST $db/_users/org.couchdb.user:wener -d '{"name":"wener","password":"wener","roles":["admin"], "type":"user"}'# 用户信息curl $db/_users/org.couchdb.user:wener
# 直接 PUT 会创建数据库curl -X PUT $db/test# 获取安全对象curl $db/test/_security# 添加访问限制 - 允许 admin 角色和 user 角色curl -X PUT $db/test/_security -d '{"members":{"roles":["user"]},"admins":{"roles":["admin"]}}'# 用刚才创建的账户进行访问curl -u wener:wener $db/test/_security 

设计文档#

{  // 一般为 JS - Query Server 定义处理  language: "javascript",  // 过滤函数定义  filters:"",  // 试图默认选项  options:{},  // 列表函数定义  lists: "",  // 重写规则  // array | string  rewrites: [],  // Show 函数  shows: {},  // 更新函数  updates:{},  // 验证文档更新函数  // function(newDoc, oldDoc, userCtx, secObj)  validate_doc_update: "",  // 视图函数  views: {}}
// https://docs.couchdb.org/en/stable/json-structure.html#request-objectinterface Request {  body;  cookie;  form;  headers;  id;  info;  method;  path;  peer;  query;  request_path;  raw_path;  secObj;  userCtx;  uuid;}
// 映射函数type MapFunction = (doc) => void
type ReduceFunction = (keys, values, rereduce?:boolean) => array
// 显示函数// 返回 string 直接显示或者返回响应对象// 返回 JSON: {json:{name:doc['name']}}// 返回 自定义头: {headers:{'Content-Type':'image/png'}, base64:''} - 内容可以使用 base64 编码type ShowFunction = (doc,req) => object|string
// 更新函数// 返回更新对象和返回给客户端的对象type UpdateFunction = (doc,req) => [any|null,any]
// 列表函数// 调用 start send 部分发送type ListFunction = (head,req) => string
// 处理对象和请求对象 - 返回是否过滤// GET /db/_changes?filter=mailbox/new_mailtype FilterFunction = (doc,req)=>boolean
// 视图过滤// GET /db/_changes?filter=_view&view=dname/viewnametype ViewFilterFunction = (doc,req)=>boolean
// 验证函数// 新文档,旧文档,用户上下文,安全对象// throw({forbidden:''}) 或 throw({unauthorized:''})// 如果为删除 newDoc 包含 _deleted 字段type ValidateFunction = (newDoc, oldDoc, userCtx, secObj) => void

_users#

_design/_auth#

  • validate_doc_update
    • 校验 _users 更新
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 (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'});  }
  for (var idx = 0; idx < newDoc.roles.length; idx++) {      if (typeof newDoc.roles[idx] !== 'string') {          throw({forbidden: 'doc.roles can only contain strings'});      }  }
  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 available_schemes = ["simple", "pbkdf2", "bcrypt"];  if (newDoc.password_scheme          && available_schemes.indexOf(newDoc.password_scheme) == -1) {      throw({          forbidden: 'Password scheme `' + newDoc.password_scheme              + '` not supported.'      });  }
  if (newDoc.password_scheme === "pbkdf2") {      if (typeof(newDoc.iterations) !== "number") {          throw({forbidden: "iterations must be a number."});      }      if (typeof(newDoc.derived_key) !== "string") {          throw({forbidden: "derived_key must be a string."});      }  }
  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.'});      }  }}

FAQ#

Referer header required#

该用 PUT 用成了 POST