API documentation

In this section, we’ll discuss how to set permissions, customize fields, and keep our GraphQL APIs secure and performant.

SchemaBuilder

The SchemaBuilder class is the entry-point into DjraphQL.

The constructor takes the following arguments:

Keyword args
  • schema: Optional. Default value is DefaultSchema().

    Must be an instance of a class that implements djraphql.schemas.abstract_type_builder.AbstractSchema.

    Provide this argument only if the type mapping used by DefaultSchema should be overridden, or if another schema should be used altogether (rare).

register_entity_classes()

Args
  • *entities: 1 or more Entity classes

Returns
  • None

Method takes as input the entity classes DjraphQL should expose in the generated GraphQL schema. Can be called multiple times.

1
2
3
4
from djraphql import SchemaBuilder

sb = SchemaBuilder()
sb.register_entity_classes(LabelEntity, ArtistEntity)

Entity classes passed to register_entity_classes will affect the result of QueryRoot and MutationRoot.

QueryRoot

Returns
  • graphene.ObjectType representing the query root of generated GraphQL schema.

Property that returns a Graphene ObjectType containing operations for reading data.

1
2
3
4
5
6
import graphene
from djraphql import SchemaBuilder

sb = SchemaBuilder()
sb.register_entity_classes(LabelEntity, ArtistEntity)
schema = graphene.Schema(query=sb.QueryRoot)

MutationRoot

Returns
  • graphene.ObjectType representing the mutation root of generated GraphQL schema.

Property that returns a Graphene ObjectType containing operations for creating, updating or deleting data.

1
2
3
4
5
6
7
import graphene
from djraphql import SchemaBuilder

sb = SchemaBuilder()
sb.register_entity_classes(LabelEntity, ArtistEntity)
schema = graphene.Schema(query=sb.QueryRoot,
                         mutation=sb.MutationRoot)

registry

Returns
  • djraphql.Registry: object containing generated Graphene types for Django models.

Useful when we want to build a custom type that uses a generated type. Both QueryRoot and MutationRoot can serve as base classes in situations where we want to build additional, custom types on top of – or using – the types generated by DjraphQL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import graphene
from djraphql import SchemaBuilder

sb = SchemaBuilder()
sb.register_entity_classes(LabelEntity, ArtistEntity)
LabelObjectType = sb.registry.get_or_create_type(
    'basic_type', model_class=Label)

class CustomQueryRoot(sb.QueryRoot):
    first_label = graphene.Field(LabelObjectType)

    def resolve_first_label(parent, info):
        return Label.objects.first()

class CustomMutationRoot(sb.MutationRoot):
    a_custom_field = graphene.String()

    def resolve_a_custom_field(parent, info):
        return "A custom string field"

schema = graphene.Schema(query=CustomQueryRoot,
                         mutation=CustomMutationRoot)

Entity

Creating and registering classes that inherit from Entity is how you build your GraphQL schema.

Meta class

The nested Meta class contains items that affect the shape of the GraphQL types being created. These items are accessed only at schema build time (vs. query resolution time).

Each entity must define a Meta class. If it is not present, an error will be thrown at schema-build time.

model

Required. Set its value to the Django Model class for which we’re building GraphQL types.

1
2
3
4
5
6
from djraphql import Entity
from songs.models import Album

class AlbumEntity(Entity):
    class Meta:
        model = Album

After registering AlbumEntity, our SchemaBuilder instance’s QueryRoot property will contain operations for reading Albums.

fields

Required. Set its value to a tuple containing instances of ModelField, ComputedField, or AllModelFields. Defines what fields are defined on the generated GraphQL type.

ModelField

Pass an instance of this class to the fields property of an entity’s Meta nested class. This will result in exposing the specified model field.

Primary key fields are read-only.

Args
  • Name of the field we want to expose

Keyword args
  • read_only: Optional, default False . Pass True if attribute should be treated as read-only.

    • Additionally useful for fields whose value is populated automatically, such as created_date.

  • graphene_type: Optional, default None . Pass the Graphene type of this field if the default type is not correct. As an example, we may pass ModelField('id', graphene_type=graphene.Int) if we want our primary-key value to be exposed as an Int and not the default type of ID.

Keyword args
  • read_only: True if the field should be read-only. False by default.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from djraphql import Entity, ModelField
from songs.models import Album

class AlbumEntity(Entity):
    class Meta:
        model = Album
        fields = (
            ModelField('id'),
            ModelField('title'),
            ModelField('released_date', read_only=True),
        )

In this case, the only Album model fields exposed in our API will be id, title, and released_date, which will be read-only.

This allow-list behavior makes for a sane default: we must explicitly declare what fields we want to expose via ModelFields. This avoids surprises by ensuring we don’t accidently expose a column we shouldn’t have.

AllModelFields
Keyword args
  • excluding: Set to a tuple containing the names of the fields we want do not want to expose.

There are cases where we just want to expose everything on a model without the tedium of maintaining an explicit list. Or perhaps instead, we want to expose everything except a few fields (a deny-list).

The AllModelFields class can be used to accomplish these use cases.

Placing an AllModelFields instance in our fields tuple is equivalent to defining a ModelField for every field on our model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from djraphql import Entity, ModelField, AllModelFields
from songs.models import Album

class AlbumEntity(Entity):
    class Meta:
        model = Album
        fields = (
            AllModelFields(excluding=('artists',)),
            ModelField('released_date', read_only=True),
        )

This code is equivalent to the example in the ModelField section above. We’re passing an AllModelFields instance, which saves us some key-strokes, but more importantly, allows us to opt-in to the deny-list behavior. By passing the excluding keyword argument, we can ensure we do not expost the artists field.

We still declare released_date as read-only via the same mechanism as above.

The priority of a field defined explicitly (via ModelField) is higher than that of a field defined implicitly (via AllModelFields).

If a field exists in the excluding keyword argument and has a ModelField defined, an error will be thrown during schema generation.

ComputedField

Args

  • A string specifying the name of our property. E.g. my_property

  • The field’s Graphene type. E.g. graphene.Int, graphene.String.

Keyword args
  • depends_on: An enumerable of field names that the ComputedField depends on. Useful when we define a ComputedField whose handler depends on a field that was not specified in our GraphQL query. In such a scenario, Django’s ORM will lazily fetch that field’s value from the database upon accessing it within the handler, which can result in an N+1 scenario. By passing depends_on=["some_dependency"], DjraphQL will ensure the field will be populated before passing the instance to your handler.

A common requirement for an API author is the ability to expose a read-only field which does not map directly to a backing column.

This allows for derived or calculated data to be part of the API response in a way that is ergonomic to the API consumer.

We can achieve this by passing a ComputedField instance in our fields tuple.

For each ComputedField, we must define a method on our entity that DjraphQL can call to obtain the value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import graphene
from djraphql import Entity,
from djraphql.fields import (
    ModelField, AllModelFields, ComputedField)
from songs.models import Album

class AlbumEntity(Entity):
    class Meta:
        model = Album
        fields = (
            AllModelFields(excluding=('artists',)),
            ModelField('released_date', read_only=True)
            ComputedField('star_count', graphene.Int),
        )

    @staticmethod
    def get_star_count(context, album):
        return album.compute_the_star_count()

Here we expose a starCount field on our Album GraphQL type, even though the Album Django model has no such field.

access_permissions

Optional. Set its value to a tuple of PermissionFlag instances.

Can be used to fulfill schema-level and request-level permissioning requirements.

filter_backends

Optional. Set its value to a tuple of FilterBackends instances.

Can be used to ensure the correct conditions are always added to the where-clause for any executed SQL relating to the model.

custom_node_name

Optional. A string to specify a custom name for the node in the schema. This is specially helpful to prevent Model name collisions.

> Take in mind that this custom name will be used for all the types generated by DjraphQL.

get_for_insert()

Args
  • context: the Django request object

Returns
  • An instance of the entity’s model

Optional. Override this static method to control how this model is set for mutations that insert new data.

As an example, say we issue a GraphQL mutation for inserting a Playlist.

We don’t want the client to determine what value to set for Playlist.team_id, because then they could create Playlists associated with teams outside of their own!

To prevent this, we can use get_for_insert to ensure that any time we are creating an object that references a Team, that the object’s team_id is set from the authenticated request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from djraphql import Entity
from auth.models import Team

class TeamEntity(Entity):
    class Meta:
        model = Team

    @staticmethod
    def get_for_insert(context):
        return context.user.team

Our get_for_insert method will be called before inserting an object that has a team_id field. The value returned by our method will always “win” no matter what’s passed by the API consumer.

Note

Defining get_for_insert on an Entity will have the effect that its referencing fields will be read-only.

For example, since our TeamEntity defines get_for_insert, API consumers will be unable to set or change the value of any other model’s team_id.

before_insert()

Args
  • data: dictionary of values representing the object to be inserted

Returns
  • None

Optional. Override this static method to alter the object being inserted or e.g. for logging or metrics purposes.

The return value is unused.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import logger
from djraphql import Entity
from auth.models import Team

class Team(Entity):
    class Meta:
        model = Team

    @staticmethod
    def before_insert(data, context):
        user_email = context.user.email
        logger.info(
            f'{user_email} creating Team {data["name"]}.'
        )
        # Or, we could mutate data before it is inserted!

This callback is executed before the data’s relations have been inserted as well.

after_insert()

Args
  • instance: object that has been inserted

Returns
  • None

Optional. Override this static method to perform operations that must happen after insertion of an object. For example, logging or emitting metrics.

The return value is unused.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import logger
from djraphql import Entity
from auth.models import Team

class Team(Entity):
    class Meta:
        model = Team

    @staticmethod
    def after_insert(instance, context):
        user_email = context.user.email
        logger.info(
            f'{user_email} created Team {instance.name}.'
        )

This callback is executed after the instance’s relations have been inserted as well.

FilterBackend

By subclassing FilterBackend and overriding its filter_backend method, we can consistently and automatically apply the correct WHERE clause to each query we execute in the process of resolving a GraphQL request.

As an example, let’s say we have a business rule that states that Playlists created by a user on one Team cannot be accessed by users on any other Teams.

We can fulfill this requirement via a FilterBackend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from djraphql import Entity, FilterBackend
from auth.models import Playlist

class IsSameTeam(FilterBackend):
    def __init__(self, django_path_to_team_id):
        self.django_path_to_team_id = django_path_to_team_id

    def filter_backend(self, request, queryset):
        return queryset.filter(**{
            self.django_path_to_team_id: request.user.team_id
        })

class PlaylistEntity(Entity):
    class Meta:
        model = Playlist

    filter_backends = (IsSameTeam('user__team_id'),)

Any time a request contains a query that accesses a Playlist (directly or indirectly via a relationship from another model), the executed SQL will contain an inner-join of Playlist to User and the where-clause will contain AND user.team_id = {team_id from request}, thus ensuring we cannot leak Playlist data across Teams!

Additionally, mutations will check that any specified relational (e.g. via team_id) model is returned by its entity’s FilterBackends, keeping our data secure.

validator

Note

Validation is heavily dependent on individual use-case, API requirements, and schema. For this reason, DjraphQL’s philosophy is to be as unopinionated as possible.

Thus, the constructs below are simply generic hooks that allow you to validate input and – if necessary – halt the mutation and return errors.

How those errors get returned to the API consumer is largly a schema-level concern: DjraphQL’s Entity API makes no assumptions about the response shape, leaving that to the schema.

Head over to the validation section to see how the default schema handles validation errors.

Entity.validator can be set to any object that contains a validate method, which must be implemented by the library user.

validate()

Args
  • A ValidatableInput object that can be used to obtain the data being validated.

Returns
  • A tuple of (boolean, dict|list). The first item needs to be True if the mutation should be allowed, otherwise False. The second item is the value that will be returne to the API consumer.

ValidatableInput

An object provided by the schema that provides a view of the input data.

It exposes a single method, to_value, which is implemented by the schema. See validatable input to learn more.

Validation example

Let’s illustrate how validation works with an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from djraphql import Entity
from auth.models import Album

class AlbumValidator:
    def validate(self, validatable_input):
        input_value = validatable_input.to_value()

        errors = {}
        if not input_value['released_date']:
            errors['released_date'] = 'Enter a valid date.'
        if not input_value['title']:
            errors['title'] = 'Enter a valid title.'

        is_valid = bool(errors)
        return (is_valid, errors)

class AlbumEntity(Entity):
    class Meta:
        model = Album

    validator = AlbumValidator()

To see how errors are returned to the API user, check out the validation errors section of the Default schema page.

PermissionFlag

Entity.access_permissions is defined as a tuple of PermissionFlag instances, which provides control over what operations are included in the schema, and whether or not a request can access those operations.

The djraphql.access_permissions module exports four classes that inherit PermissionFlag:

  • Create

  • Read

  • Update

  • Delete

Schema-level control

The default value for access_permissions is (Read(),), which means the model will be read-only: no create, update, or delete operations will exist in the GraphQL schema generated by DjraphQL.

We can provide an access_permissions field on our entity to change that.

By providing a tuple of the PermissionFlag instances representing the operations we want on our model, we can control whether or not certain operations are defined.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from djraphql import Entity
from djraphql.access_permissions import (
    Create, Read, Update, Delete)
from auth.models import Album

class AlbumEntity(Entity):
    class Meta:
        model = Album

    access_permissions = (Create(), Read(), Update(), Delete())

The above code will fully expose Album for create-, read-, update- and delete-access via our GraphQL schema.

Short-hand flags

If you prefer terseness, the djraphql.access_permissions defines a single-character field for each operation.

  • C = Create()

  • R = Read()

  • U = Update()

  • D = Delete()

The above code snippet could have defined access_permissions = (C, R, U, D), which some developers find a bit more readable.

Request-level control

The PermissionFlag constructor accepts zero or more instances of RequestAuthorizationCheck.

For more granular access control, you can define your own classes that inherit RequestAuthorizationCheck and pass instances to the PermissionFlags in your access_permissions field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from songs.models import Album
from djraphql import Entity
from djraphql.access_permissions import (
    Create, R, Update, RequestAuthorizationCheck
)
from djraphql import SchemaBuilder

class IsAdmin(RequestAuthorizationCheck):
    def is_authorized(self, context):
        return context.user.is_admin

class AlbumEntity(Entity):
    class Meta:
        model = Album

    access_permissions = (
        Create(IsAdmin()),
        R,
        Update(IsAdmin())
    )

The IsAdmin above class verifies that a user is an adminstrator by overriding the is_authorized method.

By passing an instance of IsAdmin to the Create and Update flags, we’re ensuring that everyone can read Albums, but only administrators can create or update them.

RequestAuthorizationCheck

Create classes that inherit from RequestAuthorizationCheck to perform request-level verification that a user can access a create, read, update, or delete operation.

The only requirement is that the class must provide a is_authorized(self, context) method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from djraphql.access_permissions import (
    RequestAuthorizationCheck)

class UserHasRole(RequestAuthorizationCheck):
    def __init__(self, *roles):
        self.roles = roles

    def is_authorized(self, context):
        return context.user.role in self.roles

class AlbumEntity(Entity):
    class Meta:
        model = Album

    access_permissions = (Read(UserHasRole('admin', 'staff')),)

In this example, Album can only be read if the user associated with the request has the admin or staff role.