Integrations๐Ÿ”—

In this section, we'll cover the integrations to other libraries that Undine includes.

Channels๐Ÿ”—

pip install undine[channels]

Undine provides support for GraphQL over WebSocket and GraphQL over SSE (Single Connection mode) by integrating with the channels library. Using the channels integration requires turning on Undine's Async Support.

You'll need to configure Django in your project's asgi.py file so that requests are sent to Undine's channels consumers. There are three app wrappers available depending on which protocols you need.

For WebSocket support only:

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "...")
django_application = get_asgi_application()

# Needs be imported after 'django_application' is created!
from undine.integrations.channels import get_websocket_enabled_app

application = get_websocket_enabled_app(django_application)

For SSE Single Connection mode only:

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "...")
django_application = get_asgi_application()

# Needs be imported after 'django_application' is created!
from undine.integrations.channels import get_sse_enabled_app

application = get_sse_enabled_app(django_application)

For both WebSocket and SSE Single Connection mode:

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "...")
django_application = get_asgi_application()

# Needs be imported after 'django_application' is created!
from undine.integrations.channels import get_websocket_and_sse_enabled_app

application = get_websocket_and_sse_enabled_app(django_application)

GraphiQL๐Ÿ”—

Undine includes a built-in GraphiQL interface for exploring and testing your GraphQL API. You can enable it using the GRAPHIQL_ENABLED setting. You should also set ALLOW_INTROSPECTION_QUERIES to True so that GraphiQL can introspect the schema. GraphiQL is then accessible by navigating to the GraphQL endpoint in a browser.

GraphiQL includes the explorer and history plugins. The URL encodes the current document, variables, and headers, so it can be shared with others.

By default, subscriptions use WebSockets. To use Server-Sent Events instead, use the GRAPHIQL_SSE_ENABLED setting. Single connection mode can be enabled with the GRAPHIQL_SSE_SINGLE_CONNECTION setting.

Django Debug Toolbar๐Ÿ”—

pip install undine[debug]

Undine integrates with django-debug-toolbar by modifying the toolbar HTML so that it integrates with GraphiQL. After installing django-debug-toolbar, Undine should automatically patch it without any additional configuration.

Django Model Translation๐Ÿ”—

Undine integrates with django-modeltranslation by allowing you to modify how autogenerated Fields, Inputs, Filters and Orders are created. Specifically, this happens using two settings: MODELTRANSLATION_INCLUDE_TRANSLATABLE and MODELTRANSLATION_INCLUDE_TRANSLATIONS.

Let's say you the following Model (with MODELTRANSLATION_LANGUAGES = ("en", "fi"))

1
2
3
4
5
6
7
8
9
from django.db import models


class Task(models.Model):
    name = models.CharField(max_length=255)

    # Created by modeltranslation
    name_en: str | None
    name_fi: str | None

...and the following translation options

1
2
3
4
5
6
7
8
9
from modeltranslation.decorators import register
from modeltranslation.translator import TranslationOptions

from .models import Task


@register(Task)
class TaskTranslationOptions(TranslationOptions):
    fields = ["name"]

Based on the Model's translation options, django-modeltranslation adds additional fields for each language defined by the MODELTRANSLATION_LANGUAGES setting. Let's call the added fields the "translatable" fields, and the fields they are based on the "translation" fields.

Using the MODELTRANSLATION_INCLUDE_TRANSLATABLE and MODELTRANSLATION_INCLUDE_TRANSLATIONS settings, you can control which of these fields undine will add to your schema when using autogeneration. By default, only the translation fields are added. You can of course always add the translatable fields manually.

Note that due to the way that django-modeltranslation works, the translation fields are always nullable, even for the default language.

Mypy๐Ÿ”—

Undine ships a mypy plugin that adds additional static type checking for types defined in Undine. To enable it, add the following to your mypy.ini file:

[mypy]
plugins = mypy_undine

The plugin adds the following additional type checks:

  • Check that QueryTypes, MutationTypes, FilterSets, OrderSets, and UnionTypes contain correct generic parameters
  • Check that RootTypes, QueryTypes, MutationTypes, FilterSets, OrderSets, InterfaceTypes and UnionTypes are created using the correct class definition keyword arguments
  • Check that Entrypoints, Fields, Inputs and Filters are applied to a method with the correct signature when used as decorators
  • Check that Entrypoints, Fields, Inputs, Filters and Orders decorator methods (e.g. @Field.permissions or @Input.validate) are applied to a method with the correct signature
  • Check that FilterSets and OrderSets are applied to QueryTypes or UnionTypes that are defined for the same Django Models
  • Check that FilterSets, OrderSets, and InterfaceTypes are applied to QueryTypes when using their decorator interface
  • Check that Directives are applied to objects that support them
  • Check that Directives are applied to objects that match their allowed locations
  • Check that Directives that are not repeatable are only applied once
  • Create Directive.__init__ for typing purposes based on DirectiveArguments if one does not exist

If there is a check that you think should be included, please open an issue or a pull request!

Pytest๐Ÿ”—

Undine comes with a pytest plugin that includes a testing client and few fixtures to help you write tests for your GraphQL APIs.

The GraphQLClient class is wrapper around Django's test client that makes testing your GraphQL API easier. It can be added to a test using the graphql fixture. Here is a simple example:

1
2
3
4
5
6
def test_example(graphql) -> None:
    query = "query { test }"

    response = graphql(query)

    assert response.data == {"hello": "Hello, World!"}

GraphQL requests can be made by calling the client as shown above. This makes a request to the GraphQL endpoint set by the GRAPHQL_PATH setting.

GraphQL variables can be passed using the variables argument. If these variables include any files, the client will automatically create a GraphQL multipart request instead of a normal GraphQL request.

1
2
3
4
5
6
7
def test_example(graphql) -> None:
    mutation = "mutation($input: TestInput!) { test(input: $input) }"
    data = {"name": "World"}

    response = graphql(mutation, variables={"input": data})

    assert response.data == {"hello": "Hello, World!"}

The client returns a custom response object GraphQLClientResponse, which has a number of useful properties for introspecting the response. The response object also has details on the database queries that were executed during the request, which can be useful for debugging the performance of your GraphQL API.

def test_example(graphql) -> None:
    query = "query { test { edges { node { id } } } }"

    response = graphql(query, count_queries=True)

    # The whole response
    assert response.json == {
        "data": {"test": {"edges": [{"node": {"id": "1"}}]}},
        "errors": [{"message": "Error message", "path": ["test"]}],
    }

    # Error properties
    assert response.has_errors is True
    assert response.errors == [{"message": "Error message", "path": ["test"]}]
    assert response.error_message(0) == "Error message"

    # Data properties
    assert response.data == {"test": {"edges": [{"node": {"id": "1"}}]}}
    assert response.results == {"edges": [{"node": {"id": "1"}}]}

    # Connection specific properties
    assert response.edges == [{"node": {"id": "1"}}]
    assert response.node(0) == {"id": "1"}

    # Check queries (requires `count_queries=True`)
    assert response.query_count == 1
    assert response.queries == ["SELECT 1;"]
    response.assert_query_count(1)

An async version of the client is also available, which can be accessed from the graphql_async fixture.

import pytest


@pytest.mark.asyncio  # Requires the `pytest-asyncio` plugin
@pytest.mark.django_db(transaction=True)  # For sessions
async def test_example(graphql_async) -> None:
    query = "query { test }"

    response = await graphql_async(query)

    assert response.data == {"hello": "Hello, World!"}

The plugin also includes a undine_settings fixture that allows modifying Undine's settings during testing more easily.

def test_example(undine_settings) -> None:
    undine_settings.NO_ERROR_LOCATION = True

If the channels integration is installed, the test client can also send GraphQL over WebSocket requests using the over_websocket method.

1
2
3
4
5
6
7
8
9
import pytest


@pytest.mark.asyncio  # Requires the `pytest-asyncio` plugin
@pytest.mark.django_db(transaction=True)  # For sessions
async def test_graphql(graphql) -> None:
    query = "query { test }"
    async for response in graphql.over_websocket(query):
        assert response.data == {"test": "Hello, World!"}