defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do @moduledoc """ `Rummage.Ecto.CustomHooks.KeysetPaginate` is a custom paginate hook that comes shipped with `Rummage.Ecto`. This module can be used by overriding the default paginate module. This can be done in the following ways: In the `Rummage.Ecto` call: ```elixir Rummage.Ecto.rummage(queryable, rummage, paginate: Rummage.Ecto.CustomHooks.KeysetPaginate) ``` OR Globally for all models in `config.exs`: ```elixir config :rummage_ecto, Rummage.Ecto, default_paginate: Rummage.Ecto.CustomHooks.KeysetPaginate ``` """ import Ecto.Query @behaviour Rummage.Ecto.Hook @doc """ Builds a paginate queryable on top of the given `queryable` from the rummage parameters from the given `rummage` struct. ## Examples When rummage struct passed doesn't have the key "paginate", it simply returns the queryable itself: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> KeysetPaginate.run(Parent, %{}) Parent When the queryable passed is not just a struct: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query iex> KeysetPaginate.run(queryable, %{}) #Ecto.Query When rummage `struct` passed has the key `"paginate"`, but with a value of `%{}`, `""` or `[]` it simply returns the `queryable` itself: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> KeysetPaginate.run(Parent, %{"paginate" => %{}}) Parent iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> KeysetPaginate.run(Parent, %{"paginate" => ""}) Parent iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> KeysetPaginate.run(Parent, %{"paginate" => []}) Parent When rummage struct passed has the key "paginate", with "per_page" and "page" keys it returns a paginated version of the queryable passed in as the argument: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} %{"paginate" => %{"page" => "1", "per_page" => "1"}} iex> queryable = from u in "parents" #Ecto.Query iex> KeysetPaginate.run(queryable, rummage) #Ecto.Query iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> rummage = %{"paginate" => %{"per_page" => "5", "page" => "2"}} %{"paginate" => %{"page" => "2", "per_page" => "5"}} iex> queryable = from u in "parents" #Ecto.Query iex> KeysetPaginate.run(queryable, rummage) #Ecto.Query When no `"page"` key is passed, it defaults to `1`: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query iex> rummage = %{"paginate" => %{"per_page" => "10"}} %{"paginate" => %{"per_page" => "10"}} iex> queryable = from u in "parents" #Ecto.Query iex> KeysetPaginate.run(queryable, rummage) #Ecto.Query """ @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} def run(queryable, rummage) do paginate_params = Map.get(rummage, "paginate") case paginate_params do a when a in [nil, [], {}, [""], "", %{}] -> queryable _ -> handle_paginate(queryable, paginate_params) end end @doc """ Implementation of `before_hook` for `Rummage.Ecto.CustomHooks.KeysetPaginate`. This function takes a `queryable`, `rummage` struct and an `opts` map. Using those it calculates the `total_count` and `max_page` for the paginate hook. ## Examples iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> alias Rummage.Ecto.Category iex> KeysetPaginate.before_hook(Category, %{}, %{}) %{} iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> alias Rummage.Ecto.Category iex> Ecto.Adapters.SQL.Sandbox.checkout(Rummage.Ecto.Repo) iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 1"}) iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 2"}) iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 3"}) iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} iex> KeysetPaginate.before_hook(Category, rummage, %{}) %{"paginate" => %{"max_page" => "3", "page" => "1", "per_page" => "1", "total_count" => "3"}} """ @spec before_hook(Ecto.Query.t, map, map) :: map def before_hook(queryable, rummage, opts) do paginate_params = Map.get(rummage, "paginate") case paginate_params do nil -> rummage _ -> total_count = get_total_count(queryable, opts) {page, per_page} = parse_page_and_per_page(paginate_params, opts) per_page = if per_page < 1, do: 1, else: per_page max_page_fl = total_count / per_page max_page = max_page_fl |> Float.ceil |> round page = cond do page < 1 -> 1 max_page > 0 && page > max_page -> max_page true -> page end paginate_params = paginate_params |> Map.put("page", Integer.to_string(page)) |> Map.put("per_page", Integer.to_string(per_page)) |> Map.put("total_count", Integer.to_string(total_count)) |> Map.put("max_page", Integer.to_string(max_page)) Map.put(rummage, "paginate", paginate_params) end end defp get_total_count(queryable, opts), do: length(apply(get_repo(opts), :all, [queryable])) defp get_repo(opts) do opts[:repo] || Rummage.Ecto.Config.default_repo end defp parse_page_and_per_page(paginate_params, opts) do per_page = paginate_params |> Map.get("per_page", Integer.to_string(opts[:per_page] || Rummage.Ecto.Config.default_per_page)) |> String.to_integer page = paginate_params |> Map.get("page", "1") |> String.to_integer {page, per_page} end defp handle_paginate(queryable, paginate_params) do per_page = paginate_params |> Map.get("per_page") |> String.to_integer page = paginate_params |> Map.get("page", "1") |> String.to_integer offset = per_page * (page - 1) queryable |> limit(^per_page) |> offset(^offset) end end