尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Ghost Admin API 端点开发实战:api-framework 请求管线、Controller 权限校验与 e2e 测试全流程

Ghost Admin API 端点开发实战:api-framework 请求管线、Controller 权限校验与 e2e 测试全流程 Ghost Admin API 端点开发实战api-framework 请求管线、Controller 权限校验与 e2e 测试全流程【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文以 Ghost 仓库中官方的 Add Admin API Endpoint 技能文档SKILL.md及其配套的参考、权限、校验三份指南为核心系统讲解在 Ghost 中新增 Admin API 端点的完整流程从tryghost/api-framework的五阶段请求管线、Frame对象、Controller 各配置属性到permissions的四种模式、声明式输入校验、路由注册与e2e-api测试验证帮助你在ghost/api/admin/**上安全、规范地交付新端点。一、新增 Admin API 端点的标准工作流Ghost 仓库在 .agents/skills/add-admin-api-endpoint/SKILL.md 中给出了官方推荐的操作步骤适用于在ghost/api/admin/**下新增端点创建或定位 endpoint 文件如果是全新资源在 ghost/core/core/server/api/endpoints/ 下新建 endpoint 文件否则在endpoints/目录下找到既有资源的 endpoint 文件并追加方法。编写 Controller 对象使用tryghost/api-framework提供的ControllerJSDoc 类型至少包含docName和一个端点定义例如browse。注册路由将每个端点的 HTTP 路由添加到 ghost/core/core/server/web/api/endpoints/admin/routes.js。编写 e2e 测试在 ghost/core/test/e2e-api/admin/ 下为新端点添加基础e2e-api测试确保新端点行为符合预期。运行测试并迭代在仓库根目录下执行cd ghost/core pnpm test:single test/e2e-api/admin/{test-file-name}直至测试通过。test:single脚本定义在 ghost/core/package.json 中会根据路径自动选择vitest或带数据库配置的vitest.config.db.ts来运行单个测试文件。下面结合仓库源码逐步展开每一环节背后的框架机制。二、api-framework 请求管线五阶段处理模型reference.md 将 API 框架描述为一个基于管线pipeline的系统每个 HTTP 请求在执行业务逻辑前会依次通过五个阶段从而让所有端点获得一致的校验、序列化与权限处理Input Validation输入校验—— 校验 query 参数、URL 参数与请求体Input Serialization输入序列化—— 转换入站数据例如把include映射为withRelatedPermissions权限—— 检查当前用户/API Key 是否有权访问该资源Query查询/业务逻辑—— 执行你编写的 controller 代码Output Serialization输出序列化—— 把结果格式化为客户端期望的响应结构。端点文件并不是直接生效的在 ghost/core/core/server/api/endpoints/index.js 中每个资源都通过管线包装后才对外暴露例如const apiFramework require(tryghost/api-framework); const localUtils require(./utils); module.exports { get posts() { return apiFramework.pipeline(require(./posts), localUtils); }, get tags() { return apiFramework.pipeline(require(./tags), localUtils); }, // ... };也就是说你在 endpoint 文件里写的是纯声明式配置apiFramework.pipeline(controller, localUtils)负责把校验器、权限处理器、序列化器localUtils指向endpoints/utils/装配成可调用函数需要 Content API 变体时则传入第三个参数content如pagesPublic、tiersPublic。Frame 对象贯穿全部阶段的上下文载体Frame类承载请求的全部信息并在各阶段间以引用方式传递、被就地修改。其结构如下摘自 reference.md{ original: Object, // 原始输入用于调试 options: Object, // query 参数、URL 参数、context、自定义选项 data: Object, // 请求体若配置了 data也可来自 query/URL 参数 user: Object, // 已登录用户对象 file: Object, // 单个上传文件 files: Array, // 多个上传文件 apiType: String, // content 或 admin docName: String, // 端点名如 posts method: String, // 方法名如 browse, read, add, edit response: Object // 由输出序列化阶段写入 }一个具体示例{ original: { include: tags,authors }, options: { withRelated: [tags, authors], context: { user: 123 } }, data: { posts: [{ title: My Post }] } }注意original保留原始输入而options.include在输入序列化阶段已被转换为withRelated数组——这正是管线“先转换、后消费”的典型体现。三、Controller 结构与各配置属性详解基本形态Controller 是一个带docName属性端点名必需与方法配置的对象。仓库中的真实示例见 ghost/core/core/server/api/endpoints/tags.js/** type {import(tryghost/api-framework).Controller} */ const controller { docName: tags, browse: { headers: { cacheInvalidate: false, }, options: [include, filter, fields, limit, order, page, debug], validation: { options: { include: { values: ALLOWED_INCLUDES, // [count.posts] }, }, }, permissions: true, query(frame) { return models.Tag.findPage(frame.options); }, }, // read / add / edit / destroy ... };各属性说明以下属性说明完整继承自 reference.md并结合仓库实现补充了细节。headersObject配置 HTTP 响应头。cacheInvalidate: true或{ value: /posts/* }——写操作后失效缓存disposition: { type: csv | json | yaml | file, value: export.csv }—— 用于下载类接口的文件处置头value可以是函数location: false—— 关闭add方法默认自动生成的Location响应头。optionsArray | Function允许进入frame.options的 query/URL 参数白名单如options: [include, filter, page, limit, order]。也支持函数形式按frame.apiType动态返回不同白名单。dataArray应放入frame.data而非frame.options的参数典型用于 READ 请求中模型期望findOne(data, options)的签名如data: [id, slug, email]。validationObject | Function输入校验配置详见第五节。permissionsBoolean | Object | Function权限配置必须显式声明详见第四节。queryFunction必需主业务逻辑返回 API 响应query(frame) { const { include, filter, page, limit } frame.options; // 已校验的选项 const postData frame.data.posts[0]; // 请求体 const userId frame.options.context.user; // 上下文 return models.Post.findPage(frame.options); }statusCodeNumber | FunctionHTTP 状态码默认 200。可固定statusCode: 201或按结果动态返回(result) result.posts.length ? 200 : 204。responseObject响应格式配置如response: { format: plain }以纯文本发送而非 JSON。cacheObject端点级缓存提供async get(cacheKey, fallback)与async set(cacheKey, response)两个钩子。generateCacheKeyDataFunction自定义缓存键生成默认使用frame.options可在此基础上追加字段。框架内置的通用模式reference.md 还总结了若干实战模式可直接照搬User Context 判断在query中通过frame.options.context.user / integration / member区分调用方身份为不同身份注入不同过滤条件如非管理员强制status:published。流式/特殊响应query可以返回一个函数handler(req, res, next)直接接管 Express 响应用于流式输出等场景。在 query 内设置响应头frame.setHeader(X-Custom-Header, value)。tags.js 的edit方法中就使用了该技巧——仅当模型确实发生变化时才frame.setHeader(X-Cache-Invalidate, /*)见 tags.js。四、permissions 配置四种模式与底层实现permissions.md 强调api-framework 采用基于管线的权限系统权限是五个阶段中的第三阶段每个 controller 方法必须显式定义permissions属性——省略该属性会抛出IncorrectUsageError。这是防止“无意识安全漏洞”、让权限处理显式化的硬性要求。模式 1permissions: true默认权限检查最常见的模式委托给默认权限处理器。默认处理器实现位于 ghost/core/core/server/api/endpoints/utils/permissions.js其执行逻辑单数形式推导把docName转为单数——posts→posties结尾走ies→y规则源码中对应apiConfig.docName.match(/ies$/)分支如categories→category。调用权限服务permissions.canThis(frame.options.context)[method]singular例如docName: postsedit方法即调用permissions.canThis(context).edit.post(postId, unsafeAttrs)。源码中还支持apiConfig.identifier(frame)由 controller 覆盖默认的资源标识符默认取frame.options.id以适配“改某条 setting 的 key 即标识符”这类场景。数据库核对权限服务在permissions与permissions_roles表中查找action_type匹配方法名、object_type匹配单数docName的记录并确认用户角色被授予了该权限。此外源码中还体现了两个细节权限检查可以返回excludedAttrs列表处理器会从frame.data[docName][0]中剔除这些属性后继续放行而非直接抛错见 permissions.js 注释当前主要服务于 contributor 角色编辑 posts 的场景NoPermissionError会被统一改写为You do not have permission to {method} {docName}的友好消息。数据库前提默认处理器生效需要在permissions表中存在对应记录如(Browse posts, browse, post)并在permissions_roles表中与 Administrator、Editor 等角色建立映射。这些记录通常通过两种途径维护初始 fixturesghost/core/core/server/data/schema/fixtures/fixtures.json数据库迁移使用addPermissionWithRoles()工具函数定义于 ghost/core/core/server/data/migrations/utils/permissions.js。模式 2permissions: false跳过权限完全绕过权限阶段适用于公共端点、健康检查等。permissions.md 明确警告仅在确定端点应当公开可访问时才使用——“显式地声明为公开”本身就是一种安全决策。模式 3函数式自定义权限逻辑delete: { options: [id], permissions: async function(frame) { // 未登录 → UnauthorizedError // 非资源所有者且非 admin → NoPermissionError return Promise.resolve(); }, query(frame) { return models.Resource.destroy(frame.options); } }适用于按资源变化的复杂逻辑、owner 权限、需要查库做决策的角色控制。permissions.md 给出的完整示例覆盖 owner 权限user_settings只能读写自己的记录、角色访问控制admin_settings仅 Owner/Administrator 可访问、仅 Owner 可编辑、以及带数据准备的permissions: { before }组合例如在检查前加载用户订阅状态供后续query使用。模式 4配置对象默认处理 钩子permissions: { unsafeAttrs: [author, status], before: async function(frame) { frame.user.permissions await loadUserPermissions(frame.user.id); } }unsafeAttrs指定需要提升权限才能修改的属性如只有管理员能改文章作者、发布状态、可见性、featured等。默认处理器会_.pick(frame.data[docName][0], unsafeAttrs)把这部分数据一并传给权限检查见 permissions.js。before在默认权限检查前运行的钩子用于预载权限判断所需的数据。场景与模式选择速查permissions.md 提供的对应关系如下场景推荐模式公共端点permissions: false标准认证 CRUDpermissions: true需要 unsafe attrs 追踪permissions: { unsafeAttrs: [...] }复杂自定义逻辑permissions: async function(frame) {...}需要前置数据准备permissions: { before: async function(frame) {...} }最佳实践包括权限函数只做权限检查、不掺入业务逻辑使用有意义的错误消息资源属于特定用户时必须校验所有权敏感字段一律走unsafeAttrs。错误类型统一取自tryghost/errorsUnauthorizedError未认证、NoPermissionError已认证但无权限、NotFoundError资源不存在慎用以防信息泄露、ValidationError校验失败。为新资源注册权限的迁移写法当你的端点使用permissions: true时必须通过迁移把权限写入数据库。permissions.md 给出完整模板文件放置于ghost/core/core/server/data/migrations/versions/X.X/下const {combineTransactionalMigrations, addPermissionWithRoles} require(../../utils); module.exports combineTransactionalMigrations( addPermissionWithRoles({ name: Browse my resources, action: browse, object: my_resource // docName 的单数形式 }, [Administrator, Admin Integration]), addPermissionWithRoles({ name: Read my resources, action: read, object: my_resource }, [Administrator, Admin Integration]), // ... edit / add / destroy );命名约定name为人类可读描述action为 API 方法browse/read/edit/add/destroyobject为docName的单数形式automated_email而非automated_emails。可用角色包括 Owner、Administrator、Editor、Author、Contributor、Admin Integration 等若端点仅允许管理员访问就只授予Administrator与Admin Integration两个角色。五、validation 配置声明式校验与函数式校验validation.md 将校验定位为管线的第一阶段确保必填字段存在、取值在允许列表内、数据类型正确ID、邮箱、slug 等在请求进入权限与业务逻辑前就拒绝非法结构。两种模式模式 1对象式最常用——通过配置对象声明规则browse: { options: [include, page, limit], validation: { options: { include: { values: [tags, authors], required: true }, page: { required: false } } }, permissions: true, query(frame) { return models.Post.findPage(frame.options); } }模式 2函数式——完全接管校验逻辑适合跨字段校验、条件规则、自定义错误消息add: { validation(frame) { const {ValidationError} require(tryghost/errors); const post frame.data.posts?.[0]; if (!post.title || post.title.length 3) { return Promise.reject(new ValidationError({ message: Title must be at least 3 characters })); } return Promise.resolve(); }, permissions: true, query(frame) { return models.Post.add(frame.data.posts[0], frame.options); } }校验 Optionsquery 参数必填filter: { required: true }允许值有等价的两种写法——对象记法include: { values: [tags, authors] }与数组简写include: [tags, authors]include参数的特殊行为非法取值会被静默过滤而非报错。例如请求?includetags,invalid_field,authors最终frame.options.include变为tags,authors——这是对客户端请求了不支持的 include 时的优雅降级设计。校验 Data请求体READ 操作的 data 来自 query/URL 参数如data: [id, slug]validation.data.slug.values: [featured, latest]ADD/EDIT 操作的 data 来自请求体且必须有根键root key结构如{ posts: [{ title: My Post, status: draft }] }框架自动校验根键存在、根键下是至少含一项的数组、必填字段存在且不为 null。内置全局校验器框架借助tryghost/validator对常见字段自动校验无需手写规则字段名校验规则合法示例idMongoDB ObjectId、1或me507f1f77bcf86cd799439011、meuuidUUID 格式550e8400-e29b-41d4-a716-446655440000slugURL 安全 slugmy-post-titleemail邮箱格式userexample.compage数字1、25limit数字或all10、allfrom/to日期格式2024-01-15order排序格式created_at desccolumns列名列表id,title,created_at而filter、context、forUpdate、transacting、include、formats、name默认不做全局校验。按方法区分的校验行为BROWSE / READ校验frame.data对apiConfig.data允许空 data使用全局校验器ADD依次校验根键存在 → 必填字段存在 → 必填字段非 null。典型错误No root key (posts) provided.、Validation (FieldIsRequired) failed for title、Validation (FieldIsInvalid) failed for title为 null 时EDIT执行全部 ADD 校验并额外校验 URL 与请求体中的 ID 一致性——/posts/123配 body{posts:[{id:456}]}会报Invalid id provided.特殊方法changePassword()、resetPassword()、setup()走 ADD 规则publish()走 BROWSE 规则。错误类型上校验阶段使用tryghost/errors的ValidationError字段校验失败与BadRequestError请求结构错误。函数式校验可附加context与help字段让错误对 API 消费者可操作例如return Promise.reject(new ValidationError({ message: Email address is required, context: Please provide a valid email address to continue, help: Check that the email field is included in your request }));validation.md 的最佳实践清单值得直接采纳显式列出全部允许的 options 防参数注入把常用类型交给内置校验器显式标注必填字段简单场景用数组简写复杂逻辑如日期区间不超过 30 天才上函数式错误消息具体且可执行。六、路由注册把 Controller 接入 Admin APISKILL.md 第 3 步指向的路由文件是 ghost/core/core/server/web/api/endpoints/admin/routes.js。其核心机制是tryghost/api-framework导出的http包装器const express require(../../../../../shared/express); const api require(../../../../api).endpoints; const { http } require(tryghost/api-framework); const apiMw require(../../middleware); const mw require(./middleware); module.exports function apiRoutes() { const router express.Router(admin api); router.use(apiMw.cors); // ## Public router.get(/site, mw.publicAdminApi, http(api.site.read)); // ## Posts router.get(/posts, mw.authAdminApi, http(api.posts.browse)); router.post(/posts, mw.authAdminApi, http(api.posts.add)); router.put(/posts/:id, mw.authAdminApi, http(api.posts.edit)); router.delete(/posts/:id, mw.authAdminApi, http(api.posts.destroy)); // ... };结合源码可以观察到几个注册细节认证中间件分层mw.publicAdminApi用于少数公开端点如GET /site绝大多数端点挂mw.authAdminApi带 URL 参数的场景另有mw.authAdminApiWithUrl如PUT /schedules/:resource/:id。上传类端点先挂apiMw.upload.single(postsfile)与apiMw.upload.validation({ type: posts })再交给http(api.posts.importCSV)文件随后经frame.file流入 query。Labs 开关新特性可用labs.enabledMiddleware(csvContentImporter)包裹路由按功能开关灰度开放。路由顺序敏感文件中显式注释 “browseAll must come before :id routes”如/comments必须在/comments/:id之前注册新端点时需避免与既有通配路径冲突。http(api.posts.browse)形式的包装器把管线产物适配为 Express handler因此 controller 中无需关心req/res生命周期。框架同时也支持程序化内部调用跳过 HTTP 层直接复用同一套校验/权限逻辑// 带 data 与 options const result await api.posts.add( { posts: [{ title: New Post }] }, // data { context: { user: userId } } // options ); // 仅 options const posts await api.posts.browse({ filter: status:published, include: tags, context: { user: userId } });此外endpoint 专属的校验器与序列化器可分别放在endpoints/utils/下输入校验器按{ add(apiConfig, frame) {...} }导出输出序列化器按{ posts: { browse(response, apiConfig, frame) {...} } }组织并写入frame.response。错误处理统一使用tryghost/errors的ValidationError/NotFoundError/NoPermissionError保证客户端拿到一致的错误结构。七、e2e 测试让新端点可被验证SKILL.md 第 4、5 步要求在 ghost/core/test/e2e-api/admin/ 下为新端点编写基础 e2e 测试然后以单文件模式运行直至通过cd ghost/core pnpm test:single test/e2e-api/admin/{test-file-name}该目录下已按资源组织了大量同类测试可供参照例如posts-bulk.test.js、pages.test.js、members.test.js、comments.test.js、config.test.js等它们示范了如何以 Admin API 客户端发起请求、断言状态码与响应结构、以及覆盖权限与校验错误路径。测试命名沿用e2e-api/admin/{资源}.test.js的约定与 SKILL.md 给出的命令参数保持一致。八、端到端速览与最佳实践把以上环节串起来一个新 Admin API 端点的完整交付物清单为ghost/core/core/server/api/endpoints/{资源}.js—— 声明式 controllerdocName 方法配置ghost/core/core/server/api/endpoints/index.js—— 通过apiFramework.pipeline(...)注册 getterghost/core/core/server/web/api/endpoints/admin/routes.js——router.{get,post,put,delete}mw.authAdminApihttp(...)如用permissions: true迁移文件addPermissionWithRoles(...)与/或 fixtures 中的权限记录ghost/core/test/e2e-api/admin/{资源}.test.js—— e2e 测试运行cd ghost/core pnpm test:single test/e2e-api/admin/{资源}.test.js迭代至通过。reference.md 与 permissions.md 汇总的最佳实践可浓缩为七条纪律始终显式声明permissions—— 省略即报错这是安全要求而非风格建议用options白名单收窄参数—— 未列入的参数不会进入frame.options优先声明式校验复杂逻辑再上函数式写操作设cacheInvalidate: true读操作设为false敏感字段用unsafeAttrs要求提升权限query直接返回模型响应把结构转换交给输出序列化器READ 端点用data承接findOne(data, options)所需的参数。遵循这套管线、权限与测试约定新端点即能融入 Ghost Admin API 现有的安全与工程体系校验在权限之前拦截非法输入权限在业务之前拦截越权访问序列化器保证输出一致性而 e2e 测试把整条链路固化为可持续回归的验证。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表