5.5 KiB
GraphQL API
Authentication
Authentication happens through the GraphqlController, right now this
uses the same authentication as the Rails application. So the session
can be shared.
It is also possible to add a private_token to the querystring, or
add a HTTP_PRIVATE_TOKEN header.
Authorization
Fields can be authorized using the same abilities used in the Rails
app. This can be done using the authorize helper:
module Types
class QueryType < BaseObject
graphql_name 'Query'
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
authorize :read_project
end
end
The object found by the resolve call is used for authorization.
This works for authorizing a single record, for authorizing collections, we should only load what the currently authenticated user is allowed to view. Preferably we use our existing finders for that.
Types
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.
The presenter is initialized using the object resolved by a field, and the context.
Connection Types
GraphQL uses cursor based pagination to expose collections of items. This provides the clients with a lot of flexibility while also allowing the backend to use different pagination models.
To expose a collection of resources we can use a connection type. This wraps the array with default pagination fields. For example a query for project-pipelines could look like this:
query($project_path: ID!) {
project(fullPath: $project_path) {
pipelines(first: 2) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
id
status
}
}
}
}
}
This would return the first 2 pipelines of a project and related pagination info., ordered by descending ID. The returned data would look like this:
{
"data": {
"project": {
"pipelines": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false
},
"edges": [
{
"cursor": "Nzc=",
"node": {
"id": "77",
"status": "FAILED"
}
},
{
"cursor": "Njc=",
"node": {
"id": "67",
"status": "FAILED"
}
}
]
}
}
}
}
To get the next page, the cursor of the last known element could be passed:
query($project_path: ID!) {
project(fullPath: $project_path) {
pipelines(first: 2, after: "Njc=") {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
id
status
}
}
}
}
}
Exposing permissions for a type
To expose permissions the current user has on a resource, you can call
the expose_permissions passing in a separate type representing the
permissions for the resource.
For example:
module Types
class MergeRequestType < BaseObject
expose_permissions Types::MergeRequestPermissionsType
end
end
The permission type inherits from BasePermissionType which includes
some helper methods, that allow exposing permissions as non-nullable
booleans:
class MergeRequestPermissionsType < BasePermissionType
present_using MergeRequestPresenter
graphql_name 'MergeRequestPermissions'
abilities :admin_merge_request, :update_merge_request, :create_note
ability_field :resolve_note,
description: 'Whether or not the user can resolve disussions on the merge request'
permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end
permission_field: Will act the same asgraphql-ruby'sfieldmethod but setting a default description and type and making them non-nullable. These options can still be overridden by adding them as arguments.ability_field: Expose an ability defined in our policies. This takes behaves the same way aspermission_fieldand the same arguments can be overridden.abilities: Allows exposing several abilities defined in our policies at once. The fields for these will all have be non-nullable booleans with a default description.
Resolvers
To find objects to display in a field, we can add resolvers to
app/graphql/resolvers.
Arguments can be defined within the resolver, those arguments will be made available to the fields using the resolver.
We already have a FullPathLoader that can be included in other
resolvers to quickly find Projects and Namespaces which will have a
lot of dependant objects.
To limit the amount of queries performed, we can use BatchLoader.
Testing
full stack tests for a graphql query or mutation live in
spec/requests/api/graphql.
When adding a query, the a working graphql query shared example can
be used to test if the query renders valid results.
Using the GraphqlHelpers#all_graphql_fields_for-helper, a query
including all available fields can be constructed. This makes it easy
to add a test rendering all possible fields for a query.