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 represents an Enum in the GraphQL schema. When added to a QueryType, it creates an input argument (as determined by the QUERY_TYPE_ORDER_INPUT_KEY setting) on any list Entrypoint or many-related Field that is created using that QueryType. That input can then be used to order the results returned by the Entrypoint or Field.

A basic OrderSet is created by subclassing OrderSet and adding a Django Model to it as a generic type parameter. You must also add at least one Order to the class body of the OrderSet. 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 to the QueryType 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 GraphQL Enum for an OrderSet class is the name of the OrderSet class. If you want to change the name separately, 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 for the OrderSet by adding a docstring to the class.

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 an OrderSet by providing them using the directives argument. The directive must be usable in the ENUM location.

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

You can also add directives using decorator syntax.

from graphql import DirectiveLocation

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

from .models import Task


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


@MyDirective()
class TaskOrderSet(OrderSet[Task]):
    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 using the __is_visible__ method. Hiding the OrderSet means that it will not be included in introspection queries for that user, and trying to use it in operations will result in an error that looks exactly like the argument for the OrderSet didn't exist in the first place.

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

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 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 GraphQL Enum extensions under a key defined by the ORDERSET_EXTENSIONS_KEY setting.

Order๐Ÿ”—

An Order defines a way of ordering the results returned by an Entrypoint or Field using a QueryType. An Order corresponds to anything that can be passed to a queryset.order_by() call, usually a field on the Django Model of the OrderSet it belongs to. In GraphQL, an Order represent two EnumValues on a GraphQL Enum, one for ordering in ascending direction and one for ordering in descending direction.

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 Filter 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 an 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.

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

This can be useful when the Model field corresponding to the Order has a different name and type in the GraphQL schema than on the Model.

Schema name๐Ÿ”—

By default, the name of the generated Enum values for an Order use the name of the Order on the OrderSet class (converted to camelCase if CAMEL_CASE_SCHEMA_FIELDS is enabled) as a base, with the full names having "Asc" and "Desc" suffixes added. If you want to change the base name of the Enum value separately, 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]):
    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()])

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

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() @ 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 decorating a method with the <order_name>.visible decorator. Hiding an Order 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 Order didn't exist in the first place.

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 Enum value extensions under a key defined by the ORDER_EXTENSIONS_KEY setting.