Schema🔗

In this section, we'll cover how you can set up entrypoints to you GraphQL schema in Undine, expanding on the basics introduced in the Tutorial.

RootTypes🔗

A GraphQL schema defines a RootType for each kind of operation that it supports. In GraphQL terms, a RootType is just a regular ObjectType that just happens to be the root of the GraphQL Schema.

Let's take a look at the basic setup from the Tutorial.

from undine import Entrypoint, RootType, create_schema


class Query(RootType):
    @Entrypoint
    def testing(self) -> str:
        return "Hello, World!"


schema = create_schema(query=Query)

Here you created the Query RootType. The Query RootType is required to be able to crate a GraphQL schema. Each RootType must also have at least one Entrypoint in its class body.

As the name implies, the Query RootType is for querying data. For mutating data, you'd create a Mutation RootType.

from pathlib import Path

from undine import Entrypoint, RootType, create_schema


class Query(RootType):
    @Entrypoint
    def testing(self) -> str:
        return "Hello, World!"


class Mutation(RootType):
    @Entrypoint
    def testing(self) -> int:
        return Path("foo.txt").write_text("Hello, World!", encoding="utf-8")


schema = create_schema(query=Query, mutation=Mutation)

The Mutation RootType is optional, but if created, it must also include at least one Entrypoint, just like the Query RootType.

What about Subscriptions?

Undine does not support have support for subscriptions, but it's on the roadmap.

Schema name🔗

By default, the name of the RootType type is the name of the created class. If you need to change this without changing the class name, you can do so by providing the schema_name argument.

1
2
3
4
from undine import RootType


class Query(RootType, schema_name="MyQuery"): ...

Description🔗

To provide a description for the RootType, you can add a docstring to the class.

1
2
3
4
5
from undine import RootType


class Query(RootType):
    """Operations for querying."""

Directives🔗

You can add directives to the RootType by providing them using the directives argument.

from graphql import DirectiveLocation

from undine import RootType
from undine.directives import Directive


class MyDirective(Directive, locations=[DirectiveLocation.OBJECT]): ...


class Query(RootType, directives=[MyDirective()]): ...

See the Directives section for more details on directives.

GraphQL extensions🔗

You can provide custom extensions for the RootType by providing a extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the RootType.

1
2
3
4
from undine import RootType


class Query(RootType, extensions={"foo": "bar"}): ...

RootType extensions are made available in the GraphQL ObjectType extensions after the schema is created. The RootType itself is found in the extensions under a key defined by the ROOT_TYPE_EXTENSIONS_KEY setting.

Entrypoints🔗

Entrypoints can be thought of as the "API endpoints inside the GraphQL schema". They are the fields in a RootType from which you can execute operations like queries or mutations.

An Entrypoint always requires a reference from which it will create the proper GraphQL resolver, output type, and arguments for the operation.

Function references🔗

Using a function/method as a reference is the most basic way of creating an Entrypoint.

Function references can be used for both query and mutation Entrypoints. See the example from the Tutorial.

1
2
3
4
5
6
7
from undine import Entrypoint, GQLInfo, RootType


class Query(RootType):
    @Entrypoint
    def testing(self, info: GQLInfo) -> str:
        return "Hello World!"

With a function reference, the Entrypoint will use the decorated function as its GraphQL resolver. The function's return type will be used as the Entrypoint's output type, so typing it is required. You can even use a TypedDict to return an object with multiple fields.

About method signature

A method decorated with @Entrypoint is treated as a static method by the Entrypoint.

The self argument is not an instance of the RootType, but root argument of the GraphQLField resolver. To clarify this, it's recommended to change the argument's name to root, as defined by the RESOLVER_ROOT_PARAM_NAME setting.

The value of the root argument for an Entrypoint is None by default, but can be configured using the ROOT_VALUE setting if desired.

The info argument can be left out, but if it's included, it should always have the GQLInfo type annotation.

You can add arguments to the Entrypoint by adding them to the function signature. Typing these arguments is required to determine their input type.

1
2
3
4
5
6
7
from undine import Entrypoint, RootType


class Query(RootType):
    @Entrypoint
    def testing(self, name: str) -> str:
        return f"Hello, {name}!"

This will add a non-null name string argument to the Entrypoint. Note that non-null arguments are required by GraphQL, so if you wanted to make the argument optional, you'd need to make it nullable (in which case it will be None by default) or add a default value ourselves.

1
2
3
4
5
6
7
from undine import Entrypoint, RootType


class Query(RootType):
    @Entrypoint
    def testing(self, name: str | None = None) -> str:
        return f"Hello, {name or 'World'}!"

You can add a description to the Entrypoint by adding a docstring to the method. If the method has arguments, you can add descriptions to those arguments by using reStructuredText docstrings format.

from undine import Entrypoint, RootType


class Query(RootType):
    @Entrypoint
    def testing(self, name: str) -> str:
        """
        Return a greeting.

        :param name: The name to greet.
        """
        return f"Hello, {name}!"
What about other docstring formats?

Other types of docstrings can be used by parsed by providing a custom parser to the DOCSTRING_PARSER setting that conforms to the DocstringParserProtocol from undine.typing.

QueryType references🔗

A QueryType represents a GraphQL ObjectType for querying data from a Django model in the GraphQL schema. You should read more on QueryTypes in the Queries section since this section will only cover using them in Entrypoints.

For querying a single model instance, simply use the QueryType class as the reference for the Entrypoint.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType)

This would create the following field in the Query RootType:

1
2
3
type Query {
    task(pk: Int!): TaskType
}

To query a list of model instances, simply add the many argument to the Entrypoint in addition to the QueryType.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    tasks = Entrypoint(TaskType, many=True)

This would create the following field in the Query RootType:

1
2
3
type Query {
    tasks: [TaskType!]!
}

MutationType references🔗

A MutationType represents a possible mutation operation based on a Django model. You should read more on MutationTypes in the Mutations section since this section will only cover using them in Entrypoints.

To create a mutation for a model instance (a create mutation in this example), simply use the MutationType class as the reference for the Entrypoint.

from undine import Entrypoint, MutationType, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class TaskCreateMutation(MutationType[Task]): ...


class Mutation(RootType):
    create_task = Entrypoint(TaskCreateMutation)

This would create the following field in the Mutation RootType:

1
2
3
type Mutation {
    createTask(input: TaskCreateMutation!): TaskType!
}

To make this a bulk mutation, you can add the many argument to the Entrypoint.

from undine import Entrypoint, MutationType, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class TaskCreateMutation(MutationType[Task]): ...


class Mutation(RootType):
    bulk_create_tasks = Entrypoint(TaskCreateMutation, many=True)

This would create the following field in the Mutation RootType:

1
2
3
type Mutation {
    bulkCreateTask(input: [TaskCreateMutation!]!): [TaskType!]!
}

Schema name🔗

By default, the name of the Entrypoint is the name of the method or class attribute it's defined in. If you need to change this without changing the method or class attribute name, for example if the desired name is a Python keyword (e.g. if or from), you can do so by providing the schema_name argument.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, schema_name="singleTask")

Description🔗

You can provide a description using the description argument.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, description="Fetch a single Task.")

You can also provide the description as a "class attribute docstring".

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType)
    """Fetch a single Task."""

If a description is not provided in these ways, the Entrypoint will try to determine a description from the given reference, e.g., for a method reference, it will use the method's docstring, or for a QueryType reference, it will use the QueryType's docstring.

Many🔗

As seen in this section, the many argument is used to indicate whether the Entrypoint should return a non-null list of the referenced type. However, for for function references, the many argument is not required, as the Entrypoint can determine the this from the function's signature (i.e. whether it returns a list or not).

Nullable🔗

By default, all Entrypoints are non-null (except for function references, which determine nullability from the function's signature). However, you can make an Entrypoint nullable explicitly by adding the nullable argument.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, nullable=True)

Limit🔗

The limit argument is used by UnionTypes and InterfaceTypes to limit the number of objects that are fetched when those types are used in Entrypoints. It has no effect on other Entrypoint references.

Permissions🔗

Usually, permissions for Entrypoints are checked using the QueryType or MutationType that the Entrypoint is added for. However, you can override these by decorating a method using the @<entrypoint_name>.permissions decorator.

from undine import Entrypoint, GQLInfo, QueryType, RootType
from undine.exceptions import GraphQLPermissionError

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __permissions__(cls, instance: Task, info: GQLInfo) -> None:
        # Not called if 'TaskType' is accessed from 'Query.task'
        # because it has a permissions check already
        if not info.context.user.is_superuser:
            raise GraphQLPermissionError


class Query(RootType):
    task = Entrypoint(TaskType)

    @task.permissions
    def task_permissions(self, info: GQLInfo, instance: Task) -> None:
        if info.context.user.is_authenticated:
            msg = "Need to be logged in to access Tasks."
            raise GraphQLPermissionError(msg)

Deprecation reason🔗

A deprecation_reason can be provided to mark the Entrypoint as deprecated. This is for documentation purposes only and does not affect the use of the Entrypoint.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, deprecation_reason="Use something else.")

Directives🔗

You can add directives to the Entrypoint by providing them using the directives argument.

from graphql import DirectiveLocation

from undine import Entrypoint, QueryType, RootType
from undine.directives import Directive

from .models import Task


class MyDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION]): ...


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, directives=[MyDirective()])

See the Directives section for more details on directives.

GraphQL extensions🔗

You can provide custom extensions for the Entrypoint by providing a extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the Entrypoint.

from undine import Entrypoint, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class Query(RootType):
    task = Entrypoint(TaskType, extensions={"foo": "bar"})

Entrypoint extensions are made available in the GraphQL field extensions after the schema is created. The Entrypoint itself is found in the extensions under a key defined by the ENTRYPOINT_EXTENSIONS_KEY setting.