-
由 Cynthia "Arty" Ng 创作于由 Cynthia "Arty" Ng 创作于
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:
- Exposing Global IDs.
- Mutation arguments.
- Deprecating Global IDs.
- Customer-facing Global ID documentation.
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:
- Look ahead allows you to preload data based on which fields are selected in the query.
- Batch loading allows you batch database queries together to be executed in one statement.
-
BatchModelLoader
is the recommended way to lookup records by ID to leverage batch loading. -
before_connection_authorization
allows you to address N+1 problems specific to type authorization permission checks. - Limit maximum field call count allows you to restrict how many times a field can return data where optimizations cannot be improved.
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.