ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# GraphQL API style guide > 原文:[https://docs.gitlab.com/ee/development/api_graphql_styleguide.html](https://docs.gitlab.com/ee/development/api_graphql_styleguide.html) * [How GitLab implements GraphQL](#how-gitlab-implements-graphql) * [Deep Dive](#deep-dive) * [GraphiQL](#graphiql) * [Authentication](#authentication) * [Types](#types) * [Nullable fields](#nullable-fields) * [Exposing Global IDs](#exposing-global-ids) * [Connection Types](#connection-types) * [Shortcut fields](#shortcut-fields) * [Exposing permissions for a type](#exposing-permissions-for-a-type) * [Feature flags](#feature-flags) * [`feature_flag` property](#feature_flag-property) * [Toggle the value of a field](#toggle-the-value-of-a-field) * [Deprecating fields](#deprecating-fields) * [Deprecation reason style guide](#deprecation-reason-style-guide) * [Enums](#enums) * [Descriptions](#descriptions) * [Description style guide](#description-style-guide) * [`copy_field_description` helper](#copy_field_description-helper) * [Authorization](#authorization) * [Type authorization](#type-authorization) * [Field authorization](#field-authorization) * [Type and Field authorizations together](#type-and-field-authorizations-together) * [Resolvers](#resolvers) * [Correct use of `Resolver#ready?`](#correct-use-of-resolverready) * [Look-Ahead](#look-ahead) * [Mutations](#mutations) * [Building Mutations](#building-mutations) * [Naming conventions](#naming-conventions) * [Arguments](#arguments) * [Fields](#fields) * [The `resolve` method](#the-resolve-method) * [Mounting the mutation](#mounting-the-mutation) * [Authorizing resources](#authorizing-resources) * [Errors in mutations](#errors-in-mutations) * [Success](#success) * [Failure (relevant to the user)](#failure-relevant-to-the-user) * [Failure (irrelevant to the user)](#failure-irrelevant-to-the-user) * [Categorizing errors](#categorizing-errors) * [Aliasing and deprecating mutations](#aliasing-and-deprecating-mutations) * [Validating arguments](#validating-arguments) * [GitLab’s custom scalars](#gitlabs-custom-scalars) * [`Types::TimeType`](#typestimetype) * [Testing](#testing) * [Notes about Query flow and GraphQL infrastructure](#notes-about-query-flow-and-graphql-infrastructure) * [Query limits](#query-limits) * [Documentation and Schema](#documentation-and-schema) # GraphQL API style guide[](#graphql-api-style-guide "Permalink") 本文档概述了 GitLab 的[GraphQL API](../api/graphql/index.html)的样式指南. ## How GitLab implements GraphQL[](#how-gitlab-implements-graphql "Permalink") 我们使用[Robert Mosolgo](https://github.com/rmosolgo/)编写的[GraphQL Ruby 宝石](https://graphql-ruby.org/) . 所有 GraphQL 查询都定向到单个端点( [`app/controllers/graphql_controller.rb#execute`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/graphql_controller.rb) ),该端点在`/api/graphql`处作为 API 端点`/api/graphql` . ## Deep Dive[](#deep-dive "Permalink") 在 2019 年 3 月,尼克·托马斯(Nick Thomas)在 GitLab 的[GraphQL API 上](../api/graphql/index.html)进行了一次深潜(仅限 GitLab 团队成员: `https://gitlab.com/gitlab-org/create-stage/issues/1` : [//gitlab.com/gitlab-org/create-stage/issues/1](../api/graphql/index.html) ),以与可能将来会在代码库的这一部分中工作. 您可以[在 YouTube 上](https://www.youtube.com/watch?v=-9L_1MWrjkg)找到[录音](https://www.youtube.com/watch?v=-9L_1MWrjkg) ,在[Google 幻灯片](https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit)和[PDF 中](https://gitlab.com/gitlab-org/create-stage/uploads/8e78ea7f326b2ef649e7d7d569c26d56/GraphQL_Deep_Dive__Create_.pdf)找到[幻灯片](https://docs.google.com/presentation/d/1qOTxpkTdHIp1CRjuTvO-aXg0_rUtzE3ETfLUdnBB5uQ/edit) . 自 GitLab 11.9 起,本次深入学习中涉及的所有内容都是准确的,尽管自那时以来特定细节可能有所更改,但它仍应作为一个很好的介绍. ## GraphiQL[](#graphiql "Permalink") GraphiQL 是一个交互式 GraphQL API 资源管理器,您可以在其中使用现有查询. 您可以在`https://<your-gitlab-site.com>/-/graphql-explorer`任何 GitLab 环境中访问它. 例如,用于[GitLab.com 的那个](https://gitlab.com/-/graphql-explorer) . ## Authentication[](#authentication "Permalink") 认证通过`GraphqlController` ,现在,它使用与 Rails 应用程序相同的认证. 因此可以共享会话. 也可以将`private_token`添加到查询字符串,或添加`HTTP_PRIVATE_TOKEN`标头. ## Types[](#types "Permalink") 我们使用代码优先模式,并声明 Ruby 中所有内容的类型. 例如, `app/graphql/types/issue_type.rb` : ``` graphql_name 'Issue' field :iid, GraphQL::ID_TYPE, null: true field :title, GraphQL::STRING_TYPE, null: true # we also have a method here that we've defined, that extends `field` markdown_field :title_html, null: true field :description, GraphQL::STRING_TYPE, null: true markdown_field :description_html, null: true ``` We give each type a name (in this case `Issue`). `iid` , `title`和`description`是*标量* GraphQL 类型. `iid`是`GraphQL::ID_TYPE` ,这是一种特殊的字符串类型,表示唯一的 ID. `title`和`description`是常规的`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`). 公开类型中的属性时,请确保将定义内的逻辑保持尽可能小. 相反,请考虑将任何逻辑转移到演示者中: ``` class Types::MergeRequestType < BaseObject present_using MergeRequestPresenter name 'MergeRequest' end ``` 可以使用现有的演示者,但是也可以专门为 GraphQL 创建一个新的演示者. 使用由字段解析的对象和上下文初始化演示者. ### Nullable fields[](#nullable-fields "Permalink") GraphQL 允许字段为"可为空"或"不可为空". 前者意味着可以返回`null`而不是指定类型的值. **通常** ,出于以下原因,您应该首选使用可空字段而不是不可空字段: * 数据从必需切换到不需要,然后再次返回是很常见的 * 即使没有可能成为可选字段的前景,在查询时它也可能不可**用** * 例如,可能需要从 Gitaly 查找 blob 的`content` * 如果`content`可为空,我们可以返回**部分**响应,而不是使整个查询失败 * 对于无版本模式,很难从不可为空的字段更改为可为空的字段 非空字段仅应在需要字段时使用,将来不太可能成为可选字段,并且非常容易计算. 一个示例是`id`字段. 进一步阅读: * [GraphQL Best Practices Guide](https://s0graphql0org.icopy.site/learn/best-practices/) * [Using nullability in GraphQL](https://www.apollographql.com/blog/using-nullability-in-graphql-2254f84c4ed7) ### Exposing Global IDs[](#exposing-global-ids "Permalink") 在类型上公开`ID`字段时,默认情况下,我们将通过在要渲染的资源上调用`to_global_id`来公开全局 ID. 要覆盖此行为,可以在要公开其 ID 的类型上实现`id`方法. 请确保在使用自定义方法公开`GraphQL::ID_TYPE` ,它是全局唯一的. 被曝光的记录`full_path`作为`ID_TYPE`是这些例外之一. 由于完整路径是`Project`或`Namespace`的唯一标识符. ### Connection Types[](#connection-types "Permalink") GraphQL 使用[基于光标的分页](https://s0graphql0org.icopy.site/learn/pagination/)来公开项目的集合. 这为客户提供了很大的灵活性,同时还允许后端使用不同的分页模型. 为了公开资源的集合,我们可以使用连接类型. 这将使用默认的分页字段包装数组. 例如,对项目管道的查询可能如下所示: ``` query($project_path: ID!) { project(fullPath: $project_path) { pipelines(first: 2) { pageInfo { hasNextPage hasPreviousPage } edges { cursor node { id status } } } } } ``` 这将返回项目的前两个管道和相关的分页信息,按降序 ID 排序. 返回的数据如下所示: ``` { "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" } } ] } } } } ``` 要获取下一页,可以传递最后一个已知元素的光标: ``` 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[](#shortcut-fields "Permalink") 有时似乎很容易实现"快捷字段",如果没有传递任何参数,则让解析程序返回集合的第一个. 不鼓励使用这些"快捷字段",因为它们会增加维护开销. 它们需要与规范字段保持同步,并且如果规范字段发生更改,则不建议使用或修改它们. 除非有充分的理由,否则请使用框架提供的功能. 例如,不要使用`latest_pipeline` ,而应使用`pipelines(last: 1)` . ### Exposing permissions for a type[](#exposing-permissions-for-a-type "Permalink") 若要公开当前用户对资源的权限,可以调用以单独的类型传递的`expose_permissions` ,该类型表示资源的权限. 例如: ``` module Types class MergeRequestType < BaseObject expose_permissions Types::MergeRequestPermissionsType end end ``` 权限类型继承自`BasePermissionType` ,其中包括一些帮助程序方法,这些方法允许将权限公开为不可为 null 的布尔值: ``` class MergeRequestPermissionsType < BasePermissionType present_using MergeRequestPresenter graphql_name 'MergeRequestPermissions' abilities :admin_merge_request, :update_merge_request, :create_note ability_field :resolve_note, description: 'Indicates the user can resolve discussions on the merge request' permission_field :push_to_source_branch, method: :can_push_to_source_branch? end ``` * **`permission_field`** :其作用与`graphql-ruby`的`field`方法相同,但设置默认描述和类型,并使它们不可为空. 通过将它们添加为参数,仍然可以覆盖这些选项. * **`ability_field`** :公开我们政策中定义的能力. 此行为与`permission_field`相同,并且可以覆盖相同的参数. * **`abilities`** :允许一次暴露我们政策中定义的几种能力. 这些字段都将是带有默认说明的非空布尔值. ## Feature flags[](#feature-flags "Permalink") 开发人员可以通过以下方式将[功能标志](../development/feature_flags/index.html)添加到 GraphQL 字段: * 将`feature_flag`属性添加到字段. 当禁用该标志时,这将允许从 GraphQL 模式中*隐藏*该字段. * 解析字段时切换返回值. 您可以参考以下准则来决定使用哪种方法: * 如果您的字段是实验性的,并且其名称或类型可能会发生变化,请使用`feature_flag`属性. * 如果您的字段是稳定的,并且即使删除了标志,其定义也不会更改,请改为切换字段的返回值. 请注意, [所有字段](#nullable-fields)无论如何[都应该为空](#nullable-fields) . ### `feature_flag` property[](#feature_flag-property "Permalink") `feature_flag`属性允许您在 GraphQL 模式中切换字段的[可见性](https://graphql-ruby.org/authorization/visibility.html) . 禁用该标志后,将从架构中删除该字段. 字段后[附有](https://gitlab.com/gitlab-org/gitlab/-/blob/497b556/app/graphql/types/base_field.rb#L44-53)说明,表示该说明位于功能标志的后面. **警告:**如果在禁用功能标志时客户端查询该字段,则查询将失败. 在生产中打开或关闭功能的可见性时,请考虑此问题. `feature_flag`属性不允许使用[基于 actor](../development/feature_flags/development.html)的[特征门](../development/feature_flags/development.html) . 这意味着功能标记不能仅针对特定的项目,组或用户进行切换,而只能针对所有人进行全局切换. Example: ``` field :test_field, type: GraphQL::STRING_TYPE, null: true, description: 'Some test field', feature_flag: :my_feature_flag ``` ### Toggle the value of a field[](#toggle-the-value-of-a-field "Permalink") 对字段使用特征标记的这种方法是切换字段的返回值. 这可以在解析器中,在类型中甚至在模型方法中完成,具体取决于您的偏好和情况. 当应用功能标记来切换字段的值时,该字段的`description`必须: * 说明该字段的值可以通过功能标记切换. * 命名功能标志. * 说明禁用(或启用,如果更合适的话)功能标志时字段将返回的内容. Example: ``` field :foo, GraphQL::STRING_TYPE, null: true, description: 'Some test field. Will always return `null`' \ 'if `my_feature_flag` feature flag is disabled' def foo object.foo unless Feature.enabled?(:my_feature_flag, object) end ``` ## Deprecating fields[](#deprecating-fields "Permalink") GitLab 的 GraphQL API 是无版本的,这意味着我们会与 API 的旧版本保持向下兼容性. 除了删除字段,我们还需要*弃用*该字段. 将来,GitLab [可能会删除不推荐使用的字段](https://gitlab.com/gitlab-org/gitlab/-/issues/32292) . 使用`deprecated`推荐使用的属性不推荐使用字段. 该属性的值是以下各项的`Hash`值: * `reason` -弃用的原因. * `milestone` -已弃用该字段的里程碑. Example: ``` field :token, GraphQL::STRING_TYPE, null: true, deprecated: { reason: 'Login via token has been removed', milestone: '10.0' }, description: 'Token for login' ``` 最初的`description:`现场应保持,并且*不*应该更新提折旧. ### Deprecation reason style guide[](#deprecation-reason-style-guide "Permalink") 如果弃用的原因是该字段被另一个字段替换,则`reason`必须是: ``` Use `otherFieldName` ``` Example: ``` field :designs, ::Types::DesignManagement::DesignCollectionType, null: true, deprecated: { reason: 'Use `designCollection`', milestone: '10.0' }, description: 'The designs associated with this issue', ``` 如果该字段没有被另一个字段替换,则应给出描述性的弃用`reason` . ## Enums[](#enums "Permalink") GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the following rules apply: * 值必须为大写. * 类名必须以字符串`Enum`结尾. * `graphql_name`不得包含字符串`Enum` . 例如: ``` module Types class TrafficLightStateEnum < BaseEnum graphql_name 'TrafficLightState' description 'State of a traffic light' value 'RED', description: 'Drivers must stop' value 'YELLOW', description: 'Drivers must stop when it is safe to' value 'GREEN', description: 'Drivers can start or keep driving' end 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"` . ``` module Types class EpicStateEnum < BaseEnum graphql_name 'EpicState' description 'State of a GitLab epic' value 'OPENED', value: 'opened', description: 'An open Epic' value 'CLOSED', value: 'closed', description: 'An closed Epic' end end ``` ## Descriptions[](#descriptions "Permalink") 所有字段和参数都[必须具有描述](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16438) . 使用`description:`关键字给出字段或自变量的`description:` . 例如: ``` field :id, GraphQL::ID_TYPE, description: 'ID of the resource' ``` 用户可以通过以下方式查看字段和参数的描述: * The [GraphiQL explorer](#graphiql). * The [static GraphQL API reference](../api/graphql/#reference). ### Description style guide[](#description-style-guide "Permalink") 为确保一致性,每次添加或更新描述时都应遵循以下规定: * 在描述中提及资源的名称. 示例: `'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: ``` field :id, GraphQL::ID_TYPE, description: 'ID of the Issue' field :confidential, GraphQL::BOOLEAN_TYPE, description: 'Indicates the issue is confidential' field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed' ``` ### `copy_field_description` helper[](#copy_field_description-helper "Permalink") 有时我们希望确保两个描述始终相同. 例如,当两个类型字段描述都表示相同的属性时,它们要与突变参数保持相同. 除了提供描述之外,我们还可以使用`copy_field_description`帮助器,将其类型和字段名称传递给它,以复制其描述. Example: ``` argument :title, GraphQL::STRING_TYPE, required: false, description: copy_field_description(Types::MergeRequestType, :title) ``` ## Authorization[](#authorization "Permalink") 可以使用与 Rails 应用程序中相同的功能将授权应用于类型和字段. 如果: * 当前经过身份验证的用户未通过授权,授权资源将返回`null` . * 资源是集合的一部分,该集合将被过滤以排除用户授权检查失败的对象. 另请参见[在变异中授权资源](#authorizing-resources) . **提示:**尝试仅先加载允许当前已认证用户使用我们现有的查找器查看的内容,而不依赖于授权来过滤记录. 这样可以最大程度地减少数据库查询和对已加载记录的不必要的授权检查. ### Type authorization[](#type-authorization "Permalink") 通过将能力传递给`authorize`方法来`authorize`类型. 通过检查当前经过身份验证的用户是否具有所需的能力,将对所有具有相同类型的字段进行授权. 例如,以下授权可确保当前经过身份验证的用户只能看到其具有`read_project`能力的项目(只要在使用`Types::ProjectType`的字段中返回该`Types::ProjectType` ): ``` module Types class ProjectType < BaseObject authorize :read_project end end ``` 您还可以授权多个能力,在这种情况下,所有能力检查都必须通过. 例如,以下授权可确保当前经过身份验证的用户必须具有`read_project`和`another_ability`能力才能查看项目: ``` module Types class ProjectType < BaseObject authorize [:read_project, :another_ability] end end ``` ### Field authorization[](#field-authorization "Permalink") 可以使用`authorize`选项对字段进行授权. 例如,以下授权可确保当前经过身份验证的用户必须具有`owner_access`功能才能查看项目: ``` module Types class MyType < BaseObject field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access end end ``` 还可以针对多个能力授权字段,在这种情况下,所有能力检查都必须通过. **注意:**这需要显式地将一个块传递给`field` : ``` module Types class MyType < BaseObject field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do authorize [:owner_access, :another_ability] end end end ``` **注意:**如果字段的类型已经[具有特定的授权,](#type-authorization)则无需将该相同的授权添加到字段中. ### Type and Field authorizations together[](#type-and-field-authorizations-together "Permalink") 授权是累积性的,因此,在一个字段以及该字段的类型上定义了授权的情况下,当前经过身份验证的用户将需要通过所有能力检查. 在下面的简化示例中,当前经过身份验证的用户将需要`first_permission`和`second_permission`能力,才能看到问题的作者. ``` class UserType authorize :first_permission end ``` ``` class IssueType field :author, UserType, authorize: :second_permission end ``` ## Resolvers[](#resolvers "Permalink") 我们使用存储在`app/graphql/resolvers`目录中的*解析器*定义应用程序如何响应. 解析器提供了用于检索相关对象的实际实现逻辑. 要查找要显示在字段中的对象,我们可以将解析器添加到`app/graphql/resolvers` . 可以在解析程序中定义参数,这些参数将通过解析程序提供给字段. 公开具有内部 ID( `iid` )的模型时,最好将其与名称空间路径结合使用,作为解析器中的参数,而不是数据库 ID. 否则,请使用[全局唯一 ID](#exposing-global-ids) . 我们已经有一个`FullPathLoader` ,可以将其包含在其他解析器中,以快速查找将有很多依赖对象的项目和命名空间. 为了限制执行的查询数量,我们可以使用`BatchLoader` . ### Correct use of `Resolver#ready?`[](#correct-use-of-resolverready "Permalink") 解析器有两个公共 API 方法作为框架的一部分: `#ready?(**args)`和`#resolve(**args)` . 我们可以使用`#ready?` 无需调用`#resolve`即可执行设置,验证或提前退货. 有充分理由使用`#ready?` 包括: * 验证互斥参数(请参阅[验证参数](#validating-arguments) ) * 如果事先知道没有结果,则返回`Relation.none` * 执行诸如初始化实例变量的设置(尽管为此考虑了延迟初始化的方法) [`Resolver#ready?(**args)`](https://graphql-ruby.org/api-doc/1.10.9/GraphQL/Schema/Resolver#ready?-instance_method)应返回`(Boolean, early_return_data)` ,如下所示: ``` def ready?(**args) [false, 'have this instead'] end ``` 因此,无论何时调用解析器(主要是在测试中-作为框架抽象,不应将解析器视为可重用的,最好使用查找器),还记得调用`ready?` 方法,并在调用`resolve`之前检查布尔值标志! 在我们的[`GraphQLHelpers`](https://gitlab.com/gitlab-org/gitlab/-/blob/2d395f32d2efbb713f7bc861f96147a2a67e92f2/spec/support/helpers/graphql_helpers.rb#L20-27)可以看到一个示例. ### Look-Ahead[](#look-ahead "Permalink") 完整查询是在执行期间预先知道的,这意味着我们可以利用[超前](https://graphql-ruby.org/queries/lookahead.html)查询来优化查询,并知道我们将需要的批处理负载关联. 考虑在解析器中添加前瞻性支持,以避免`N+1`性能问题. 为了支持常见的前瞻用例(在请求子字段时预加载关联),可以包含[`LooksAhead`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/resolvers/concerns/looks_ahead.rb) . 例如: ``` # Assuming a model `MyThing` with attributes `[child_attribute, other_attribute, nested]`, # where nested has an attribute named `included_attribute`. class MyThingResolver < BaseResolver include LooksAhead # Rather than defining `resolve(**args)`, we implement: `resolve_with_lookahead(**args)` def resolve_with_lookahead(**args) apply_lookahead(MyThingFinder.new(current_user).execute) end # We list things that should always be preloaded: # For example, if child_attribute is always needed (during authorization # perhaps), then we can include it here. def unconditional_includes [:child_attribute] end # We list things that should be included if a certain field is selected: def preloads { field_one: [:other_attribute], field_two: [{ nested: [:included_attribute] }] } end end ``` 需要做的最后一件事是,使用此解析器的每个字段都需要公告提前查询的需求: ``` # in ParentType field :my_things, MyThingType.connection_type, null: true, extras: [:lookahead], # Necessary resolver: MyThingResolver, description: 'My things' ``` 有关实际使用的示例,请参见[`ResolvesMergeRequests`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/resolvers/concerns/resolves_merge_requests.rb) . ## Mutations[](#mutations "Permalink") 变异用于更改任何存储的值或触发动作. 与 GET 请求不应修改数据的方式相同,我们无法在常规 GraphQL 查询中修改数据. 但是我们可以突变. 要查找突变的对象,需要指定参数. 与[解析程序一样](#resolvers) ,最好使用内部 ID 或全局 ID(而不是数据库 ID)(如果需要). ### Building Mutations[](#building-mutations "Permalink") 突变存在于`app/graphql/mutations`理想情况下,突变是根据它们正在突变的资源进行分组的,类似于我们的服务. 他们应该继承`Mutations::BaseMutation` . 突变的结果将返回在突变上定义的字段. ### Naming conventions[](#naming-conventions "Permalink") 每个突变都必须定义一个`graphql_name` ,这是 GraphQL 模式中的突变名称. Example: ``` class UserUpdateMutation < BaseMutation graphql_name 'UserUpdate' end ``` 我们的 GraphQL 突变名称在历史上是不一致的,但是新的突变名称应遵循约定`'{Resource}{Action}'`或`'{Resource}{Action}{Attribute}'` . **创建**新资源的变异应使用动词`Create` . Example: * `CommitCreate` **更新**数据的突变应使用: * 动词`Update` . * 特定于域的动词,例如`Set` , `Add`或`Toggle`如果更合适). Examples: * `EpicTreeReorder` * `IssueSetWeight` * `IssueUpdate` * `TodoMarkDone` **删除**数据的突变应使用: * 动词`Delete`而不是`Destroy` . * 特定于域的动词,例如" `Remove`如果更合适). Examples: * `AwardEmojiRemove` * `NoteDelete` 如果您需要有关突变命名的建议,请查看 Slack `#graphql`渠道以获取反馈. ### Arguments[](#arguments "Permalink") 突变所需的参数可以定义为字段所需的参数. 这些将被包装为突变的输入类型. 例如,带有 GraphQL 名称`MergeRequestSetWip`的`Mutations::MergeRequests::SetWip` `MergeRequestSetWip`定义了以下参数: ``` argument :project_path, GraphQL::ID_TYPE, required: true, description: "The project the merge request to mutate is in" argument :iid, GraphQL::STRING_TYPE, required: true, description: "The iid of the merge request to mutate" argument :wip, GraphQL::BOOLEAN_TYPE, required: false, description: <<~DESC Whether or not to set the merge request as a WIP. If not passed, the value will be toggled. DESC ``` 这将自动生成一个名为`MergeRequestSetWipInput`的输入类型, `MergeRequestSetWipInput`包含我们指定的 3 个参数和`clientMutationId` . 然后将这些参数作为关键字参数传递给突变的`resolve`方法. ### Fields[](#fields "Permalink") 在最常见的情况下,变异会返回 2 个字段: * 正在修改的资源 * 错误列表,说明无法执行该操作的原因. 如果突变成功,此列表将为空. 通过从`Mutations::BaseMutation`继承任何新的突变, `errors`字段将自动添加. 还添加了一个`clientMutationId`字段,当在单个请求中执行多个变异时,客户端可以使用它来标识单个变异的结果. ### The `resolve` method[](#the-resolve-method "Permalink") `resolve`方法接收变异的参数作为关键字参数. 从这里,我们可以调用将修改资源的服务. 然后, `resolve`方法应返回一个哈希,该哈希具有与在突变上定义的字段名称相同的字段名称,包括`errors`数组. 例如, `Mutations::MergeRequests::SetWip`定义了`merge_request`字段: ``` field :merge_request, Types::MergeRequestType, null: true, description: "The merge request after mutation" ``` 这意味着在此突变中从`resolve`返回的哈希应如下所示: ``` { # The merge request modified, this will be wrapped in the type # defined on the field merge_request: merge_request, # An array of strings if the mutation failed after authorization. # The `errors_on_object` helper collects `errors.full_messages` errors: errors_on_object(merge_request) } ``` ### Mounting the mutation[](#mounting-the-mutation "Permalink") 为了使变异可用,必须在存在于`graphql/types/mutation_types`的变异类型上进行定义. `mount_mutation`帮助器方法将基于突变的 GraphQL 名称定义一个字段: ``` module Types class MutationType < BaseObject include Gitlab::Graphql::MountMutation graphql_name "Mutation" mount_mutation Mutations::MergeRequests::SetWip end end ``` 将生成一个名为`mergeRequestSetWip`的字段, `Mutations::MergeRequests::SetWip`字段将要解决的`Mutations::MergeRequests::SetWip` . ### Authorizing resources[](#authorizing-resources "Permalink") 要授权某个变异内的资源,我们首先要提供所需的变异能力,如下所示: ``` module Mutations module MergeRequests class SetWip < Base graphql_name 'MergeRequestSetWip' authorize :update_merge_request end end end ``` 然后,我们可以致电`authorize!` 在`resolve`方法中,传入我们要验证其功能的资源. 或者,我们可以添加一个`find_object`方法,该方法将在突变上加载对象. 这将允许您使用`authorized_find!` 辅助方法. 当不允许用户执行该操作或找不到对象时,我们应该引发`Gitlab::Graphql::Errors::ResourceNotAvailable`错误. 哪些将正确呈现给客户端. ### Errors in mutations[](#errors-in-mutations "Permalink") 我们鼓励遵循[错误](https://graphql-ruby.org/mutations/mutation_errors)的做法,将其[作为](https://graphql-ruby.org/mutations/mutation_errors)突变的[数据](https://graphql-ruby.org/mutations/mutation_errors) ,从而根据错误的相关者,定义的错误处理者来区分错误. 关键点: * 所有突变响应都有一个`errors`字段. 如果失败,则应填充此文件;如果成功,则应填充该文件. * 考虑谁需要看到错误: **用户**还是**开发人员** . * 客户在执行突变时应始终请求`errors`字段. * 错误可能会以`$root.errors` (顶级错误)或`$root.data.mutationName.errors` (变异错误)的形式报告给用户. 位置取决于这是什么类型的错误以及它所包含的信息. 考虑一个示例变体`doTheThing` ,该变`doTheThing`返回带有两个字段的响应: `errors: [String]`和`thing: ThingType` . 由于我们正在考虑错误,因此`thing`本身的特定性质与这些示例无关. 突变响应可以处于三种状态: * [Success](#success) * [Failure (relevant to the user)](#failure-relevant-to-the-user) * [Failure (irrelevant to the user)](#failure-irrelevant-to-the-user) #### Success[](#success "Permalink") 在快乐的道路上, *可能*会返回错误以及预期的有效负载,但是如果一切成功,则`errors`应该是一个空数组,因为没有任何问题需要通知用户. ``` { data: { doTheThing: { errors: [] // if successful, this array will generally be empty. thing: { .. } } } } ``` #### Failure (relevant to the user)[](#failure-relevant-to-the-user "Permalink") 发生了影响**用户**的错误. 我们将这些称为*突变错误* . 在这种情况下通常没有`thing`来回报: ``` { data: { doTheThing: { errors: ["you cannot touch the thing"], thing: null } } } ``` 例如: * Model validation errors: the user may need to change the inputs. * 权限错误:用户需要知道他们不能执行此操作,他们可能需要请求权限或登录. * 应用程序状态问题阻止了用户的操作,例如:合并冲突,资源被锁定等等. 理想情况下,我们应该防止用户走得太远,但是如果这样做了,则需要告诉他们出了什么问题,以便他们了解失败的原因以及可以实现其意图的方法,即使那很简单重试请求. 可以与变异数据一起返回*可恢复的*错误. 例如,如果用户上载 10 个文件,而其中 3 个失败,其余文件成功,则可以将失败的错误以及有关成功的信息与用户一起使用. #### Failure (irrelevant to the user)[](#failure-irrelevant-to-the-user "Permalink") 可以在*顶层*返回一个或多个*不可恢复的*错误. 这些是**用户**几乎无法控制的事情,主要应该是**开发人员**需要了解的系统或编程问题. 在这种情况下,没有`data` : ``` { errors: [ {"message": "argument error: expected an integer, got null"}, ] } ``` 这是在突变过程中引发错误的结果. 在我们的实现中,参数错误和验证错误的消息将返回给客户端,所有其他`StandardError`实例将被捕获,记录并呈现给客户端,并将消息设置为`"Internal server error"` . 有关详细信息,请参见[`GraphqlController`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/controllers/graphql_controller.rb) . 这些代表编程错误,例如: * A GraphQL syntax error, where an `Int` was passed instead of a `String`, or a required argument was not present. * 我们架构中的错误,例如无法为不可为空的字段提供值. * 系统错误:例如,Git 存储异常或数据库不可用. 用户在常规使用中不应导致此类错误. 此类错误应视为内部错误,并且不向用户详细显示. 我们需要在突变失败时通知用户,但是我们不必告诉他们原因,因为他们不可能造成突变,尽管我们可以提供重试突变的方法,但他们无能为力. #### Categorizing errors[](#categorizing-errors "Permalink") 当我们编写突变时,我们需要意识到错误状态属于这两个类别中的哪一个(并与前端开发人员进行沟通以验证我们的假设). 这意味着将*用户*的需求与*客户*的需求区分开. > *除非用户需要了解错误,否则切勿捕获错误.* 如果用户确实需要了解它,请与前端开发人员进行交流,以确保我们传回的错误信息有用. 另请参见[前端 GraphQL 指南](../development/fe_guide/graphql.html#handling-errors) . ### Aliasing and deprecating mutations[](#aliasing-and-deprecating-mutations "Permalink") `#mount_aliased_mutation`帮助器允许我们将突变别名作为`MutationType`另一个名称. 例如,将名为`FooMutation`的突变别名为`BarMutation` : ``` mount_aliased_mutation 'BarMutation', Mutations::FooMutation ``` 结合[`deprecated`](#deprecating-fields)参数,这使我们可以重命名突变并继续支持旧名称. Example: ``` mount_aliased_mutation 'UpdateFoo', Mutations::Foo::Update, deprecated: { reason: 'Use fooUpdate', milestone: '13.2' } ``` 不赞成使用的突变应添加到`Types::DeprecatedMutations`并在`Types::MutationType`的单元测试中进行测试. 合并请求[!34798](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34798)可以称为此示例,包括测试已弃用的别名突变的方法. ## Validating arguments[](#validating-arguments "Permalink") 要验证单个参数,请照常使用[`prepare`选项](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/fields/arguments.md) . 有时,变异或解析器可以接受许多可选参数,但是我们仍然要验证是否至少提供了一个可选参数. 在这种情况下,请考虑使用`#ready?` 突变或解析器中提供验证的方法. `#ready?` 在`#resolve`方法中完成任何工作之前,将调用方法. Example: ``` def ready?(**args) if args.values_at(:body, :position).compact.blank? raise Gitlab::Graphql::Errors::ArgumentError, 'body or position arguments are required' end # Always remember to call `#super` super end ``` 如果将来将此[RFC](https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md)合并,则可以使用`InputUnions`完成. ## GitLab’s custom scalars[](#gitlabs-custom-scalars "Permalink") ### `Types::TimeType`[](#typestimetype "Permalink") [`Types::TimeType`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/types/time_type.rb)必须用作处理 Ruby `Time`和`DateTime`对象的所有字段和参数的类型. 该类型是[一个自定义标量](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/scalars.md#custom-scalars) : * 当用作 GraphQL 字段的类型时,将 Ruby 的`Time`和`DateTime`对象转换为标准化的 ISO-8601 格式的字符串. * 当用作 GraphQL 参数的类型时,将 ISO-8601 格式的时间字符串转换为 Ruby `Time`对象. 这使我们的 GraphQL API 具有标准化的方式来表示时间并处理时间输入. Example: ``` field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was created' ``` ## Testing[](#testing "Permalink") 在`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`帮助器. 使用这些帮助器,我们可以建立如下规格: ``` let(:mutation) do graphql_mutation( :merge_request_set_wip, project_path: 'gitlab-org/gitlab-foss', iid: '1', wip: true ) end it 'returns a successful response' do post_graphql_mutation(mutation, current_user: user) expect(response).to have_gitlab_http_status(:success) expect(graphql_mutation_response(:merge_request_set_wip)['errors']).to be_empty end ``` ## Notes about Query flow and GraphQL infrastructure[](#notes-about-query-flow-and-graphql-infrastructure "Permalink") 可以在`lib/gitlab/graphql`找到 GitLab 的 GraphQL 基础架构. [检测](https://graphql-ruby.org/queries/instrumentation.html)是环绕正在执行的查询的功能. 它被实现为使用`Instrumentation`类的模块. Example: `Present` ``` module Gitlab module Graphql module Present #... some code above... def self.use(schema_definition) schema_definition.instrument(:field, ::Gitlab::Graphql::Present::Instrumentation.new) end end end end ``` [查询分析器](https://graphql-ruby.org/queries/ast_analysis.html#analyzer-api)包含一系列回调,以在执行查询之前对其进行验证. 每个字段都可以通过分析仪,最终值也可供您使用. [多重查询](https://graphql-ruby.org/queries/multiplex.html)使多个查询可以在单个请求中发送. 这减少了发送到服务器的请求数量. (GraphQL Ruby 提供了自定义的 Multiplex 查询分析器和 Multiplex 工具). ### Query limits[](#query-limits "Permalink") 查询和变异受到深度,复杂性和递归的限制,以保护服务器资源免受过度野心或恶意查询的侵害. 这些值可以设置为默认值,并根据需要在特定查询中覆盖. 也可以为每个对象设置复杂度值,并根据返回的对象数来评估最终查询的复杂度. 这对于昂贵的对象(例如需要 Gitaly 调用)很有用. 例如,解析器中的条件复杂度方法: ``` def self.resolver_complexity(args, child_complexity:) complexity = super complexity += 2 if args[:labelName] complexity end ``` 有关复杂性的更多信息: [GraphQL Ruby 文档](https://graphql-ruby.org/queries/complexity_and_depth.html) . ## Documentation and Schema[](#documentation-and-schema "Permalink") 我们的模式位于`app/graphql/gitlab_schema.rb` . 有关详细信息,请参见[架构参考](../api/graphql/reference/index.html) . 模式更改时,需要更新此生成的 GraphQL 文档. 有关生成 GraphQL 文档和架构文件的信息,请参阅[更新架构文档](rake_tasks.html#update-graphql-documentation-and-schema-definitions) .