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 isDefaultSchema().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
DefaultSchemashould be overridden, or if another schema should be used altogether (rare).
register_entity_classes()¶
- Args
 *entities: 1 or moreEntityclasses
- 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.ObjectTyperepresenting 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.ObjectTyperepresenting 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, defaultFalse. PassTrueif attribute should be treated as read-only.Additionally useful for fields whose value is populated automatically, such as
created_date.
graphene_type: Optional, defaultNone. Pass the Graphene type of this field if the default type is not correct. As an example, we may passModelField('id', graphene_type=graphene.Int)if we want our primary-key value to be exposed as anIntand not the default type ofID.
- Keyword args
 read_only:Trueif the field should be read-only.Falseby 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_propertyThe field’s Graphene type. E.g.
graphene.Int,graphene.String.
- Keyword args
 depends_on: An enumerable of field names that theComputedFielddepends on. Useful when we define aComputedFieldwhose 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 passingdepends_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
ValidatableInputobject that can be used to obtain the data being validated.
- Returns
 A tuple of
(boolean, dict|list). The first item needs to beTrueif the mutation should be allowed, otherwiseFalse. 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.