Vext 集成
场景
该示例加载原生 Vext 插件,通过 routes.protect 保护路由模板,通过透明 app.db.collection() 和 app.db.model() 读取当前租户可见的数据,执行公开/未认证/拒绝/允许请求,证明路由重载要求重启,并验证插件关闭不会关闭宿主数据库。
运行
规范源码是 examples/vext/index.mjs 中 docs:vext:start 到 docs:vext:end 的内容,以及 examples/vext/app/src/routes/index.mjs。
先看结果
运行成功先看状态码:公开路由 200、未认证 401、无权限 403、普通有权限路由 200、collection 数据路由 200、model 数据路由 200、路由变更后 503。再看 requestDataBody.items 和 modelDataBody.items 只包含当前租户的订单字段,最后确认 permissionCoreClosedByPlugin 和 hostDatabaseStillConnected 都为 true。
源码解读
受保护路由本身位于 examples/vext/app/src/routes/index.mjs:
routes.protect 推导出对路由模板的 invoke,所以路由定义里不用重复写 permission: true。透明数据门面会检查 db:orders 的 read,自动把当前 subject 的 tenantId 合入查询,并按字段权限裁剪结果。这里配置 data.collections.vext_orders.resource 是因为示例的物理集合名叫 vext_orders,但权限资源希望叫 db:orders;如果你的物理集合本来就叫 orders,可以不写 collections,默认就是 db:orders。测试专用 header middleware 提供可重复 req.auth;生产环境使用真实认证插件。
1. 启动 Vext 测试宿主与插件
目的与目标。 createTestApp 启动 fixture host;permissionPlugin.setup(即 permissionPlugin(...) 返回的 .setup)把 permission-core 安装进 Vext;server:beforeListen 在接受请求前完成 startup probe。
状态、参数与结果。 插件接收宿主已连接的 MonSQLize instance 和 collection prefix,随后暴露 app.permission。Public route 保持未保护;它之后返回 200,证明 bootstrap 后 host 与 route graph 可用。
失败与下一步。 MonSQLize 缺失/不兼容、PermissionCore 初始化失败或 route metadata 无效时,readiness 不应通过。应修正 host configuration 并重启,不能让受保护 route 在插件只初始化一半时对外服务。
API 参考。 参见Vext 插件 API,了解 plugin option、setup hook、解析后的 host state 与 startup error。
createTestApp() 是 Vext 测试 fixture,返回 app/request/close 控制面;permissionPlugin(options) 先返回插件描述符,.setup(app) 才初始化 PermissionCore。server:beforeListen hook resolve void,用于证明启动检查已完成,不是 HTTP 响应。
2. 准备路由权限策略
目的与目标。 scope 选择 Vext host 的 tenant context;roles.create 创建 route-reader,roles.allow 允许对 normalized template api:GET:/orders/:id 执行 invoke,userRoles.assign 把角色追加给 u-vext。
状态、参数与结果。 Permission resource 匹配由 routes.protect 推导的 route template,而不是具体 /orders/42 URL。正是该持久化状态让 u-vext 得到 200,而另一个已认证用户得到 403。
失败与下一步。 action/resource template 不同、scope 错误或 assignment 缺失都会导致默认拒绝。应对比 route manifest、已存规则与 subject scope,再修正后端策略,不能弱化 route。
API 参考。 参见角色 API、用户角色 API和Vext 插件 API。
app.permission.scope(scope) 与普通 pc.scope() 相同;create/allow/assign 各自返回 mutation envelope。示例省略保存这些返回,只用后续真实 HTTP 结果验证授权生效。
3. 覆盖公开、认证与权限结果
目的与目标。 六次 request.get 分别访问 public route、没有认证的 protected route、使用无权限身份的同一路由、使用 u-vext 的普通路由、使用 u-vext 的 collection 数据路由,以及使用 u-vext 的 model 数据路由。
状态、参数与结果。 Fixture header middleware 只为提供了测试用户的请求创建 req.auth。插件把缺少认证的 401 与已认证但被拒绝的 403 区分开;允许的 handler 读取可信 permission subject 并生成 allowedBody。
失败与下一步。 401 表示 authentication 没有提供可信 identity;403 表示 authorization 拒绝具体 route。应分别诊断两层,不能把两者都改成通用 success 或 redirect。
API 参考。 参见Vext 插件了解请求 lifecycle,并参见Vext 插件 API了解 request context helper 与 error mapping。
testApp.request.get(path) 返回测试 HTTP response;.set() 只在 fixture 中模拟认证插件输入。六个普通请求响应分别读取 status,允许响应还从 allowed.body.data 读取 handler 结果。
4. 通过透明 app.db.collection 读取受保护数据
目的与目标。 app.db.collection 是 Vext handler 或 service 里读取业务数据的低心智入口。它看起来仍是原生 Vext DB 访问,但在受保护请求里实际走 permission-core 的授权集合门面,不是裸 MonSQLize collection。
状态、参数与结果。 data.collections.vext_orders.resource 把物理集合映射为逻辑资源 db:orders;这是覆盖项,不是普通同名 collection 的必填配置。scopeFields.tenantId 把当前 subject 的租户写成查询条件。data.transparent: true 让 app.db.collection('vext_orders') 在受保护请求里自动套权限。角色拥有 read + db:orders 后,/orders-data 返回 200,requestDataBody 只包含当前租户和允许字段。
失败与下一步。 少了 route invoke、少了数据资源 read、filter 不安全、没有配置租户字段,都会 fail closed。应修正角色规则或插件 data 配置,不要绕回裸数据库查询。
API 参考。 参见Vext 插件 API了解透明 DB 门面,并参见授权集合 API了解 find() 等集合方法。
5. 通过透明 app.db.model 读取受保护数据
目的与目标。 这一步展示 Vext model 层路径。handler 调用 app.db.model('Order');app.db.model 是 Vext 原生 model 入口,权限门面会先解析 model 对应的 collectionName,再按同一套数据规则处理。
状态、参数与结果。 Order model 指向 vext_orders,所以仍然使用 data.collections.vext_orders.resource 映射到 db:orders。角色拥有 read + db:orders 后,/orders-model 返回 200,modelDataBody.items 只包含当前租户和允许字段,modelDataBody.collectionName 报告 model 的物理集合名。
失败与下一步。 如果宿主 MonSQLize 没有暴露 model(),或受保护请求里调用 model 的 raw()、aggregate() 等高级方法,permission-core 会以 DATA_OPERATION_UNSUPPORTED fail closed。应使用受支持 CRUD,或为高级操作单独设计权限资源、规则和审计边界。
API 参考。 参见Vext 插件 API了解 VextAuthorizedModel 和透明 app.db.model()。
6. 拒绝热路由重载
目的与目标。 启动后发出 routes:ready 模拟 route graph 变化,再用 request.get 验证 permission-core 不会继续使用 stale manifest 服务。
状态、参数与结果。 插件把 route graph 标记为 restart-required,并让后续请求返回 503;routeReloadRequiresRestart 记录该 operational fail-closed 响应。
失败与下一步。 不能忽略 503 或继续使用旧 route permission。必须冷重启进程,让 startup 重新构建并验证完整 route manifest。
API 参考。 参见Vext 插件 API和故障排查,了解 VEXT_ROUTE_RESTART_REQUIRED 处理方式。
hooks.emit('routes:ready', ...) resolve 后把插件置为 restart-required;它不返回业务状态。随后 request.get('/public') 的原始 HTTP response status 为 503,证明整个 app fail closed。
7. 只关闭插件拥有的状态
目的与目标。 testApp.close 让插件关闭它创建的 PermissionCore instance;随后调用 monsqlize.health,证明宿主拥有的 database 仍保持连接。
状态、参数与结果。 Ownership 是非对称的:plugin shutdown 会 drain permission work,而宿主仍负责 shared database。两个 lifecycle 布尔值分别报告契约两侧。
失败与下一步。 Shutdown 失败时,应停止接受请求,完成 PermissionCore drain/close,再只在 host lifecycle boundary 关闭 MonSQLize。不能让 plugin 静默 dispose shared connection。
API 参考。 参见Vext 插件 API了解 teardown ownership,并参见核心与上下文 API了解 PermissionCore.close()。
testApp.close() resolve void 并触发插件 teardown;monsqlize.health() 返回宿主数据库 health object。permissionCoreClosedByPlugin 是已完成 close 的教程布尔量,hostDatabaseStillConnected 则从 health 字段计算。
预期输出
以下 JSON 是 printExample() 将多个 HTTP response、允许 body、数据 body 和两个生命周期事实组合后的示例汇总输出,不是 Vext 插件或某个 request 方法的原始响应。
responses 来源。 每个 status 都来自一个真实 fixture request.get response。Reload status 由独立 route-change probe 生成,因此这些值覆盖 public、authentication、authorization、collection/model data success 和 restart-required 边界。
allowedBody 来源。 只有允许的 request.get 会进入 protected-route handler 并输出该 body;其中 route parameter 与 subject user ID 证明 business code 使用可信 request context 前已经完成授权。
requestDataBody 来源。 该输出来自 vext-request-data 步骤中的 app.db.collection。它是受保护数据路由的 handler 响应,已经经过租户过滤和字段权限处理,不是数据库原始输出。
modelDataBody 来源。 该输出来自 vext-model-data 步骤中的 app.db.model。它证明 model 路径和 collection 路径使用同一套租户与字段权限。
lifecycle 来源。 testApp.close 证明 PermissionCore 一侧;关闭后的 monsqlize.health response 证明 host database 仍为 up 且 connected。
生产边界
createTestApp、内存数据库和 x-example-user 认证都是 fixture。生产环境在正常 Vext 插件图中注册 permissionPlugin,先加载认证,传入/发现宿主 MonSQLize 3.1 实例,并在路由变化后执行冷重启。用户请求路径中的业务数据读取优先走透明 app.db.collection() / app.db.model(),裸 MonSQLize 访问留给宿主基础设施。