尧图精选

graphql-engine 元数据多语言 SDK 生成与类型安全实践:@hasura/metadata 工具链深度解析

🕒 发布时间:2026/9/19 4:13:15 📁 来源:尧图网络
graphql-engine 元数据多语言 SDK 生成与类型安全实践hasura/metadata 工具链深度解析【免费下载链接】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-engineHasura GraphQL Enginegraphql-engine使用metadata.json与replace_metadata端点来声明化描述整个 GraphQL API表、权限、事件触发器、定时任务、远端 Schema、Actions 等。手工维护这些元数据极易出错。contrib/metadata-types子项目为此提供了一套完整的解决方案以手写 TypeScript 类型为唯一事实来源借助 Quicktype 自动生成 Go、Haskell、Python、JSON Schema 等多语言 SDK并提供运行时校验、回归测试与 IDE 类型检查集成。本文将以 contrib/metadata-types/README.md 为主线结合仓库源码与生成产物完整讲解这套工具链的配置、用法与底层原理读完即可在自己的项目中落地“写元数据不出错、改元数据可追溯、IDE 有提示”的工作流。为什么需要共享的元数据类型定义在 contrib/metadata-types/RFC.md 中记录了这一子项目的设计动机Hasura 的 Metadata API 变更频繁而metadata.json/metadata.yaml缺乏强类型约束团队跨语言协作CLI 是 Go、服务端是 Haskell、控制台是 TypeScript时需要在各语言间反复手工对齐类型定义。RFC 提出的方案是TypeScript → JSON Schema →其他语言YAML Schema → 解析为 JSON →其他语言JSON Schema →其他语言即以一份主类型定义TypeScript 或 JSON Schema为源自动化派生出所有语言的目标类型。这样做能显著减少元数据维护的工程成本并借助强类型与 docstring 提升跨团队的类型安全与生产力。实现路径为手工转录metadata.json与replace_metadata端点用到的每一个元数据类型形成 src/types/HasuraMetadataV2.ts约 830 行含大量 JSDoc 注释由它生成 JSON / YAML Schema 版本src/types/HasuraMetadataV2.schema.json 与 src/types/HasuraMetadataV2.schema.yaml再由可配置的生成脚本 src/generateTypes.ts 一次性产出 Go、Haskell、Python、TypeScript 等多语言 SDK配合同样可配置的测试脚本 src/test.ts用真实元数据样例做自动化回归校验。值得注意的语义细节RFC 中明确指出部分类型仅具语义区分作用结构完全相同例如QualifiedFunction与QualifiedTable都是{ name: string; schema: string }另有不少类型只是原始类型的别名例如RoleName、ComputedFieldName、PGColumnType都只是string。这一点在 src/types/HasuraMetadataV2.ts 中可以得到印证。hasura/metadata 包的安装与基本用法contrib/metadata-types同时也是一个可发布的 npm 包hasura/metadata版本 1.0.2MIT 许可见 package.json它包含 Hasura Metadata V2 的 TypeScript 类型。安装方式yarn add hasura/metadata # npm i hasura/metadata安装后即可在项目里导入类型import { HasuraMetadataV2, Action, ComputedField, } from hasura/metadata-types;包结构上types字段指向 v2/index.d.ts后者再导出dist/HasuraMetadataV2files白名单为v2与dist。也就是说发布到 npm 的是编译产物与类型声明而生成器本体保留在仓库中作为开发期工具。一图看懂这套 SDK 生成器能帮你做什么按 README 的 TL;DR你的诉求不同入口也不同想让 IDE 对元数据文件做类型检查与文档提示→ 走 Metadata IDE 类型检查集成想在项目里直接用现成的强类型 SDK→ 从 generated 目录下载对应语言产物按下文“SDK 用法示例TypeScript”使用想为 generated 目录中尚不存在的语言生成 SDK或定制现有语言的生成选项→ 修改 config.yaml然后yarn install或npm install并执行yarn generate-types或npm run generate-types。下面的动图展示了两种核心体验TypeScript SDK 的运行时解析校验以及在 YAML 元数据文件内直接获得类型检查与文档提示。生成器配置文件config.yaml 全参数解析生成器的一切行为都由项目根目录下的 config.yaml 驱动对应文档中的完整示例仓库中为实际生效版本。运行入口为yarn generate-types/npm run generate-types。各字段说明如下配置字段作用取值/说明selected_input_language选择输入语言Typescript或JsonSchema也可用 CLI 参数--typescript/--jsonschema覆盖input_files输入文件 glob 表达式按语言分别配置值可以是单个字符串或字符串数组仅与selected_input_language匹配的那一组生效output_directory输出目录默认./generated输出文件名为输入文件名加上新语言的扩展名quicktype_config按语言配置 Quicktype 的rendererOptions每个键是一种目标语言键值为该语言的渲染选项对象~null表示使用默认选项仓库中实际生效的配置为# Accepts Typescript or JsonSchema # Override this with --typescript or --jsonschema from CLI selected_input_language: Typescript # Glob patterns for the target input files of selected language input_files: # Paths can be either a string, or an array of strings # JsonSchema: ./src/types/**.schema.json Typescript: ./src/types/**.ts # Output file directory output_directory: ./generated # Quicktype config per-language # Config is an object of type rendererOptions quicktype_config: # c: ~ # crystal: ~ # csharp: ~ # dart: ~ # elm: ~ # flow: ~ go: package: hasura_metadata haskell: ~ # java: # package: org.hasura.metadata # kotlin: # framework: kotlinx # package: org.hasura.metadata # objective-c: ~ # pike: ~ python: python-version: 3.7 # ruby: ~ # rust: ~ schema: ~ # swift: ~ # typescript: # just-types: true要点解读当前启用了四个目标语言go包名hasura_metadata、haskell默认选项、python指定python-version: 3.7与schema输出 JSON Schema。其余语言如 c、csharp、dart、java、kotlin、ruby、rust、swift 均以注释形式给出模板取消注释即启用。glob 数组展开源码 src/generateTypes.ts 会把数组形式的 glob 自动合并为 Quicktype 可识别的{a,b}花括号表达式例如[./src/types/**.ts, ./src/otherfolder/**.ts]会被转换为{./src/types/**.ts,./src/otherfolder/**.ts}。语言→扩展名映射源码 src/generateTypes.ts 维护了一张完整的映射表例如go → .go、haskell → .hs、python → .py、schema → .json、typescript → .ts输出文件名为输入 basename 加对应扩展名。JSON Schema → YAML 的后处理生成流程的最后一步src/generateTypes.ts 与 L235-L238会对generated/目录下所有.json产物执行jsonSchemaToYAML将 JSON Schema 同时转写一份 YAML 版本便于人工阅读。CLI 标志方面src/generateTypes.ts支持--help打印用法当config.yaml未设置selected_input_language时必须且只能传--typescript与--jsonschema之一源码用assert强制校验若配置中已设置且 CLI 同时传入标志则 CLI 优先覆盖。生成的 SDK 产物一览运行生成后产物落在generated/目录。仓库当前保留了两代元数据版本的产物Metadata V2HasuraMetadataV2.ts、HasuraMetadataV2.go、HasuraMetadataV2.hs、HasuraMetadataV2.py、HasuraMetadataV2.json、HasuraMetadataV2.yamlMetadata V3HasuraMetadataV3.go、HasuraMetadataV3.hs、HasuraMetadataV3.py、HasuraMetadataV3.json、HasuraMetadataV3.yaml。以 V2 的 TypeScript 产物为例其文件头generated/HasuraMetadataV2.ts直接给出了使用范式导出TableName、QualifiedTable、TableEntry、CronTrigger、HasuraMetadataV2等全部类型并生成一个名为Convert的顶层类其中每个类型对应一对方法Convert.toTypeName(json)将 JSON 字符串解析并运行时校验为目标类型校验失败会抛错Convert.toTypeNameJson/typeNameToJson则将类型实例序列化回格式化的 JSON 字符串。SDK 用法示例TypeScript扩展 Convert 类TypeScript SDK 生成的Convert类包含了运行时解析与校验逻辑例如public static toCronTrigger(json: string): CronTrigger { return cast(JSON.parse(json), r(CronTrigger)); } public static cronTriggerToJson(value: CronTrigger): string { return JSON.stringify(uncast(value, r(CronTrigger)), null, 2); }你可以从另一个文件继承该类以扩展功能。下面示例为其增加 YAML 转换、克隆与结构化/文本双轨 diff 能力保存为customMetadataConverter.ts// customMetadataConverter.ts import fs from fs; import { load, dump } from js-yaml; import { createPatch } from diff; import { detailedDiff } from deep-object-diff; import { Convert as _Convert, TableEntry, Action, CustomTypes, CronTrigger, HasuraMetadataV2, } from ../generated/HasuraMetadataV2; interface DiffOutput { structuralDiff: object; textDiff: string; } interface WriteDiffOpts { folder: string; file: string; diffs: DiffOutput; } export class Convert extends _Convert { public static loadYAML load; public static dumpYAML dump; public static diffYaml createPatch; public static diffJson detailedDiff; public static clone(obj: any) { if (obj null || typeof obj ! object) return obj; let temp new obj.constructor(); for (var key in obj) { if (obj.hasOwnProperty(key)) { temp[key] Convert.clone(obj[key]); } } return temp; } public static diff(before: object, after: object): DiffOutput { const originalYaml Convert.metadataToYaml(before); const updatedYaml Convert.metadataToYaml(after); const structuralDiff Convert.diffJson(before, after); const textDiff Convert.diffYaml(, originalYaml, updatedYaml); return { structuralDiff, textDiff }; } public static writeDiff(opts: WriteDiffOpts) { const { file, folder, diffs } opts; fs.writeFileSync(${folder}/${file}.diff, diffs.textDiff); fs.writeFileSync( ${folder}/${file}.json, JSON.stringify(diffs.structuralDiff, null, 2), ); } /** * Converts metadata objects into YAML strings */ public static metadataToYaml(value: object): string { // JSON Stringify Parse to remove undefined key/values from YAML return dump(JSON.parse(JSON.stringify(value))); } }设计意图loadYAML/dumpYAML直接复用 js-yaml 的load/dump负责 YAML ⇆ JSON 对象互转diff同时产出结构化 diffdeep-object-diff的detailedDiff适合程序判断与文本 diffdiff包的createPatch适合人读与 code reviewmetadataToYaml通过“JSON 序列化再反序列化”剔除undefined键值避免 YAML 中出现空字段writeDiff将两类 diff 分别落盘为.diff与.json文件。SDK 用法示例TypeScript程序化交互元数据下面这段示例覆盖了与元数据脚本化交互最常见的场景读取tables.yaml与actions.yaml、新增表、生成并写出 diff、以metadata.jsonmetadata.yaml同理为整体做同样的“克隆 → 修改 → 对比 → 落盘”循环import { Convert } from ./customMetadataConverter; import { TableEntry, Action, CustomTypes, HasuraMetadataV2, } from ../generated/HasuraMetadataV2; // Read tables.yaml file as text from filesystem const tablesMetadataFile fs.readFileSync(./metadata/tables.yaml, utf8); // Convert it to JSON object with type annotation using loadYAML utility const tablesMetadata: TableEntry[] Convert.loadYAML(tablesMetadataFile); tablesMetadata.forEach(console.log); // Read actions.yaml file as text from filesystem const actionMetadataFile fs.readFileSync(./metadata/actions.yaml, utf8); // Convert it to JSON object with type annotation using loadYAML utility const actionMetadata: { actions: Action[]; custom_types: CustomTypes; } Convert.loadYAML(actionMetadataFile); actionMetadata.actions.forEach(console.log); console.log(actionMetadata.custom_types); // Make a new table object const newTable: TableEntry { table: { schema: public, name: user }, select_permissions: [ { role: user, permission: { limit: 100, allow_aggregations: false, columns: [id, name, etc], computed_fields: [my_computed_field], filter: { id: { _eq: X-Hasura-User-ID }, }, }, }, ], }; // Clone the tables for comparison after changes using diff() const originalTablesMetadata Convert.clone(tablesMetadata); // Add the new table to tables metadata tablesMetadata.push(newTable); // Generate a structural and text diff from the changes between original and now const tableDiff Convert.diff(originalTablesMetadata, tablesMetadata); // Write the diffs to /diffs folder, will output tables.json and tables.diff Convert.writeDiff({ folder: diffs, file: tables, diffs: tableDiff }); // Ouput the updated tables.yaml to filesystem fs.writeFileSync( ./tables-updated.yaml, Convert.metadataToYAML(tablesMetadata), ); // Read metadata.json const metadataFile fs.readFileSync(./metadata.json, utf-8); // Convert.totypeName does runtime validation of the type const allMetadata: HasuraMetadataV2 Convert.toHasuraMetadataV2(metadataFile); console.log(allMetadata); // Clone, add table const beforeMetadataChanges Convert.clone(allMetadata); allMetadata.tables.push(newTable); // Diff, write diff const metadataDiff Convert.diff(beforeMetadataChanges, allMetadata); Convert.writeDiff({ folder: diffs, file: metadata, diffs: metadataDiff });注意这里的两类校验路径YAML 路径走Convert.loadYAML得到的是编译期类型标注的对象不强校验而Convert.toHasuraMetadataV2(metadataFile)走的是生成代码中的cast/r(...)运行时检查即使 JSON 语法本身合法只要结构不匹配目标类型就会抛错见 generated/HasuraMetadataV2.ts 的注释说明。这也解释了为何newTable里的filter使用了X-Hasura-User-ID这类 session 变量占位符——它与元数据权限系统的_eq语义完全一致。测试配置文件test-config.yaml 与回归测试yarn test/npm run test会启动基于ava的自动化测试见 package.json 中test: ./node_modules/.bin/ava --verbose。测试配置 test-config.yaml 的用途是取若干样例 JSON 文件喂给生成的 TypeScript SDK逐一做类型校验作为回归防线。测试配置结构为顶层是typeDefinitionFile含元数据类型的 TS 文件路径加一组jsonInputTests其中files可以是一个或多个文件路径/glob 表达式expectType指定期望的目标类型名。注释中说明了命名约定该类型会被调用为Convert.to(expectType)例如expectType: HasuraMetadataV2对应Convert.toHasuraMetadataV2。文档给出的最小示意// myTypes.ts interface MyType { name: string; age: number; }// test-data1.json { name: John, age: 30 }对应配置--- - typeDefinitionFile: ./generated/HasuraMetadataV2.ts jsonInputTests: - files: ./src/tests/**.json # This gets called as Convert.to(expectType) - e.g Convert.toHasuraMetadataV2 in generated TS SDK expectType: HasuraMetadataV2源码实现src/test.ts的流程是读取test-config.yaml→ 对每个条目的files做 glob 展开 → 对每个输入文件动态import类型定义文件并调用Convertto expectType→ 用t.notThrows断言不抛异常。仓库自带的样例数据位于 src/tests/sample-metadata-1.json 与sample-metadata-2.json前者是一份version: 2的完整元数据包含public.post表的select_permissions与基于_not/_exists/_and的复杂布尔表达式过滤条件是很好的回归样本。测试运行效果如下图所示程序化使用把生成器当库调用生成器理论上既可作 CLI 可执行文件运行也可作为库被导入例如在 CI/CD 流水线中定制行为。文档给出如下调用范式generateTypes() .then((outputs) { console.log(Finished generateTypes(), outputs are, outputs); for (let output of outputs) { // This is the input file path console.log(File:, output.file); // This contains the generated text console.log(Results:, output.results); } }) .catch((err) { console.log(Got error, err); }) .finally(async () { // Convert the generated JSON Schema to YAML, for example const generatedFolder path.join(pathFromRoot, generated, /); const jsonSchemas await glob(generatedFolder **.json); jsonSchemas.forEach(jsonSchemaToYAML); });从源码 src/generateTypes.ts 看generateTypes的核心是遍历config.quicktype_config中的每种语言对每个输入文件依次执行runTypeConversion内部通过JSONSchemaInput/InputData装配 Quicktype 输入再调用quicktype(...)最后把SerializedRenderResult按扩展名写入output_directory。返回值outputs中每个元素包含输入文件路径file与渲染结果results因此调用方可以像示例中那样在finally里做任意后处理如 JSON Schema → YAML。一个值得注意的实现细节src/generateTypes.ts当输入为 JSON Schema 且目标语言也是schema即“JSON Schema → JSON Schema”时生成器会跳过 Quicktype 转换直接格式化原 schema 返回避免二次转换破坏输出——这是源码中以注释形式标记的“dirty hack”理解它有助于你在扩展生成器时避免踩坑。Metadata IDE 类型检查集成VS Code / JetBrains手写 Hasura Metadata YAML 定义时最痛苦的是频繁回查文档、以及对抗 YAML 的缩进敏感。借助生成的 JSON Schema可以为编辑器接入完整的类型检查、文档提示与自动补全。VS CodeVS Code 原生支持为 JSON 文件绑定 JSON SchemaRed Hat 的 YAML 扩展则将同样的能力延伸到 YAML 文件。推荐在.vscode/extensions.json中声明插件{ recommendations: [redhat.vscode-yaml] }然后在.vscode/settings.json中把各 Schema 文件映射到对应的元数据文件模式{ json.schemas: [ { fileMatch: [**/metadata.json], url: ./MetadataExport.schema.json } ], yaml.schemas: { ./ActionsYAML.schema.json: **/actions.yaml, ./AllowListYAML.schema.json: **/allow_list.yaml, ./CronTriggerYAML.schema.json: **/cron_triggers.yaml, ./FunctionsYAML.schema.json: **/functions.yaml, ./QueryCollectionsYAML.schema.json: **/query_collections.yaml, ./RemoteSchemasYAML.schema.json: **/remote_schemas.yaml, ./TablesYAML.schema.json: **/tables.yaml } }以上引用的各*.schema.json文件与仓库 src/metadata-schemas 目录下的产物一一对应ActionsYAML.schema.json、AllowListYAML.schema.json、CronTriggerYAML.schema.json、FunctionsYAML.schema.json、QueryCollectionsYAML.schema.json、RemoteSchemasYAML.schema.json、TablesYAML.schema.json、MetadataExport.schema.json等。它们是“薄壳”Schema核心通过$ref指向完整的HasuraMetadataV2.schema.json中的#definitions/..../MetadataExport.schema.json顶层入口metadata.json用它{ type: object, $ref: ./HasuraMetadataV2.schema.json#definitions/HasuraMetadataV2 }./ActionsYAML.schema.json{ type: object, properties: { actions: { type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/Action } }, custom_types: { type: object, $ref: ./HasuraMetadataV2.schema.json#definitions/CustomTypes } } }./AllowListYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/AllowList } }./CronTriggerYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/CronTrigger } }./FunctionsYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/Function } }./QueryCollectionsYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/QueryCollectionEntry } }./RemoteSchemasYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/RemoteSchema } }./TablesYAML.schema.json{ type: array, items: { $ref: ./HasuraMetadataV2.schema.json#definitions/TableEntry } }配置完成后编辑metadata.json/tables.yaml/actions.yaml等文件时IDE 会基于这些 Schema 提供字段自动补全、类型校验与逐字段文档说明效果即上文第二张动图所示。仓库中 Schema 的实际用法可对照 contrib/metadata-types/src/metadata-schemas/HasuraMetadataV2.schema.json 阅读。JetBrains 系 IDEJetBrains 系 IDEIntelliJ IDEA、RubyMine 等同样支持为 JSON / YAML 文件挂接自定义 JSON SchemaYAML在设置中为 YAML 文件配置远程或本地 JSON Schemayaml设置面板的 JSON Schema 映射功能JSON通过 JSON Schema 映射json设置面板中的 Add Custom Schema Mapping将metadata.json指向MetadataExport.schema.json将各 YAML 文件指向对应的*YAML.schema.json。映射方式与 VS Code 等价核心都是把“文件名匹配模式 → Schema 文件路径”关联起来编辑器随即获得同样的校验、补全与文档能力。具体操作可参考各自 IDE 设置中关于 JSON Schema 映射的帮助文档。小结与进阶路线contrib/metadata-types提供了一条从“单一 TypeScript 事实来源”到“多语言 SDK 运行时校验 自动化回归 IDE 提示”的完整链路源类型src/types/HasuraMetadataV2.ts含完整 JSDoc是最权威的类型说明生成器src/generateTypes.ts配置驱动CLI/库双模式支持 16 种语言扩展名映射测试器src/test.tsava 驱动glob 收集样例Convert.toType校验配置config.yaml 与 test-config.yaml产物generated 目录下的 V2/V3 多语言 SDK 与 JSON/YAML Schema。对正在使用 graphql-engine 的团队而言最直接的落地路径是先用hasura/metadata获得编译期类型安全再把metadata.json与各 YAML 文件接入 IDE Schema 以获得编辑期保障最后将yarn test纳入 CI让每一份样例元数据都经过生成 SDK 的运行时校验从而把元数据变更的风险前移形成“改得放心、审得清楚、上线无惊”的工程闭环。【免费下载链接】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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →