DjraphQL builds a flexible & performant GraphQL schema by examining your Django models.
Project description
DjraphQL Schema Builder
What
DjraphQL ("GiraffeQL") is a library that examines your Django models and builds a flexible, performant GraphQL schema using Graphene. No resolvers necessary.
You can of course extend this schema, reference the defined types and build custom business logic into your own resolvers. The goal is to remove the monotonous 90% of boilerplate generalized C.R.U.D. and allow you to focus on stuff that can't be auto-generated.
Why
Django is a wonderful and ubiquitous framework that provides tons of great tooling and an active ecosystem of additional libraries. One such library is Django Rest Framework, which is great for serving a REST API.
I became frustrated with how DRF handles nested inserts and updates and how our Serializers tend to bloat into a mess of if-statements and validations and hacks like overriding to_internal_value
and to_representation
to get things working. You might say, "you're doing it wrong." Fair. I probably was. :)
Still, every time a frontend component needed a piece of data that wasn't already provided by an existing endpoint, you had to add the endpoint to urls.py, build the handler View, and use the ORM to serve the data. Don't forget error handling. Don't forget to check permissions. Multiply this by 10, 20, 100 developers and you have an explosion of endpoints all built in different ways: View, APIView, ViewSet. Some use Serializers, some use bespoke parsing.
What I'm getting at is a human problem: we could have done better at coaching people to do it The Right Way. But in a startup the time for that tends to slip as the end of the sprint approaches.
But it can also be the case that an API becomes more than what's easily served by REST. We have a lot of RPC-style endpoints that follow more of a command pattern. We have very deeply nested data served by our API. These things map well to GraphQL.
How
You provide a list of Django models and some associated metadata. For example, you can define the QuerySet that will be used for each fetch of a certain model type (you have access to the Django request object in the definition lambda for the QuerySet). By providing a QuerySet that is already filtered by e.g., user account, the library will use that QuerySet in the resolvers it generates. The requirement that you must remember to filter by account is all but eliminated.
It builds each relationship into the schema, so you can traverse from Label
to Artists
to Albums
to Songs
in a single query, still retreiving only the data you need.
You can specify arbitrarily complex SQL statements: the schema allows specifying filtering via where
, orderBy
, offset
, limit
clauses.
It allows smooth (nested, if you're bold enough!) updates & inserts.
It even allows aggregate queries. You can sum
, avg
, max
, min
columns right from the frontend.
Perhaps best of all, it automatically generates select_related
and prefetch_related
calls to your QuerySet. This avoids the classic GraphQL N+1 problem usually solved by things like dataloader.
A hand-rolled schema providing all of this for a single model would be hundreds of lines of code. Multiply that by a realistic, mature application with a hundred or more models, and you have an unmaintainable mess. This library auto-generates it for you, leaving the "fun" stuff for you.
Show me some code!
Let's walk through some examples. We'll start with creating our Django models for our Spotify clone.
from djraphql import SchemaBuilder
from djraphql.entities import Entity
from sample_music_app.models import Artist, Album, Label, Song
from graphene import Schema
class LabelEntity(Entity):
class Meta:
model = Label
class ArtistEntity(Entity):
class Meta:
model = Artist
class AlbumEntity(Entity):
class Meta:
model = Album
class SongEntity(Entity):
class Meta:
model = Song
type_generator = SchemaBuilder([
LabelEntity,
ArtistEntity,
AlbumEntity,
SongEntity
])
schema = Schema(
query=type_generator.QueryRoot,
mutation=type_generator.MutationRoot,
)
We've created a Entity
class for each Django model we want to add to our schema. We pass these classes to the SchemaBuilder
which will then expose two properties: QueryRoot
and MutationRoot
. We pass these properties to our Schema
instance, imported from the graphene
library.
Let's insert some data:
parlophone = Label.objects.create(name='Parlophone', established_year=1896)
Artist.objects.create(name='The Beatles', label=parlophone)
sue_records = Label.objects.create(name='Sue Records', established_year=1957)
Artist.objects.create(name='Jimmy Hendrix', label=sue_records)
Now, let's query it.
result = schema.execute("""
query {
ArtistsMany {
name
label {
name
establishedYear
}
}
}
""")
assert result.data == {
'ArtistsMany': [
{
'name': 'The Beatles',
'label': {
'name': 'Parlophone',
'establishedYear': 1896,
}
}, {
'name': 'Jimmy Hendrix',
'label': {
'name': 'Sue Records',
'establishedYear': 1957,
}
}
]
}
Default behavior of entity objects
Each class inheriting from Entity
requires a single subclass called Meta
, which must contain a model
field that indicates which Django model class we're dealing with.
This will expose 3 queries:
MyModelByPk(pk: Int!): MyModel!
MyModelsMany(where: WhereMyModel orderBy: OrderByMyModel limit: Int offset: Int): [MyModel!]!
MyModelsAggregate(where: WhereMyModel orderBy: OrderByMyModel): MyModelAggregateResult!
By default, no mutations are generated, because Entity
s are read-only unless specified otherwise (via the access_permissions
field).
Customizing entity objects
There are a few properties we can leverage on Entity
to customize the behavior of our generated schema.
access_permissions
A read-only API is useful, but at some point we'll need to mutate our data. We can add mutations by defining an access_permissions
field on our entity class.
Entity
contains a access_permissions
bit-field with a default value of R
(READ
).
To add the ability to create, update, or delete an object, we must override access_permissions
:
from cool_app.models import MyModel
from djraphql.access_permissions import C, R, U, D
# If too much brevity, CREATE, READ, UPDATE, DELETE works too!
class MyModelEntity(Entity):
class Meta:
model = MyModel
access_permissions = C | R | U | D
This will instruct the schema generator to create 3 GraphQL mutations:
insertMyModel(data: MyModelInput! tag: String): InsertMyModel
(C
/CREATE
required)updateMyModel(pk: Int! data: MyModelUpdateInput! tag: String): UpdateMyModel
(U
/UPDATE
required)deleteMyModel(pk: Int!): DeleteMyModel
(D
/DELETE
required)
filter_backends
TODO
include_fields
/exclude_fields
TODO
properties
TODO
get_for_insert
TODO
before_insert
TODO
Contributing
Virtual environments
Create virtual environments to run the Django app (assumes python --version
prints Python 2.7.17
):
virtualenv .venv-py2
virtualenv -p python3 .venv-py3
Activate one of them
In VSCode, it may be necessary to
cmd+shift+p
,Python: Select interpreter
to set the correct venv for the debugger.
source .venv-py3/bin/activate
Install dependencies. Leave Django out of requirements and explicitly install it, so that we can easily test across multiple versions.
pip install -r requirements.txt django==1.11.17
Run tests
First activate one of your virtual environments:
python ./manage.py test
Run tests w/ test coverage metrics
coverage run --source djraphql -m pytest
coverage report -m
Remove unused imports
pip install autoflake
autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports ./**/*.py
Build for distribution
See tutorial here.
Install setuptools
pip install --user --upgrade setuptools wheel
Package the library
python setup.py sdist bdist_wheel
Build the docs
pip install -r requirements-docs.txt
cd docs && make html
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.