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 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.