首页 > 自考资讯 > 自考知识

理解 GraphQL 指令:Zalando 的实际用例译文

头条共创 2024-08-08

在这篇博文中,我们深入探讨了 GraphQL 指令在 Zalando 的实际应用。通过简单示例,我们旨在强调它们如何增强我们的用例。从定义精确的授权要求到高效处理元数据,GraphQL 指令为我们的 API 开发流程提供了灵活性和控制力。

GraphQL 指令

在 GraphQL 中,如果您使用以 开头的语法@,例如 ,@foo则您使用了 GraphQL 指令。指令提供了一种使用受支持的语法扩展 GraphQL 语言功能的方法。某些指令内置于 GraphQL 中,例如@skip、@include、@deprecated和@specifiedBy,并且所有 GraphQL 引擎都支持这些指令。

如果我们仔细观察,就会发现其中两个指令(@skip和@include)仅用于查询,而另外两个指令(@deprecated和@specifiedBy)仅用于模式。这是因为 GraphQL 指令是针对两种不同类别的位置定义的 -TypeSystem和ExecutableDefinition。TypeSystem指令是针对模式定义的,而ExecutableDefinition指令是针对查询定义的。我们将在下一节中详细讨论这一点。

查询指令通常用于帮助客户端表达查询的某些类型的元数据。架构指令通常用于以声明方式指定常见的服务器端行为,例如授权要求、标记敏感数据等。

第 1 部分:Zalando 的 Schema 指令

架构指令是指针对TypeSystem位置定义的指令。类型系统指令适用于下列位置。请考虑@foo针对第一列中提到的位置的指令。

directive @foo on LOCATION_IN_FIRST_COLUMN

指令位置

例子

架构

schema @foo { query: Query }

标量

scalar x @foo

目的

type Product @foo { }

字段定义

type X { field: String @foo }

参数定义

type X { field(arg: Int @foo): String }

界面

interface X @foo {}

联盟

union X @foo = A | B

枚举

enum X @foo { A B }

枚举值

enum X { A @foo B }

输入对象

input X @foo { }

输入字段定义

input X { field: String @foo }

公会 - https://the-guild.dev有一篇很棒的文章,以及通过他们的graphql-tools包实现架构指令的机制。我强烈建议阅读它并使用 graphql-tools 来实现架构指令。

要点是,您可以在架构中定义一个指令,并在解析器层中实现该指令。该指令被实现为一个函数,该函数将解析器函数作为参数并返回一个新的解析器函数。新的解析器函数可用于实现指令逻辑。

您可以将架构指令视为以声明方式注入解析器函数的一些函数调用。请考虑下图以了解在解析器上下文中可以调用指令函数的位置。

/** * Illustration of schema directives execution in * the query execution pipeline */const resolvers = { Query: { async product(_, { id }) { // schema directives schemaDirectivesExecutions(); // resolver logic const product = await getProduct(id); // schema directives schemaDirectivesExecutions(); return product; }, },};

@isAuthenticated

在 Zalando,我们使用 SSO 进行客户身份验证和逐步身份验证。我们的 GraphQL 服务器处理产品数据等公开数据,还处理客户相关数据等机密数据。

查询可以包含客户字段以及产品字段和其他非客户数据。在这里,每当在查询中使用包含客户信息的字段或突变时,我们都需要确保客户经过身份验证并具有正确的真实性级别(ACR 值)。因此,我们需要一种方法来针对架构中的不同数据点进行精细控制。该指令@isAuthenticated就是为此目的而使用的。

该指令在架构中定义如下 -

scalar ACRValue @specifiedBy(url: "https://example.com/zalando-acr-value")directive @isAuthenticated( """ The ACR value, which indicates the level of authenticity expected to perform the operation. Optional. If not provided, the default behavior is to simply validate a user is authenticated and has no ACR requirements. """ acrValue: ACRValue) on FIELD_DEFINITION

例如,它在突变定义中的用法如下 -

type Query { customer: Customer @isAuthenticated}type Mutation { updateCustomerInfo( email: String phoneNumber: String ): UpdateCustomerInfoResult @isAuthenticated(acrValue: HIGH)}

@sensitive

我们通过 GraphQL API 公开客户敏感数据(例如电子邮件地址、客户姓名、电话号码、地址等),以呈现客户资料页面。我们还使用可观察性工具和监控工具,例如日志记录和跟踪。我们不希望日志和跟踪中包含此类敏感客户数据。因此,我们需要一种控制日志记录的方法,以便日志包含足够的信息来调试问题,但不包含敏感的客户数据。该指令@sensitive就是为此目的而使用的。

directive @sensitive( "An optional reason why the field is marked as sensitive" reason: String) on ARGUMENT_DEFINITION

例如,它在突变定义中的用法如下 -

type Mutation { updateCustomerInfo( email: String @sensitive(reason: "Customer email address") phoneNumber: String @sensitive(reason: "Customer phone number") ): UpdateCustomerInfoResult}

主动@sensitive在架构中添加正确的参数可能有点手动且容易忘记。因此,我们还依靠架构 linter 在字段/参数名称包含敏感关键字password(如email、、、、、、、、、、、、等)时自动失败。这样,我们可以确保不会忘记添加到正确的字段/参数。phonebankbicaccountownerordertokenvouchercustomer@sensitive

实现这个指令也相当简单,不需要任何解析器逻辑。它可以在 NodeJS 中实现如下(实现被缩短以适合一篇文章)-

function getSensitiveVariables(schema, document) { const sensitiveVariables = []; require("graphql").validate(schema, document, [ (context) => ({ Variable(node) { const isSensitive = context .getArgument() ?.astNode?.directives?.some( (directive) => directive.name.value === "sensitive" ); if (isSensitive) { sensitiveVariables.push(node.name.value); } }, }), ]); return sensitiveVariables;}

@requireExplicitEndpoint

使用 GraphQL,所有种类的 HTTP 请求都符合一个模式 - POST /graphql。它使 REST API 可以使用的技术和工具(例如速率限制、机器人保护、缓存和其他安全实践)无法开箱即用。因此,我们需要一种方法来控制通过不同的 HTTP 端点公开的不同架构部分。指令@requireExplicitEndpoint就是为此目的而使用的。

directive @requireExplicitEndpoint(endpoints: [String!]!) on FIELD_DEFINITION

在实现此指令时,我们会覆盖使用它的相应字段的解析器。我们可以通过在 HTTP 上运行 GraphQL 来访问请求参数(如路径名)。然后,我们将路径名与指令中提供的端点列表进行匹配,如果不匹配则返回错误。

该指令允许我们为不同的模式部分定义自定义路由,并防止客户端通过单个 HTTP 端点访问整个模式,POST /graphql.例如,让我们看看如何为updateDeliveryAddress变异定义该指令。

type Mutation { updateDeliveryAddress( id: ID! newAddress: CustomerAddress! ): UpdateDeliveryAddressResult @requireExplicitEndpoint(endpoints: ["/customer-addresses"])}

因此,通过端点执行时,类似以下的突变查询将会失败并出现错误/graphql-

# POST /graphqlmutation { updateDeliveryAddress(id: "1234", newAddress: { name: "Boopathi" }) { id }}

@draft,@allowedFor

我们使用持久查询,并为架构的不同部分定义不同的架构稳定性级别。我们有一篇单独的博客文章详细解释了Zalando 如何使用持久查询以及我们如何考虑架构稳定性和精细控制。

和@draft指令@allowedFor就是用于此目的。它可以防止客户端持久保存尚不稳定的查询。

# Draftdirective @draft on FIELD_DEFINITION# Restricted usage: Only for the specified componentsdirective @component(name: String!) on QUERYdirective @allowedFor(componentNames: [String!]!) on FIELD_DEFINITION

@final

GraphQL 中的枚举很难演变。向枚举添加新值不算重大更改,但仍属于“危险”更改。之所以“危险”,是因为客户端可能没有新值的处理程序。更新 Web 应用程序的客户端代码很容易,但对于发布到应用商店的移动原生应用,更新客户端代码是不可能的。虽然我们采用防御性编码实践来处理未知值,但我们仍然需要一种安全的方式来控制枚举的演变。指令@final就是用于此目的。

directive @final on ENUM

此指令的实现绝对没有任何意义 - 即,它不需要任何运行时行为。它仅用于在构建时执行的 GraphQL linter,并防止向标记为 final 的枚举添加新值。当我们想要进行危险更改时,我们会@final在第一个拉取请求中删除该指令,并推理并查看旧应用程序是否会因进行此“危险”更改而中断。扩展枚举后,我们会将其添加到单独的拉取请求中。这个过程很麻烦,但这是故意的。进行危险更改必须更加复杂,这是我们愿意做出的权衡。

理想的情况是所有枚举都默认视为 final,并且此指令从一开始就不需要。在架构演变期间,您的用例可能需要此类指令来控制平稳的架构演变。

@extensibleEnum

我们正在讨论枚举,枚举指令的另一个用例主要是一次性用例,扩展它们是常见情况。在这些情况下,为一个用例创建枚举很棘手,扩展它会带来危险的后果。在 Zalando,我们有 RESTful API 指南,其中一条建议是使用x-extensible-enum来表示所有枚举。此建议是为了让枚举可以不断发展,并且客户端从名称就知道它是可扩展的。我们将指令@extensibleEnum用于此目的。字段在 GraphQL 中的类型为String,并且指令用于提供允许值的列表。

directive @extensibleEnum(values: [String!]!) on FIELD_DEFINITION

例如,它在查询定义中如下使用 -

type CustomerConsent { status: String! @extensibleEnum(values: ["GRANTED", "REJECTED"])}

通过@extensibleEnum,我们发现架构的贡献者更有可能考虑架构的演变。我们还注意到,与 GraphQL 原生枚举相比,贡献者更有可能使用此指令来定义枚举,因为此指令对枚举的可扩展性有更明确的说明。

@resolveEntityId

我们的 GraphQL 模式将某些类型定义为与实体关系模型相关的实体。我们抽象地将实体定义为设计客户体验的基本构建块。例如,产品、客户、品牌等都是一些实体。实体定义具有一些属性 -

它遵循特定的解析器模板/模式,对于所有实体来说,该模板/模式基本相同它具有架构中定义的特定类型名称它具有特定模式的唯一 ID(entity:product:1234例如type Product)它有一组所有实体共有的字段

@resolveEntityId为了全面解决这些情况,我们使用针对模式中每个实体定义定义的指令。

directive @resolveEntityId( "An optional override name for the entity name in its ID" override: String) on OBJECT

使用方法如下 -

type Product implements Entity @resolveEntityId { id: ID!}

该指令的实现有两个方面。首先,我们根据指令生成 TypeScript 代码resolveEntityId。此代码生成使我们能够开发实体 ID 类型定义和解析器的样板代码 - 例如,解析器__typename。另一部分是运行时,其中id添加了解析器来包装实体 ID - 例如,考虑产品 -entity:product:1234是完整的实体 ID,并且1234称为产品的 SKU。

第 2 部分:Zalando 的查询指令

查询指令是针对位置定义的指令ExecutableDefinition。可执行指令适用于以下列出的位置。考虑@foo针对第一列中提到的位置的指令。

directive @foo on LOCATION_IN_FIRST_COLUMN

指令位置

例子

询问

query name @foo {}

突变

mutation name @foo {}

订阅

subscription name @foo {}

场地

query { product @foo {} }

片段定义

fragment x on Query @foo { }

片段扩展

query { ...x @foo }

内联片段

query { ... @foo { } }

变量定义

query ($id: ID @foo) { }

与架构指令不同,graphql-tools不支持像查询指令那样将函数附加到解析器。它们还有一个很好的观点:查询指令适合用元数据注释查询,而不是解析器逻辑。同样,我们的大多数用例包括在查询级别附加元数据,以及一个用于可观察性和监控的用例。

对于查询元数据,实现非常简单,只需浏览已解析的 GraphQL 文档(AST - 抽象语法树)并从查询指令中提取元数据即可。对于将行为添加到字段(特别是指令,如下所述)的用例,我们使用两步方法@omitErrorTag。在执行前的第一步中,我们提取具有此指令的字段的字段路径。在第二步中,在执行后,我们匹配错误路径并省略这些提取路径的错误标签。

@component

该@component指令由客户端为查询定义组件名称。该指令用于我们的可观察性和监控工具以及架构稳定性 - 在生产中的使用受到限制。有关更多详细信息,请参阅我们的博客文章GraphQL 持久查询和架构稳定性。

directive @component(name: String!) on QUERY

@tracingTag

该@tracingTag指令为查询定义了一个OpenTelemetry跟踪标记。在查询上使用此指令会将特定的客户端定义标记添加到我们的跟踪范围中。然后,客户端可以跟踪跟踪并按此标记进行筛选以查找特定查询的跟踪。此指令对于调试、故障排除、监控特定查询集等非常有用。

directive @tracingTag(value: String!) on QUERY | MUTATION | SUBSCRIPTION

@omitErrorTag

该@omitErrorTag指令用于忽略将跟踪范围标记为错误。此指令可用于查询中的特定字段。此指令允许客户端定义某些字段错误不严重,不应报告以进行警报。这样,全天候值班团队就可以专注于关键错误,而不会被噪音分散注意力。

directive @omitErrorTag on FIELD

@maxCountInBatch

该@maxCountInBatch指令用于查询级别,以声明单个请求中可以批量处理的最大查询数。此指令由客户端控制,即它仅在构建/持久时可用。在运行时,该指令用于防止数据过度获取和机器人滥用 GraphQL API。

我们的 GraphQL 服务器允许在一个批次中批量处理多个查询。对于持久查询,我们仅发送查询的 ID,并且客户端无法在生产中发送原始查询。因此,系统设计允许maxCountInBatch由客户端控制的安全使用。

directive @maxCountInBatch(value: Int!) on QUERY

上述所有查询指令的示例用法

query product_card($id: ID!)## component directive@component(name: "web-product-card")## tracing tag directive to add a tag to the tracing span@tracingTag(value: "slo-1s")## maxCountInBatch directive to limit the number of queries in a batch request@maxCountInBatch(value: 50) { product(id: $id) { id name brand { id name } # omitErrorTag directive to omit marking the tracing # span as an error if inWishlist field errors inWishlist @omitErrorTag }}

结论

查询指令允许客户端定义元数据,在极少数情况下还可以定义行为。另一方面,架构指令允许服务器以声明方式定义行为、验证和解析逻辑。架构指令的另一个优势是,服务器可以对这些指令进行重大更改,因为这些指令不会被客户端使用 - 它们只会体验到结果行为。在设计指令时,重要的是要考虑其属性、用例、权衡以及控制应该在哪里。

本篇博文概述的用例代表了我们在 Zalando 使用 GraphQL 指令的一些方式。我们将在未来的博文中介绍许多其他案例。我希望本文能为您探索 GraphQL 指令及其实际应用提供一个良好的起点。

作者:Boopathi Rajaa Nedunchezhiyan Principal Engineer

出处:https://engineering.zalando.com/posts/2023/10/understanding-graphql-directives-practical-use-cases-zalando.html

版权声明:本文转载于今日头条,版权归作者所有,如果侵权,请联系本站编辑删除

猜你喜欢