Skip to content
代码片段 群组 项目
代码所有者
将用户和群组指定为特定文件更改的核准人。 了解更多。
api_graphql_styleguide.md 90.31 KiB
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.

Backend GraphQL API guide

This document contains style and technical guidance for engineers implementing the backend of the GitLab GraphQL API.

Relation to REST API

See the GraphQL and REST APIs section.

Versioning

The GraphQL API is versionless.

Learning GraphQL at GitLab

Backend engineers who wish to learn GraphQL at GitLab should read this guide in conjunction with the guides for the GraphQL Ruby gem. Those guides teach you the features of the gem, and the information in it is generally not reproduced here.

To learn about the design and features of GraphQL itself read the guide on graphql.org which is an accessible but shortened version of information in the GraphQL spec.

Deep Dive

In March 2019, Nick Thomas hosted a Deep Dive (GitLab team members only: https://gitlab.com/gitlab-org/create-stage/issues/1) on the GitLab GraphQL API to share domain-specific knowledge with anyone who may work in this part of the codebase in the future. You can find the recording on YouTube, and the slides on Google Slides and in PDF. Specific details have changed since then, but it should still serve as a good introduction.

How GitLab implements GraphQL

We use the GraphQL Ruby gem written by Robert Mosolgo. In addition, we have a subscription to GraphQL Pro. For details see GraphQL Pro subscription.

All GraphQL queries are directed to a single endpoint (app/controllers/graphql_controller.rb#execute), which is exposed as an API endpoint at /api/graphql.

GraphiQL

GraphiQL is an interactive GraphQL API explorer where you can play around with existing queries. You can access it in any GitLab environment on https://<your-gitlab-site.com>/-/graphql-explorer. For example, the one for GitLab.com.

Reviewing merge requests with GraphQL changes

The GraphQL framework has some specific gotchas to be aware of, and domain expertise is required to ensure they are satisfied.

If you are asked to review a merge request that modifies any GraphQL files or adds an endpoint, have a look at our GraphQL review guide.

Reading GraphQL logs

See the Reading GraphQL logs guide for tips on how to inspect logs of GraphQL requests and monitor the performance of your GraphQL queries.

Authentication

Authentication happens through the GraphqlController, right now this uses the same authentication as the Rails application. So the session can be shared.

It's also possible to add a private_token to the query string, or add a HTTP_PRIVATE_TOKEN header.

Limits

Several limits apply to the GraphQL API and some of these can be overridden by developers.

Max page size

By default, connections can only return at most a maximum number of records defined in app/graphql/gitlab_schema.rb per page.

Developers can specify a custom max page size when defining a connection.

Max complexity

Complexity is explained on our client-facing API page.

Fields default to adding 1 to a query's complexity score, but developers can specify a custom complexity when defining a field.

The complexity score of a query can itself be queried for.

Request timeout

Requests time out at 30 seconds.

Limit maximum field call count

In some cases, you want to prevent the evaluation of a specific field on multiple parent nodes because it results in an N+1 query problem and there is no optimal solution. This should be considered an option of last resort, to be used only when methods such as lookahead to preload associations, or using batching have been considered.

For example:

# This usage is expected.
query {
  project {
    environments
  }
}

# This usage is NOT expected.
# It results in N+1 query problem. EnvironmentsResolver can't use GraphQL batch loader in favor of GraphQL pagination.
query {
  projects {
    nodes {
      environments
    }
  }
}

To prevent this, you can use the Gitlab::Graphql::Limit::FieldCallCount extension on the field:

# This allows maximum 1 call to the `environments` field. If the field is evaluated on more than one node,
# it raises an error.
field :environments do
        extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
      end

or you can apply the extension in a resolver class:

module Resolvers
  class EnvironmentsResolver < BaseResolver
    extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
    # ...
  end
end

When you add this limit, make sure that the affected field's description is also updated accordingly. For example,

field :environments,
      description: 'Environments of the project. This field can only be resolved for one project in any single request.'

Breaking changes

The GitLab GraphQL API is versionless which means developers must familiarize themselves with our Deprecation and Removal process.

Breaking changes are:

  • Removing or renaming a field, argument, enum value, or mutation.
  • Changing the type or type name of an argument. The type of an argument is declared by the client when using variables, and a change would cause a query using the old type name to be rejected by the API.
  • Changing the scalar type of a field or enum value where it results in a change to how the value serializes to JSON. For example, a change from a JSON String to a JSON Number, or a change to how a String is formatted. A change to another object type can be allowed so long as all scalar type fields of the object continue to serialize in the same way.
  • Raising the complexity of a field or complexity multipliers in a resolver.
  • Changing a field from being not nullable (null: false) to nullable (null: true), as discussed in Nullable fields.
  • Changing an argument from being optional (required: false) to being required (required: true).
  • Changing the max page size of a connection.
  • Lowering the global limits for query complexity and depth.
  • Anything else that can result in queries hitting a limit that previously was allowed.

See the deprecating schema items section for how to deprecate items.

Breaking change exemptions

See the GraphQL API breaking change exemptions documentation.

Global IDs

The GitLab GraphQL API uses Global IDs (i.e: "gid://gitlab/MyObject/123") and never database primary key IDs.

Global ID is a convention used for caching and fetching in client-side libraries.

See also:

We have a custom scalar type (Types::GlobalIDType) which should be used as the type of input and output arguments when the value is a GlobalID. The benefits of using this type instead of ID are:

  • it validates that the value is a GlobalID
  • it parses it into a GlobalID before passing it to user code
  • it can be parameterized on the type of the object (for example, GlobalIDType[Project]) which offers even better validation and security.

Consider using this type for all new arguments and result types. Remember that it is perfectly possible to parameterize this type with a concern or a supertype, if you want to accept a wider range of objects (such as GlobalIDType[Issuable] vs GlobalIDType[Issue]).

Optimizations

By default, GraphQL tends to introduce N+1 problems unless you actively try to minimize them.

For stability and scalability, you must ensure that our queries do not suffer from N+1 performance issues.

The following are a list of tools to help you to optimize your GraphQL code:

How to see N+1 problems in development

N+1 problems can be discovered during development of a feature by:

  • Tailing development.log while you execute GraphQL queries that return collections of data. Bullet may help.
  • Observing the performance bar if executing queries in the GitLab UI.
  • Adding a request spec that asserts there are no (or limited) N+1 problems with the feature.

Fields

Types

We use a code-first schema, and we declare what type everything is in Ruby.

For example, app/graphql/types/project_type.rb:

graphql_name 'Project'

field :full_path, GraphQL::Types::ID, null: true
field :name, GraphQL::Types::String, null: true

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

The full_path and name are of scalar GraphQL types. full_path is a GraphQL::Types::ID (see when to use GraphQL::Types::ID). name is a regular GraphQL::Types::String type. You can also declare custom GraphQL data types for scalar data types (for example TimeType).

When exposing a model through the GraphQL API, we do so by creating a new type in app/graphql/types.

When exposing properties in a type, make sure to keep the logic inside the definition as minimal as possible. Instead, consider moving any logic into a presenter:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

An existing presenter could be used, but it is also possible to create a new presenter specifically for GraphQL.