Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Release type: minor

Add `offset` argument to relay connections for first/after and last/before pagination.
56 changes: 53 additions & 3 deletions strawberry_django/fields/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from strawberry import UNSET, relay
from strawberry.annotation import StrawberryAnnotation
from strawberry.extensions.field_extension import FieldExtension
from strawberry.types.arguments import StrawberryArgument
from strawberry.types.field import _RESOLVER_TYPE # ruff: ignore[import-private-name]
from strawberry.types.fields.resolver import StrawberryResolver
from strawberry.types.info import Info
Expand Down Expand Up @@ -77,9 +78,11 @@

from graphql.pyutils import AwaitableOrValue
from strawberry import BasePermission
from strawberry.extensions.field_extension import SyncExtensionResolver
from strawberry.extensions.field_extension import (
AsyncExtensionResolver,
SyncExtensionResolver,
)
from strawberry.relay.types import NodeIterableType
from strawberry.types.arguments import StrawberryArgument
from strawberry.types.base import WithStrawberryObjectDefinition
from strawberry.types.field import StrawberryField
from strawberry.types.unset import UnsetType
Expand Down Expand Up @@ -544,7 +547,17 @@ def default_resolver(
)

try:
return super().apply(field)
ret = super().apply(field)
field.arguments.append(
StrawberryArgument(
python_name="offset",
graphql_name=None,
type_annotation=StrawberryAnnotation(int | None),
description="Offset to start slicing the list from.",
default=None,
)
)
return ret
finally:
if original_signature is UNSET:
resolver.__dict__.pop("signature", None)
Expand All @@ -561,6 +574,7 @@ def resolve(
after: str | None = None,
first: int | None = None,
last: int | None = None,
offset: int | None = None,
**kwargs: Any,
) -> Any:
assert self.connection_type is not None
Expand All @@ -578,6 +592,7 @@ async def async_resolver():
after=after,
first=first,
last=last,
offset=offset,
max_results=self.max_results,
**kwargs,
)
Expand All @@ -595,10 +610,45 @@ async def async_resolver():
after=after,
first=first,
last=last,
offset=offset,
max_results=self.max_results,
**kwargs,
)

async def resolve_async(
self,
next_: AsyncExtensionResolver,
source: Any,
info: Info,
*,
before: str | None = None,
after: str | None = None,
first: int | None = None,
last: int | None = None,
offset: int | None = None,
**kwargs: Any,
) -> Any:
assert self.connection_type is not None
nodes = next_(source, info, **kwargs)
if inspect.isawaitable(nodes):
nodes = await nodes

resolved = self.connection_type.resolve_connection(
cast("Iterable[relay.Node]", nodes),
info=info,
before=before,
after=after,
first=first,
last=last,
offset=offset,
max_results=self.max_results,
**kwargs,
)

if inspect.isawaitable(resolved):
resolved = await resolved
return resolved


class StrawberryOffsetPaginatedExtension(FieldExtension):
paginated_type: type[OffsetPaginated]
Expand Down
9 changes: 8 additions & 1 deletion strawberry_django/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ def _optimize_prefetch_queryset(
DjangoCursorConnection,
apply_cursor_pagination,
)
from strawberry_django.relay.utils import resolve_offset_to_after

if (
not config
Expand Down Expand Up @@ -665,12 +666,17 @@ def _optimize_prefetch_queryset(
field_ = unwrap_type(field_)
edge_class = cast("Edge", field_)

after = resolve_offset_to_after(
field_kwargs.get("offset"),
field_kwargs.get("after"),
prefix=edge_class.CURSOR_PREFIX,
)
slice_metadata = SliceMetadata.from_arguments(
Info(_raw_info=info, _field=field),
first=field_kwargs.get("first"),
last=field_kwargs.get("last"),
before=field_kwargs.get("before"),
after=field_kwargs.get("after"),
after=after,
max_results=connection_extension.max_results,
prefix=edge_class.CURSOR_PREFIX,
)
Expand All @@ -694,6 +700,7 @@ def _optimize_prefetch_queryset(
info=Info(_raw_info=info, _field=field),
first=field_kwargs.get("first"),
last=field_kwargs.get("last"),
offset=field_kwargs.get("offset"),
before=field_kwargs.get("before"),
after=field_kwargs.get("after"),
max_results=connection_extension.max_results,
Expand Down
11 changes: 9 additions & 2 deletions strawberry_django/relay/cursor_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,14 @@ def apply_cursor_pagination(
after: str | None,
first: int | None,
last: int | None,
offset: int | None = None,
max_results: int | None,
) -> tuple[QuerySet, list[OrderingDescriptor]]:
max_results = (
max_results if max_results is not None else info.schema.config.relay_max_results
)
if offset is not None and offset < 0:
raise ValueError("Argument 'offset' must be a non-negative integer.")

qs, ordering_descriptors, original_order_by = annotate_ordering_fields(qs)
if after:
Expand Down Expand Up @@ -256,7 +259,8 @@ def apply_cursor_pagination(
raise ValueError("Argument 'first' must be a non-negative integer.")
if first > max_results:
raise ValueError(f"Argument 'first' cannot be higher than {max_results}.")
slice_ = slice(first + 1)
start = offset or 0
slice_ = slice(start, start + first + 1)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
elif last is not None:
# when using last, optimize by reversing the QuerySet ordering in the DB,
# then slicing from the end (which is now the start in QuerySet ordering)
Expand Down Expand Up @@ -368,6 +372,7 @@ def resolve_connection(
after: str | None = None,
first: int | None = None,
last: int | None = None,
offset: int | None = None,
max_results: int | None = None,
**kwargs: Any,
) -> AwaitableOrValue[Self]:
Expand All @@ -385,6 +390,7 @@ def resolve_connection(
after=after,
first=first,
last=last,
offset=offset,
max_results=max_results,
)
else:
Expand Down Expand Up @@ -417,7 +423,8 @@ def resolve_connection(

def finish_resolving():
nonlocal qs
has_previous_page = has_next_page = False
has_previous_page = bool(offset and offset > 0)
has_next_page = False

results = list(qs)

Expand Down
24 changes: 24 additions & 0 deletions strawberry_django/relay/list_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from strawberry_django.pagination import get_cached_total_count, get_total_count
from strawberry_django.queryset import get_queryset_config
from strawberry_django.relay.utils import resolve_offset_to_after
from strawberry_django.resolvers import django_resolver
from strawberry_django.utils.typing import unwrap_type

Expand Down Expand Up @@ -88,8 +89,27 @@ def resolve_connection(
after: str | None = None,
first: int | None = None,
last: int | None = None,
offset: int | None = None,
**kwargs: Any,
) -> AwaitableOrValue[Self]:
if offset is not None:
if offset < 0:
raise ValueError("Argument 'offset' must be a non-negative integer.")
type_def = get_object_definition(cls)
assert type_def
field_def = type_def.get_field("edges")
assert field_def
field = field_def.resolve_type(type_definition=type_def)
while isinstance(field, StrawberryContainer):
field = field.of_type
edge_class = cast("relay.Edge[relay.NodeType]", field)
after = resolve_offset_to_after(
offset,
after,
prefix=edge_class.CURSOR_PREFIX,
)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
offset = None

if isinstance(nodes, models.QuerySet) and (
queryset_config := get_queryset_config(nodes)
):
Expand Down Expand Up @@ -127,6 +147,7 @@ def resolve_connection(
after=after,
first=first,
last=last,
offset=offset,
**kwargs,
)

Expand Down Expand Up @@ -225,6 +246,7 @@ def resolve_optimized_last_connection(
after: str | None = None,
first: int | None = None,
last: int | None = None,
offset: int | None = None,
**kwargs: Any,
) -> AwaitableOrValue[Self]:
"""Resolve the connection being paginated only via `last`.
Expand Down Expand Up @@ -254,6 +276,7 @@ async def wrapper():
after=after,
first=first,
last=last,
offset=None,
**kwargs,
)
return await conn if inspect.isawaitable(conn) else conn
Expand All @@ -269,6 +292,7 @@ async def wrapper():
after=after,
first=first,
last=last,
offset=offset,
**kwargs,
)

Expand Down
28 changes: 28 additions & 0 deletions strawberry_django/relay/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"resolve_model_id_attr",
"resolve_model_node",
"resolve_model_nodes",
"resolve_offset_to_after",
]


Expand Down Expand Up @@ -343,3 +344,30 @@ def resolve_model_id(
return str(root.__dict__[id_attr])
except KeyError:
return django_getattr(root, id_attr)


def resolve_offset_to_after(
offset: int | None,
after: str | None = None,
*,
prefix: str = relay.types.PREFIX,
) -> str | None:
"""Adjust or create an `after` cursor based on an `offset` integer argument."""
if offset is None:
return after

if offset < 0:
raise ValueError("Argument 'offset' must be a non-negative integer.")

if after:
after_type, after_parsed = relay.from_base64(after)
if after_type != prefix:
raise TypeError("Argument 'after' contains a non-existing value.")
start_offset = int(after_parsed) + 1 + offset
else:
start_offset = offset

if start_offset > 0:
return relay.to_base64(prefix, start_offset - 1)

return None
Loading
Loading