Skip to main content

GraphQL API

What is GraphQL#

"GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools."

โ€” graphql.org

Integrates GraphQL implementation#

From late 2018 to mid-2019 we gradually migrated from a REST-like API to GraphQL, first using Graphene, but since it didn't support ASGI and async execution, in early 2020 we replaced it for ariadne.

Integrates currently uses the ariadne library, developed by Mirumee Labs

All GraphQL queries are directed to a single endpoint, which is exposed at /api.

The API layer was inspired by GitLab's GraphQL API and is structured in the following way:

api
|_ mutations
|_ mutation_name.py
|_ resolvers
|_ entity_name
|_ field_resolver_name.py
|_ schema
|_ enums
|_ __init__.py <- Index for all enum bindings
|_ enums.graphql <- GraphQL SDL declaring the available enums
|_ scalars
|_ __init__.py <- Index for all scalar bindings
|_ scalars.graphql <- GraphQL SDL declaring the available scalars
|_ types
|_ type_name.py <- Type bindings
|_ type_name.graphql <- GraphQL SDL type definition

GraphQL Playground#

The GraphQL Playground is a tool that allows to perform queries against the API or to explore the schema definitions in a graphic and interactive way. You can access it on:

Types#

Integrates GraphQL types are defined in api/schema/types

There are two approaches to defining a GraphQL schema:

  1. Code-first
  2. Schema-first

We use the latter, which implies defining the structure using GraphQL SDL (Schema definition language) and binding it to python functions.

For example:

api/schema/types/user.graphql

type User {
email: String!
}

api/schema/types/user.py

from ariadne import ObjectType
from api.resolvers.user import email
USER = ObjectType('User')
USER.set_field('email', email.resolve)

Further reading:

Enums#

Integrates GraphQL enums are defined in api/schema/enums

api/schema/enums/enums.graphql

enum AuthProvider {
BITBUCKET
GOOGLE
MICROSOFT
}

NOTE: By default, enum values passed to resolver functions will match their name

To map the value to something else, you can specify it in the enums binding index, for example:

api/schema/enums/__init__.py

from ariadne import EnumType
ENUMS: Tuple[EnumType, ...] = (
...,
EnumType(
'AuthProvider',
{
'BITBUCKET': 'bitbucket-oauth2',
'GOOGLE': 'google-oauth2',
'MICROSOFT': 'azuread-tenant-oauth2'
}
),
...
)

Scalars#

Integrates GraphQL scalars are defined in api/schema/scalars

GraphQL provides some primitive scalars, such as String, Int and Boolean, but in some cases, it is required to define custom ones that aren't included by default due to not (yet) being part of the spec, like Datetime, JSON and Upload

Further reading:

Resolvers#

Integrates GraphQL resolvers are defined in api/resolvers

A resolver is a function that receives two arguments:

  • Parent: The value returned by the parent resolver, usually a dictionary. If it's a root resolver this argument will be None
  • Info: An object whose attributes provide details about the execution AST and the HTTP request.

It will also receive keyword arguments if the GraphQL field defines any.

api/resolvers/user/email.py

from graphql.type.definition import GraphQLResolveInfo
def resolve(parent: Any, info: GraphQLResolveInfo, **kwargs: Dict[str, Any]):

The function must return a value whose structure matches the type defined in the GraphQL schema

IMPORTANT: Avoid reusing the resolver function. Other than the binding, it should never be called in other parts of the code

Further reading:

Mutations#

Integrates GraphQL mutations are defined in api/mutations

Mutations are a kind of GraphQL operation explicitly meant to change data.

NOTE: Mutations are also resolvers, just named differently for the sake of separating concerns, and just like a resolver function, they receive the parent argument (always None), the info object and their defined arguments

Most mutations only return {'success': bool} also known as "SimplePayload", but they aren't limited to that. If you need your mutation to return other data, just define the type in api/schema/types/mutation_payloads.graphql and use it

api/schema/types/mutation.graphql

type Mutation {
createUser(email: String!): SimplePayload!
}

api/mutations/create_user.py

from graphql.type.definition import GraphQLResolveInfo
def mutate(parent: None, info: GraphQLResolveInfo, **kwargs: Dict[str, Any]):
user_domain.create(kwargs['email'])
return {'success': True}

api/schema/types/mutation.py

from ariadne import MutationType
from api.mutations import create_user
MUTATION = MutationType()
MUTATION.set_field('createUser', create_user.mutate)

IMPORTANT: Python code style prefers snake_case variables, so if the mutation receives camelCased arguments, decorate the mutate function with the @convert_kwargs_to_snake_case decorator from ariadne.utils

Further reading:

Errors#

All exceptions raised, handled or unhandled will be reported in the "errors" field of the response

Raising exceptions can be useful to enforce business rules and report back to the client in cases the operation could not be completed successfully

Further reading:

Authentication#

The Integrates API enforces authentication by checking for the presence and validity of a JWT in the request cookies or headers

For resolvers or mutations that require authenticated users, decorate the function with the @require_login from decorators

Authorization#

The Integrates API enforces authorization implementing an ABAC model.

There are currently three levels of authorization

  • User
  • Organization
  • Group

The system then validates if the user can perform the action in a certain authz level according to the policies defined in authz/model.py

For resolvers or mutations that require authorized users, decorate the function with the appropriate decorator from decorators

  • @enforce_user_level_auth_async
  • @enforce_organization_level_auth_async
  • @enforce_group_level_auth_async

Performance optimizations#

In order to make the API more performant, we are moving towards a fully async backend. For better comprehension on how it's done in python, here's an article that provides a good explanation: Writing fast and concurrent code, even at architectural windward

Implementing and using dataloaders#

Work in progress, please check back later

Caching resolvers#

Work in progress, please check back later

Guides#

Adding new fields#

Work in progress, please check back later

  1. Declare the field in the schema using SDL
  2. Write the resolver function
  3. Bind the resolver function to the schema

Deprecation and removal of fields#

Unlike REST-like APIs, GraphQL encourages to avoid versioning, but there are still some things to keep in mind in order to avoid disruptions while evolving the APs.

Our current policy mandates removal 6 months after marking the field as deprecated.

To mark fields or enums as deprecated, use the @deprecated directive, e.g:

type ExampleType {
oldField: String @deprecated(reason: "reason text")
}

Deprecation reason guidelines#

The reason should follow something similar to:

This {field|mutation} is deprecated and will be removed after {date}.

If it was replaced or there is an alternative, it should include:

Use the {alternative} {field|mutation} instead.

Testing#

Work in progress, please check back later