Ghost 中 @tryghost/admin-api-schema 包深度解析:Admin API 请求校验的单一事实来源

Ghost 中 @tryghost/admin-api-schema 包深度解析:Admin API 请求校验的单一事实来源 Ghost 中 tryghost/admin-api-schema 包深度解析Admin API 请求校验的单一事实来源【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文围绕 Ghost 仓库中的 packages/admin-api-schema/README.md 展开讲解tryghost/admin-api-schema这个私有工作区包如何以 JSON Schema 为底座成为 Ghost Admin API 所有请求校验的单一事实来源single source of truth你将了解它的三个核心 APIlist/get/validate、definitions action schema 双文件组织约定、底层 Ajv 引擎的定制配置以及它在 Ghost Core 校验层中的真实接入方式与错误处理语义。包的定位私有、内聚、随 Ghost 发布README 对该包的定位非常明确The package serves as a single source of truth when validating requests coming into Ghosts Admin API endpoints. It uses JSON Schema definitions under the hood to describe expected format of validated data.即所有进入 Ghost Admin API 端点的请求其数据格式校验都收敛到这一个包。它有两个关键工程属性私有工作区包package.json 中声明private: true、version: 0.0.0不会被独立发布到 npm仅供 Ghost Core 在 monorepo 内引用打包进发布产物README 指出它 is bundled into the Ghost release artifact and is not published independently——也就是说 Ghost 最终发布的产物里包含它编译后的代码而不是一个外部依赖。这一点可以从其exports配置看出default指向./build/index.jstsc 编译产物同时提供了source: ./src/index.ts入口供 monorepo 内部以源码形式消费。这种私有 随宿主发布的模式是 Ghost 内部基础设施包的典型组织方式——避免给核心运行时引入不可控的第三方版本漂移同时保证校验逻辑在开发期走source与发布期走build行为一致。三个核心 APIlist / get / validate包的公开 API 定义在 src/index.ts只有三个函数外加默认导出聚合对象import * as jsonSchema from tryghost/admin-api-schema; // check available schemas jsonSchema.list() /* [ comment_bans-add, images-upload, media-upload, labels-add, labels-edit, members-add, members-edit, members-upload, pages-add, pages-edit, posts-add, posts-edit, products-add, products-edit, tiers-add, tiers-edit, snippets-add, snippets-edit, tags-add, tags-edit, webhooks-add, webhooks-edit ] */ // get schema definition jsonSchema.get(tags-edit); /* { $schema: http://json-schema.org/draft-07/schema#, $id: tags.edit, title: tags.edit, description: Schema for tags.edit, type: object, additionalProperties: false, properties: { tags: { type: array, minItems: 1, maxItems: 1, items: [Object] } }, required: [ tags ] } */ // validate data const data { posts: [{ title: valid }] }; async function validate() { try { await jsonSchema.validate({data, schema: posts-add}); } catch (err) { console.log(validation error:, err); } } validate();对照源码三个函数各自承担的职责是API行为源码依据list()返回所有动作类schema 名称如posts-add不含纯 definitions 文件如postssrc/index.ts 直接展开actionSchemaNamesget(name)按名称取回 schema 对象名称不存在或为继承属性如toString时返回null而非抛错src/index.ts内部用Object.hasOwn防止原型链污染validate({data, schema, definition})异步执行校验成功时静默返回Promisevoid失败抛错src/index.tsvalidate有一个值得注意的细节definition参数默认从schema名称推导——取schema?.split(-)[0]。也就是说posts-add会自动搭配postsdefinitions 文件。这与双文件结构严格对应后面会展开。而list()只暴露动作类 schema并非偷懒test/api.test.ts 中有专门断言list()的结果必须与src/schemas/目录下所有匹配\w-\w.json命名的文件一一对应——即新增 schema 文件却忘记注册时测试会直接失败。文件组织约定definitions 与 action schema 分离打开 src/schemas/ 目录每个资源都有两个文件tags.json与tags-edit.json。这不是冗余而是一套成文的命名约定记录在 src/schemas/README.md 中definitions 文件以资源在 API 中的名字即 controller 配置里的docName命名。例如 ghost/core/core/server/api/endpoints/tags.js 对应的 definitions 文件就是tags.jsonaction schema 文件按{resourceName}-{methodName}.json命名。例如tags.edit方法对应tags-edit.json新增后必须在 src/schemas/index.ts 中 import 并注册进schemas表动作类schema 还要加入actionSchemaNames数组satisfies readonly SchemaName[]提供编译期约束这样才会被list()返回。两类文件的分工可以直观地看这对文件tags-edit.json 描述请求体长什么样顶层必须是对象tags必须是恰好 1 个元素的数组且additionalProperties: falsetags.json 描述单个 tag 长什么样在definitions.tag下声明name1–191 字符、pattern: ^([^,]|$)禁止逗号、slug、visibility枚举public/internal、meta_title、og_image等全部字段及各自的maxLength。两者通过 JSON Schema 的$ref关键词连接tags-edit.json里的items: { $ref: tags#/definitions/tag }。这正是 src/schemas/README.md 所说的 schema 复用模式——definitions 文件被多个动作 schema 共享避免同一资源的字段约束在 add/edit 之间漂移。文档也坦承该模式有局限JSON Schema 的$ref无法覆盖被引用定义的一部分所以约定是能复用尽量复用直到复用变成负担为止。全部动作 schema 清单与list()输出一致共 22 个覆盖 Ghost Admin API 的主要写操作资源comment_bans-add images-upload media-upload labels-add/-edit members-add/-edit/-upload pages-add/-edit posts-add/-edit products-add/-edit tiers-add/-edit snippets-add/-edit tags-add/-edit webhooks-add/-edit校验引擎Ajv 定制与两个方言扩展README 只说用 JSON Schema 描述期望格式而真正的实现细节在 src/utils/json-schema.ts。引擎是 Ajvdraft-07实例被配置为三个关键选项const ajv new Ajv({ allErrors: true, // 收集全部错误而不是遇到第一个就停 useDefaults: true, // 自动填充 schema 中声明的默认值 removeAdditional: true, // 静默删除 additionalProperties: false 对象中的未声明属性 });allErrorsremoveAdditional组合出了一个对 API 使用者非常友好的语义请求里夹带的未知字段不会导致 400而是被就地剥离数据对象被原地 mutation。test/api.test.ts 的 Unknown fields get ignored and trimmed 用例明确验证了这一点const data { posts: [{ title: valid, author: Beccy, // 未声明字段 something: else, // 未声明字段 }], }; await apiSchema.validate({ data, schema: posts-add, definition: posts }); assert.equal(data.posts[0].something, undefined); // 校验后字段已被移除 assert.equal(data.posts[0].author, undefined);这个静默修剪语义有一面需要小心的暗面测试文件用两个用例专门钉住了它的边界metafields 必须存活test/api.test.tsmembers的 definitions 里显式声明了metafields目的就是阻止removeAdditional把它当作未声明属性悄悄删掉——注释直言 dropping it is the failure mode the declaration exists to prevent不深究 metafields 内部结构无论值是null、数组、数字还是嵌套对象校验层都不检查、不递归剥离因为只有站点自己的字段定义才知道某个 key 接受什么真正的字段类型校验由下游的字段类型目录负责test/api.test.ts。在这套基础配置之上该包注册了两个标准 JSON Schema 没有的扩展自定义 formatjson-stringposts.json 中mobiledoc等正文内容字段声明了format: json-string。这个 format 在 json-schema.ts 中实现对字符串执行一次JSON.parse成功即通过。测试用例验证了合法 mobiledoc 字符串{version:0.3.1,atoms:[]}通过、not valid json被拒绝test/api.test.ts。这让必须是被序列化为 JSON 字符串的正文内容这类约束可以像uri-reference那样声明在 schema 里而不是散落在业务代码中。自定义 keywordisLowercasewebhooks.json 中event字段声明了isLowercase: true。实现位于 is-lowercase-keyword.ts是一个布尔型 schema 的字符串 keyword校验逻辑就是data data.toLowerCase()。测试覆盖了三个边界post.added通过、Post.Added拒绝、空字符串通过 .toLowerCase()天然成立test/api.test.ts。这两个扩展说明该包的设计取向凡是可以声明式的约束都尽量下沉到 JSON Schema 文本里使 schema 文件本身就是完整、自文档化的校验规则。错误处理语义两种异常一个统一的 ValidationErrorvalidate抛出两类完全不同的错误对应两种完全不同的故障来源1.IncorrectUsageError——使用方的调用错误。当schema或definition在注册表中查不到时抛出src/index.ts。注意这是同步抛出而非 reject——测试用assert.throws而非assert.rejects验证test/api.test.ts。这意味着引用了不存在的 schema是集成 bug比如 Ghost Core 的 endpoint 配置了docName/method但没有对应的 schema 文件应在开发期暴露。2.ValidationError——请求数据不合法。由 json-schema.ts 抛出携带三个信息throw new errors.ValidationError({ message: Validation failed for ${key}., property: key, // 出错字段名 errorDetails: validation.errors, // Ajv 的完整错误数组allErrors 的产物 });其中property的推导有 fallback 逻辑优先取第一条错误instancePath的最后一个路径段当instancePath为空例如顶层就缺required字段时回退到schema.$id的第一段。测试 Uses schema $id for error key when dataPath is empty 验证了传{}给posts-add时property为poststest/api.test.ts。这样 Admin API 的 HTTP 错误响应总能指出是哪个资源字段出了问题而不是只给一个模糊的 invalid。Ghost Core 中的接入方式README 给出的核心接入代码与仓库现状完全一致。Ghost Core 的 API 框架在 ghost/core/core/server/api/endpoints/utils/validators/utils/json-schema.js 中提供了一个极薄的桥接函数const jsonSchema require(tryghost/admin-api-schema); const validate async (apiConfig, frame) await jsonSchema.validate({ data: frame.data, schema: ${apiConfig.docName}-${apiConfig.method}, }); module.exports.validate validate;这里能看到整个体系的闭环endpoint 的 controller 配置docNamemethod→ 拼出${docName}-${method}→ 查tryghost/admin-api-schema注册表 → Ajv 执行。也就是说endpoint 文件如 ghost/core/core/server/api/endpoints/tags.js里的路由元数据与src/schemas/下的文件名通过同一个命名约定强绑定新增一个tags.something方法而忘记创建tags-something.json时请求会在运行期以IncorrectUsageError的形式暴露。definition参数则依赖split(-)[0]的默认推导自动落回tags.json这类共享 definitions。开发与验证从 Ghost 仓库根目录按 README 使用 pnpm filter 操作该包pnpm --filter tryghost/admin-api-schema test pnpm --filter tryghost/admin-api-schema lint pnpm --filter tryghost/admin-api-schema build对照 package.json 的 scripts 可以看到具体构成test是test:unit与test:types的组合前者以NODE_ENVtesting跑 vitest带覆盖率后者用独立的 test/tsconfig.json 做类型检查build就是tsc产物落在build/与exports.default指向一致并由 nx 配置登记为缓存输出lint分lint:code与lint:test两段分别覆盖src/与test/。api.test.ts 是理解该包行为的最佳入口——它同时验证了公开 API 形态、list()与目录文件的一致性、未知字段修剪、metafields存活、json-string/isLowercase两个自定义校验、以及两类错误的抛出路径基本把前文所有语义都固化成了可回归的断言。小结tryghost/admin-api-schema用一个简洁的三函数 APIlist/get/validate和一套definitions action schema双文件约定把 Ghost Admin API 全量 22 个动作端点的请求校验收敛为可复用的 JSON Schema 资产schema 文件即契约Ajv 的removeAdditional提供了宽松的修剪而非拒绝语义两个自定义扩展json-stringformat、isLowercasekeyword让正文序列化格式与大小写约束也能声明式表达而ValidationError.property的推导逻辑则保证了面向 API 使用者的错误信息始终指向具体字段。对于维护 Ghost Admin API 的开发者而言新增一个端点校验时只需遵循 src/schemas/README.md 的三步约定建文件、import 注册、动作 schema 入actionSchemaNames一致性便由list()对齐测试自动守护。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考