Ordering๐Ÿ”—

In this section, we'll cover the everything necessary for ordering results returned by your QueryTypes.

OrderSet๐Ÿ”—

An OrderSet is a collection of Order objects that can be applied to a QueryType. In GraphQL, they represent a GraphQL Enum, which when added to a QueryType creates an input argument for ordering the results of a QueryType.

A basic OrderSet is created by subclassing OrderSet and adding its Django Model as a generic type parameter. Then, the OrderSet can be added to a QueryType using the orderset argument.

from undine import Field, Order, OrderSet, QueryType

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()
    done = Order()
    created_at = Order()


class TaskType(QueryType[Task], orderset=TaskOrderSet):
    name = Field()

You can also add the OrderSet using decorator syntax.

from undine import Field, Order, OrderSet, QueryType

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()
    done = Order()
    created_at = Order()


@TaskOrderSet
class TaskType(QueryType[Task]):
    name = Field()

Auto-generation๐Ÿ”—

An OrderSet can automatically introspect its Django model and convert the model's fields to Orders on the OrderSet. For example, if the Task model has the following fields:

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


class Task(models.Model):
    name = models.CharField(max_length=255)
    done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

An auto-generated OrderSet has all of the Task model's fields translated into GraphQL Enum values, both in ascending and descending directions.

enum TaskOrderSet {
  pkAsc
  pkDesc
  nameAsc
  nameDesc
  doneAsc
  doneDesc
  createdAtAsc
  createdAtDesc
}

To use auto-generation, either set AUTOGENERATION setting to True to enable it globally, or set the auto argument to True in the OrderSet class definition. With this, you can leave the OrderSet class body empty.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task], auto=True):
    name = Order()

Your can exclude some model fields from the auto-generation by setting the exclude argument:

1
2
3
4
5
6
from undine import OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task], auto=True, exclude=["pk"]): ...

Schema name๐Ÿ”—

By default, the name of the generated Enum is the same as the name of the OrderSet class. If you want to change the name, you can do so by setting the schema_name argument:

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task], schema_name="TaskOrderingChoices"):
    name = Order()

Description๐Ÿ”—

You can provide a description using the description argument.

1
2
3
4
5
6
7
8
9
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    """Description."""

    name = Order()

Directives๐Ÿ”—

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

from graphql import DirectiveLocation

from undine import Order, OrderSet
from undine.directives import Directive

from .models import Task


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


class TaskOrderSet(OrderSet[Task], directives=[MyDirective()]):
    name = Order()

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 OrderSet from certain users by adding the visible argument to the OrderSet. Hiding an orderset set means that it will not be included in introspection queries for that user, and it cannot be used in operations by that user.

from undine import Order, OrderSet
from undine.typing import DjangoRequestProtocol

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()

    @classmethod
    def __is_visible__(cls, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser

GraphQL extensions๐Ÿ”—

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

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task], extensions={"foo": "bar"}):
    name = Order()

OrderSet extensions are made available in the GraphQL Enum extensions after the schema is created. The OrderSet itself is found in the extensions under a key defined by the ORDERSET_EXTENSIONS_KEY setting.

Order๐Ÿ”—

An Order is a class that is used to define a possible ordering for an OrderSet. Usually Orders correspond to fields on the Django model for their respective OrderSet. In GraphQL, it represents an EnumValue in a GraphQL Enum.

An Order always requires a reference which it will use to create the Django OrderBy expression for the Order.

Model field references๐Ÿ”—

For Orders corresponding to Django model fields, the Order can be used without passing in a reference, as its attribute name in the OrderSet class body can be used to identify the corresponding model field.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()

To be a bit more explicit, you could use a string referencing the model field:

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order("name")

For better type safety, you can also use the model field itself:

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(Task.name)

Being explicit like this is only required if the name of the attribute in the GraphQL schema is different from the model field name.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    title = Order("name")

Expression references๐Ÿ”—

Django ORM expressions can also be used as the references.

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

from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(Reverse("name"))

Remember that subqueries are also counted as expressions.

from django.db.models import OuterRef

from undine import Order, OrderSet
from undine.utils.model_utils import SubqueryCount

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    copies = Order(SubqueryCount(Task.objects.filter(name=OuterRef("name"))))

Null placement๐Ÿ”—

If the model field or expression used by the Order is nullable, the null_placement argument can be used to specify the position of null values.

1
2
3
4
5
6
7
8
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(null_placement="first")
    created_at = Order(null_placement="last")

By default, null values are placed according to your database default.

Aliases๐Ÿ”—

Sometimes a Order may require additional expressions to be added as aliases to the queryset when the Order is used. For this, you can define a function that returns a dictionary of expressions and decorate it with the aliases decorator.

from django.db.models.functions import Lower

from undine import DjangoExpression, GQLInfo, Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order("name_lower")

    @name.aliases
    def name_lower(self, info: GQLInfo, *, descending: bool) -> dict[str, DjangoExpression]:
        return {"name_lower": Lower("name")}

Field name๐Ÿ”—

A field_name can be provided to explicitly set the Django model field name that the Order corresponds to. This can be useful when the field has a different name and type in the GraphQL schema than in the model.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    title = Order(field_name="name")

Schema name๐Ÿ”—

A schema_name can be provided to override the name of the Order in the GraphQL schema. This can be useful for renaming fields for the schema, or when the desired name is a Python keyword and cannot be used as the Order attribute name.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(schema_name="title")

Description๐Ÿ”—

By default, an Order is able to determine its description based on its reference. For example, for a model field, the description is taken from its help_text.

If the reference has no description, or you wish to add a different one, this can be done in two ways:

1) By setting the description argument.

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(description="Order by task name.")

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

1
2
3
4
5
6
7
8
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()
    """Order by task name."""

Deprecation reason๐Ÿ”—

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

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(deprecation_reason="Use something else.")

Directives๐Ÿ”—

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

from graphql import DirectiveLocation

from undine import Order, OrderSet
from undine.directives import Directive

from .models import Task


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


class TaskOrderSet(OrderSet[Task]):
    name = Order(directives=[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 Order from certain users by adding the visible argument to the Order. Hiding an order means that it will not be included in introspection queries for that user, and it cannot be used in operations by that user.

from undine import Order, OrderSet
from undine.typing import DjangoRequestProtocol

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order()

    @name.visible
    def name_visible(self, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser

GraphQL extensions๐Ÿ”—

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

1
2
3
4
5
6
7
from undine import Order, OrderSet

from .models import Task


class TaskOrderSet(OrderSet[Task]):
    name = Order(extensions={"foo": "bar"})

Order extensions are made available in the GraphQL Enum value extensions after the schema is created. The Order itself is found in the extensions under a key defined by the ORDER_EXTENSIONS_KEY setting.