
Laf database-proxy 实战用一个数据库代理 API 替代 90% 的后端 CRUD 接口【免费下载链接】lafLaf is a vibrant cloud development platform that provides essential tools like cloud functions, databases, and storage solutions. It enables developers to quickly unleash their creativity and bring innovative ideas to life with ease.项目地址: https://gitcode.com/GitHub_Trending/la/lafdatabase-proxy 是 Laf 云开发平台中实现「前端安全直连数据库」的核心组件它把传统后端 90% 的增删改查接口收敛为单一的数据访问代理 API通过声明式的「访问控制规则」在请求执行前完成鉴权与数据校验。读完本文你将掌握如何用几行服务端代码搭出一个合规的数据库代理端点如何用 laf-client-sdk 在客户端直接读写数据以及如何编写覆盖多用户权限、字段级数据验证的访问规则并理解 Proxy / Policy / Accessor 三组件在源码中的真实调用链。一、database-proxy 是什么一个「超级 API」database-proxy 的官方说明文档将其定义为一个「超级API」一个 API 替代服务端 90% 的传统 APIs。其设计目标是让前端开发者无需再与服务端逐个对接 REST 接口而是服务端只暴露一个数据库代理端点如/proxy用一套「访问控制规则」声明每个集合collection在 read / update / add / remove 等操作下的准入条件客户端通过 laf-client-sdk仓库中为packages/client-sdk像操作本地数据库一样直接在客户端发起条件查询、更新、新增等请求。从源码结构看整个组件由三个核心类协作完成组件源码位置职责Proxysrc/proxy.ts对外门面解析请求参数、触发校验、分发执行Policysrc/policy/policy.ts访问控制策略加载规则、实例化验证器、执行校验AccessorMongoAccessor/ MySQL 实现src/accessor/数据库访问层真正把Params翻译为数据库操作入口文件 src/index.ts 同时导出了Proxy、Policy、访问器、dbi协议以及database-ql的Db说明该包与 Laf 自家的数据库查询语言 packages/database-ql 是配套设计的。二、快速上手安装与搭建服务端代理端点安装npm install database-proxy服务端代码示例下面是 README 给出的完整服务端示例基于 Express 搭建。其核心思路是从请求头解析出用户身份uid作为「注入变量」交给策略层做规则判断随后走parseParams → validate → execute三步流程。const app require(express)() const { Proxy, MongoAccessor, Policy } require(database-proxy) const { MongoClient } require(mongodb) app.use(express.json()) // design the access control policy rules const rules { categories: { read: true, update: !uid, add: !uid, remove: !uid } } const client new MongoClient(mongodb://localhost:27017) client.connect() // create an accessor const accessor new MongoAccessor(client) // create a policy const policy new Policy(accessor) policy.load(rules) // create an proxy const proxy new Proxy(accessor, policy) app.post(/proxy, async (req, res) { const { uid } parseToken(req.headers[authorization]) const injections { uid: uid } // parse params const params proxy.parseParams(req.body) // validate query const result await proxy.validate(params, injections) if (result.errors) { return res.send({ code: 1, error: result.errors }) } // execute query const data await proxy.execute(params) return res.send({ code: 0, data }) }) app.listen(8080, () console.log(listening on 8080))注意示例中的权限写法!uid这是一个 JS 表达式当注入变量uid不存在未登录时表达式为真、请求被拒绝的取反逻辑——即「必须登录」。这类表达式的具体执行机制见下文「condition 验证器」一节。三步流程在源码中的对应关系Proxy类src/proxy.ts只有三个核心方法与服务端代码一一对应parseParams(reqParams)从请求体中取出action字段再通过Proxy.parse按动作类型白名单拷贝合法参数validate(params, injections)委托给Policy执行规则校验execute(params)将校验通过的Params交给accessor.execute真正落库。其中参数解析的关键细节在Proxy.parsesrc/proxy.ts中每种动作都声明了各自「允许携带的字段」只拷贝白名单内的字段其余一律丢弃。例如read允许query / order / offset / limit / projection / multi / count / joins / nested而add只允许data / multi见 src/types.ts。这意味着客户端即使提交了多余字段也不会进入执行阶段从源头上限制了参数注入面。动作类型本身采用语义化命名定义于 src/types.tsActionType取值对应权限名READdatabase.queryDocumentreadADDdatabase.addDocumentaddUPDATEdatabase.updateDocumentupdateREMOVEdatabase.deleteDocumentremoveCOUNTdatabase.countDocumentcountAGGREGATEdatabase.aggregateDocumentsaggregateWATCHdatabase.watchDocumentwatch三、客户端使用laf-client-sdk 直读直写客户端安装对应的 SDKnpm install laf-client-sdk然后初始化云环境并操作数据库以下示例完整来自 READMEconst cloud require(laf-client-sdk).init({ dbProxyUrl: http://localhost:8080/proxy, getAccessToken: () localStorage.getItem(access_token) }) const db cloud.database() // 查询文档 const res await db.collection(categories).get() // 条件查询 const res await db.collection(articles) .where({status: published}) .orderBy({createdAt: asc}) .offset(0) .limit(20) .get() // 更新 const res await db.collection(articles) .doc(the-doc-id).update({ title: new-title })客户端 SDK 的职责就是把链式 APIwhere/orderBy/offset/limit/doc等序列化为上述action collection 白名单字段的请求体并通过getAccessToken提供鉴权凭证服务端解析出身份后作为injections注入规则表达式。更多 SDK 用法可参考仓库中 packages/client-sdk/README.md。四、访问控制规则四个由浅入深的完整示例规则是一个以集合名为顶层键的 JSON 对象每个集合下配置read / update / add / remove等权限项以及可选的$schema数据结构约束。以下四个示例完整继承自 README并逐条解释其语义。示例 1简单博客{ categories: { read: true, update: $admin true, add: $admin true, remove: $admin true }, articles: { read: true, update: $admin true, add: $admin true, remove: $admin true } }read: true表示任何人含匿名可读写操作统一要求$admin true——$前缀的变量来自服务端注入的injections如从 token 解析出的$admin、$userid。示例 2多用户博客{ articles: { read: true, update: $userid $userid query.createdBy, add: $userid data.createdBy $userid, remove: $userid query.createBy || $admin true } }这里出现了规则表达式的三类变量变量形式来源含义$userid/$admin服务端injections用户身份与角色query.xxx请求参数query客户端本次查询条件中的字段值data.xxx请求参数data客户端本次提交的数据字段值例如 update 规则要求请求中携带query.createdBy即目标文档的创建者 id且与登录用户$userid相等——只允许作者修改自己的文章。add 规则则要求提交的数据里data.createdBy必须等于当前用户 id。提示update: $userid $userid query.createdBy这类写法隐含了一个约定——客户端在更新请求的query中必须携带createdBy字段作为定位条件规则才可能通过。复杂示例 1数据验证$schema{ articles: { add: { condition: $userid data.createdBy $userid }, remove: $userid query.createBy || $admin true, $schema: { title: {length: [1, 64], required: true}, content: {length: [1, 4096]}, like: { number: [0,], default: 0} } } }这个示例引入了两个重要能力权限项从字符串升级为对象。add: { condition: ... }表示该权限由名为condition的验证器处理。当权限值是字符串或布尔时内部会自动归一化为[{ condition: 表达式 }]的形式见 policy.ts 的wrapRawPermissionRuleToArray$schema字段级约束。对add/update操作$schema会被自动附加到相应权限的验证器配置中policy.ts支持length长度区间、required必填、number数值范围、default默认值、in枚举取值、match正则、exists跨集合存在性检查等约束类型。这些约束的具体实现与边界用例可参考单元测试 tests/units/policy/data.add.constraints/ 与 query 约束测试。复杂示例 2站内消息表字段级数据约束场景用户之间的站内消息表访问规则。{ messages: { read: $userid ($userid query.receiver || $userid query.sender), update: { condition: $userid $userid query.receiver, data: { read: {in: [true]} } }, add: { condition: $userid $userid data.sender, data: { read: {in: [false]} } }, remove: false, $schema: { content: {length: [1, 20480], required: true}, receiver: {exists: /users/id}, read: { in: [true, false], default: false } } } }规则语义拆解read只有消息的接收方或发送方本人能查询查询条件中必须指明receiver或senderupdate仅接收方可更新且data验证器限定本次更新中read字段只能被置为true已读——配合「更新即已读」的业务语义add发送方必须是自己且新消息的read字段只能为false未读$schema中default: false会在缺省时补默认值remove: false直接禁用删除receiver: {exists: /users/id}从源码结构看该约束会按/集合名/字段形式即/users/id到另一集合中查询是否存在匹配记录用于保证消息接收者是一个真实用户类似机制在condition验证器的get(/collection/field)辅助查询中同样可见见 src/validators/condition/index.ts。五、规则引擎源码剖析一次请求是如何被校验的5.1 Policy规则加载与验证器编排Policy的load(rules)逐集合调用set将原始 JSON 规则编译为「验证器处理器数组」policy.ts。每个权限项无论原本是布尔、字符串还是对象最终都会被实例化为Processor的列表Processor是「验证器名 处理函数 配置」的三元组封装src/processor.ts。组件内置了六个验证器注册于 src/validators/index.ts验证器名别名作用conditioncond用 JS 表达式判断准入条件支持get()跨集合查询dataschema校验提交/更新数据是否符合$schema约束query—校验查询条件query是否符合约束multi—限制multi参数是否允许批量操作join/lookup—对连表 / lookup 操作做限制5.2 validate规则数组的「任一通过即放行」Policy.validatepolicy.ts的执行顺序是校验collection是否在规则配置中不在则返回collection xxx not found校验action是否合法并从rules[collection][权限名]取出验证器列表依次尝试每条规则单条规则内的所有验证器全部通过才算该条通过任一验证器失败则跳过本条规则尝试下一条全部规则都不匹配才返回errors数组任一条通过则返回{ matched }。这套「OR 语义 验证器 AND 链」的设计使得同一权限可以配置多组备选规则例如「本人或管理员」可以拆成两条规则而非一条长表达式。5.3 condition 验证器表达式在沙箱中执行condition验证器src/validators/condition/index.ts把规则字符串当作 JS 表达式在 Node.js 的vm沙箱中执行const global { ...injections, ...params } // $admin、$userid query、data 等 // ... const script new vm.Script(config) const result script.runInNewContext(global) if (result) return null // 真值 通过 return the expression evaluated to a falsy value这解释了规则表达式的全部可用变量injections服务端注入的身份变量与params本次请求参数包含query、data等被合并进沙箱全局作用域。若表达式中调用了get(/collection/field)验证器会先用一轮「mock 执行」收集出跨集合查询再通过accessor.get真实查库后二次执行从而支持「$userid get(/users/role)」这类依赖数据库数据的判断。5.4 data 验证器add 与 update 的差异处理dataschema验证器src/validators/data/index.ts按动作类型走不同分支add数据中不允许出现任何更新操作符$set/$inc等字段必须在$schema声明范围内并逐一执行字段约束updatemergetrue增量更新必须携带$set等更新操作符平铺后检查字段白名单且约束检查只作用于$set数据同时忽略required/default约束因为更新时不应强制补全全部字段updatemergefalse整体替换按 add 的完整校验逻辑处理。此外所有字段名在进入数据库前都会经过 SecurityUtil 的黑名单检查字段名不允许包含空格、;、引号、-、/、*等字符防止字段名注入 SQL/查询语句query与data中的字段还会被递归提取并核对是否在允许范围内isAllowedFields。查询操作符$eq、$or、$elemMatch等、逻辑操作符$and/$or/$not/$nor与更新操作符$set/$inc/$push等的合法清单统一定义在 src/types.ts 中。5.5 规则版本演进Policy内置了 v1 权限名.read/.update…到 v2read/update…的自动转换convertPermissionConfig。仓库的 docs/ 目录还保留了rules-v1.json、rules-v2.json两份规则样例以及 ruler_v2_design.md 设计文档可作为规则格式演进的参考资料。六、运行测试单元测试与真实数据库集成测试以下命令均完整来自 README建议在packages/database-proxy目录下执行。安装依赖npm i单元测试npx mocha tests/units/*.test.js单元测试覆盖访问器、SQL 构建器、代理层以及最核心的规则引擎包括各约束类型的边界用例见 tests/units/。Mongo 集成测试使用 Docker 启动测试数据库docker pull mongo docker run --rm -p 27018:27017 --name mongotest -d mongo执行测试用例npx mocha tests/mongo_db/*.test.js停止并删除 Mongo 实例docker rm -f mongotest测试文件位于 tests/mongo_db/覆盖 add / read / update / remove / count / aggregate 等操作在真实 Mongo 上的行为。MySQL 集成测试启动 MySQL 容器docker pull mysql docker run --name mysqltest -e MYSQL_ROOT_PASSWORDkissme -e MYSQL_DATABASEtestdb -d -p 3306:3306 mysql手动创建测试数据表create table IF NOT EXISTS categories ( id int not null auto_increment, name varchar(64) not null, created_at int, primary key(id) )ENGINEInnoDB DEFAULT CHARSETutf8; create table IF NOT EXISTS articles ( id int not null auto_increment, title varchar(64) not null, category_id int, content text, created_at int, updated_at int, created_by int, primary key(id) )ENGINEInnoDB DEFAULT CHARSETutf8;执行测试用例npx mocha tests/mysql_db/*.test.js停止并删除 MySQL 实例docker rm -f mysqltest说明当前仓库中 tests/mysql_db/ 下的用例文件以.test.ignore.js结尾被标记为忽略因此 MySQL 集测的实际可用性以仓库当前状态为准Mongo 侧用例为正常的.test.js可直接运行。执行全部测试请确保已经运行 mongo 和 mysql 的测试实例。npx mocha tests/**/*.test.js七、参考路径组件说明文档packages/database-proxy/README.md包配置packages/database-proxy/package.json代理门面packages/database-proxy/src/proxy.ts策略引擎packages/database-proxy/src/policy/policy.ts、策略接口验证器集合packages/database-proxy/src/validators/index.ts、condition 实现、data 实现安全工具packages/database-proxy/src/utils/security.ts动作与参数定义packages/database-proxy/src/types.ts客户端 SDKpackages/client-sdk/README.md【免费下载链接】lafLaf is a vibrant cloud development platform that provides essential tools like cloud functions, databases, and storage solutions. It enables developers to quickly unleash their creativity and bring innovative ideas to life with ease.项目地址: https://gitcode.com/GitHub_Trending/la/laf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考