defmodule Chunkr do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
alias Chunkr.{Cursor, Opts, Page}
@default_max_limit 100
@doc false
defmacro __using__(opts) do
quote do
@default_chunkr_opts unquote(opts) ++
[{:repo, __MODULE__}, {:max_limit, unquote(@default_max_limit)}]
def paginate!(queryable, strategy, sort_dir, opts) do
unquote(__MODULE__).paginate!(queryable, strategy, sort_dir, opts ++ @default_chunkr_opts)
end
def paginate(queryable, strategy, sort_dir, opts) do
unquote(__MODULE__).paginate(queryable, strategy, sort_dir, opts ++ @default_chunkr_opts)
end
end
end
@spec paginate!(any, atom(), Chunkr.Opts.sort_dir(), keyword) :: Chunkr.Page.t()
@doc """
Same as `paginate/4`, but raises an error for invalid input.
"""
def paginate!(queryable, strategy, sort_dir, opts) do
case paginate(queryable, strategy, sort_dir, opts) do
{:ok, page} -> page
{:error, message} -> raise ArgumentError, message
end
end
@spec paginate(any, any, Chunkr.Opts.sort_dir(), keyword) ::
{:error, String.t()} | {:ok, Chunkr.Page.t()}
@doc """
Paginates a query.
Extends the provided query with the necessary filtering, ordering, and cursor field
selection for the sake of pagination, then executes the query and returns a `Chunkr.Page`
or results.
## Options
* `:max_limit` — The maximum number or results the user can request for this query.
The default is #{@default_max_limit}.
* `:first` — Retrieve the first _n_ results; must be between `0` and `:max_limit`.
* `:last` — Retrieve the last _n_ results; must be between `0` and `:max_limit`.
* `:after` — Return results starting after the provided cursor; optionally pairs with `:first`.
* `:before` — Return results ending at the provided cursor; optionally pairs with `:last`.
"""
def paginate(queryable, strategy, sort_dir, options) do
case Opts.new(queryable, strategy, sort_dir, options) do
{:ok, opts} ->
extended_rows =
queryable
|> apply_where(opts)
|> apply_order(opts)
|> apply_select(opts)
|> apply_limit(opts.limit + 1, opts)
|> opts.repo.all()
requested_rows = Enum.take(extended_rows, opts.limit)
rows_to_return =
case opts.paging_dir do
:forward -> requested_rows
:backward -> Enum.reverse(requested_rows)
end
{:ok,
%Page{
raw_results: rows_to_return,
has_previous_page: has_previous_page?(opts, extended_rows, requested_rows),
has_next_page: has_next_page?(opts, extended_rows, requested_rows),
start_cursor: List.first(rows_to_return) |> row_to_cursor(),
end_cursor: List.last(rows_to_return) |> row_to_cursor(),
opts: opts
}}
{:invalid_opts, message} ->
{:error, message}
end
end
defp has_previous_page?(%{paging_dir: :forward} = opts, _, _), do: !!opts.cursor
defp has_previous_page?(%{paging_dir: :backward}, rows, requested_rows),
do: rows != requested_rows
defp has_next_page?(%{paging_dir: :forward}, rows, requested_rows), do: rows != requested_rows
defp has_next_page?(%{paging_dir: :backward} = opts, _, _), do: !!opts.cursor
defp row_to_cursor(nil), do: nil
defp row_to_cursor({cursor_values, _record}), do: Cursor.encode(cursor_values)
defp apply_where(query, %{cursor: nil}), do: query
defp apply_where(query, opts) do
cursor_values = Cursor.decode!(opts.cursor)
opts.planner.beyond_cursor(
query,
opts.strategy,
opts.sort_dir,
opts.paging_dir,
cursor_values
)
end
defp apply_order(query, opts) do
opts.planner.apply_order(query, opts.strategy, opts.sort_dir, opts.paging_dir)
end
defp apply_select(query, opts) do
opts.planner.apply_select(query, opts.strategy)
end
defp apply_limit(query, limit, opts) do
opts.planner.apply_limit(query, limit)
end
end