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/laf
database-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)像操作本地数据库一样,直接在客户端发起条件查询、更新、新增等请求。
从源码结构看,整个组件由三个核心类协作完成:
| 组件 | 源码位置 | 职责 |
|---|---|---|
Proxy | src/proxy.ts | 对外门面:解析请求参数、触发校验、分发执行 |
Policy | src/policy/policy.ts | 访问控制策略:加载规则、实例化验证器、执行校验 |
Accessor(MongoAccessor/ 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.parse(src/proxy.ts)中:每种动作都声明了各自「允许携带的字段」,只拷贝白名单内的字段,其余一律丢弃。例如read允许query / order / offset / limit / projection / multi / count / joins / nested,而add只允许data / multi(见 src/types.ts)。这意味着客户端即使提交了多余字段也不会进入执行阶段,从源头上限制了参数注入面。
动作类型本身采用语义化命名,定义于 src/types.ts:
| ActionType | 取值 | 对应权限名 |
|---|---|---|
READ | database.queryDocument | read |
ADD | database.addDocument | add |
UPDATE | database.updateDocument | update |
REMOVE | database.deleteDocument | remove |
COUNT | database.countDocument | count |
AGGREGATE | database.aggregateDocuments | aggregate |
WATCH | database.watchDocument | watch |
三、客户端使用:laf-client-sdk 直读直写
客户端安装对应的 SDK:
npm install laf-client-sdk然后初始化云环境并操作数据库(以下示例完整来自 README):
const 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 的职责就是把链式 API(where/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或sender); - update:仅接收方可更新,且
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:
| 验证器名 | 别名 | 作用 |
|---|---|---|
condition | cond | 用 JS 表达式判断准入条件(支持get()跨集合查询) |
data | schema | 校验提交/更新数据是否符合$schema约束 |
query | — | 校验查询条件query是否符合约束 |
multi | — | 限制multi参数(是否允许批量操作) |
join/lookup | — | 对连表 / lookup 操作做限制 |
5.2 validate:规则数组的「任一通过即放行」
Policy.validate(policy.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 的差异处理
data(schema)验证器(src/validators/data/index.ts)按动作类型走不同分支:
- add:数据中不允许出现任何更新操作符(
$set/$inc等),字段必须在$schema声明范围内,并逐一执行字段约束; - update(merge=true,增量更新):必须携带
$set等更新操作符,平铺后检查字段白名单,且约束检查只作用于$set数据,同时忽略required/default约束(因为更新时不应强制补全全部字段); - update(merge=false,整体替换):按 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…)到 v2(read/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_PASSWORD=kissme -e MYSQL_DATABASE=testdb -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) )ENGINE=InnoDB DEFAULT CHARSET=utf8; 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) )ENGINE=InnoDB DEFAULT CHARSET=utf8;执行测试用例:
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
- 客户端 SDK:packages/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),仅供参考