Skip to content

Commit

Permalink
Document the new Paginated generic
Browse files Browse the repository at this point in the history
  • Loading branch information
bellini666 committed Oct 19, 2024
1 parent eda46e7 commit 4c9c068
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 32 deletions.
180 changes: 175 additions & 5 deletions docs/guide/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,190 @@ An interface for limit/offset pagination can be use for basic pagination needs:
@strawberry_django.type(models.Fruit, pagination=True)
class Fruit:
name: auto


@strawberry.type
class Query:
fruits: list[Fruit] = strawberry_django.field()
```

Would produce the following schema:

```graphql title="schema.graphql"
type Fruit {
name: String!
}

type Query {
fruits(pagination: PaginationInput): [Fruit!]!
}
```

And can be queried like:

```graphql title="schema.graphql"
query {
fruits(pagination: { offset: 0, limit: 2 }) {
name
color
}
}
```

There is not default limit defined. All elements are returned if no pagination limit is defined.
The `pagination` argument can be given to the type, which will enforce the pagination
argument every time the field is annotated as a list, but you can also give it directly
to the field for more control, like:

```python title="types.py"
@strawberry_django.type(models.Fruit)
class Fruit:
name: auto


@strawberry.type
class Query:
fruits: list[Fruit] = strawberry_django.field(pagination=True)
```

Which will produce the exact same schema.

> [!NOTE]
> There is no default limit defined. All elements are returned if no pagination limit is defined.
## Paginated Generic

For more complex pagination needs, you can use the `Paginated` generic, which alongside
the `pagination` argument, will wrap the results in an object that contains the results
and the pagination information, together with the `totalCount` of elements excluding pagination.

```python title="types.py"
from strawberry_django.pagination import Paginated


@strawberry_django.type(models.Fruit)
class Fruit:
name: auto


@strawberry.type
class Query:
fruits: Paginated[Fruit] = strawberry_django.field()
```

Would produce the following schema:

```graphql title="schema.graphql"
type Fruit {
name: String!
}

type PaginatedInfo {
limit: Int!
offset: Int!
}

type FruitPaginated {
pageInfo: PaginatedInfo!
totalCount: Int!
results: [Fruit]!
}

type Query {
fruits(pagination: PaginationInput): [FruitPaginated!]!
}
```

Which can be queried like:

```graphql title="schema.graphql"
query {
fruits(pagination: { offset: 0, limit: 2 }) {
totalCount
pageInfo {
limit
offset
}
results {
name
}
}
}
```

### Customizing the pagination

Like other generics, `Paginated` can be customized to modify its behavior or to
add extra functionality in it. For example, suppose we want to add the average
price of the fruits in the pagination:

```python title="types.py"
from strawberry_django.pagination import Paginated


@strawberry_django.type(models.Fruit)
class Fruit:
name: auto
price: auto


@strawberry.type
class FruitPaginated(Paginated[Fruit]):
@strawberry_django.field
def average_price(self) -> Decimal:
if self.queryset is None:
return Decimal(0)

return self.queryset.aggregate(Avg("price"))["price__avg"]

@strawberry_django.field
def paginated_average_price(self) -> Decimal:
paginated_queryset = self.get_paginated_queryset()
if paginated_queryset is None:
return Decimal(0)

return paginated_queryset.aggregate(Avg("price"))["price__avg"]


@strawberry.type
class Query:
fruits: FruitPaginated = strawberry_django.field()
```

Would produce the following schema:

```graphql title="schema.graphql"
type Fruit {
name: String!
}

type PaginatedInfo {
limit: Int!
offset: Int!
}

type FruitPaginated {
pageInfo: PaginatedInfo!
totalCount: Int!
results: [Fruit]!
averagePrice: Decimal!
paginatedAveragePrice: Decimal!
}

type Query {
fruits(pagination: PaginationInput): [FruitPaginated!]!
}
```

The following attributes/methods can be accessed in the `Paginated` class:

- `queryset`: The queryset original queryset with any filters/ordering applied,
but not paginated yet
- `pagination`: The `OffsetPaginationInput` object, with the `offset` and `limit` for pagination
- `get_total_count()`: Returns the total count of elements in the queryset without pagination
- `get_paginated_queryset()`: Returns the queryset with pagination applied

## Relay pagination
## Cursor pagination (aka Relay style pagination)

For more complex scenarios, a cursor pagination would be better. For this,
use the [relay integration](./relay.md) to define those.
Another option for pagination is to use a
[relay style cursor pagination](https://graphql.org/learn/pagination). For this,
you can leverage the [relay integration](./relay.md) provided by strawberry
to create a relay connection.
53 changes: 35 additions & 18 deletions strawberry_django/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,58 @@ class OffsetPaginationInput:
limit: int = DEFAULT_LIMIT


@strawberry.type
class PaginatedInfo:
limit: int
offset: int


@strawberry.type
class Paginated(Generic[NodeType]):
queryset: strawberry.Private[Optional[QuerySet]]
pagination: strawberry.Private[OffsetPaginationInput]

@strawberry.field
def limit(self) -> int:
return self.pagination.limit

@strawberry.field
def offset(self) -> int:
return self.pagination.limit
def page_info(self) -> PaginatedInfo:
return PaginatedInfo(
limit=self.pagination.limit,
offset=self.pagination.offset,
)

@strawberry.field(description="Total count of existing results.")
@django_resolver
def total_count(self, root) -> int:
if self.queryset is None:
return 0

return get_total_count(self.queryset)
def total_count(self) -> int:
return self.get_total_count()

@strawberry.field(description="List of paginated results.")
@django_resolver
def results(self) -> list[NodeType]:
paginated_queryset = self.get_paginated_queryset()

return cast(
list[NodeType], paginated_queryset if paginated_queryset is not None else []
)

def get_total_count(self) -> int:
"""Retrieve tht total count of the queryset without pagination."""
return get_total_count(self.queryset) if self.queryset is not None else 0

def get_paginated_queryset(self) -> Optional[QuerySet]:
"""Retrieve the queryset with pagination applied.
This will apply the paginated arguments to the queryset and return it.
To use the original queryset, access `.queryset` directly.
"""
from strawberry_django.optimizer import is_optimized_by_prefetching

if self.queryset is None:
return []

if is_optimized_by_prefetching(self.queryset):
results = self.queryset._result_cache # type: ignore
else:
results = apply(self.pagination, self.queryset)
return None

return cast(list[NodeType], results)
return (
self.queryset._result_cache # type: ignore
if is_optimized_by_prefetching(self.queryset)
else apply(self.pagination, self.queryset)
)


def apply(
Expand Down
8 changes: 6 additions & 2 deletions tests/projects/snapshots/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,7 @@ type IssueTypeEdge {
}

type IssueTypePaginated {
limit: Int!
offset: Int!
pageInfo: PaginatedInfo!

"""Total count of existing results."""
totalCount: Int!
Expand Down Expand Up @@ -522,6 +521,11 @@ type PageInfo {
endCursor: String
}

type PaginatedInfo {
limit: Int!
offset: Int!
}

type ProjectConnection {
"""Pagination data for this connection"""
pageInfo: PageInfo!
Expand Down
8 changes: 6 additions & 2 deletions tests/projects/snapshots/schema_with_inheritance.gql
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ type IssueTypeEdge {
}

type IssueTypePaginated {
limit: Int!
offset: Int!
pageInfo: PaginatedInfo!

"""Total count of existing results."""
totalCount: Int!
Expand Down Expand Up @@ -300,6 +299,11 @@ type PageInfo {
endCursor: String
}

type PaginatedInfo {
limit: Int!
offset: Int!
}

input ProjectOrder {
id: Ordering
name: Ordering
Expand Down
13 changes: 8 additions & 5 deletions tests/test_paginated_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from tests import models


def test_pagination_schema():
def test_paginated_schema():
@strawberry_django.type(models.Fruit)
class Fruit:
id: int
Expand All @@ -35,8 +35,7 @@ class Query:
}
type ColorPaginated {
limit: Int!
offset: Int!
pageInfo: PaginatedInfo!
"""Total count of existing results."""
totalCount: Int!
Expand All @@ -51,8 +50,7 @@ class Query:
}
type FruitPaginated {
limit: Int!
offset: Int!
pageInfo: PaginatedInfo!
"""Total count of existing results."""
totalCount: Int!
Expand All @@ -66,6 +64,11 @@ class Query:
limit: Int! = -1
}
type PaginatedInfo {
limit: Int!
offset: Int!
}
type Query {
fruits(pagination: OffsetPaginationInput): FruitPaginated!
colors(pagination: OffsetPaginationInput): ColorPaginated!
Expand Down

0 comments on commit 4c9c068

Please sign in to comment.