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
63 changes: 62 additions & 1 deletion lib/paginator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ defmodule Paginator do
* `:total_count_limit` - Running count queries on tables with a large number
of records is expensive so it is capped by default. Can be set to `:infinity`
in order to count all the records. Defaults to `10,000`.
* `:page_booleans` - populates `:has_next_page` and `:has_previous_page` booleans.
Always returns an `:after` and `:before` cursor (if available). This mimics
[relay style pagination](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo)

## Repo options

Expand Down Expand Up @@ -187,7 +190,9 @@ defmodule Paginator do
after: after_cursor(paginated_entries, sorted_entries, config),
limit: config.limit,
total_count: total_count,
total_count_cap_exceeded: total_count_cap_exceeded
total_count_cap_exceeded: total_count_cap_exceeded,
has_next_page: has_next_page(paginated_entries, sorted_entries, config),
has_previous_page: has_previous_page(paginated_entries, sorted_entries, config),
}
}
end
Expand Down Expand Up @@ -250,6 +255,11 @@ defmodule Paginator do
Map.get(schema, field)
end

defp before_cursor([], [], %Config{before: c_before, page_booleans: true})
when not is_nil(c_before) do
c_before
end

defp before_cursor([], [], _config), do: nil

defp before_cursor(_paginated_entries, _sorted_entries, %Config{after: nil, before: nil}),
Expand All @@ -260,6 +270,10 @@ defmodule Paginator do
first_or_nil(paginated_entries, config)
end

defp before_cursor(paginated_entries, _sorted_entries, %Config{page_booleans: true} = config) do
first_or_nil(paginated_entries, config)
end

defp before_cursor(paginated_entries, sorted_entries, config) do
if first_page?(sorted_entries, config) do
nil
Expand All @@ -276,13 +290,22 @@ defmodule Paginator do
end
end

defp after_cursor([], [], %Config{after: c_after, page_booleans: true})
when not is_nil(c_after) do
c_after
end

defp after_cursor([], [], _config), do: nil

defp after_cursor(paginated_entries, _sorted_entries, %Config{before: c_before} = config)
when not is_nil(c_before) do
last_or_nil(paginated_entries, config)
end

defp after_cursor(paginated_entries, _sorted_entries, %Config{page_booleans: true} = config) do
last_or_nil(paginated_entries, config)
end

defp after_cursor(paginated_entries, sorted_entries, config) do
if last_page?(sorted_entries, config) do
nil
Expand All @@ -299,6 +322,44 @@ defmodule Paginator do
end
end

defp has_next_page(_paginated_entries, _sorted_entries, %Config{page_booleans: false}) do
nil
end

defp has_next_page([], [], _config) do
false
end

defp has_next_page(_paginated_entries, _sorted_entries, %Config{before: c_before})
when not is_nil(c_before) do
true
end

defp has_next_page(_paginated_entries, sorted_entries, config) do
!last_page?(sorted_entries, config)
end

defp has_previous_page(_paginated_entries, _sorted_entries, %Config{page_booleans: false}) do
nil
end

defp has_previous_page([], [], _config) do
false
end

defp has_previous_page(_paginated_entries, _sorted_entries, %Config{after: nil, before: nil}) do
false
end

defp has_previous_page(_paginated_entries, _sorted_entries, %Config{after: c_after})
when not is_nil(c_after) do
true
end

defp has_previous_page(_paginated_entries, sorted_entries, config) do
!first_page?(sorted_entries, config)
end

defp fetch_cursor_value(schema, %Config{
cursor_fields: cursor_fields,
fetch_cursor_value_fun: fetch_cursor_value_fun
Expand Down
6 changes: 4 additions & 2 deletions lib/paginator/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ defmodule Paginator.Config do
:limit,
:maximum_limit,
:sort_direction,
:total_count_limit
:total_count_limit,
:page_booleans
]

@default_total_count_primary_key_field :id
Expand Down Expand Up @@ -48,7 +49,8 @@ defmodule Paginator.Config do
opts[:total_count_primary_key_field] || @default_total_count_primary_key_field,
limit: limit(opts),
sort_direction: opts[:sort_direction],
total_count_limit: opts[:total_count_limit] || @default_total_count_limit
total_count_limit: opts[:total_count_limit] || @default_total_count_limit,
page_booleans: opts[:page_booleans] || false
}
|> convert_deprecated_config()
end
Expand Down
6 changes: 4 additions & 2 deletions lib/paginator/page/metadata.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ defmodule Paginator.Page.Metadata do
before: opaque_cursor() | nil,
limit: integer(),
total_count: integer() | nil,
total_count_cap_exceeded: boolean() | nil
total_count_cap_exceeded: boolean() | nil,
has_next_page: boolean() | nil,
has_previous_page: boolean() | nil
}

defstruct [:after, :before, :limit, :total_count, :total_count_cap_exceeded]
defstruct [:after, :before, :limit, :total_count, :total_count_cap_exceeded, :has_next_page, :has_previous_page]
end
83 changes: 83 additions & 0 deletions test/paginator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,40 @@ defmodule PaginatorTest do
assert page.metadata.after == nil
end

test "paginates forward with page_booleans", %{
payments: {p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12}
} do
opts = [cursor_fields: [:charged_at, :id], sort_direction: :asc, limit: 4, page_booleans: true]

page = payments_by_charged_at() |> Repo.paginate(opts)
assert to_ids(page.entries) == to_ids([p5, p4, p1, p6])
assert page.metadata.before == nil
assert page.metadata.after == encode_cursor(%{charged_at: p6.charged_at, id: p6.id})
assert page.metadata.has_previous_page == false
assert page.metadata.has_next_page == true

page = payments_by_charged_at() |> Repo.paginate(opts ++ [after: page.metadata.after])
assert to_ids(page.entries) == to_ids([p7, p3, p10, p2])
assert page.metadata.before == encode_cursor(%{charged_at: p7.charged_at, id: p7.id})
assert page.metadata.after == encode_cursor(%{charged_at: p2.charged_at, id: p2.id})
assert page.metadata.has_next_page == true
assert page.metadata.has_previous_page == true

page = payments_by_charged_at() |> Repo.paginate(opts ++ [after: page.metadata.after])
assert to_ids(page.entries) == to_ids([p12, p8, p9, p11])
assert page.metadata.before == encode_cursor(%{charged_at: p12.charged_at, id: p12.id})
assert page.metadata.after == encode_cursor(%{charged_at: p11.charged_at, id: p11.id})
assert page.metadata.has_next_page == false
assert page.metadata.has_previous_page == true

page = payments_by_charged_at() |> Repo.paginate(opts ++ [after: page.metadata.after])
assert to_ids(page.entries) == to_ids([])
assert page.metadata.before == nil
assert page.metadata.after == encode_cursor(%{charged_at: p11.charged_at, id: p11.id})
assert page.metadata.has_next_page == false
assert page.metadata.has_previous_page == false
end

test "paginates forward with legacy cursor", %{
payments: {p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12}
} do
Expand Down Expand Up @@ -74,6 +108,43 @@ defmodule PaginatorTest do
assert page.metadata.before == nil
end

test "paginates backward with page_booleans", %{
payments: {p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12}
} do
opts = [cursor_fields: [:charged_at, :id], sort_direction: :asc, limit: 4, page_booleans: true]

page =
payments_by_charged_at()
|> Repo.paginate(opts ++ [before: encode_cursor(%{charged_at: p11.charged_at, id: p11.id})])

assert to_ids(page.entries) == to_ids([p2, p12, p8, p9])
assert page.metadata.before == encode_cursor(%{charged_at: p2.charged_at, id: p2.id})
assert page.metadata.after == encode_cursor(%{charged_at: p9.charged_at, id: p9.id})
assert page.metadata.has_next_page == true
assert page.metadata.has_previous_page == true

page = payments_by_charged_at() |> Repo.paginate(opts ++ [before: page.metadata.before])
assert to_ids(page.entries) == to_ids([p6, p7, p3, p10])
assert page.metadata.before == encode_cursor(%{charged_at: p6.charged_at, id: p6.id})
assert page.metadata.after == encode_cursor(%{charged_at: p10.charged_at, id: p10.id})
assert page.metadata.has_next_page == true
assert page.metadata.has_previous_page == true

page = payments_by_charged_at() |> Repo.paginate(opts ++ [before: page.metadata.before])
assert to_ids(page.entries) == to_ids([p5, p4, p1])
assert page.metadata.before == encode_cursor(%{charged_at: p5.charged_at, id: p5.id})
assert page.metadata.after == encode_cursor(%{charged_at: p1.charged_at, id: p1.id})
assert page.metadata.has_next_page == true
assert page.metadata.has_previous_page == false

page = payments_by_charged_at() |> Repo.paginate(opts ++ [before: page.metadata.before])
assert to_ids(page.entries) == to_ids([])
assert page.metadata.before == encode_cursor(%{charged_at: p5.charged_at, id: p5.id})
assert page.metadata.after == nil
assert page.metadata.has_next_page == false
assert page.metadata.has_previous_page == false
end

test "returns an empty page when there are no results" do
page =
payments_by_status("failed")
Expand All @@ -84,6 +155,18 @@ defmodule PaginatorTest do
assert page.metadata.before == nil
end

test "returns an empty page when there are no results with page_booleans" do
page =
payments_by_status("failed")
|> Repo.paginate(cursor_fields: [:charged_at, :id], limit: 10, page_booleans: true)

assert page.entries == []
assert page.metadata.after == nil
assert page.metadata.before == nil
assert page.metadata.has_next_page == false
assert page.metadata.has_previous_page == false
end

describe "paginate a collection of payments, sorting by charged_at" do
test "sorts ascending without cursors", %{
payments: {p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12}
Expand Down