ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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),仅供参考
返回列表