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
DefaultSchema
should be overridden, or if another schema should be used altogether (rare).
register_entity_classes()
¶
- Args
*entities
: 1 or moreEntity
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, defaultFalse
. PassTrue
if 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 anInt
and not the default type ofID
.
- 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 ModelField
s. 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 theComputedField
depends on. Useful when we define aComputedField
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 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 Playlist
s 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 Playlist
s
created by a user on one Team
cannot be accessed by users on any other Team
s.
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 Team
s!
Additionally, mutations will check that any specified relational (e.g. via team_id
)
model is returned by its entity’s FilterBackend
s, 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 beTrue
if 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 Album
s, but only
administrators can create or update them.