Persisted Documents๐Ÿ”—

In this section, we'll cover Undine's support for persisted documents, which offer a way to persist known GraphQL documents on the server for caching, reducing network traffic, or to use as an operation allow-list.

Installation๐Ÿ”—

To enable persisted documents, you must first add the undine.persisted_documents app to your INSTALLED_APPS:

1
2
3
4
5
6
INSTALLED_APPS = [
    # ...
    "undine",
    "undine.persisted_documents",
    # ...
]

Then, add the persisted document registration view to your URLconf:

1
2
3
4
5
6
from django.urls import include, path

urlpatterns = [
    path("", include("undine.http.urls")),
    path("", include("undine.persisted_documents.urls")),
]

Before running migrations, you should have a look at the PersistedDocument Model in undine.persisted_documents.model. This Model can be swapped out with your own implementation using the UNDINE_PERSISTED_DOCUMENTS_MODEL setting, similar to how the User model can be swapped out with AUTH_USER_MODEL. Whether you decide to do this or not, remember to run migrations afterwards.

Usage๐Ÿ”—

Once the app is installed, Undine is ready to accept persisted documents. Persisted documents work through the same GraphQL endpoint used for regular GraphQL requests, but instead of a query string, you must provide a documentId instead.

1
2
3
4
{
  "documentId": "sha256:75d3580309f2b2bbe92cecc1ff4faf7533e038f565895c8eef25dc23e6491b8d",
  "variables": {}
}

A documentId can be obtained by registering a new persisted document using the persisted document registration view, as specified by the PERSISTED_DOCUMENTS_PATH setting. The view accepts a dictionary of documents like this

1
2
3
4
5
6
{
  "documents": {
    "foo": "query { example }",
    "bar": "query { testing }"
  }
}

...and returns a dictionary of documentIds like this

1
2
3
4
5
6
7
8
{
  "data": {
    "documents": {
      "foo": "sha256:1ce1ad479d1905f8d89262a1bccb87b9b4fe6b85161cd8cecb00b87d21d8889f",
      "bar": "sha256:75d3580309f2b2bbe92cecc1ff4faf7533e038f565895c8eef25dc23e6491b8d"
    }
  }
}

...where each key in the documents dictionary is defined by the user, so that a documentId corresponding to a document is returned in the same key. The keys are not used for anything else.

Response for this view follows the GraphQL response format, so any errors are returned in the "errors" key.

{
  "data": null,
  "errors": [
    {
      "message": "Validation error",
      "extensions": {
        "status_code": 400
      }
    }
  ]
}

Note that a document with the same selection set produces a different documentId if they have different whitespace, newlines, or comments. This is to ensure that error locations stay consistent.

Permissions๐Ÿ”—

You'll likely want to protect the persisted documents registration view with a permission check so that only some users can register new persisted documents. This can be done by setting the PERSISTED_DOCUMENTS_PERMISSION_CALLBACK setting to a function that accepts a request and a document_map as arguments.

1
2
3
4
5
6
7
8
9
from django.http import HttpRequest

from undine.exceptions import GraphQLPermissionError


def persisted_documents_permissions(request: HttpRequest, document_map: dict[str, str]) -> None:
    if not request.user.is_superuser:
        msg = "You do not have permission to register persisted documents."
        raise GraphQLPermissionError(msg)

Allow-list mode๐Ÿ”—

Persisted documents can be used to create an allow-list for GraphQL operations. Usually this is done to enhance security of a system by preventing malicious queries from being executed. Undine can be configured to only accept persisted documents by setting the PERSISTED_DOCUMENTS_ONLY setting to True.

1
2
3
UNDINE = {
    "PERSISTED_DOCUMENTS_ONLY": True,
}

When operating in this mode, your clients should call PersistedDocumentsView during build time to register their queries and mutations.