Hasura GraphQL Engine 元数据 API 类型分离 RFC 解读:以 DTO 与 OpenAPI 规范驱动元数据工程化
Hasura GraphQL Engine 元数据 API 类型分离 RFC 解读以 DTO 与 OpenAPI 规范驱动元数据工程化【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine导读rfcs/separate-metadata-api-types.md是 Hasura GraphQL Engine 的一项设计提案核心议题是把 Metadata API 实际收发的 JSON 数据形状从隐含在手写 Aeson 实例里的隐性类型提升为一组显式的、可自省的 DTOData Transfer Object数据传输对象类型并以此为单一事实来源自动生成 OpenAPI 规范。读完本文你将理解为什么内部业务类型无法直接充当 API 契约、DTO 层如何以增量方式引入而不破坏现有 API、sum type 如何描述元数据多版本格式以及当前仓库中这一设计落地后的真实产物metadata.openapi.json与metadata-api-types/多语言类型生成管线。RFC 背景为什么需要一个准确的元数据规范Hasura 的元数据是描述 GraphQL Engine 配置的完整 JSON 文档通过 Metadata API如replace_metadata、export_metadata收发。RFC 的出发点非常明确我们想要一份导出的元数据的精确规范而获得规范的最佳方式是从代码中自动生成它。目前难以推导规范的原因在于序列化Metadata类型时走的是手写的 Aeson 实例Haskell 中 Aeson 是 JSON 编解码库。Metadata类型定义在 server/src-lib/Hasura/RQL/Types/Metadata.hs 中它是一个内部业务逻辑类型并不精确反映 Metadata API 实际消费和产生的数据。API 真正承载的其实是另一套类型——它隐式地存在于手写的parseJSON和toJSON函数里。因为是隐式的所以无法对它进行内省introspection也就无法自动推导出规范。从源码结构看这一判断在后续演进中得到了印证Hasura.RQL.Types.Metadata被拆分为多个模块Common.hs、Backend.hs、Instances.hs、Object.hs、Serialization.hs其中 Serialization.hs 正是承载序列化辅助函数的模块其模块头注释明确写着Helpers used in the implementations of metadataToOrdJSON and metadataToDTO——也就是说仓库中已经出现了把元数据转换为普通 JSON 与转换为 DTO 的两条路径并存的状态这正是本 RFC 所设想的过渡期形态。一份完整规范带来的收益RFC 列出了精确规范的四项直接收益Console 与 CLI 团队的编译期正确性可以基于生成的 TypeScript 与 Go 类型定义在类型检查阶段就验证操作元数据的代码是否正确。直接编辑元数据的用户获得更好支持OpenAPI 规范可以成为 IDE 插件的基础大幅改善手写metadata.json的编辑体验。API 破坏性变更检测自动化很可能把元数据 API 是否发生破坏性变更的检查自动化让服务端开发者在改动元数据或 API 时更有信心。更清晰、更低成本的元数据变更沟通。不改变可观察 API 的约束RFC 特别强调任何对元数据 API 本身的改动都会因需要各方共识与需要客户沟通而形成推进障碍。因此整体计划是在尽可能不改变可观察 API 的前提下完成包括避免改变 JSON 中字段的顺序。如果确有必要改动 API 才能完整描述它也应把问题点缩小到最小范围以便在规划变更时掌握最充分的信息。核心方案引入显式 DTO 层RFC 提议引入一组与 Metadata API 当前收发的 JSON 形状完全一致的新 DTO 类型。DTO 是软件工程中常见的术语指专门用于表示序列化数据的类型。理想情况下最终将为完整元数据导出以及每个 Metadata API 操作都建立 DTO 类型实施从顶层开始即完整的导出类型full-export type。Metadata API 调用将随实现进度逐步切换到这些新类型DTO 值在 API 处理器中会被立即翻译为或来自现有的元数据业务类型——绝大多数既有代码和新增的业务逻辑代码继续使用原有类型。也就是说转换链路从变为插入 DTO 层的价值在于得到一份显式的 API 数据表示可以对它做内省introspection并从中自动推导规范。而手写的 Aeson 实现则被复用到 DTO ⟷ 内部 Metadata 类型的翻译上而不是 JSON ⟷ Metadata 之间复用既有序列化代码能最大程度保证新序列化实现与旧实现产出完全相同的结果。目标规范格式为什么选 OpenAPI规范目标格式是 OpenAPI要求覆盖完整导出元数据的数据结构并最终覆盖每个 Metadata API 端点期望的数据。选择 OpenAPI 的理由生态广度存在大量以 OpenAPI 为输入、为各种语言生成类型定义的工具对 sum type 的支持OpenAPI 通过 discriminated unions判别联合基于discriminator字段或合适的非判别联合使用oneOf操作符但不指定discriminator来支持和类型sum types而这对于表达元数据格式的各种可能变体必不可少。RFC 提到 GDC 团队已经在用 autodocodec 生成 OpenAPI 规范因此元数据部分计划复用同一库。备选方案包括生成 TypeScript 类型定义例如用 aeson-typescriptJSONSchema。两者在描述数据方面与 OpenAPI 能力相近同样支持 sum type但据 RFC 所知以 OpenAPI 为输入、为各语言生成类型定义的选项更多且 OpenAPI 同时描述数据与 REST API而另外两者只描述数据。若 OpenAPI 遇到严重问题再评估切换。仓库中的落地产物这份 RFC 的设想在当前仓库中已经可以看到明确的落地证据。仓库根目录下的 metadata.openapi.json约 9800 行就是元数据 API 的 OpenAPI 规范本体。以其中ActionDefinition_Mutation_GraphQLType_InputWebhook为例可以看到 RFC 所描述的自动生成规范的典型形态字段带default值如forward_client_headers默认false、ignored_client_headers默认一整组 HTTP 头名单且headers字段通过oneOf引用HeaderConfValue与HeaderConfFromEnv两个 schema——这正是 RFC 中用oneOf表达和类型的具体实现。基于这份规范仓库内建立了 metadata-api-types 目录作为多语言类型生成管线Makefile 声明SCHEMA_FILE : $(abspath ../metadata.openapi.json)提供generate-types目标一次性生成 TypeScript、Go、Rust、Haskell、Kotlin 五种语言的类型另有typecheck目标对生成的类型定义做类型检查scripts/generate-typescript-types.sh 使用openapi生成器--useUnionTypes、--exportServices false、--exportCore false从 schema 生成 TypeScript 源码并支持通过patches/*.patch对生成结果打补丁scripts/generate-types-for-lang.sh 使用quicktype以已生成的 TypeScript 类型为输入为其他语言产出类型typescript/README.md 说明了用法import type { MetadataV3 } from hasura/metadata-api即可拿到导出元数据的根类型其余类型均为导出元数据内部属性的别名。这套流程与 RFC 的预期高度吻合OpenAPI 作为中间契约多语言类型由工具自动生成从而让 Console、CLI 与用户侧都能在编译期获得类型安全保障。增量设计从MetadataDTO顶层类型开始RFC 明确反对一次性交出完整规范的计划——那需要太多前置工作几乎注定无法跨越。因此方案是增量推进先引入顶层MetadataDTO类型RFC 作者表示如果命名不讨喜可以讨论开始时MetadataDTO结构中大部分位置使用 Aeson 的Value类型作为TODO 占位符让这些部分能直接透传给既有序列化逻辑随时间推移逐步把Value替换为具体类型并把Value占位符不断向树的深层推进直到全部消除。这样可以在立即开始生成 OpenAPI 规范——只不过定义中与Value占位符对应的位置会出现泛化的object占位符随着转换工作的推进生成的规范会越来越有用。如果发现某些元数据特性无法按 RFC 计划完整描述可以保留这些部分为占位符直到想出合适方案API 始终是可用的已准确描述的部分也能持续产生价值。用 sum type 表示多种格式导出的元数据并非只有一种格式某些区域中允许字段与必填字段会因后端数据库类型而异另一些区域中某些元数据值会触发向后兼容逻辑从而接受旧格式。RFC 承认目前尚不清楚是否能在单个 OpenAPI 文档中精确捕捉所有这些差异但计划是在 DTO 类型内部用 sum type 表示不同的可能格式以尽量逼近完整描述。RFC 给出了 Haskell 侧与 OpenAPI 侧的对照示例。Haskell 侧的 DTO 和类型data MetadataDTO MetadataDTOV1 MetadataV1 | MetadataDTOV2 MetadataV2 | MetadataDTOV3 MetadataV3对应的 OpenAPI 片段使用oneOfdiscriminator以version字段为判别属性schema: oneOf: - $ref: #/components/schemas/MetadataV1 - $ref: #/components/schemas/MetadataV2 - $ref: #/components/schemas/MetadataV3 discriminator: propertyName: version mapping: 1: #/components/schemas/MetadataV1 2: #/components/schemas/MetadataV2 3: #/components/schemas/MetadataV3 properties: version: type: integer它描述的 JSON 值形如{ version: 3 // 更多字段由 #/components/schemas/MetadataV3 对应的 OpenAPI 文档描述 }对照仓库中 metadata.openapi.json 的实际内容可以看到同样的手法被广泛使用oneOf出现在大量属性定义中如HeaderConfValue与HeaderConfFromEnv的联合印证了 RFC 所主张的用和类型表达格式变体策略已经在规范生成中落地。测试策略双路径等价性验证在过渡期内Metadata类型原有的 JSON 转换函数继续保留用途是测试JSON ⟷ DTO ⟷ Metadata的转换结果与既有JSON ⟷ Metadata转换结果一致反向转换同样等价。即用旧实现作为参照基准逐步验证新 DTO 链路的正确性保证每一步合入都不改变可观察 API。为什么不用已有的 Metadata Types RFC仓库中已经存在 Gavin 的 Metadata Types RFC对应目录 contrib/metadata-types。该 RFC 的方案是维护一份手写的规范供客户消费并从规范生成 Haskell 类型这些类型承担与本 RFC 中 DTO 相同的角色而本 RFC 反其道而行以 Haskell 类型作为 source-of-truth。RFC 说明在与 Gavin 和 Vamshi 讨论后团队认为以 Haskell 类型为事实来源对 Server 团队是摩擦最小的方案——这很重要因为需要 Server 团队的认可才能推进和维护这项工作同时Haskell 作为事实来源还能充分发挥GHC 类型检查器的能力帮助验证生成的规范与服务端实现一致。当然Gavin 的工作仍具价值他产出的规范是设计 Haskell DTO 的绝佳起点。从仓库现状看contrib/metadata-types/中保留着 config.yaml生成器配置可指定输入语言 Typescript/JsonSchema、输入文件 glob、输出目录以及按语言透传给 Quicktype 的 rendererOptions例如go的 package 名、python的版本等与src/generateTypes.ts、src/test.ts等脚本即手写 TS 类型 → 生成 JSON/YAML Schema → 再生成各语言 SDK的路线而 metadata-api-types 走的是服务端 DTO 类型 → OpenAPI → 各语言类型的路线。两条路线在仓库中并存正对应 RFC 中仍可从 Gavin 的工作中获得价值的判断。关于 GraphQL未来元数据 API 的铺垫RFC 明确表示本提案不排除未来推出 GraphQL 版本的元数据 API甚至会让这件事变得更容易。当前不做的核心原因是GraphQL 方案意味着一个全新 API伴随大量设计决策本 RFC 的目的只是把客户正在使用的既有 API 描述得更精确。未来实现 GraphQL API 时会有一个类似metadataExport的查询字段该字段需要一个良定义的类型——本提案的元数据 DTO 类型正好可以承担这一角色。潜在缺点与设计考量双类型维护成本引入 DTO 类型后任何元数据 API 的变更都需要同时修改元数据业务类型与DTO。必须确保两套类型完全互映避免某些功能意外地无法通过 API 触达这有赖于服务端开发者在新增特性时主动把新特性反映到 DTO 类型中。而对 OpenAPI 规范的破坏性变更检查可以捕捉到某功能意外从 DTO 中消失的潜在情况。保持元素顺序序列化逻辑从Metadata类型移到 DTO 类型后序列化属性顺序的职责也随之转移保持顺序一致至关重要。RFC 指出升级到 aeson v2 使这变得更容易——aeson v2 会按字母顺序序列化对象属性这种顺序在新类型中很容易模拟。仓库中的 Serialization.hs 大量使用Data.Aeson.Ordered有序 JSON且函数名包含ToOrdJSON风格的后缀可以看作这一方向的具体落实。元数据版本间默认值处理差异RFC 提到元数据版本 1 与 2 之间默认值处理方式有变化旧版本导出元数据时省略默认值新版本即使值等于默认值也会全部序列化。这一行为差异可以在 DTO ⟷ Metadata 转换中分别捕获而不必改动对外 JSON 形状。小结separate-metadata-api-typesRFC 给出了一条低风险、增量的工程路径以显式 DTO 类型显性化元数据 API 的 JSON 契约以 Haskell 类型为事实来源自动生成 OpenAPI 规范再借 OpenAPI 生态为 Console、CLI 与用户生成多语言类型定义全程以不改变可观察 API含字段顺序为硬约束、以新旧转换路径等价性测试为质量闸门。当前仓库中的 metadata.openapi.json 与 metadata-api-types 多语言生成管线正是该 RFC 设计思路落地后的可见成果也是后续继续深入阅读实现细节的入口。【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →