Graphile Crystal Monorepo 全景指南:从 Grafast规划执行引擎到 PostGraphile 自动 GraphQL API
【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal
Graphile Crystal 是 Graphile 团队围绕 GraphQL 技术栈打造的 monorepo 仓库,核心承载两大明星项目:下一代 GraphQL 规划与执行引擎Gra*fast*,以及基于 PostgreSQL 自动生成高性能 GraphQL API 的PostGraphile,同时还包含pg-sql2、pg-introspection、graphile-config、graphile-export等可独立使用的基础库。本文以仓库根目录 README.md 为骨架,结合各子包源码与文档,系统梳理整个技术栈的定位、用法与底层实现,帮助读者快速判断哪些包适合引入自己的项目,以及如何将它们组合成一条高效的 GraphQL 后端链路。
仓库定位:Graphile 的 "Crystal" 全家桶
据根目录 README.md 介绍,这个仓库收纳了 Graphile 旗下几乎所有与 GraphQL 相关的包——包括与 GraphQL 相关的包、以及与"与 GraphQL 相关的包"相关的包。两个主角分别是:
- Gra*fast*:面向 GraphQL.js 的尖端规划与执行引擎,可作为 GraphQL.js 官方
execute方法的即插即用替代品。通过把传统 resolver 迁移到 Gra*fast* 的 "plan resolver"(计划解析器),可以借助 GraphQL 请求的声明式特性,以最高效的方式执行业务逻辑,从而降低服务器负载。 - PostGraphile:以 PostgreSQL 数据库为核心数据源,自动生成结构良好、高性能的 GraphQL API,主打低投入、高性能、自动最佳实践与可扩展性。
仓库的工程组织方式是 Yarn Workspaces 多包仓库(见 package.json 中workspaces字段),工作区覆盖grafast/*、graphile-build/*、postgraphile/*、utils/*四组包。环境要求为node >= 22、yarn >= 1.3.2,且通过packageManager声明使用yarn@4.12.0。
提示:需要 PostGraphile V4(旧版)的用户,README 指明请前往
legacy分支查看,本仓库主线对应的是 PostGraphile V5 及配套的新一代技术栈。
Gra*fast*:面向 GraphQL 的规划与执行引擎
核心理念:从 resolver 到 plan resolver
grafast/grafast/README.md 详细描述了 Gra*fast* 的工作方式:它理解 GraphQL,并在"你的帮助下"理解业务逻辑,从而以极高效率编排一次 GraphQL 请求的数据需求。
传统 GraphQL.js 的执行方式是为每个字段直接调用 resolver 函数;而 Gra*fast* 引入了plan resolver:每个字段不再直接返回数据,而是描述"要执行该字段所需的抽象步骤(step)"。一次请求中所有字段的 step 会被组合成一个operation plan(操作计划),该计划在执行前可以被整体重写与优化,并且常常能被未来相似的查询直接复用——这是 Gra*fast* 效率优势的核心来源。
关键的一点是,Gra*fast*向后兼容传统 resolver:绝大多数现有 GraphQL.js schema 都能直接交给 Gra*fast* 执行,且通常能获得小幅提速。只有把 resolver 替换为 plan resolver,才能看到真正的效率跃升。此外,Gra*fast* schema 本身就是 GraphQL.js schema,因此 schema-first、code-first 或自动生成等构建方式全都适用。
执行流程
当 Gra*fast* 第一次见到某个 GraphQL 请求时,会经历如下阶段(见 grafast/grafast/README.md):
- 规划(plan):分析请求的数据需求、需要执行的步骤、如何把结果写入响应;
- 优化(optimize):对"初稿计划"进行重写优化,例如删除冗余或重复的处理步骤、重写并合并处理步骤,以获得最佳性能;
- 执行(execute):执行计划并把响应返回给客户端;
- 复用:后续与该计划兼容的请求可以直接执行,无需重新规划。
从源码结构看,规划与执行逻辑分布在 grafast/grafast/src/engine/ 下(如executeBucket.ts、executeOutputPlan.ts等),而对外导出的execute函数定义在 grafast/grafast/src/execute.ts,印证了 README 所述"即插即用替代execute"的接口设计。
使用方式:两行代码迁移
README 给出了两种迁移方式,把从graphql模块导入换成从grafast导入即可:
-import { graphql } from "graphql"; +import { grafast as graphql } from "grafast";-import { execute } from "graphql"; +import { execute } from "grafast";任何允许替换execute方法的 GraphQL 服务器(包括所有完整支持 Envelop 的服务器)都可以接入 Gra*fast*。
兼容性要求与建议
要在 Gra*fast* 上正常运行,schema 需要满足以下条件(摘自 grafast/grafast/README.md):
- 使用GraphQL.js v16+;
- 不得覆盖 GraphQL 默认字段 resolver(该能力暂未支持);
- 每次请求的
context必须是一个对象(需要能作为WeakMap的键使用;如果不需要 context,传{}即可); - 仅支持 schema 中显式定义的字段 resolver,通过
rootValue传入的 resolver 暂不支持; - 对传统 resolver 第四参数
resolveInfo的支持未完全对齐,尤其是resolveInfo.path属性目前不支持。
为了最大化收益,README 还给出三条关键实践建议(见 grafast/grafast/README.md):
- 关键:用 LRU 缓存缓存解析后的 GraphQL document(AST),使同一文档反复复用同一 AST——
grafserv会帮你处理,Envelop 用户可用@envelop/parser-cache; - 不要使用
rootValue(改用context); - 尽量对 variables 对象做 memoize(例如基于
canonicalJSONStringify(variables)做缓存),使相同 variables 命中同一内存对象; - 用 LRU 缓存 GraphQL
context对象,让同一用户复用同一 context(这条相对次要,迁移期仍需使用 DataLoader 时可以放宽)。
这些建议背后的逻辑是:Gra*fast* 的规划结果复用程度越高,性能收益越大。
配套计划类库
Gra*fast* 生态提供了两组针对具体数据源的 plan 类:
- @dataplan/pg:用于与 PostgreSQL 交互的高度优化 Gra*fast* step 类集合。README 将其定位为 "A collection of extremely highly optimized Gra*fast* step classes for interacting with PostgreSQL",PostGraphile 的 SQL 数据访问正是建立在它之上。
- @dataplan/json:用于 JSON 编解码的 plan 类。
两者都以独立 npm 包的形式位于grafast/工作区下,可脱离 PostGraphile 单独用于自建 Gra*fast* schema。
PostGraphile:把 PostgreSQL 变成 GraphQL 事实源
工作方式
PostGraphile 的定位是"只写真正带来价值的代码"。开箱即用地分析 PostgreSQL 数据库(表、关系、函数、索引、权限以及你的配置),生成一个完整、一致的 GraphQL schema,并且这个"活的基础"会随数据库演进。在此基础上可以无缝叠加定制与扩展:
- 用自定义类型和字段扩展 schema(字段可以执行 SQL 或 Node.js 代码);
- 用数据库权限决定暴露哪些部分,快速、符合人体工学、粒度精细,还能提升安全态势;
- 用强大的插件与 preset 系统对生成的 schema 施加通用偏好;
- 用简单的 "tags"(智能注释)微调单个数据库实体:重命名、决定暴露方式、改变类型/呈现/nullability、标注抽象类型(interface/union)、引入额外关系等;
- 用 "inflection" 系统全局重构生成的命名;
- 使用第三方插件扩展能力。
PostGraphile 几乎所有功能——从 introspection 到类型生成再到分页参数——都是通过插件实现的,插件 API 面向使用者设计,并提供帮助工厂(helper factories)让常见需求更易用。
在效率方面,PostGraphile 由 Gra*fast* 引擎驱动,README 指出其性能通常优于使用传统 GraphQL.js resolver 的手写 schema。同时它"无锁定":必要时可以通过 graphile-export 把 schema 导出为可执行代码,脱离 PostGraphile 自行维护,仍保留完整规划的执⾏性能优势。
实战示例一:用 SQL 函数与智能注释定制字段
PostGraphile README 给出了一个电商结账场景:数据库只有products、prices、cart_items等底层表,而前端需要subtotal、tax等汇总字段。第一种做法是直接在数据库里处理(见 postgraphile/postgraphile/README.md):
-- 从 GraphQL schema 中隐藏 "prices" 表 comment on table prices is '@behavior -*'; -- 创建 `Product.unitPrice` 字段,获取商品当前价格 create function products_unit_price(p products) returns money as $$ select unit_price from prices where product_id = p.product_id and now() >= valid_from and now() < valid_until; $$ language sql stable; -- 为该字段添加文档 comment on function products_unit_price is 'The unit price at the current time, reflecting promotional discounts.';这段 SQL 会自动生成Product.unitPrice字段,按时间有效性取当前单价(假设各时间段不重叠;若可能重叠,可在查询中追加order by unit_price asc limit 1)。这正是"用智能注释驱动 schema 生成"的代表性用法。
实战示例二:用 extendSchema 接入 Node.js 业务逻辑
如果业务逻辑更复杂(例如需要查询外部服务计算运费),可以在 TypeScript 侧用extendSchema扩展 schema(见 postgraphile/postgraphile/README.md):
import { extendSchema } from "postgraphile/utils"; import { constant, context, get } from "postgraphile/grafast"; import { batchSummarizeCart } from "./businessLogic/cart"; export default extendSchema((build) => { const { pgExecutor, pgResources: { cartItems, products, prices }, } = build; return { typeDefs: /* GraphQL */ ` extend type Product { "The unit price at the current time, reflecting promotional discounts." unitPrice: Money! } extend type Cart { summary: CartSummary } type CartSummary { subtotal: Money! shipping: Money! tax: Money! total: Money! } `, plans: { Product: { unitPrice($item) { // 找到对应的价格记录 const productId = $item.get("product_id"); const $prices = prices.find({ productId: $productId }); $prices.where(sql`now() >= valid_from and now() < valid_until`); // 恰好一行,取回并返回单价 return $prices.single().get("unit_price"); }, }, Cart: { summary($cart) { const $cartId = $cart.get("id"); return loadOne($cartId, batchSummarizeCart); }, }, // CartSummary 无需 plan resolver,使用默认即可。 }, }; });配套的批量加载业务逻辑(来自 postgraphile/postgraphile/README.md)展示了如何把 DataLoader 风格的回调接到loadOne/loadMany上,并通过shared: () => context()让 loader 访问运行时 GraphQL context:
// businessLogic/cart.ts import { context } from "postgraphile/grafast"; export const batchSummarizeCart = { // 计划步骤:在 loader 中获取 GraphQL context shared: () => context(), // `cartIds` 是 Cart 标识符的批,`shared` 是运行时 GraphQL context(整批共享) async load(cartIds, { shared }) { const carts = await batchGetCartInfo(shared, cartIds); const cartsWithShipping = await batchCalculateShippingCosts(carts); const cartsWithTax = await batchCalculateTax(cartsWithShipping); return cartIds.map((cartId) => { const cartInfo = cartsWithTax.find((c) => c.cart_id === cartId); const { subtotal, shipping, tax } = cartInfo; const total = subtotal + shipping + tax; return { subtotal, shipping, tax, total }; }); }, };其中的batchGetCartInfo对整批 cart 只发起一次数据库查询(where carts.id = any($1::int[])),这正是 Gra*fast* 批量执行消除 N+1 问题的体现。
通用工具与基础设施包
除了两大主角,README 还罗列了一批可独立使用或支撑上层的基础包:
graphile-export 与 eslint-plugin-graphile-export
graphile-export 可以在合适的条件下,把内存中动态构建的 GraphQL schema 导出为可直接导入执行的原始 JavaScript 源码——这是 PostGraphile "无锁定"能力的技术基础。eslint-plugin-graphile-export 则是配套 ESLint 插件,帮助开发者编写与 graphile-export 兼容的代码(如ExhaustiveDeps.ts、ExportInstances.ts、ExportMethods.ts、ExportPlans.ts等规则,见其src/目录)。
graphile-config:统一配置层
graphile-config 处理 Graphile 全家桶的插件、preset 与配置文件,是一个通用配置层,提供Plugin与Preset(别名Config)两个接口。Plugin 对象包含:
name(string):插件名,必须唯一,用于skipPlugins等能力;version(string):semver 兼容版本,通常与package.json一致(也可不同,例如一个模块包含多个插件);description(可选string):CommonMark(Markdown)格式的人类可读描述;provides(可选string[]):该插件提供的 "feature labels",主要用于决定插件(及其 hooks/events)的执行顺序,在已加载插件中必须唯一,缺省时取插件名;after(可选string[]):声明应在指定 feature(若存在)之后加载;before(可选string[]):声明应在指定 feature(若存在)之前加载。
Preset 则把一组插件与各 scope 的选项打包,可同时使用多个 preset,preset 之间也可以互相组合(extends)。解析时按ResolvePresets算法合并:依序解析所有extends,插件按集合合并(每个插件只出现一次)。
graphile-build 与 graphile-build-pg
graphile-build 是一个从 "plugins" 构建 GraphQL.js schema 的系统,特别适合自动生成的 GraphQL API(PostGraphile 即使用它),也适合手写 schema 中连接(connections)、命名等模块化且广泛使用的关注点。graphile-build-pg 则提供理解@dataplan/pg(即 PostgreSQL)服务的插件,可为数据库资源生成类型、关系、变更(mutations)等。
pg-sql2 与 pg-introspection
pg-sql2 是一个使用 tagged template literals 构建高度动态、防 SQL 注入的 PostgreSQL 查询的库,其核心卖点是"强大灵活地构建动态 SQL 而不向注入攻击敞开大门",且把性能作为明确的设计目标。典型用法(见 utils/pg-sql2/README.md):
const { default: sql } = require("pg-sql2"); // 或 import sql from 'pg-sql2'; const tableName = "user"; // ... 之后通过 sql`...` 模板拼接查询pg-introspection 是 PostgreSQL 的强类型 introspection 库,依据 PostgreSQL 官方文档生成,为每个 introspection 字段提供最新细节。PostGraphile 的数据库分析正是建立在它之上。
jest-serializer-graphql-schema 与 @graphile/lru
jest-serializer-graphql-schema 是一个理解 GraphQL schema 的 Jest 序列化器,避免快照被"""等冗长描述填满。@graphile/lru 是一个"近乎偏执地追求性能"的 LRU 缓存(README 称其可能是 Node.js 中最快的通用 LRU 缓存之一,但功能集极小),并坦率地建议:绝大多数情况下你可能更想要 @isaacs 的lru-cache而非它。
"Crystal" 之名的由来
为什么这个 monorepo 叫 "crystal"?README.md 给出了有趣的答案:Gra*fast*(前身 DataPlanner)最早的项目代号就是 "Graphile Crystal"。团队在正式公开前用 🔮 水晶球 emoji 作为圈内暗号指代该项目。如今 Gra*fast* 已成为规划执行引擎的正式名称,而仓库里又包含不少与 GraphQL 并非严格相关的东西,需要一个不那么 GraphQL 化的名字,于是沿用最初的外号把 monorepo 命名为 Crystal。README 还幽默地澄清:与维护者"水晶婚纪念日"有关的传闻纯属夸大其词。
在仓库中进一步探索
如果你希望深入了解上述技术栈,可以从以下路径继续:
- Gra*fast* 源码与引擎实现:grafast/grafast/src/、grafast/grafast/src/execute.ts、grafast/grafast/src/engine/;
- Gra*fast* 官方文档站点源码:grafast/website/;
- PostGraphile 插件与 preset 实现:postgraphile/postgraphile/src/plugins/、postgraphile/postgraphile/src/presets/;
- PostGraphile 测试与 schema 导出脚本:postgraphile/postgraphile/tests/、postgraphile/postgraphile/scripts/test-schema-exports.mjs;
- 配置层与基础库:utils/graphile-config/、utils/pg-sql2/、utils/pg-introspection/、utils/graphile-export/;
- 仓库构建与测试命令:package.json(
build-init、build、test、postgraphile等脚本)。
小结
Graphile Crystal monorepo 提供了一条从数据库到 GraphQL API 的完整技术链路:pg-introspection负责读懂 PostgreSQL,pg-sql2负责安全地构造 SQL,@dataplan/pg提供数据访问的 plan 步骤,graphile-build/graphile-build-pg负责 schema 生成,graphile-config统一插件与配置,而 Gra*fast* 与 PostGraphile 分别面向"自建高效 GraphQL 引擎"与"PostgreSQL 驱动的高性能 API"两种核心诉求;graphile-export则确保这一切不会把你锁死在框架里。无论你是想直接基于 PostgreSQL 快速上线一个 GraphQL API,还是想为自己的 GraphQL.js schema 引入规划式执行以换取更高效率,都可以在本仓库中找到对应的组件,并按需独立取用。
【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考