.. _Relay tutorial:

Relay tutorial
========================================

Graphene has a number of additional features that are designed to make
working with Django *really simple*.

Note: The code in this quickstart is pulled from the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.

A good idea is to check the following things first:

* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
* `GraphQL Relay Specification <https://relay.dev/docs/guides/graphql-server-specification/>`__

Setup the Django project
------------------------

We will setup the project, create the following:

-  A Django project called ``cookbook``
-  An app within ``cookbook`` called ``ingredients``

.. code:: bash

    # Create the project directory
    mkdir cookbook
    cd cookbook

    # Create a virtualenv to isolate our package dependencies locally
    virtualenv env
    source env/bin/activate  # On Windows use `env\Scripts\activate`

    # Install Django and Graphene with Django support
    pip install django
    pip install graphene_django

    # Set up a new project with a single application
    django-admin.py startproject cookbook .  # Note the trailing '.' character
    cd cookbook
    django-admin.py startapp ingredients

Now sync your database for the first time:

.. code:: bash

    python manage.py migrate

Let's create a few simple models...

Defining our models
^^^^^^^^^^^^^^^^^^^

Let's get started with these models:

.. code:: python

    # cookbook/ingredients/models.py
    from django.db import models


    class Category(models.Model):
        name = models.CharField(max_length=100)

        def __str__(self):
            return self.name


    class Ingredient(models.Model):
        name = models.CharField(max_length=100)
        notes = models.TextField()
        category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)

        def __str__(self):
            return self.name

Don't forget to create & run migrations:

.. code:: bash

    python manage.py makemigrations
    python manage.py migrate

Load some test data
^^^^^^^^^^^^^^^^^^^

Now is a good time to load up some test data. The easiest option will be
to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following:

.. code:: bash

    $ python ./manage.py loaddata ingredients

    Installed 6 object(s) from 1 fixture(s)

Alternatively you can use the Django admin interface to create some data
yourself. You'll need to run the development server (see below), and
create a login for yourself too (``./manage.py createsuperuser``).

Schema
------

GraphQL presents your objects to the world as a graph structure rather
than a more hierarchical structure to which you may be accustomed. In
order to create this representation, Graphene needs to know about each
*type* of object which will appear in the graph.

This graph also has a *root type* through which all access begins. This
is the ``Query`` class below. In this example, we provide the ability to
list all ingredients via ``all_ingredients``, and the ability to obtain
a specific ingredient via ``ingredient``.

Create ``cookbook/ingredients/schema.py`` and type the following:

.. code:: python

    # cookbook/ingredients/schema.py
    from graphene import relay, ObjectType
    from graphene_django import DjangoObjectType
    from graphene_django.filter import DjangoFilterConnectionField

    from ingredients.models import Category, Ingredient


    # Graphene will automatically map the Category model's fields onto the CategoryNode.
    # This is configured in the CategoryNode's Meta class (as you can see below)
    class CategoryNode(DjangoObjectType):
        class Meta:
            model = Category
            fields = '__all__'
            filter_fields = ['name', 'ingredients']
            interfaces = (relay.Node, )


    class IngredientNode(DjangoObjectType):
        class Meta:
            model = Ingredient
            fields = '__all__'
            # Allow for some more advanced filtering here
            filter_fields = {
                'name': ['exact', 'icontains', 'istartswith'],
                'notes': ['exact', 'icontains'],
                'category': ['exact'],
                'category__name': ['exact'],
            }
            interfaces = (relay.Node, )


    class Query(ObjectType):
        category = relay.Node.Field(CategoryNode)
        all_categories = DjangoFilterConnectionField(CategoryNode)

        ingredient = relay.Node.Field(IngredientNode)
        all_ingredients = DjangoFilterConnectionField(IngredientNode)


The filtering functionality is provided by
`django-filter <https://django-filter.readthedocs.org>`__. See the
`usage
documentation <https://django-filter.readthedocs.org/en/latest/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. While optional, this
tutorial makes use of this functionality so you will need to install
``django-filter`` for this tutorial to work:

.. code:: bash

    pip install django-filter

Note that the above ``Query`` class is marked as 'abstract'. This is
because we will now create a project-level query which will combine all
our app-level queries.

Create the parent project-level ``cookbook/schema.py``:

.. code:: python

    import graphene

    import ingredients.schema


    class Query(ingredients.schema.Query, graphene.ObjectType):
        # This class will inherit from multiple Queries
        # as we begin to add more apps to our project
        pass

    schema = graphene.Schema(query=Query)

You can think of this as being something like your top-level ``urls.py``
file (although it currently lacks any namespacing).

Testing everything so far
-------------------------

Update settings
^^^^^^^^^^^^^^^

Next, install your app and GraphiQL in your Django project. GraphiQL is
a web-based integrated development environment to assist in the writing
and executing of GraphQL queries. It will provide us with a simple and
easy way of testing our cookbook project.

Add ``ingredients`` and ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:

.. code:: python

    INSTALLED_APPS = [
        ...
        # This will also make the `graphql_schema` management command available
        'graphene_django',

        # Install the ingredients app
        'ingredients',
    ]

And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:

.. code:: python

    GRAPHENE = {
        'SCHEMA': 'cookbook.schema.schema'
    }

Alternatively, we can specify the schema to be used in the urls definition,
as explained below.

Creating GraphQL and GraphiQL views
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Unlike a RESTful API, there is only a single URL from which GraphQL is
accessed. Requests to this URL are handled by Graphene's ``GraphQLView``
view.

This view will serve as GraphQL endpoint. As we want to have the
aforementioned GraphiQL we specify that on the params with ``graphiql=True``.

.. code:: python

    from django.conf.urls import url, include
    from django.contrib import admin

    from graphene_django.views import GraphQLView

    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
    ]


If we didn't specify the target schema in the Django settings file
as explained above, we can do so here using:

.. code:: python

    from django.conf.urls import url, include
    from django.contrib import admin

    from graphene_django.views import GraphQLView

    from cookbook.schema import schema

    urlpatterns = [
        url(r'^admin/', admin.site.urls),
        url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
    ]


Testing our GraphQL schema
^^^^^^^^^^^^^^^^^^^^^^^^^^

We're now ready to test the API we've built. Let's fire up the server
from the command line.

.. code:: bash

    $ python ./manage.py runserver

    Performing system checks...
    Django version 3.1.7, using settings 'cookbook.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CONTROL-C.

Go to `localhost:8000/graphql <http://localhost:8000/graphql>`__ and
type your first query!

.. code::

    query {
      allIngredients {
        edges {
          node {
            id,
            name
          }
        }
      }
    }

The above will return the names & IDs for all ingredients. But perhaps
you want a specific ingredient:

.. code::

    query {
      # Graphene creates globally unique IDs for all objects.
      # You may need to copy this value from the results of the first query
      ingredient(id: "SW5ncmVkaWVudE5vZGU6MQ==") {
        name
      }
    }

You can also get each ingredient for each category:

.. code::

    query {
      allCategories {
        edges {
          node {
            name,
            ingredients {
              edges {
                node {
                  name
                }
              }
            }
          }
        }
      }
    }

Or you can get only 'meat' ingredients containing the letter 'e':

.. code::

    query {
      # You can also use `category: "CATEGORY GLOBAL ID"`
      allIngredients(name_Icontains: "e", category_Name: "Meat") {
        edges {
          node {
            name
          }
        }
      }
    }



Final Steps
^^^^^^^^^^^

We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.
