Schema๐Ÿ”—

In this section, we'll cover how you can set up entrypoints to you GraphQL schema for executing operations in Undine.

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 this example 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've created the Query RootType. In Undine, the Query RootType is required to exist for a schema to be created. 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.

For Subscription RootTypes, see the Subscriptions section.

Schema name๐Ÿ”—

By default, the name of the generated GraphQL ObjectType from a RootType class is the name of the RootType class. If you need to change the name separately, you can do so by providing the schema_name argument.

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


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

Description๐Ÿ”—

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

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


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

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

Directives๐Ÿ”—

You can add directives to the RootType by providing them using the directives argument. The directive must be usable in the OBJECT location.

from graphql import DirectiveLocation

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


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


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

You can also add them using decorator syntax.

from graphql import DirectiveLocation

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


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


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

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
5
6
7
from undine import Entrypoint, RootType


class Query(RootType, extensions={"foo": "bar"}):
    @Entrypoint
    def testing(self) -> str:
        return "Hello, World!"

RootType extensions are made available in the GraphQL ObjectType extensions after the schema is created. The RootType itself is found in the GraphQL ObjectType 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" from which you can execute operations like queries or mutations. In GraphQL terms, they are the fields on the ObjectType created from a RootType

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. You can read more on QueryTypes in the Queries section. This section will only cover using them in Entrypoints.

To create an Entrypoint for querying a single Model instance by its primary key, simply use the QueryType class as the reference for the Entrypoint.

from undine import Entrypoint, Field, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


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 crete an Entrypoint for listing all instances of the Model, add the many argument to the Entrypoint.

from undine import Entrypoint, Field, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


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!]!
}

With a list Entrypoint, if a FilterSet or an OrderSet has been added to your QueryType, they will show up as arguments on the Entrypoint.

1
2
3
4
5
6
type Query {
  tasks(
    filter: TaskFilterSet
    orderBy: [TaskOrderSet!]
  ): [TaskType!]!
}

MutationType references๐Ÿ”—

A MutationType represents a possible mutation operation based on a Django Model. You can read more on MutationTypes in the Mutations section. This section will only cover using them in Entrypoints.

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

from undine import Entrypoint, Field, Input, MutationType, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


class TaskCreateMutation(MutationType[Task]):
    name = Input()
    done = Input()


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 create an Entrypoint for mutating multiple Model instances in bulk, add the many argument to the Entrypoint.

from undine import Entrypoint, Field, Input, MutationType, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


class TaskCreateMutation(MutationType[Task]):
    name = Input()
    done = Input()


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!]!
}

Note that the total amount of objects that can be mutated in a bulk mutation is limited by the MUTATION_INSTANCE_LIMIT setting.

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 using 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 Entrypoints based on either QueryTypes, UnionTypes, or InterfaceTypes that return a list of items (i.e. many=True) to limit the number of objects that are fetched. By default, this is set by the LIST_ENTRYPOINT_LIMIT setting.

from undine import Entrypoint, QueryType, RootType

from .models import Task


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


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

Permissions๐Ÿ”—

To add permission checks to your Entrypoint, use 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)

Note that permissions for Entrypoints based on QueryTypes or MutationTypes are checked using that QueryType's or MutationType's permissions if no permission checks have been defined on the Entrypoint.

Custom resolver๐Ÿ”—

You can override the resolver for an Entrypoint by decorating a method using the @<entrypoint_name>.resolve decorator. This can be used, e.g., to add special-case Entrypoints for QueryTypes.

from undine import Entrypoint, GQLInfo, QueryType, RootType
from undine.optimizer import optimize_sync

from .models import Task


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


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

    @task_by_name.resolve
    def resolve_task_by_name(self, info: GQLInfo, name: str) -> Task | None:
        return optimize_sync(Task.objects.all(), info, name=name)
About method signature

The decorated method 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.

Note that when using this decorator, you'll override the resolver and arguments based on the reference used in the Entrypoint. Arguments will be taken from the additional arguments passed to the resolver, e.g., "name" in the example above.

When overriding the resolver for Entrypoints based on QueryTypes, the QueryType's FilterSet and OrderSet will not be available on the Entrypoint

Overriding the resolver for Entrypoints using MutationTypes is not recommended, as it bypasses the whole mutation process and many MutationType functions will not work.

If the resolver returns a Django Model that resolves using QueryType, you should call the optimizer in the resolver using optimize_sync or optimize_async, like in the above example, so that queries are optimized.

Schema name๐Ÿ”—

By default, the name of the ObjectType field generated from an Entrypoint is the same as the name of the Entrypoint on the RootType class (converted to camelCase if CAMEL_CASE_SCHEMA_FIELDS is enabled). If you want to change the name of the ObjectType field separately, you can do so by setting 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")

This can be useful when the desired name of the ObjectType field is a Python keyword and cannot be used as the Entrypoint attribute name.

Description๐Ÿ”—

By default, an Entrypoint is able to determine its description based on its reference. For example, for a QueryType, the description is taken from the class docstring. If the reference has no description, or you wish to add a different one, you can provide a description in one of two ways:

1) By setting 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.")

2) As class attribute docstrings, if ENABLE_CLASS_ATTRIBUTE_DOCSTRINGS is enabled.

from undine import Entrypoint, QueryType, RootType

from .models import Task


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


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

When using function references, instead of a class attribute docstring, you add a docstring to the function/method used as the reference instead.

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.")

Complexity๐Ÿ”—

The complexity value of an Entrypoint is used by Undine to calculate how expensive a given query to the schema would be. Queries are rejected by Undine if they would exceed the maximum allowed complexity, as set by the MAX_QUERY_COMPLEXITY setting.

Usually, complexity is set by QueryType Fields, but you can also set complexity on the Entrypoint itself. This can be useful for declaring complexity of Entrypoints not based on QueryTypes. Note that when the Entrypoint is based on a QueryType, this complexity adds to any complexity calculated from the QueryType's Fields.

Directives๐Ÿ”—

You can add directives to the Entrypoint by providing them using the directives argument. The directive must be usable in the FIELD_DEFINITION location.

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()])

You can also add them using the @ operator (which kind of looks like GraphQL syntax):

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) @ MyDirective()

See the Directives section for more details on directives.

Visibility๐Ÿ”—

This is an experimental feature that needs to be enabled using the EXPERIMENTAL_VISIBILITY_CHECKS setting.

You can hide an Entrypoint from certain users by decorating a method with the <entrypoint_name>.visible decorator. Hiding an Entrypoint means that it will not be included in introspection queries, and trying to use it in operations will result in an error that looks exactly like the Entrypoint didn't exist in the first place.

from undine import Entrypoint, RootType
from undine.typing import DjangoRequestProtocol


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

    @testing.visible
    def testing_visible(self, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser
About method signature

The decorated method is treated as a static method by the Entrypoint.

The self argument is not an instance of the RootType, but the instance of the Entrypoint that is being used.

Since visibility checks occur in the validation phase of the GraphQL request, GraphQL resolver info is not yet available. However, you can access the Django request object using the request argument. From this, you can, e.g., access the request user for permission checks.

When using visibility checks, you should also disable "did you mean" suggestions using the ALLOW_DID_YOU_MEAN_SUGGESTIONS setting. Otherwise, a hidden field might show up in them.

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 ObjectType field extensions after the schema is created. The Entrypoint itself is found in the GraphQL field extensions under a key defined by the ENTRYPOINT_EXTENSIONS_KEY setting.

Schema export๐Ÿ”—

Undine includes a management command to export your GraphQL schema. It prints the schema to STDOUT, which can be redirected to a file like so:

python manage.py print_schema > schema.graphql