首页/新闻资讯/正文详情

PostGraphile v5 迁移指南:使用 wrapPlans 全面替代 makeWrapResolversPlugin

发布时间:2026/9/24 18:49:30 来源:云帆数科 栏目:资讯中心
PostGraphile v5 迁移指南:使用 wrapPlans 全面替代 makeWrapResolversPlugin
后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载PostGraphile v5 抛弃了传统 GraphQL resolver全面转向 Grafast的 plan 体系因此 v4 中广为人知的makeWrapResolversPlugin也随之退役。本篇指南以官方迁移文档为主线系统讲解为什么 resolver 包装在 v5 中已无意义、新插件生成器wrapPlans的 API 简化点以及 v4 时代三大典型用法设置 CRUD mutation 列值、执行前访问检查、返回数据加工如何一一映射到 v5 的 plan 世界。读完你将掌握wrapPlans的两种调用方法、lambda/sideEffect/context()等核心 step 的配合技巧以及迁移过程中可能遇到的 resolver 模拟emulation警告的规避方式。为什么 No makeWrapResolversPluginPostGraphile v5 的核心引擎 Grafast不再基于 GraphQL.js 传统的逐字段 resolver 执行而是先构建一份 plan执行计划再统一执行。既然没有 resolver 可包装makeWrapResolversPlugin自然就失去了意义——原迁移文档的标题直接写作 No makeWrapResolversPlugin以最直白的方式宣告了这个 API 的终结。但这并不意味着你失去了定制能力。恰恰相反由于 v5 的一切都发生在 plan 层面你可以通过包装 plan 做到比以前多得多的事情不仅可以影响返回的数据v4 的 resolver 包装也只能做到这一步还可以改变将要做什么这个计划本身——例如给插入操作追加一个列值、在 SQL 生成前插入一个校验步骤。wrapPlans正是接替makeWrapResolversPlugin的新插件生成器。它的 API 风格相似但做了大幅简化与 v4 的对应关系如下v4makeWrapResolversPluginv5wrapPlans说明requires声明siblingColumns/childColumns拉取额外列数据不再需要直接用 step 的方法获取所需数据例如$user.get(id)resolveInfo参数移除Grafast不需要它context参数移除需要时通过context()step 获取包装的是resolve(source, args, context, resolveInfo)包装的是plan($source, fieldArgs, info)见下文 Plan wrapper 函数签名下面把 v4 中你可能用makeWrapResolversPlugin做过的几件事逐一搬进 v5。场景一为 create/update mutation 设置列值v4 时代想在内置 CRUD mutation 里写入某个特定列值通常要用makeWrapResolversPlugin笨拙地覆盖系统对参数的认知甚至还要借助requires去拼接数据。v5 的 plan 体系让你可以直接、正面地解决这个问题——即使该列根本不在你的 GraphQL schema 里你也能在插入前给它赋值。import { wrapPlans } from postgraphile/utils; import { lambda } from postgraphile/grafast; const plugin wrapPlans({ Mutation: { // 该模式对 update 系列 mutation 同样适用 createPost(plan, $source, { $firstName, $lastName }) { // 先调用原始 plan得到原本要执行的结果 const $planResult plan(); // 拿到 PgInsertSingleStep 的引用。 // 记住它现在只是一个 step尚未真正执行我们仍然可以 // 增强它未来将要做的事情。 const $insert $planResult.get(result); // 假设遗留的 name 字段需要由 firstName/lastName 拼接而成 // 针对每一组字段元组执行回调 const $name lambda( [$firstName, $lastName], ([firstName, lastName]) ${firstName} ${lastName}, // 回调是同步的且不会抛错可以放心声明 sync-and-safe true, ); // 将 $name 设置为 PgInsertSingleStep 中 name 列的值 $insert.set(name, $name); // 返回值保持与原始 plan 一致否则依赖它的其他 plan 可能出错 return $planResult; }, }, }); export default plugin;几个关键点值得展开$planResult.get(result)的由来内置 CRUD mutation以及函数型 mutation的字段 plan 返回的通常是一个对象 step形如object({ result: $step })真正的插入/更新/删除 step或函数调用 step挂在result属性上。因此get(result)是跨 mutation 类型保持一致的标准取法这也是官方刻意为之方便插件作者统一处理。更完整的说明见 wrap-plans.md 中的对应注解。PgInsertSingleStep.set()的底层实现在 pgInsertSingle.ts 中PgInsertSingleStep实现了SetterCapable接口其set(name, attVal)第 352 行会先通过resource.codec.attributes[name]校验列是否合法不认识的列会直接抛错Attribute ${name} not recognized然后把该列连同值一起追加进即将生成的INSERTSQL 语句中。也就是说你在这一步为 plan 追加的任何set()调用最终都会体现在真正发给 PostgreSQL 的 SQL 里——这正是改变计划本身的威力。lambda的取舍lambda是 Grafast的逃生舱不做批处理只适合同步、轻量的纯转换拼接字符串、映射数组等。它在 lambda.ts 中的实现会通过multistep()把传入的数组自动包装成 list step并在回调执行时做isSyncAndSafe相关的优化判断若回调带副作用则会被提示改用sideEffect。详细的行为约束可查阅 lambda.md。场景二在字段 plan 执行前做访问检查带副作用的 step 永远不会被树摇tree shake或去重de-duplicate所以如果你希望在 mutation 真正发生之前抛错可以在 plan 里显式插入一个前置检查stepimport { sideEffect, context } from postgraphile/grafast; import { wrapPlans } from postgraphile/utils; const plugin wrapPlans({ Mutation: { createUser(plan) { // 从 GraphQL context 中取出 isAdmin 属性 const $isAdmin context().get(isAdmin); // 若不是管理员则抛出错误 const $preCheck sideEffect($isAdmin, (isAdmin) { if (!isAdmin) { throw new Error(Abort); } }); // 再调用底层 plan如果上面抛错这些 plan 永远不会执行 return plan(); }, }, }); export default plugin;对应到源码context()定义在 global.ts返回一个代表 GraphQLcontextValue的 step之后用.get(isAdmin)提取具体字段。官方推荐通过 TypeScript 的声明合并declare global { namespace Grafast { interface Context { ... } } }让context().get(...)具备类型安全参见 context.md。sideEffect()定义在 sideEffect.ts其SideEffectStep在构造时无条件把hasSideEffects置为true第 32 行这正是它永不被树摇/去重的机制来源。它支持传单个 step、null无输入、元组或对象形式的 multistep详见 sideEffect.md。:::warning 带副作用的 plan 只在Mutation类型的字段 plan 中被官方支持/预期。在其他位置放置副作用 plan 可能导致意想不到的结果——这一点与 GraphQL 规范副作用只允许出现在 mutation 根选择集的约定一致。 :::场景三加工字段返回的数据以 email 掩码为例开发者常把用户email直接存在users表里下方有为什么不应该这么做的提示。通常你不希望其他用户看到别人的邮箱于是可以包装字段 plan 来掩码import { wrapPlans } from postgraphile/utils; import { context, lambda } from postgraphile/grafast; const plugin wrapPlans({ User: { email(plan, $user, args, info) { // 从 GraphQL context 中取 userId const $myUserId context().get(userId); // 取该用户的 ID const $theirUserId $user.get(id); // 通过原始 plan 拿到 email const $email plan(); // 返回一个新 plan仅当 ID 匹配时才返回 email return lambda( [$myUserId, $theirUserId, $email], ([myUserId, theirUserId, email]) { if (myUserId theirUserId) { return email; } else { return null; // TODO: 请确认 email 字段本身是可空的 } }, ); }, }, }); export default plugin;注意这里与 v4 的巨大差异v4 中要做同样的事需要在requires里声明siblingColumns: [{ column: id, alias: $user_id }]然后从解析后的user.$user_id与context.jwtClaims.user_id比较。而 v5 中父级数据本身就是一个 step$user.get(id)直接拿到所需列的 steprequires机制就此退场。更妙的是你还可以在掩码与置空之间自由选择。官方 wrapPlans 文档wrap-plans.md里给出了掩码而非置空的变体用默认 plan 解析器取到真实值再用正则把它变成so***su***.com这类形态export default wrapPlans({ User: { email(plan) { const $email plan(); return lambda($email, (email) // someonesub.example.com - so***su***.com email.replace( /^(.{1,2})[^]*(.{,2})[^.]*\.([A-z]{2,})$/, $1***$2***.$3, ), ); }, }, });:::tip 把email存进users表通常是不好的设计一是它会让安全模型复杂化见下方警告二是多样性问题——从 1 个邮箱升级到 2 个邮箱远比从 2 个升级到 3 个困难。即使起步阶段只允许一个邮箱也建议设计成支持多个邮箱的系统例如把邮箱存入独立的user_emails表。 ::::::danger 上面的示例只在直接按字段获取时掩码了email但仍然存在侧信道攻击风险攻击者可以按邮箱地址排序然后从cursor中提取邮箱也可以利用高级过滤做字典攻击来猜测某个用户的邮箱。强烈建议把邮箱等私密信息存进单独的表以获得最佳安全性。 :::深入 wrapPlans两种调用方法wrapPlans有两个重载function overload对应两种调用方式。完整类型签名与规则对象定义见 wrap-plans.md。方法一包装已知字段的单个 resolverfunction wrapPlans( rulesOrGenerator: PlanWrapperRules | PlanWrapperRulesGenerator, options?: WrapPlansOptions, ): GraphileConfig.Plugin; interface PlanWrapperRules { [typeName: string]: { [fieldName: string]: PlanWrapperRule | PlanWrapperFn; }; } interface PlanWrapperRule { /** 计划包装函数 */ plan?: PlanWrapperFn; /** * 设为 false 表示当你调用底层 plan 时不希望我们确保 fieldArgs 已经应用。 */ autoApplyFieldArgs?: boolean; }如果你只想包装一两个已知类型/字段的 plan例如上面的Mutation.createPost、User.email方法一最顺手。规则对象是typeName - fieldName - 规则或包装函数的两层映射也可以传一个生成器函数(build) PlanWrapperRules利用build对象读取预设的 schema 选项或 registry 中的内容。当有多个字段需要以完全相同的方式包装时方法一同样适用。比如 v4 的经典validateUserData模式在 v5 中长这样import { sideEffect } from postgraphile/grafast; import { wrapPlans } from postgraphile/utils; function assertValidUserData(data) { if (!data || data.username?.length 0) { throw new Error(Invalid data); } } const validateUserData (propName) { return (plan, $source, fieldArgs) { const $user fieldArgs.getRaw([input, propName]); // 回调若发现非法数据则抛错 sideEffect($user, (user) assertValidUserData(user)); return plan(); }; }; export default wrapPlans({ Mutation: { createUser: validateUserData(user), updateUser: validateUserData(userPatch), updateUserById: validateUserData(userPatch), updateUserByEmail: validateUserData(userPatch), }, });方法二按过滤器包装所有匹配的 resolverfunction wrapPlansT( filter: ( context: GraphileBuild.ContextObjectFieldsField, build: GraphileBuild.Build, field: GrafastFieldConfig, ) T | null, rule: (match: T) PlanWrapperRule | PlanWrapperFn, options?: WrapPlansOptions, ): GraphileConfig.Plugin;当你想要用同样的方式包装一大批 plan时方法二更灵活第一个函数对每个字段调用返回真值表示该字段需要被包装返回null表示跳过第二个函数对每个通过过滤器的字段调用接收过滤器的返回值并返回一个包装函数或规则。过滤器参数说明context字段的Context值其中context.scope最常用如context.scope.isRootMutation、context.scope.fieldNamebuildBuild对象包含大量辅助工具field字段规格本身。过滤器可以返回任意真值把上面三个参数中你需要的信息打包进去即可。官方示例——给每个 mutation 打印执行前后的日志import { wrapPlans } from postgraphile/utils; import { sideEffect } from postgraphile/grafast; // 示例在每个 mutation 执行前后打日志 export default wrapPlans( (context) { if (context.scope.isRootMutation) { return { scope: context.scope }; } return null; }, ({ scope }) (plan, _, fieldArgs) { sideEffect(fieldArgs.getRaw(), (args) { console.log( Mutation ${scope.fieldName} starting with arguments:, args, ); }); const $payload plan(); sideEffect($payload, (payload) { console.log(Mutation ${scope.fieldName} payload:, payload); }); return $payload; }, );Plan 包装函数PlanWrapperFn包装函数与 Grafast的 plan resolver 几乎一样只是在最前面多了一个plan参数用于委托给被包装的 plan resolvertype PlanWrapperFn ( plan: SmartFieldPlanResolver, $source: Step, fieldArgs: FieldArgs, info: FieldInfo, ) any;调用规则调用plan()时可以不传、也可以传$source, fieldArgs, info中的一个或多个来覆盖原值完全不传参数则原样透传。一个值得注意的演进点较新版本的wrapPlans会在你调用底层plan()时自动应用fieldArgs即你的包装逻辑生效于字段参数被应用之后这有助于避免因包装器引入副作用而导致的非法 plan 层级问题见 CHANGELOG 中 #2736 的说明。若你确实不希望如此可以用规则对象形式显式关闭wrapPlans({ Query: { someField: { autoApplyFieldArgs: false, plan(plan, $parent, fieldArgs) { // 自行决定何时应用 fieldArgs }, }, }, });加载 wrapPlans 生成的插件wrapPlans返回的是一个标准 schema plugin加载方式与任何插件一致——在graphile.config.mjs或类似 preset 文件的plugins数组中注册import MyWrapPlugin from ./myWrapPlugin.mjs; export default { // ... plugins: [MyWrapPlugin], };注意从 v5 开始导入路径也变了——wrapPlans从postgraphile/utils导入lambda、sideEffect、context等从postgraphile/grafast导入而不是 v4 的graphile-utils。仓库自带的 graphile.config.ts 中就有wrapPlans的真实使用示例可作为参照。更完整的插件加载说明见 extending.mdx。关于 resolver 模拟emulation警告如果wrapPlans包装的字段恰好没有自定义 plan它会默认去包装 Grafast的defaultPlanResolver其实现位于 defaultPlanResolver.ts本质就是get($source, info.fieldName)——从父 step 上取出同名属性。此时若 schema 中混入了传统 resolverGrafast会进入 resolver emulation 模式而这可能改变喂给 resolver 的数据、引发难以排查的问题。因此当你的包装范围很广时wrapPlans会发出形如下面的警告完整说明见 wpr.md[WARNING]: wrapPlans(...) plugin WrapPlansPlugin_1 has wrapped the default plan resolver at field coordinate User.email. If this is an impure schema (one that mixes traditional resolvers with Gra*fast* plan resolvers) then this may result in hard to track down issues - hence this warning.纯 Grafastschema只用 plan resolver、没有任何传统resolve/subscribe可以安全忽略此警告甚至用disableResolverEmulationWarnings: true关掉它。混合 schema通过extendSchema()等方法掺入了传统 resolver建议按下面任一方式处理。规避方式有三种WrapPlansOptions支持name、version、description、disableResolverEmulationWarnings等选项便于调试与定位为被包装的字段提供一个非默认的plan resolver避免包装defaultPlanResolver例如在过滤器里先检查字段是否用的是默认解析器再决定是否包装const MyPlugin wrapPlans( (context, build, field) { const { grafast: { defaultPlanResolver }, } build; const plan field.extensions?.grafast?.plan ?? defaultPlanResolver; // 不包装默认 plan resolver if (plan defaultPlanResolver) return null; // ... }, // ... );确认 schema 安全后显式关闭警告const MyPlanWrapperPlugin wrapPlans(rules, { name: MyPlanWrapperPlugin, disableResolverEmulationWarnings: true, });迁移要点小结思维模型转变v4 包装的是执行后的结果v5 包装的是即将执行的计划。前者只能影响返回数据后者还能改变将要执行的 SQL 与行为序列。三个 API 简化requires被 step 方法取代resolveInfo彻底移除context改为通过context()step 按需获取。三个核心 steplambda同步纯转换、sideEffect副作用/校验/日志、context()读取 GraphQL context是编写 plan 包装器的主要积木均可在 standard-steps 目录 下找到对应文档。通用兜底新增字段/类型请用extendSchema相关用法见 extend-schema.mdwrapPlans专门用于保留既有字段、只调整它的计划或解析方式。安全红线plan 包装器中的sideEffect只应出现在Mutation字段上对私密字段做返回时加工不能替代根本不要把私密数据放在同一张表的架构决策。赞分享后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载相关推荐PostGraphile V5 迁移指南用 wrapPlans 取代 makeWrapResolversPluginPostGraphile V5 迁移指南用 wrapPlans 取代 makeWrapResolversPlugin PostGraphile V5 全面转向后端API网关PostGraphile v5 迁移用 inflection.add 与 inflection.replace 取代 makeAddInflectorsPluginPostGraphile v5 迁移用 inflection.add 与 inflection.replace 取代 makeAddInflectorsPlu后端API网关PostGraphile V5 迁移指南从 makeAddPgTableOrderByPlugin 到 addPgTableOrderByPostGraphile V5 迁移指南从 makeAddPgTableOrderByPlugin 到 addPgTableOrderBy PostGraph后端API网关上一篇5个免费足球开源数据集清单如何用Soccer Analytics Handbook快速获取StatsBomb与Metrica数据下一篇MDT版本更新日志v0.2.28新特性详解支持游戏1.4.2版本与性能优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

C++ Win32塔防游戏教学框架:纯标准库实现可调试游戏骨架
C++ Win32塔防游戏教学框架:纯标准库实现可调试游戏骨架

简介:本资源是一套基于C开发的塔防类游戏源码,完整复刻《王国保卫战》核心玩法,专为计算机、自动化等专业本科生课程设计与毕业设计实践打造。代码结构清晰,涵盖游戏主循环、关卡管理、塔与怪物基类、UI界面及音效系统等模块&… · 2026/9/24 18:49:15

Java Web毕设实战:JSP超市管理系统部署与答辩全指南
Java Web毕设实战:JSP超市管理系统部署与答辩全指南

简介:本资源是一套完整的Java毕业设计项目——基于JSP与MySQL开发的超市管理系统,面向计算机专业本科生及Java初学者,用于课程设计、毕设参考或Web开发入门实践。压缩包为ZIP格式,大小4.13MB,包含可直接运行的源码、配… · 2026/9/24 18:49:15

C++塔防游戏框架:SFML+VS2022可编译可调试原型
C++塔防游戏框架:SFML+VS2022可编译可调试原型

简介:这是一份基于C开发的塔防类游戏源码,完整复刻《王国保卫战》核心玩法,专为计算机、自动化等专业本科生课程设计与毕业设计打造。项目采用面向对象设计,涵盖游戏主循环、塔防逻辑、怪物AI、关卡系统及UI交互等完整模块&#x… · 2026/9/24 18:49:15

2026年抗跌IT岗位盘点:从成本视角看哪些技术岗更稳
2026年抗跌IT岗位盘点:从成本视角看哪些技术岗更稳

最近跟几个做技术管理的朋友聊天,大家有一个共同的感受:IT岗位这两年的变化,比过去十年都要剧烈。我所在的城市,过去半年陆陆续续听到不少团队调整的消息,有的是整条业务线收缩,有的是组织架构重组&#xf… · 2026/9/24 19:32:28

Java Web图书管理系统毕业设计:JSP+Servlet+Layui+MySQL完整实现与避坑指南
Java Web图书管理系统毕业设计:JSP+Servlet+Layui+MySQL完整实现与避坑指南

简介:这是一套面向高校计算机专业学生与Java Web初学者整理的图书管理系统完整项目,可直接用于毕业设计、课程大作业或自学练手。系统基于JSP、Servlet、Layui与MySQL开发,运行环境为IDEA、JDK1.8、MySQL5.7与Tomcat9,界面美观且带… · 2026/9/24 19:32:28

MySQL MHA与Redis Sentinel高可用架构实战解析
MySQL MHA与Redis Sentinel高可用架构实战解析

1. 我为什么把 MySQL MHA 和 Redis 哨兵放在一起聊 先给结论:MySQL 的高可用和 Redis 的高可用,看起来都是“主从切换”,背后是两套完全不同的思路。MySQL 主从复制只能保证数据有副本,真出故障时要靠 MHA 这样的工具去“拼一把”… · 2026/9/24 19:32:22

YOLOv5吸烟检测实战:从数据标注到部署的完整指南
YOLOv5吸烟检测实战:从数据标注到部署的完整指南

简介:本资源为基于YOLOv5-6.0训练完成的吸烟行为检测模型包,面向计算机视觉学习者、行为识别方向的研究人员及需要落地吸烟检测功能的开发者。包内提供YOLOv5m与yolov5s两个已训练权重,目标类别为smoke,在数千张吸烟数据上训练&am… · 2026/9/24 19:32:22

SpringBoot+Vue实现个性化音乐推荐系统:协同过滤与工程化实践
SpringBoot+Vue实现个性化音乐推荐系统:协同过滤与工程化实践

做音乐推荐系统之前,我以为最麻烦的部分是推荐算法。真正把SpringBoot后端和Vue前端完全打通、让推荐结果在页面上流畅跑起来之后,我才意识到,算法只占三成工作量,剩下七成全在工程化——用户行为怎么埋点、数据怎么清洗、冷启动怎… · 2026/9/24 19:32:22

C++模拟算法详解:从约瑟夫环到扫雷展开的实战指南
C++模拟算法详解:从约瑟夫环到扫雷展开的实战指南

2. 先把模拟算法说清楚:它到底在“模拟”什么这几年在算法社区里看帖,发现一个很有意思的现象:一提起模拟题,很多人的第一反应是“这不就是照着题目写代码吗,有什么技术含量”。但真到了比赛或者实际项目里&#xff0c… · 2026/9/24 19:32:22

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码