215 lines
5.5 KiB
Markdown
215 lines
5.5 KiB
Markdown
|
# 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:
|
||
|
|
||
|
```ruby
|
||
|
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:
|
||
|
|
||
|
```ruby
|
||
|
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](https://graphql.org/learn/pagination/#pagination-and-edges)
|
||
|
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:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"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:
|
||
|
|
||
|
```ruby
|
||
|
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:
|
||
|
|
||
|
```ruby
|
||
|
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 as `graphql-ruby`'s
|
||
|
`field` method 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 as `permission_field` and 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.
|