设计规范与实现解析:从 RFC 到路由源码)
后端API网关数据库GraphQL【免费下载链接】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点击查看免费下载导读本文以 Hasura GraphQL Engine 官方设计文档 rfcs/rest-endpoints.md 为核心完整解读 Hasura 的REST EndpointsRESTified Endpoints功能它允许将一条固定的 GraphQL 查询或变更mutation映射为一个符合 REST 习惯的 HTTP 端点从而同时获得 GraphQL 的开发迭代速度与 REST 二十年积累的生态工具链缓存、CDN、OpenAPI 文档、现有客户端。读完本文你将掌握端点的四要素模型、URL 模板语法、HTTP 方法约束、路由匹配算法、三种变量传递方式、参数类型限制、响应码/响应体约定与元数据校验规则并能对照仓库源码server/src-lib/Hasura/Server/Rest.hs、server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs理解其底层实现原理。背景与动机为什么要在 GraphQL 引擎上提供 REST 端点GraphQL 的优势在于 API 设计与使用阶段可以快速迭代而 REST 的劣势在于不够灵活但它拥有近二十年的生产环境使用经验、成熟的优化手段与工具链。RFC 的出发点很明确鱼与熊掌兼得——给定一条固定的 GraphQL 查询或变更Hasura 可以为其生成一个符合习惯的 REST 端点从而复用已有的 REST API 工具与生态利用浏览器或 CDN 层的 HTTP 缓存优化无需手写任何自定义代码即可对外暴露接口。在实际产品中这一特性被命名为RESTified Endpoints官方文档位于 docs/docs/restified/overview.mdx其定位被明确表述为RESTified 查询不是Hasura 的主要接口主要接口仍是原生 GraphQL而是用于增强 Hasura、让它在偏重 REST 的环境中更容易被采纳。端点Endpoint的四要素模型RFC 规定Hasura 元数据中包含若干定义的endpoints每个端点由以下四部分数据组成要素说明名称Name作为 Hasura 元数据中的主键除此之外不作他用URL 模板URL template端点对外暴露的路径模式HTTP 方法集合该端点可接受的 HTTP 方法查询或变更query/mutation一条不含变量的 GraphQL 操作定义RFC 给出的运行示例Nameuser_by_idURL 模板/users/:user_idMethodsGET、POSTQueryquery cached ($user_id: String!) { users(where: { id: { _eq: $user_id } }) { name email role } }从源码看这一模型被原样实现为 server/src-lib/Hasura/RQL/Types/Endpoint.hs 中的EndpointMetadata记录data EndpointMetadata query EndpointMetadata { _ceName :: EndpointName, _ceUrl :: EndpointUrl, _ceMethods :: NonEmpty EndpointMethod, _ceDefinition :: EndpointDef query, _ceComment :: Maybe Text }其中EndpointMethod枚举了五种方法GET、POST、PUT、DELETE、PATCH见 server/src-lib/Hasura/RQL/Types/Endpoint.hs。而EndpointUrl仅约束为「非空文本」server/src-lib/Hasura/RQL/Types/Endpoint.hs实际路径语义由下一节的模板语法解析。通过元数据 API 创建端点在 Console 中「REST」标签页或 GraphiQL 的 REST 按钮即可创建端点同时也可以通过元数据 API 以编程方式管理。create_rest_endpoint请求示例见 docs/docs/api-reference/schema-metadata-api/restified-endpoints.mdxPOST /v1/query HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin { type: create_rest_endpoint, args: { name: example-name, url: example, methods: [POST,PUT,PATCH], definition: { query: { query_name: example_mutation, collection_name: test_collection } }, comment: some optional comment } }其中definition.query通过query_name与collection_name引用查询集合Query Collection中的既有查询对应的删除操作是drop_rest_endpoint。在服务端create_rest_endpoint由 server/src-lib/Hasura/RQL/DDL/Endpoint.hs 中的runCreateEndpoint处理先检查同名端点是否已存在存在则返回 400AlreadyExists再通过buildSchemaCacheFor构建端点对应的 schema 缓存并写入元数据。URL 模板字面量Path Literal与路径参数Path ParameterRFC 用语法形式化定义了 URL 模板的组成一个 URL 模板是一系列path parts的序列每个 part 要么是path literal路径字面量要么是path parameter路径参数Part : :, segment-nz-nc ; path parameter | segment-nz-nc ; path literal Template : *(/, Part) ; URL template其中segment-nz-nc非零长度且不含冒号的段定义于 RFC 3986 §1.1.1。示例/users/:user_id由两部分组成路径字面量users后跟名为user_id的路径参数。注意路径参数的名字与 GraphQL 查询中唯一变量的名字恰好一致。RFC 还明确了一个不对称约束每个路径参数都必须对应一个已定义的查询变量但反过来不成立——变量也可以通过 URL 的 query 部分或请求体来提供详见「变量」一节。源码中路径的拆分与识别实现在 server/src-lib/Hasura/RQL/Types/Endpoint.hssplitPath :: (T.Text - a) - (T.Text - a) - EndpointUrl - [a] splitPath var lit map toPathComponent . T.split ( /) . toTxt where toPathComponent x | : T.isPrefixOf x var x | otherwise lit x即以/切分路径后以:前缀判定路径参数。而 server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs 定义了对应的组件数据类型data PathComponent a PathLiteral a | PathParam deriving stock (Show, Eq, Ord, Generic)HTTP 方法约束RFC 对方法与操作类型的配对关系给出了两条硬性约束查询query操作只允许GET和POST使用任何其他方法都会导致校验错误变更mutation操作禁止GET使用GET会导致校验错误。这一规则的原因在 RFC 的 Validation 一节中说明允许POST是为了支持非原始非标量类型的变量——因为 URL 中只能传标量复杂对象变量必须放到POST请求体里。GET则天然不具备请求体故 mutation 不可用GET。方法集合在源码中是非空的NonEmpty EndpointMethod列表见上文的EndpointMetadata每种方法都有对应的 JSON 字符串表示server/src-lib/Hasura/RQL/Types/Endpoint.hs并且在构建路由时同一个端点会按方法逐一挂载到 trie 上server/src-lib/Hasura/RQL/Types/Endpoint.hs。路由Routing请求如何匹配到端点给定一组已定义的端点、HTTP 请求 URL 与方法RFC 规定按以下算法确定正确端点按 RFC 3986 将 HTTP 请求 URL 的 path 部分拆分为若干段segments对每个已定义的端点2.1检查请求方法是否在该端点接受的方法集合中不在则跳到下一个端点2.2检查 URL 模板的 path parts 数量是否等于请求 URL 的段数不等则跳到下一个端点2.3按顺序逐对检查段/part若 part 是字面量检查段文本是否与字面量完全一致不一致则跳到下一个端点若 part 是参数记录「参数名 → 段文本」的映射此时每个端点要么不匹配要么匹配并附带一组参数名到段的赋值若存在多个匹配端点返回500 Internal Server Error重叠端点在校验阶段就应被检测出来见「重叠端点」一节若恰好一个端点匹配返回该端点以及参数名到变量值的映射——对每对参数名/段从查询定义中确定对应 GraphQL 变量的类型并按该类型解析段文本见「参数类型」一节若无任何端点匹配则按情况返回错误码若没有任何端点使用同一 URL 模板无论方法返回404 Not Found若存在同一 URL 模板的端点但方法不同返回405 Method Not Allowed并在Allow:响应头中列出支持的方法。在运行示例中唯一端点能匹配GET /users/abc123但以下请求都不匹配GET /users段数不符GET /users/abc123/purchases段数不符PUT /users/abc123方法不符源码中的实现基于 Trie 的匹配RFC 的路由算法在服务端被实现为一个多值路径 TrieMultiMapPathTrie路径组件 → 方法的 MultiMap → 端点元数据类型定义见 server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs端点列表通过 server/src-lib/Hasura/RQL/Types/Endpoint.hs 的buildEndpointsTrie构建。matchPath的匹配结果是一个定义了偏序lattice的代数数据类型MatchResultserver/src-lib/Hasura/RQL/Types/Endpoint/Trie.hsdata MatchResult a k v MatchAmbiguous -- 多个端点同时匹配 | MatchFound v [a] -- 唯一匹配返回端点和参数绑定列表 | MatchMissingKey (NonEmpty k) -- 路径匹配但方法不匹配返回允许的方法列表 | MatchNotFound -- 路径未找到MatchAmbiguous对应 RFC 步骤 4 的 500MatchFound步骤 5MatchMissingKey步骤 6.2 的 405附带允许的方法列表MatchNotFound步骤 6.1 的 404。这正是 HTTP 处理入口 server/src-lib/Hasura/Server/Rest.hs 中runCustomEndpoint的分支逻辑MatchNotFound - throw404 Endpoint not found、MatchMissingKey allowedMethods - throw405 ...、MatchAmbiguous - throw500 Multiple endpoints match request。该函数在 server/src-lib/Hasura/Server/App.hs 中被装配进 HTTP 应用端点统一挂载在/api/rest/前缀下RestRequest注释表明reqPath是api/rest之后的剩余路径见 server/src-lib/Hasura/Server/Rest.hs。仓库的 Python 集成测试也覆盖了这些场景例如 server/tests-py/queries/endpoints/endpoint_simple_wrong_method.yaml错误方法、server/tests-py/queries/endpoints/endpoint_missing.yaml404等可在 server/tests-py/queries/endpoints/ 目录下查阅。变量Variables三种传递方式RFC 规定 GraphQL 变量可以通过以下三种方式之一提供URL 模板中的路径参数段仅限标量类型变量URL 的 query 部分仅限标量类型变量请求体以 JSON 对象编码Content-Type: application/json或以键/值对编码Content-Type: application/x-www-form-urlencoded。同名重复变量会导致 400 Bad Request。无论变量以何种方式提供它们都会被合并为单一的键/值集合传给底层 GraphQL 操作效果等同于在显式执行的 GraphQL 请求的 variables 段中提供此后的变量检查也完全等同于用户直接发起该请求。RFC 继续用运行示例说明user_by_id查询把 user_id 捕获在 URL 模板中但也可以换一个端点改为通过 query 参数或请求体捕获。例如如下变体Nameget_userURL 模板/users/getMethodsGET, POSTQueryquery cached ($user_id: String!) { … }客户端可以这样调用该端点curl -X GET /users/get?user_idabc123curl -X POST /users/get?user_idabc123curl -X POST /users/get \ -d { user_id: abc123 } \ -H Content-Type: application/jsoncurl -X POST /users/get \ -d user_idabc123 \ -H Content-Type: application/x-www-form-urlencoded源码中的变量解析流程runCustomEndpoint中变量处理的完整链路[server/src-lib/Hasura/Server/Rest.hs](https://link.gitcode.com/i/80e2d23ec95b8322837deac36f6828b2#L50-L85, L138-L166)可以拆解为三步提取路径参数名parseVariableNames从端点 URL 中取出所有以:开头的段名server/src-lib/Hasura/Server/Rest.hs对齐期望与已提供变量alignVars将查询定义中的变量定义列表与「URL 参数 请求体参数 路径参数」合并后的集合做 join得到These四态组合——期望且提供、仅期望、仅提供server/src-lib/Hasura/Server/Rest.hs逐变量解析resolveVar处理四种情况server/src-lib/Hasura/Server/Rest.hs期望但缺失 → 赋Nothing交给查询执行层做 null 默认值处理未期望但出现 → 报错「Unexpected variable」来自请求体 → 直接透传无需解析来自 URL路径或 query→ 按变量类型解析详见下节。解析完成后构造一个GQLReqmkPassthroughRequestserver/src-lib/Hasura/Server/Rest.hs把原始查询字符串与合并后的变量一起转发给底层 GraphQL 执行层GH.runGQ相当于内部走一遍/v1/graphql。这正是 RFC 所说「合并后传给底层 GraphQL 操作」的实现。参数类型Parameter TypesURL 变量仅限标量RFC 的关键限制通过 URL路径或 query 部分提供的变量仅限于原始标量类型即必须是以下五种 GraphQL 原始类型之一String、ID、Int、Float、Boolean。解析规则这些类型的值会从解码后的 URL 文本中按RFC 7159JSON字面量的编码方式解析——String与ID按 JSON 字符串字面量解码Boolean按 JSON 布尔字面量解码Int与Float按 JSON 数字字面量解码。Nullable 类型与 List列表类型的解析当前不支持RFC 注明可能在未来版本中添加对应文末「Future Work」。从源码看resolveVar对 URL 提供的值先尝试 JSON 解码命中布尔/数字且与变量类型匹配时按 JSON 字面量取值否则按字符串字面量处理若变量类型是TypeList列表类型则直接报错「List variables are not currently supported in URL or Query parameters」若是可空类型且值为空则解析为nullserver/src-lib/Hasura/Server/Rest.hs。这也印证了 RFC 的标量限制与 JSON 字面量解析语义。响应头Response Headers通过 Cache-Control 支持缓存RFC 规定服务端通过提供Cache-Control响应头来支持客户端缓存。要启用该行为查询中应包含cached指令即运行示例中的query cached (...)。返回的Cache-Control头会带有一个max-age参数表示服务端对返回数据缓存如 Redis 缓存的剩余生存时间。这一设计让浏览器或 CDN 可以直接利用 HTTP 缓存语义。仓库测试 server/tests-py/queries/endpoints/endpoint_simple_cached.yaml 正是针对带cached指令端点场景的用例。官方文档也建议与其把是否使用缓存交给客户端决定可能被遗漏不如通过cached指令在服务端查询上固化缓存行为见 docs/docs/restified/restified-config.mdx 的「Simplifying caching」一节。响应码Response Codes与响应体Response BodyRFC 规定生成的端点对用户错误与配置错误返回标准 HTTP 错误码包括但不限于状态码含义触发场景400 Bad Request请求格式错误意外或错误的查询变量具体见错误消息404 Not Found端点不存在URL 模板无任何匹配405 Method Not Acceptable端点存在但方法不符同一 URL 模板下存在其他方法的端点RFC 正文中此处写作 405 Method Not Allowed409 Conflict请求包含不一致数据例如重叠的端点定义500 Internal Server Error服务端内部状态错误如多个端点同时匹配响应体约定操作成功时响应体包含等价 GraphQL 响应的data键的内容注意不包一层{data: ...}直接返回 data 本身——官方 quickstart 中的示例响应即为裸对象见 docs/docs/restified/create.mdx操作失败时响应体包含所发生错误的 JSON 表示并伴随语义明确的 HTTP 状态码。校验Validation重叠端点Overlapping Endpoints两个端点定义为重叠当且仅当存在一个合法 HTTP 请求能够同时匹配这两个端点依据上文 Routing 一节的匹配规则。在元数据校验阶段任何包含重叠端点的元数据都应被拒绝返回409 Conflict错误码并附上重叠端点的名称。这也解释了路由算法中「多个端点匹配时返回 500」为何只是运行时兜底——正常配置下重叠在入库前已被拦截。源码层面Trie 的lookupPath允许PathParam匹配任意段server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs因此像/users/:id与/users/me这类模板就可能重叠需要靠校验去重。runCustomEndpoint中的注释也指出/:a/b与/a/:b这类端点在请求/a/b时是「运行时才判定无效」的歧义场景server/src-lib/Hasura/Server/Rest.hs。仓库中的 server/tests-py/queries/endpoints/endpoint_conflicting.yaml 即为重叠端点的测试用例。操作类型Operation Type校验基于底层 GraphQL 操作的类型进行queryHTTP 方法应为GET或POST允许POST以支持非原始变量类型mutationHTTP 方法不得为GETsubscription不允许RFC 注明未来可能改变。对应地server/tests-py/queries/endpoints/endpoint_subscription.yaml 验证了订阅操作的拒绝行为。变量名与类型Variable Names and TypesURL 模板中提及的每个变量都必须以原始类型GraphQL 变量的名字出现在底层操作中即模板参数必须对应操作里声明的变量且类型必须为标量。未来工作Future WorkRFC 在文末列出了若干可能的演进方向说明该规范是刻意收敛边界、留有扩展空间的设计Subscription 支持未来可能借助server-sent events这类标准在 HTTP 之上编码单向事件流并在浏览器兼容性上优雅降级嵌套错误支持目前任何顶层错误都会转换为 HTTP 错误码但嵌套错误的处理行为尚未定义未来也可能通过底层操作上的 GraphQL 指令选择性地把嵌套错误转成 HTTP 错误码Nullable 与 List 类型支持URL 变量的原始类型限制自然地扩展到 list 与 nullable 类型生成 Swagger 文档与元数据利用实体与字段上存储的注释生成 Swagger/OpenAPI 文档与元数据。其中「Swagger/OpenAPI」方向已部分落地Console 的 REST 端点页可一键Export OpenAPI Spec下载覆盖全部 RESTified 端点的 OpenAPI 3.0 规范 JSON见 docs/docs/restified/export-oas.mdx服务端对应实现为 server/src-lib/Hasura/Server/OpenAPI.hs。这正呼应了 RFC 开篇「利用现有 REST 工具链与自动化集成」的初衷。小结RFC rfcs/rest-endpoints.md 为 Hasura 的 RESTified Endpoints 提供了完整而克制的规范端点由名称、URL 模板、方法集合与固定 GraphQL 操作四要素构成URL 模板区分字面量与参数路由按「方法 → 段数 → 逐段匹配」的算法裁决并区分 404/405/500变量支持路径参数、URL query 与请求体三种来源且 URL 仅限标量cached指令驱动Cache-Control缓存元数据校验拒绝重叠端点、非法方法组合与越界变量。对照 server/src-lib/Hasura/Server/Rest.hs、server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs 与 server/tests-py/queries/endpoints/ 的测试用例可以确认规范中的每一项语义都有对应的工程实现与验证。对于想在偏 REST 的环境中渐进式采纳 Hasura、或希望以 OpenAPI 打通自动化集成的团队这一特性提供了从 GraphQL 到 REST 的低成本桥梁。赞分享后端API网关数据库GraphQL【免费下载链接】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点击查看免费下载相关推荐Headlamp 源码解析lib/k8s/endpoints 中的 Endpoints 类如何封装 Kubernetes Endpoints 资源Headlamp 源码解析lib/k8s/endpoints 中的 Endpoints 类如何封装 Kubernetes Endpoints 资源 本文以 H云原生开发工具Hasura GraphQL Engine 的 Apollo Federation v1 支持从 RFC 设计到源码实现Hasura GraphQL Engine 的 Apollo Federation v1 支持从 RFC 设计到源码实现 导读 本文以 rfcs/apollo后端API网关数据库GraphQLShields Badge URL 设计规范从路由约定到源码实现Shields Badge URL 设计规范从路由约定到源码实现 导读 本文以 doc/badge urls.md https://link.gitcode开发工具后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考