尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Keystone 6 Hooks 实战指南:在 CRUD GraphQL 操作中嵌入自定义业务逻辑

发布时间:2026/9/24 15:01:42

资讯中心
01
ARTICLE

Keystone 6 Hooks 实战指南:在 CRUD GraphQL 操作中嵌入自定义业务逻辑

Keystone 6 Hooks 实战指南:在 CRUD GraphQL 操作中嵌入自定义业务逻辑
Keystone 6 Hooks 实战指南在 CRUD GraphQL 操作中嵌入自定义业务逻辑【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址: https://gitcode.com/gh_mirrors/key/keystoneKeystone 6 为每个 list 自动生成完整的 CRUD GraphQL API而hooks机制允许你在这套核心操作的生命周期中注入自定义业务逻辑。本指南以官方文档docs/content/docs/guides/hooks.md为骨架结合packages/core源码实现系统讲解如何用 hooks 改写入参、校验数据、触发副作用并深入对比 list hooks 与 field hooks 的使用场景。什么是 HookHook 是定义在 schema 配置 中的函数在 GraphQL 操作执行时被触发。Keystone 支持四种核心 hookresolveInput、validate、beforeOperation和afterOperation分别覆盖数据解析、校验、写入前、写入后四个阶段。Hook 支持async函数除了resolveInput需要返回值外其余 hook 无需返回值在批量操作如createMany时每个被操作的数据项都会各自触发一次 hook。先看一个最基础的示例每次创建新用户时在控制台打印日志。import { config, list } from keystone-6/core; import { text } from keystone-6/core/fields; export default config({ lists: { User: list({ fields: { name: text(), email: text(), }, hooks: { afterOperation: ({ operation, item }) { if (operation create) { console.log(New user created. Name: ${item.name}, Email: ${item.email}); } } }, }), }, });这个函数会在 GraphQL API 执行create、update或deletemutation 时触发。由于afterOperation支持create/update/delete三种操作我们通过检查operation参数判断当前操作类型再用item参数读取新创建用户的值。用 resolveInput 改写入库数据当执行create或update操作时你可能希望在数据写入数据库前做预处理。例如保证博客文章的title字段首字母大写。resolveInputhook 允许我们拿到 GraphQL mutation 传入的数据并修改后再保存。import { config, list } from keystone-6/core; import { text } from keystone-6/core/fields; export default config({ lists: { Post: list({ fields: { title: text({ validation: { isRequired: true } }), content: text({ validation: { isRequired: true } }), }, hooks: { resolveInput: ({ resolvedData }) { const { title } resolvedData; if (title) { return { ...resolvedData, // Ensure the first letter of the title is capitalised title: title[0].toUpperCase() title.slice(1) } } // We always return resolvedData from the resolveInput hook return resolvedData; } }, }), }, });注意resolveInputhook 必须始终返回修改后的resolvedData即使你没有做任何修改。resolveInput在 create/update 时被调用resolvedData中的值是经过字段类型输入解析器处理后的结果。例如password字段会把明文转换成加密哈希。如果只想查看原始输入请使用inputData参数。在 update 操作中还可以通过item参数访问数据库中当前存储的值。所有 hook 都会收到context参数提供完整的 context API 访问能力。理解 resolvedData 的解析阶段从源码看resolveInput是 数据解析过程 的最终阶段。在packages/core/src/lib/core/mutations/index.ts中getResolvedData函数按以下顺序处理 create/update 数据初始化将resolvedData设置为 GraphQL mutation 的data输入值默认值内置仅 createresolvedData中为undefined且配置了默认值的字段被设为默认值关系字段内置关系字段的值被转换为 Prisma 嵌套写对象nested write objects嵌套 create 操作在此阶段执行并返回 ID所有connect、set、disconnect的项都会校验存在性to-many 关系返回{ connect, set, disconnect }对象to-one 关系返回{ connect }或{ disconnect: true }字段值内置某些字段类型将 GraphQL 输入转换为数据库所需的不同类型或格式如password的哈希转换见packages/core/src/fields/types/password/index.ts字段 hooks用户定义resolveInput字段 hook 可为单个字段返回新值列表 hooks用户定义resolveInput列表 hook 可为整个resolvedData对象返回新值字段级 hook 和列表级 hook 的resolveInput都在访问控制access control之后执行这保证了只有通过权限校验的数据才会进入你的业务逻辑。用 validate 校验输入数据在将解析后的数据写入数据库前常常需要根据业务规则校验。例如空字符串在 GraphQL 中是合法的String值但业务上可能不允许博客标题为空。validatehook 可以拒绝这类输入import { config, list } from keystone-6/core; import { text } from keystone-6/core/fields; export default config({ lists: { Post: list({ fields: { title: text({ validation: { isRequired: true } }), content: text({ validation: { isRequired: true } }), }, hooks: { validate: ({ resolvedData, addValidationError }) { const { title } resolvedData; if (title ) { addValidationError(The title of a blog post cannot be the empty string); } } }, }), }, });validatehook 接收的是默认值和resolveInputhook 完成之后的resolvedData。通过addValidationError函数上报错误信息可以多次调用来收集不同问题。Keystone 会中止操作并将这些错误消息转换为 GraphQL 错误返回给调用方。validatehook 还接收operation、inputData、item和context参数可用于更复杂的检查。从源码packages/core/src/lib/core/hooks.ts可见字段级校验 hooks 会并行执行Promise.all错误消息会以${list.listKey}.${fieldKey}: ${msg}的格式聚合列表级校验 hook 的错误则以${list.listKey}: ${msg}格式聚合最终抛出validationFailureError在 GraphQL API 层表现为ValidationFailureError。重要提醒不要把数据校验validation和访问控制access control混淆。若想判断用户是否被允许执行某操作应配置 访问控制规则而不是在validate里实现权限逻辑。用 beforeOperation / afterOperation 触发副作用系统数据变更时你可能需要触发外部副作用例如用户首次创建账号后发送欢迎邮件import { config, list } from keystone-6/core; import { text } from keystone-6/core/fields; // Keystone leaves it up to you to decide how best to implement email in your system import { sendWelcomeEmail } from ./lib/welcomeEmail; export default config({ lists: { User: list({ fields: { name: text(), email: text(), }, hooks: { afterOperation: ({ operation, item }) { if (operation create) { sendWelcomeEmail(item.name, item.email); } } }, }), }, });beforeOperation与afterOperation很相似但用途不同beforeOperation的item参数包含操作执行前数据库中已存储的数据create 时为undefinedafterOperation的item表示数据库中更新后的新数据更新前的原数据通过originalItem提供create 操作没有已存在的数据项delete 操作中afterOperation的item为null源码中为undefined见packages/core/src/lib/core/mutations/index.ts的deleteSingle__delete 时传入item: undefined, originalItem: item若beforeOperation抛出异常操作返回错误数据不会写入数据库若afterOperation抛出异常数据仍保留在数据库中。因此afterOperation应仅用于“执行失败不构成关键问题”的副作用从源码看runSideEffectOnlyHookpackages/core/src/lib/core/hooks.ts对beforeOperation/afterOperation的执行顺序有明确规定先并行执行字段级 hooks再执行列表级 hooks。一个值得注意的细节是对于 create/update 操作字段级操作 hooks 只在原始输入中显式包含该字段时才执行通过检查inputData的 key 集合判断而 delete 操作时字段级 hooks 始终执行。与 Prisma 写入的关系在createSingle__/updateSingle__packages/core/src/lib/core/mutations/index.ts中可以看到完整的调用链访问控制 →resolveInputForCreateOrUpdate包含getResolvedData解析 validate校验→beforeOperation()→context.prisma[list.listKey].create/update(...)→afterOperation(result)。也就是说beforeOperation执行时 Prisma 尚未写入afterOperation执行时数据已落库。delete 操作则是校验 →beforeOperation→ Prisma delete →afterOperation。List Hooks 与 Field Hooks 的选择以上示例都是 list 级别的 hooks。Keystone 同样支持在单个字段上配置 hooks所有同名的 hooks 都可用参数也一致只是额外多一个fieldKey参数。字段级 hooks 适合表达字段专属规则。例如把邮件格式校验写成一个字段 hook代码会清晰得多import { config, list } from keystone-6/core; import { text } from keystone-6/core/fields; export default config({ lists: { User: list({ fields: { name: text(), email: text({ validation: { isRequired: true }, hooks: { validate: ({ addValidationError, resolvedData, fieldKey }) { const email resolvedData[fieldKey]; if (email ! undefined email ! null !email.includes()) { addValidationError(The email address ${email} provided for the field ${fieldKey} must contain an character); } }, }, }), }, }), }, });在packages/core/src/types/config/hooks.ts的类型定义中字段级 hooks 相比列表级 hooks 多出inputFieldData、itemField、resolvedFieldData、originalItemField等参数分别对应字段的原始输入、数据库当前值、解析后值与原值方便在单字段维度做精细化处理。官方示例hooks 的完整用法仓库中的 examples/hooks/schema.ts 是一个完整的演示 schema展示了 hooks 在生产场景下的组合用法值得参考createdAt字段的resolveInput.create自动写入当前时间updatedAt字段的resolveInput.update在更新时刷新时间戳list 级resolveInput.create/update从context.req中提取客户端 IP 和 User-Agent写入createdBy/updatedBy字段实现审计追踪list 级validate.create/update对标题、正文和反馈做敏感词过滤/profanity/i并在delete时检查preventDelete标记阻止删除beforeOperation记录将要写入的数据afterOperation分别针对 createinput → item、updateoriginalItem → item、deleteoriginalItem → deleted打印差异日志Hook 参数速查列表级与字段级 hook 的常用参数汇总如下完整签名见 Hooks API 参考参数说明listKey被操作的列表 keyfieldKey被操作的字段 key仅字段级 hookoperation操作类型create/update/deleteinputDatamutation 传入的data原始值delete 时为undefinedinputFieldData输入数据中该字段的值仅字段级 hookdelete 时为undefineditem数据库当前存储的数据项create 时为undefinedoriginalItem更新/删除前的原始数据项仅afterOperationcreate 时为undefinedresolvedData经过默认值、关系解析器、字段解析器与resolveInputhooks 处理后的数据delete 时为undefinedcontext发起操作的 KeystoneContext 对象addValidationError(msg)上报校验错误仅validatehook相关资源Hooks API Referencemutation 生命周期各阶段执行代码的完整参考包含每个 hook 的参数表与resolvedData解析阶段详解Hooks Guide本文对应的官方指南examples/hooks可运行的完整 hooks 示例 schema源码实现validate与runSideEffectOnlyHook的运行时实现mutation 执行链hook 在 create/update/delete 全流程中的实际调用位置【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址: https://gitcode.com/gh_mirrors/key/keystone创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。