GraphQL API style guide

原文:https://docs.gitlab.com/ee/development/api_graphql_styleguide.html

GraphQL API style guide

本文档概述了 GitLab 的GraphQL API的样式指南.

How GitLab implements GraphQL

我们使用Robert Mosolgo编写的GraphQL Ruby 宝石 .

所有 GraphQL 查询都定向到单个端点( app/controllers/graphql_controller.rb#execute ),该端点在/api/graphql处作为 API 端点/api/graphql .

Deep Dive

在 2019 年 3 月,尼克·托马斯(Nick Thomas)在 GitLab 的GraphQL API 上进行了一次深潜(仅限 GitLab 团队成员: https://gitlab.com/gitlab-org/create-stage/issues/1 : //gitlab.com/gitlab-org/create-stage/issues/1 ),以与可能将来会在代码库的这一部分中工作. 您可以在 YouTube 上找到录音 ,在Google 幻灯片PDF 中找到幻灯片 . 自 GitLab 11.9 起,本次深入学习中涉及的所有内容都是准确的,尽管自那时以来特定细节可能有所更改,但它仍应作为一个很好的介绍.

GraphiQL

GraphiQL 是一个交互式 GraphQL API 资源管理器,您可以在其中使用现有查询. 您可以在https://<your-gitlab-site.com>/-/graphql-explorer任何 GitLab 环境中访问它. 例如,用于GitLab.com 的那个 .

Authentication

认证通过GraphqlController ,现在,它使用与 Rails 应用程序相同的认证. 因此可以共享会话.

也可以将private_token添加到查询字符串,或添加HTTP_PRIVATE_TOKEN标头.

Types

我们使用代码优先模式,并声明 Ruby 中所有内容的类型.

例如, app/graphql/types/issue_type.rb

  1. graphql_name 'Issue'
  2. field :iid, GraphQL::ID_TYPE, null: true
  3. field :title, GraphQL::STRING_TYPE, null: true
  4. # we also have a method here that we've defined, that extends `field`
  5. markdown_field :title_html, null: true
  6. field :description, GraphQL::STRING_TYPE, null: true
  7. markdown_field :description_html, null: true

We give each type a name (in this case Issue).

iidtitledescription标量 GraphQL 类型. iidGraphQL::ID_TYPE ,这是一种特殊的字符串类型,表示唯一的 ID. titledescription是常规的GraphQL::STRING_TYPE类型.

When exposing a model through the GraphQL API, we do so by creating a new type in app/graphql/types. You can also declare custom GraphQL data types for scalar data types (e.g. TimeType).

公开类型中的属性时,请确保将定义内的逻辑保持尽可能小. 相反,请考虑将任何逻辑转移到演示者中:

  1. class Types::MergeRequestType < BaseObject
  2. present_using MergeRequestPresenter
  3. name 'MergeRequest'
  4. end

可以使用现有的演示者,但是也可以专门为 GraphQL 创建一个新的演示者.

使用由字段解析的对象和上下文初始化演示者.

Nullable fields

GraphQL 允许字段为”可为空”或”不可为空”. 前者意味着可以返回null而不是指定类型的值. 通常 ,出于以下原因,您应该首选使用可空字段而不是不可空字段:

  • 数据从必需切换到不需要,然后再次返回是很常见的
  • 即使没有可能成为可选字段的前景,在查询时它也可能不可
    • 例如,可能需要从 Gitaly 查找 blob 的content
    • 如果content可为空,我们可以返回部分响应,而不是使整个查询失败
  • 对于无版本模式,很难从不可为空的字段更改为可为空的字段

非空字段仅应在需要字段时使用,将来不太可能成为可选字段,并且非常容易计算. 一个示例是id字段.

进一步阅读:

Exposing Global IDs

在类型上公开ID字段时,默认情况下,我们将通过在要渲染的资源上调用to_global_id来公开全局 ID.

要覆盖此行为,可以在要公开其 ID 的类型上实现id方法. 请确保在使用自定义方法公开GraphQL::ID_TYPE ,它是全局唯一的.

被曝光的记录full_path作为ID_TYPE是这些例外之一. 由于完整路径是ProjectNamespace的唯一标识符.

Connection Types

GraphQL 使用基于光标的分页来公开项目的集合. 这为客户提供了很大的灵活性,同时还允许后端使用不同的分页模型.

为了公开资源的集合,我们可以使用连接类型. 这将使用默认的分页字段包装数组. 例如,对项目管道的查询可能如下所示:

  1. query($project_path: ID!) { project(fullPath: $project_path) { pipelines(first: 2) { pageInfo { hasNextPage hasPreviousPage } edges { cursor node { id status } } } } }

这将返回项目的前两个管道和相关的分页信息,按降序 ID 排序. 返回的数据如下所示:

  1. { "data": { "project": { "pipelines": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false }, "edges": [ { "cursor": "Nzc=", "node": { "id": "gid://gitlab/Pipeline/77", "status": "FAILED" } }, { "cursor": "Njc=", "node": { "id": "gid://gitlab/Pipeline/67", "status": "FAILED" } } ] } } } }

要获取下一页,可以传递最后一个已知元素的光标:

  1. query($project_path: ID!) { project(fullPath: $project_path) { pipelines(first: 2, after: "Njc=") { pageInfo { hasNextPage hasPreviousPage } edges { cursor node { id status } } } } }

为了确保获得一致的顺序,我们将在主键上按降序附加顺序. 这通常是id ,因此基本上我们将在关系的末尾添加order(id: :desc) . 基础表上必须有主键.

Shortcut fields

有时似乎很容易实现”快捷字段”,如果没有传递任何参数,则让解析程序返回集合的第一个. 不鼓励使用这些”快捷字段”,因为它们会增加维护开销. 它们需要与规范字段保持同步,并且如果规范字段发生更改,则不建议使用或修改它们. 除非有充分的理由,否则请使用框架提供的功能.

例如,不要使用latest_pipeline ,而应使用pipelines(last: 1) .

Exposing permissions for a type

若要公开当前用户对资源的权限,可以调用以单独的类型传递的expose_permissions ,该类型表示资源的权限.

例如:

  1. module Types
  2. class MergeRequestType < BaseObject
  3. expose_permissions Types::MergeRequestPermissionsType
  4. end
  5. end

权限类型继承自BasePermissionType ,其中包括一些帮助程序方法,这些方法允许将权限公开为不可为 null 的布尔值:

  1. class MergeRequestPermissionsType < BasePermissionType
  2. present_using MergeRequestPresenter
  3. graphql_name 'MergeRequestPermissions'
  4. abilities :admin_merge_request, :update_merge_request, :create_note
  5. ability_field :resolve_note,
  6. description: 'Indicates the user can resolve discussions on the merge request'
  7. permission_field :push_to_source_branch, method: :can_push_to_source_branch?
  8. end
  • permission_field :其作用与graphql-rubyfield方法相同,但设置默认描述和类型,并使它们不可为空. 通过将它们添加为参数,仍然可以覆盖这些选项.
  • ability_field :公开我们政策中定义的能力. 此行为与permission_field相同,并且可以覆盖相同的参数.
  • abilities :允许一次暴露我们政策中定义的几种能力. 这些字段都将是带有默认说明的非空布尔值.

Feature flags

开发人员可以通过以下方式将功能标志添加到 GraphQL 字段:

  • feature_flag属性添加到字段. 当禁用该标志时,这将允许从 GraphQL 模式中隐藏该字段.
  • 解析字段时切换返回值.

您可以参考以下准则来决定使用哪种方法:

  • 如果您的字段是实验性的,并且其名称或类型可能会发生变化,请使用feature_flag属性.
  • 如果您的字段是稳定的,并且即使删除了标志,其定义也不会更改,请改为切换字段的返回值. 请注意, 所有字段无论如何都应该为空 .

feature_flag property

feature_flag属性允许您在 GraphQL 模式中切换字段的可见性 . 禁用该标志后,将从架构中删除该字段.

字段后附有说明,表示该说明位于功能标志的后面.

警告:如果在禁用功能标志时客户端查询该字段,则查询将失败. 在生产中打开或关闭功能的可见性时,请考虑此问题.

feature_flag属性不允许使用基于 actor特征门 . 这意味着功能标记不能仅针对特定的项目,组或用户进行切换,而只能针对所有人进行全局切换.

Example:

  1. field :test_field, type: GraphQL::STRING_TYPE,
  2. null: true,
  3. description: 'Some test field',
  4. feature_flag: :my_feature_flag

Toggle the value of a field

对字段使用特征标记的这种方法是切换字段的返回值. 这可以在解析器中,在类型中甚至在模型方法中完成,具体取决于您的偏好和情况.

当应用功能标记来切换字段的值时,该字段的description必须:

  • 说明该字段的值可以通过功能标记切换.
  • 命名功能标志.
  • 说明禁用(或启用,如果更合适的话)功能标志时字段将返回的内容.

Example:

  1. field :foo, GraphQL::STRING_TYPE,
  2. null: true,
  3. description: 'Some test field. Will always return `null`' \
  4. 'if `my_feature_flag` feature flag is disabled'
  5. def foo
  6. object.foo unless Feature.enabled?(:my_feature_flag, object)
  7. end

Deprecating fields

GitLab 的 GraphQL API 是无版本的,这意味着我们会与 API 的旧版本保持向下兼容性. 除了删除字段,我们还需要弃用该字段. 将来,GitLab 可能会删除不推荐使用的字段 .

使用deprecated推荐使用的属性不推荐使用字段. 该属性的值是以下各项的Hash值:

  • reason -弃用的原因.
  • milestone -已弃用该字段的里程碑.

Example:

  1. field :token, GraphQL::STRING_TYPE, null: true,
  2. deprecated: { reason: 'Login via token has been removed', milestone: '10.0' },
  3. description: 'Token for login'

最初的description:现场应保持,并且应该更新提折旧.

Deprecation reason style guide

如果弃用的原因是该字段被另一个字段替换,则reason必须是:

  1. Use `otherFieldName`

Example:

  1. field :designs, ::Types::DesignManagement::DesignCollectionType, null: true,
  2. deprecated: { reason: 'Use `designCollection`', milestone: '10.0' },
  3. description: 'The designs associated with this issue',

如果该字段没有被另一个字段替换,则应给出描述性的弃用reason .

Enums

GitLab GraphQL enums are defined in app/graphql/types. When defining new enums, the following rules apply:

  • 值必须为大写.
  • 类名必须以字符串Enum结尾.
  • graphql_name不得包含字符串Enum .

例如:

  1. module Types
  2. class TrafficLightStateEnum < BaseEnum
  3. graphql_name 'TrafficLightState'
  4. description 'State of a traffic light'
  5. value 'RED', description: 'Drivers must stop'
  6. value 'YELLOW', description: 'Drivers must stop when it is safe to'
  7. value 'GREEN', description: 'Drivers can start or keep driving'
  8. end
  9. end

If the enum will be used for a class property in Ruby that is not an uppercase string, you can provide a value: option that will adapt the uppercase value.

在以下示例中:

  • GraphQL inputs of OPENED will be converted to 'opened'.
  • Ruby 的'opened'值将在 GraphQL 响应中转换为"OPENED" .
  1. module Types
  2. class EpicStateEnum < BaseEnum
  3. graphql_name 'EpicState'
  4. description 'State of a GitLab epic'
  5. value 'OPENED', value: 'opened', description: 'An open Epic'
  6. value 'CLOSED', value: 'closed', description: 'An closed Epic'
  7. end
  8. end

Descriptions

所有字段和参数都必须具有描述 .

使用description:关键字给出字段或自变量的description: . 例如:

  1. field :id, GraphQL::ID_TYPE, description: 'ID of the resource'

用户可以通过以下方式查看字段和参数的描述:

Description style guide

为确保一致性,每次添加或更新描述时都应遵循以下规定:

  • 在描述中提及资源的名称. 示例: 'Labels of the issue' (问题就是资源).
  • 尽可能"{x} of the {y}" . 示例: 'Title of the issue' . 不要下手描述The .
  • GraphQL::BOOLEAN_TYPE字段的描述应回答以下问题:”此字段的作用是什么?”. 示例: 'Indicates project has a Git repository' .
  • 描述类型为Types::TimeType的参数或字段时,请始终包含单词"timestamp" . 这使读者知道该属性的格式将是Time ,而不仅仅是Date .
  • 没有. 在字符串末尾.

Example:

  1. field :id, GraphQL::ID_TYPE, description: 'ID of the Issue'
  2. field :confidential, GraphQL::BOOLEAN_TYPE, description: 'Indicates the issue is confidential'
  3. field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed'

copy_field_description helper

有时我们希望确保两个描述始终相同. 例如,当两个类型字段描述都表示相同的属性时,它们要与突变参数保持相同.

除了提供描述之外,我们还可以使用copy_field_description帮助器,将其类型和字段名称传递给它,以复制其描述.

Example:

  1. argument :title, GraphQL::STRING_TYPE,
  2. required: false,
  3. description: copy_field_description(Types::MergeRequestType, :title)

Authorization

可以使用与 Rails 应用程序中相同的功能将授权应用于类型和字段.

如果:

  • 当前经过身份验证的用户未通过授权,授权资源将返回null .
  • 资源是集合的一部分,该集合将被过滤以排除用户授权检查失败的对象.

另请参见在变异中授权资源 .

提示:尝试仅先加载允许当前已认证用户使用我们现有的查找器查看的内容,而不依赖于授权来过滤记录. 这样可以最大程度地减少数据库查询和对已加载记录的不必要的授权检查.

Type authorization

通过将能力传递给authorize方法来authorize类型. 通过检查当前经过身份验证的用户是否具有所需的能力,将对所有具有相同类型的字段进行授权.

例如,以下授权可确保当前经过身份验证的用户只能看到其具有read_project能力的项目(只要在使用Types::ProjectType的字段中返回该Types::ProjectType ):

  1. module Types
  2. class ProjectType < BaseObject
  3. authorize :read_project
  4. end
  5. end

您还可以授权多个能力,在这种情况下,所有能力检查都必须通过.

例如,以下授权可确保当前经过身份验证的用户必须具有read_projectanother_ability能力才能查看项目:

  1. module Types
  2. class ProjectType < BaseObject
  3. authorize [:read_project, :another_ability]
  4. end
  5. end

Field authorization

可以使用authorize选项对字段进行授权.

例如,以下授权可确保当前经过身份验证的用户必须具有owner_access功能才能查看项目:

  1. module Types
  2. class MyType < BaseObject
  3. field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access
  4. end
  5. end

还可以针对多个能力授权字段,在这种情况下,所有能力检查都必须通过. 注意:这需要显式地将一个块传递给field

  1. module Types
  2. class MyType < BaseObject
  3. field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
  4. authorize [:owner_access, :another_ability]
  5. end
  6. end
  7. end

注意:如果字段的类型已经具有特定的授权,则无需将该相同的授权添加到字段中.

Type and Field authorizations together

授权是累积性的,因此,在一个字段以及该字段的类型上定义了授权的情况下,当前经过身份验证的用户将需要通过所有能力检查.

在下面的简化示例中,当前经过身份验证的用户将需要first_permissionsecond_permission能力,才能看到问题的作者.

  1. class UserType
  2. authorize :first_permission
  3. end
  1. class IssueType
  2. field :author, UserType, authorize: :second_permission
  3. end

Resolvers

我们使用存储在app/graphql/resolvers目录中的解析器定义应用程序如何响应. 解析器提供了用于检索相关对象的实际实现逻辑.

要查找要显示在字段中的对象,我们可以将解析器添加到app/graphql/resolvers .

可以在解析程序中定义参数,这些参数将通过解析程序提供给字段. 公开具有内部 ID( iid )的模型时,最好将其与名称空间路径结合使用,作为解析器中的参数,而不是数据库 ID. 否则,请使用全局唯一 ID .

我们已经有一个FullPathLoader ,可以将其包含在其他解析器中,以快速查找将有很多依赖对象的项目和命名空间.

为了限制执行的查询数量,我们可以使用BatchLoader .

Correct use of Resolver#ready?

解析器有两个公共 API 方法作为框架的一部分: #ready?(**args)#resolve(**args) . 我们可以使用#ready? 无需调用#resolve即可执行设置,验证或提前退货.

有充分理由使用#ready? 包括:

  • 验证互斥参数(请参阅验证参数
  • 如果事先知道没有结果,则返回Relation.none
  • 执行诸如初始化实例变量的设置(尽管为此考虑了延迟初始化的方法)

Resolver#ready?(**args)应返回(Boolean, early_return_data) ,如下所示:

  1. def ready?(**args)
  2. [false, 'have this instead']
  3. end

因此,无论何时调用解析器(主要是在测试中-作为框架抽象,不应将解析器视为可重用的,最好使用查找器),还记得调用ready? 方法,并在调用resolve之前检查布尔值标志! 在我们的GraphQLHelpers可以看到一个示例.

Look-Ahead

完整查询是在执行期间预先知道的,这意味着我们可以利用超前查询来优化查询,并知道我们将需要的批处理负载关联. 考虑在解析器中添加前瞻性支持,以避免N+1性能问题.

为了支持常见的前瞻用例(在请求子字段时预加载关联),可以包含LooksAhead . 例如:

  1. # Assuming a model `MyThing` with attributes `[child_attribute, other_attribute, nested]`,
  2. # where nested has an attribute named `included_attribute`.
  3. class MyThingResolver < BaseResolver
  4. include LooksAhead
  5. # Rather than defining `resolve(**args)`, we implement: `resolve_with_lookahead(**args)`
  6. def resolve_with_lookahead(**args)
  7. apply_lookahead(MyThingFinder.new(current_user).execute)
  8. end
  9. # We list things that should always be preloaded:
  10. # For example, if child_attribute is always needed (during authorization
  11. # perhaps), then we can include it here.
  12. def unconditional_includes
  13. [:child_attribute]
  14. end
  15. # We list things that should be included if a certain field is selected:
  16. def preloads
  17. {
  18. field_one: [:other_attribute],
  19. field_two: [{ nested: [:included_attribute] }]
  20. }
  21. end
  22. end

需要做的最后一件事是,使用此解析器的每个字段都需要公告提前查询的需求:

  1. # in ParentType
  2. field :my_things, MyThingType.connection_type, null: true,
  3. extras: [:lookahead], # Necessary
  4. resolver: MyThingResolver,
  5. description: 'My things'

有关实际使用的示例,请参见ResolvesMergeRequests .

Mutations

变异用于更改任何存储的值或触发动作. 与 GET 请求不应修改数据的方式相同,我们无法在常规 GraphQL 查询中修改数据. 但是我们可以突变.

要查找突变的对象,需要指定参数. 与解析程序一样 ,最好使用内部 ID 或全局 ID(而不是数据库 ID)(如果需要).

Building Mutations

突变存在于app/graphql/mutations理想情况下,突变是根据它们正在突变的资源进行分组的,类似于我们的服务. 他们应该继承Mutations::BaseMutation . 突变的结果将返回在突变上定义的字段.

Naming conventions

每个突变都必须定义一个graphql_name ,这是 GraphQL 模式中的突变名称.

Example:

  1. class UserUpdateMutation < BaseMutation
  2. graphql_name 'UserUpdate'
  3. end

我们的 GraphQL 突变名称在历史上是不一致的,但是新的突变名称应遵循约定'{Resource}{Action}''{Resource}{Action}{Attribute}' .

创建新资源的变异应使用动词Create .

Example:

  • CommitCreate

更新数据的突变应使用:

  • 动词Update .
  • 特定于域的动词,例如SetAddToggle如果更合适).

Examples:

  • EpicTreeReorder
  • IssueSetWeight
  • IssueUpdate
  • TodoMarkDone

删除数据的突变应使用:

  • 动词Delete而不是Destroy .
  • 特定于域的动词,例如” Remove如果更合适).

Examples:

  • AwardEmojiRemove
  • NoteDelete

如果您需要有关突变命名的建议,请查看 Slack #graphql渠道以获取反馈.

Arguments

突变所需的参数可以定义为字段所需的参数. 这些将被包装为突变的输入类型. 例如,带有 GraphQL 名称MergeRequestSetWipMutations::MergeRequests::SetWip MergeRequestSetWip定义了以下参数:

  1. argument :project_path, GraphQL::ID_TYPE,
  2. required: true,
  3. description: "The project the merge request to mutate is in"
  4. argument :iid, GraphQL::STRING_TYPE,
  5. required: true,
  6. description: "The iid of the merge request to mutate"
  7. argument :wip,
  8. GraphQL::BOOLEAN_TYPE,
  9. required: false,
  10. description: <<~DESC Whether or not to set the merge request as a WIP.
  11. If not passed, the value will be toggled. DESC

这将自动生成一个名为MergeRequestSetWipInput的输入类型, MergeRequestSetWipInput包含我们指定的 3 个参数和clientMutationId .

然后将这些参数作为关键字参数传递给突变的resolve方法.

Fields

在最常见的情况下,变异会返回 2 个字段:

  • 正在修改的资源
  • 错误列表,说明无法执行该操作的原因. 如果突变成功,此列表将为空.

通过从Mutations::BaseMutation继承任何新的突变, errors字段将自动添加. 还添加了一个clientMutationId字段,当在单个请求中执行多个变异时,客户端可以使用它来标识单个变异的结果.

The resolve method

resolve方法接收变异的参数作为关键字参数. 从这里,我们可以调用将修改资源的服务.

然后, resolve方法应返回一个哈希,该哈希具有与在突变上定义的字段名称相同的字段名称,包括errors数组. 例如, Mutations::MergeRequests::SetWip定义了merge_request字段:

  1. field :merge_request,
  2. Types::MergeRequestType,
  3. null: true,
  4. description: "The merge request after mutation"

这意味着在此突变中从resolve返回的哈希应如下所示:

  1. {
  2. # The merge request modified, this will be wrapped in the type
  3. # defined on the field
  4. merge_request: merge_request,
  5. # An array of strings if the mutation failed after authorization.
  6. # The `errors_on_object` helper collects `errors.full_messages`
  7. errors: errors_on_object(merge_request)
  8. }

Mounting the mutation

为了使变异可用,必须在存在于graphql/types/mutation_types的变异类型上进行定义. mount_mutation帮助器方法将基于突变的 GraphQL 名称定义一个字段:

  1. module Types
  2. class MutationType < BaseObject
  3. include Gitlab::Graphql::MountMutation
  4. graphql_name "Mutation"
  5. mount_mutation Mutations::MergeRequests::SetWip
  6. end
  7. end

将生成一个名为mergeRequestSetWip的字段, Mutations::MergeRequests::SetWip字段将要解决的Mutations::MergeRequests::SetWip .

Authorizing resources

要授权某个变异内的资源,我们首先要提供所需的变异能力,如下所示:

  1. module Mutations
  2. module MergeRequests
  3. class SetWip < Base
  4. graphql_name 'MergeRequestSetWip'
  5. authorize :update_merge_request
  6. end
  7. end
  8. end

然后,我们可以致电authorize!resolve方法中,传入我们要验证其功能的资源.

或者,我们可以添加一个find_object方法,该方法将在突变上加载对象. 这将允许您使用authorized_find! 辅助方法.

当不允许用户执行该操作或找不到对象时,我们应该引发Gitlab::Graphql::Errors::ResourceNotAvailable错误. 哪些将正确呈现给客户端.

Errors in mutations

我们鼓励遵循错误的做法,将其作为突变的数据 ,从而根据错误的相关者,定义的错误处理者来区分错误.

关键点:

  • 所有突变响应都有一个errors字段. 如果失败,则应填充此文件;如果成功,则应填充该文件.
  • 考虑谁需要看到错误: 用户还是开发人员 .
  • 客户在执行突变时应始终请求errors字段.
  • 错误可能会以$root.errors (顶级错误)或$root.data.mutationName.errors (变异错误)的形式报告给用户. 位置取决于这是什么类型的错误以及它所包含的信息.

考虑一个示例变体doTheThing ,该变doTheThing返回带有两个字段的响应: errors: [String]thing: ThingType . 由于我们正在考虑错误,因此thing本身的特定性质与这些示例无关.

突变响应可以处于三种状态:

Success

在快乐的道路上, 可能会返回错误以及预期的有效负载,但是如果一切成功,则errors应该是一个空数组,因为没有任何问题需要通知用户.

  1. {
  2. data: {
  3. doTheThing: {
  4. errors: [] // if successful, this array will generally be empty.
  5. thing: { .. }
  6. }
  7. }
  8. }

Failure (relevant to the user)

发生了影响用户的错误. 我们将这些称为突变错误 . 在这种情况下通常没有thing来回报:

  1. {
  2. data: {
  3. doTheThing: {
  4. errors: ["you cannot touch the thing"],
  5. thing: null
  6. }
  7. }
  8. }

例如:

  • Model validation errors: the user may need to change the inputs.
  • 权限错误:用户需要知道他们不能执行此操作,他们可能需要请求权限或登录.
  • 应用程序状态问题阻止了用户的操作,例如:合并冲突,资源被锁定等等.

理想情况下,我们应该防止用户走得太远,但是如果这样做了,则需要告诉他们出了什么问题,以便他们了解失败的原因以及可以实现其意图的方法,即使那很简单重试请求.

可以与变异数据一起返回可恢复的错误. 例如,如果用户上载 10 个文件,而其中 3 个失败,其余文件成功,则可以将失败的错误以及有关成功的信息与用户一起使用.

Failure (irrelevant to the user)

可以在顶层返回一个或多个不可恢复的错误. 这些是用户几乎无法控制的事情,主要应该是开发人员需要了解的系统或编程问题. 在这种情况下,没有data

  1. {
  2. errors: [
  3. {"message": "argument error: expected an integer, got null"},
  4. ]
  5. }

这是在突变过程中引发错误的结果. 在我们的实现中,参数错误和验证错误的消息将返回给客户端,所有其他StandardError实例将被捕获,记录并呈现给客户端,并将消息设置为"Internal server error" . 有关详细信息,请参见GraphqlController .

这些代表编程错误,例如:

  • A GraphQL syntax error, where an Int was passed instead of a String, or a required argument was not present.
  • 我们架构中的错误,例如无法为不可为空的字段提供值.
  • 系统错误:例如,Git 存储异常或数据库不可用.

用户在常规使用中不应导致此类错误. 此类错误应视为内部错误,并且不向用户详细显示.

我们需要在突变失败时通知用户,但是我们不必告诉他们原因,因为他们不可能造成突变,尽管我们可以提供重试突变的方法,但他们无能为力.

Categorizing errors

当我们编写突变时,我们需要意识到错误状态属于这两个类别中的哪一个(并与前端开发人员进行沟通以验证我们的假设). 这意味着将用户的需求与客户的需求区分开.

除非用户需要了解错误,否则切勿捕获错误.

如果用户确实需要了解它,请与前端开发人员进行交流,以确保我们传回的错误信息有用.

另请参见前端 GraphQL 指南 .

Aliasing and deprecating mutations

#mount_aliased_mutation帮助器允许我们将突变别名作为MutationType另一个名称.

例如,将名为FooMutation的突变别名为BarMutation

  1. mount_aliased_mutation 'BarMutation', Mutations::FooMutation

结合deprecated参数,这使我们可以重命名突变并继续支持旧名称.

Example:

  1. mount_aliased_mutation 'UpdateFoo',
  2. Mutations::Foo::Update,
  3. deprecated: { reason: 'Use fooUpdate', milestone: '13.2' }

不赞成使用的突变应添加到Types::DeprecatedMutations并在Types::MutationType的单元测试中进行测试. 合并请求!34798可以称为此示例,包括测试已弃用的别名突变的方法.

Validating arguments

要验证单个参数,请照常使用prepare选项 .

有时,变异或解析器可以接受许多可选参数,但是我们仍然要验证是否至少提供了一个可选参数. 在这种情况下,请考虑使用#ready? 突变或解析器中提供验证的方法. #ready?#resolve方法中完成任何工作之前,将调用方法.

Example:

  1. def ready?(**args)
  2. if args.values_at(:body, :position).compact.blank?
  3. raise Gitlab::Graphql::Errors::ArgumentError,
  4. 'body or position arguments are required'
  5. end
  6. # Always remember to call `#super`
  7. super
  8. end

如果将来将此RFC合并,则可以使用InputUnions完成.

GitLab’s custom scalars

Types::TimeType

Types::TimeType必须用作处理 Ruby TimeDateTime对象的所有字段和参数的类型.

该类型是一个自定义标量

  • 当用作 GraphQL 字段的类型时,将 Ruby 的TimeDateTime对象转换为标准化的 ISO-8601 格式的字符串.
  • 当用作 GraphQL 参数的类型时,将 ISO-8601 格式的时间字符串转换为 Ruby Time对象.

这使我们的 GraphQL API 具有标准化的方式来表示时间并处理时间输入.

Example:

  1. field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was created'

Testing

spec/requests/api/graphql实时进行针对 graphql 查询或突变的全栈测试.

添加查询时,可以使用a working graphql query共享示例来测试该查询是否呈现有效结果.

使用GraphqlHelpers#all_graphql_fields_for -helper,可以构造一个包含所有可用字段的查询. 这使得添加测试渲染所有可能的查询字段变得容易.

为了测试 GraphQL 突变请求, GraphqlHelpers提供了 2 个助手: graphql_mutation ,它使用突变的名称;以及带有该突变输入的哈希. 这将返回带有变异查询和预备变量的结构.

然后可以将此结构传递给post_graphql_mutation帮助器,该帮助器将使用正确的参数发布请求,就像 GraphQL 客户端所做的那样.

要访问突变的响应,可以使用graphql_mutation_response帮助器.

使用这些帮助器,我们可以建立如下规格:

  1. let(:mutation) do
  2. graphql_mutation(
  3. :merge_request_set_wip,
  4. project_path: 'gitlab-org/gitlab-foss',
  5. iid: '1',
  6. wip: true
  7. )
  8. end
  9. it 'returns a successful response' do
  10. post_graphql_mutation(mutation, current_user: user)
  11. expect(response).to have_gitlab_http_status(:success)
  12. expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty
  13. end

Notes about Query flow and GraphQL infrastructure

可以在lib/gitlab/graphql找到 GitLab 的 GraphQL 基础架构.

检测是环绕正在执行的查询的功能. 它被实现为使用Instrumentation类的模块.

Example: Present

  1. module Gitlab
  2. module Graphql
  3. module Present
  4. #... some code above...
  5. def self.use(schema_definition)
  6. schema_definition.instrument(:field, ::Gitlab::Graphql::Present::Instrumentation.new)
  7. end
  8. end
  9. end
  10. end

查询分析器包含一系列回调,以在执行查询之前对其进行验证. 每个字段都可以通过分析仪,最终值也可供您使用.

多重查询使多个查询可以在单个请求中发送. 这减少了发送到服务器的请求数量. (GraphQL Ruby 提供了自定义的 Multiplex 查询分析器和 Multiplex 工具).

Query limits

查询和变异受到深度,复杂性和递归的限制,以保护服务器资源免受过度野心或恶意查询的侵害. 这些值可以设置为默认值,并根据需要在特定查询中覆盖. 也可以为每个对象设置复杂度值,并根据返回的对象数来评估最终查询的复杂度. 这对于昂贵的对象(例如需要 Gitaly 调用)很有用.

例如,解析器中的条件复杂度方法:

  1. def self.resolver_complexity(args, child_complexity:)
  2. complexity = super
  3. complexity += 2 if args[:labelName]
  4. complexity
  5. end

有关复杂性的更多信息: GraphQL Ruby 文档 .

Documentation and Schema

我们的模式位于app/graphql/gitlab_schema.rb . 有关详细信息,请参见架构参考 .

模式更改时,需要更新此生成的 GraphQL 文档. 有关生成 GraphQL 文档和架构文件的信息,请参阅更新架构文档 .