.. DjraphQL documentation master file, created by sphinx-quickstart on Tue Jan 12 21:31:26 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Walkthrough =========== .. toctree:: :maxdepth: 3 :caption: Contents: Let's create an application from scratch. We're going to build a music sharing app using Django with a GraphQL API powered by Graphene and DjraphQL. Project setup ************* Ready? Let's dive in! Virtual environment ################### Let's create and activate our virtual environment: .. code-block:: bash # Optional: ensure we're using Python 3 via -p virtualenv -p python3.7 .venv source .venv/bin/activate Next, let's install the dependencies we'll need. You can use your dependency manager of choice. For simplicity, we'll use `pip`. .. code-block:: bash pip install django graphene graphene_django djraphql Django setup ############ Let's create our Django project, which we will call ``songster``. .. code-block:: bash .venv/bin/django-admin startproject songster And now we'll create our Django app within our project. Let's call it ``songs``. .. code-block:: bash cd songster python manage.py startapp songs Next, we need to add our new app's configuration to ``INSTALLED_APPS`` in ``settings.py``. .. code-block:: python :linenos: :emphasize-lines: 2 INSTALLED_APPS = [ 'songs.apps.SongsConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', Creating Django models ###################### Now we'll create our models in ``songster/songs/models.py``. .. code-block:: python from django.db import models class Label(models.Model): name = models.CharField(null=False, max_length=512) established_year = models.IntegerField() class Artist(models.Model): name = models.CharField(null=False, max_length=512) label = models.ForeignKey(Label, null=True, related_name="artists", on_delete=models.CASCADE) class Album(models.Model): title = models.CharField(null=False, max_length=512) released_date = models.DateField() artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE) class Song(models.Model): album = models.ForeignKey(Album, null=True, related_name="songs", on_delete=models.CASCADE) title = models.CharField(null=False, max_length=512) duration_seconds = models.PositiveIntegerField(null=False) collaborators = models.ManyToManyField(Artist) Build the migration files via ``makemigrations``. .. code-block:: bash python manage.py makemigrations songs Finally, run the migration. .. code-block:: bash python manage.py migrate The output from the last command should display something like ``Applying songs.0001_initial... OK``. Seeding data ############ It's convenient to have a bit of data to test with. Django's `fixtures` feature makes that easy. First, create a file to hold our fixture data: .. code-block:: bash mkdir songs/fixtures touch songs/fixtures/songs.json Copy the content from `this Github gist `_ and paste it into ``songs.json``. Then, use the ``loaddata`` command: .. code-block:: bash python manage.py loaddata songs Build a schema ************** Now that we've created our Django application and have built a few models for our app, we can turn toward the fun part: creating our GraphQL API! Entity classes ############## To tell ``djraphql`` how to build our GraphQL schema, we must define classes that inherit from ``Entity``, which we can use to customize our API. But first, where should we define these classes? Our ``songs`` app seems like a natural place and is certainly reasonable. But as our startup grows and we add more apps to our project (each of which with their own set of entity classes), our entities would end up strewn across the project. To ensure that our GraphQL-specific logic stays in a single place in our project, let's create a new app called ``graphql_api`` and place our entities there, along with any other GraphQL-specific logic. .. code-block:: bash python manage.py startapp graphql_api In the newly created ``graphql_api`` directory, create an ``entities.py`` file: .. code-block:: python from songs.models import * from graphene import Schema from djraphql import SchemaBuilder, Entity, AllModelFields class LabelEntity(Entity): class Meta: model = Label fields = (AllModelFields(),) class ArtistEntity(Entity): class Meta: model = Artist fields = (AllModelFields(),) class AlbumEntity(Entity): class Meta: model = Album fields = (AllModelFields(),) class SongEntity(Entity): class Meta: model = Song fields = (AllModelFields(),) schema_builder = SchemaBuilder() schema_builder.register_entity_classes( LabelEntity, ArtistEntity, AlbumEntity, SongEntity) schema = Schema(query=schema_builder.QueryRoot) Let's walk through the above code. First off, imports. We're grabbing the model classes we want to build our schema around. We also are importing the ``Schema`` class from the `graphene `_ library, which is used to build the schema with the types generated by DjraphQL. Our last two imports are the ``Entity`` and ``SchemaBuilder`` classes from DjraphQL. We then define four classes, each inheriting from ``Entity``. Each class wraps a model we want to include in our GraphQL schema. For demonstration purposes, we'll use the ``AllModelFields`` helper to conveniently expose all fields on eachs model. We then build our Graphene types by instantiating a ``SchemaBuilder`` and registering our entity classes. Finally, we instantiate the ``Schema``, passing ``schema_builder.QueryRoot`` as the ``query`` argument. Queries ####### Let's execute our first GraphQL query. Open the Django shell via: .. code-block:: bash python manage.py shell Once in the shell, execute the following commands: .. code-block:: python from graphql_api.entities import schema schema.execute("query { ArtistsMany { id name } }").data # {'ArtistsMany': [{'id': 1, 'name': 'The Beatles'}, {'id': 2, 'name': 'George Martin'}, {'id': 3, 'name': 'Jimi Hendrix'}]} And just like that, we've got a working schema! Mutations ######### In order to perform mutations, we need to briefly touch on the permissions model that DjraphQL uses. The ``Entity`` class has an ``access_permissions`` field that can be used to control whether or not the schema will contain types corresponding to each C.R.U.D. (create, read, update, delete) operation. The default value is ``access_permissions = (R,)``. So, if left unaltered, all types will be readable, but not mutable. Let's change that. Update the ``SongEntity`` class in ``entities.py`` like so: .. code-block:: python :linenos: :emphasize-lines: 4,13,18 from songs.models import * from graphene import Schema from djraphql import SchemaBuilder, Entity from djraphql.access_permissions import C, R, U, D ... class SongEntity(Entity): class Meta: model = Song fields = (AllModelFields(),) access_permissions = (C, R, U, D) ... schema = Schema(query=schema_builder.QueryRoot, mutation=schema_builder.MutationRoot) This change will cause DjraphQL to build additional operations for creating, updating, and deleting ``Song``\s. Let's pop open the Django shell again. .. code-block:: python schema.execute('mutation { insertSong(data: { albumId: 3, title: "Secret Hidden Track", durationSeconds: 1 }) { success result { id } } }').data # OrderedDict([('insertSong', {'success': True, 'result': {'id': 37}})]) schema.execute('query { SongsMany(where: { title: { _eq: "Secret Hidden Track" } }) { id } }').data # {'SongsMany': [{'id': 37}]} schema.execute('query { SongByPk(pk: 37) { title } }').data # {'SongByPk': {'title': 'Secret Hidden Track'}} schema.execute('mutation { updateSong(pk: 37 data: { title: "Secret Hidden Track #2", durationSeconds: 12 }) { success result { id } } }').data # OrderedDict([('updateSong', {'success': True, 'result': {'id': 37}})]) schema.execute('query { SongByPk(pk: 37) { title } }').data # {'SongByPk': {'title': 'Secret Hidden Track #2'}} schema.execute('mutation { deleteSong(pk: 37) { success } }').data # OrderedDict([('deleteSong', {'success': True})]) schema.execute('query { SongByPk(pk: 37) { title } }').errors # [GraphQLLocatedError('Song matching query does not exist.')] As you can see, we added a ``Song`` via ``insertSong``, updated it via ``updateSong``, and deleted it via ``deleteSong``. All by adding a single line to our ``Entity``. Using GraphiQL ************** Now that we have a very basic GraphQL schema, we need to expose it as an API. We can use `graphene-django `_ to easily accomplish this. More settings updates ##################### First, we need to update our ``INSTALLED_APPS`` entry in ``settings.py`` again. .. code-block:: python :linenos: :emphasize-lines: 3 INSTALLED_APPS = [ 'songs.apps.SongsConfig', 'graphene_django', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', Next, add a ``/graphql`` endpoint to ``songster/urls.py``: .. code-block:: python :linenos: :emphasize-lines: 3,7 from django.contrib import admin from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ path('admin/', admin.site.urls), path('graphql', GraphQLView.as_view(graphiql=True)), ] And one more change to ``settings.py``. We must tell Graphene where to look for the schema object: .. code-block:: python GRAPHENE = { "SCHEMA": "graphql_api.entities.schema" } Running our project ################### All our setup is done, let's run our project! .. code-block:: bash python manage.py runserver Now you should be able to navigate to http://127.0.0.1:8000/graphql and see the GraphiQL interface! Try running queries with the convenience of auto-complete. One nice thing GraphiQL does is include the CSRF token automatically in requests it sends to the server. But if we want to use something like curl to test our API, we'd have to either provide the CSRF token manually: .. code-block:: bash curl http://127.0.0.1:8000/graphql \ -H 'Cookie: csrftoken=1234mycsrftokenhere5678' \ -H 'X-CSRFToken: 1234mycsrftokenhere5678' \ -H 'Content-Type: application/json' \ -X POST \ --data-raw '{"query": "query GetAlbum($id: Int!) { AlbumByPk(pk: $id) { title } }", "variables": {"id": 1}}' Or, we could use the ``csrf_exempt`` helper from the ``django.views.decorators.csrf`` module. .. code-block:: python :linenos: :emphasize-lines: 3,8 from django.urls import path from graphene_django.views import GraphQLView from django.views.decorators.csrf import csrf_exempt urlpatterns = [ path('admin/', admin.site.urls), path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))), ] With this change, we wouldn't have to pass the CSRF token along with the curl request. This is certainly more convenient, but **don't do this in production**! .. Indices and tables .. ================== .. * :ref:`genindex` .. * :ref:`modindex` .. * :ref:`search`