Packages

Completely Decoupled Pagination library for Ecto Queries

Current section

Files

Jump to
pager lib pager.ex
Raw

lib/pager.ex

defmodule Pager do
import Ecto.Query, warn: false
@moduledoc """
Provides pagination functionality for Ecto queries.
This module offers a simple way to paginate Ecto query results with automatic
handling of page boundaries, flexible parameter types, and consistent return structure.
"""
@typedoc """
Pagination result containing pagination metadata and list of items.
Fields:
* `has_next` - Indicates if there are more pages after the current one
* `has_prev` - Indicates if there are pages before the current one
* `prev_page` - Previous page number
* `page` - Current page number
* `next_page` - Next page number
* `first` - Index of first item on current page
* `last` - Index of last item on current page
* `count` - Total number of items across all pages
* `list` - List of results/items on the current page
"""
@type page_result :: %{
has_next: boolean(),
has_prev: boolean(),
prev_page: integer(),
page: integer(),
next_page: integer(),
first: integer(),
last: integer(),
count: integer(),
list: [any()]
}
@doc """
Paginates an Ecto query with flexible parameter handling.
# Parameters
* `query` - An Ecto query to paginate
* `repo` - The Ecto repo to execute the query
* `page` - Page number (integer or string, defaults to 1)
* `per_page` - Items per page (integer or string, defaults to 50)
# Examples
iex> alias Pager.{User, Repo}
iex> query = from(u in User)
iex> result = Pager.page(query, Repo, 1, 20)
iex> Map.drop(result, [:list])
%{count: 0, first: 1, has_next: false, has_prev: false, last: 0, next_page: 2, page: 1, prev_page: 0}
# String parameters (e.g. from Phoenix params)
iex> alias Pager.{User, Repo}
iex> query = from(u in User)
iex> result = Pager.page(query, Repo, "2", "10")
iex> Map.drop(result, [:list])
%{count: 0, first: 11, has_next: false, has_prev: true, last: 0, next_page: 3, page: 2, prev_page: 1}
# With defaults
iex> alias Pager.{User, Repo}
iex> query = from(u in User)
iex> result = Pager.page(query, Repo, nil, 50)
iex> Map.drop(result, [:list])
%{count: 0, first: 1, has_next: false, has_prev: false, last: 0, next_page: 2, page: 1, prev_page: 0}
"""
@spec page(Ecto.Query.t(), Ecto.Repo.t(), integer() | binary() | nil, integer() | binary() | nil) :: page_result()
def page(query, repo, nil, per_page), do: page(query, repo, 1, per_page)
def page(query, repo, page, per_page) when is_nil(per_page) or per_page == "", do: page(query, repo, page, 50)
def page(query, repo, page, per_page) when is_binary(page) and is_binary(per_page), do: page(query, repo, String.to_integer(page), String.to_integer(per_page))
def page(query, repo, page, per_page) when is_binary(page), do: page(query, repo, String.to_integer(page), per_page)
def page(query, repo, page, per_page) when is_binary(per_page), do: page(query, repo, page, String.to_integer(per_page))
def page(query, repo, page, per_page) do
results = query(query, repo, page, per_page: per_page)
count = repo.one(from(t in subquery(query), select: count("*")))
first = (page - 1) * per_page + 1
if (first > count && count > 0) do
page(query, repo, 1, per_page)
else
%{
has_next: length(results) > per_page,
has_prev: page > 1,
prev_page: page - 1,
page: page,
next_page: page + 1,
first: (page - 1) * per_page + 1,
last: Enum.min([page * per_page, count]),
count: count,
list: Enum.slice(results, 0, per_page)
}
end
end
defp query(query, repo, page, per_page: per_page) do
query
|> limit(^(per_page + 1))
|> offset(^(per_page * (page - 1)))
|> repo.all()
end
end