73 lines
1.9 KiB
Elixir
73 lines
1.9 KiB
Elixir
defmodule WhoNeedHelp.Pagination do
|
|
@moduledoc """
|
|
Small keyset-pagination primitives shared by database-backed lists.
|
|
|
|
Cursors contain only the final row's UTC timestamp and UUID. They are
|
|
validated before being used as query parameters and carry no authorization
|
|
decision.
|
|
"""
|
|
|
|
@default_limit 24
|
|
@maximum_limit 100
|
|
|
|
defmodule Page do
|
|
@moduledoc false
|
|
defstruct entries: [], next_cursor: nil
|
|
end
|
|
|
|
def limit(options, default \\ @default_limit) do
|
|
options
|
|
|> Keyword.get(:limit, default)
|
|
|> normalize_limit(default)
|
|
end
|
|
|
|
def cursor(options) do
|
|
case decode(Keyword.get(options, :after)) do
|
|
{:ok, cursor} -> cursor
|
|
:error -> nil
|
|
end
|
|
end
|
|
|
|
def page(rows, limit, cursor_fields) when is_function(cursor_fields, 1) do
|
|
entries = Enum.take(rows, limit)
|
|
|
|
next_cursor =
|
|
if length(rows) > limit do
|
|
entries
|
|
|> List.last()
|
|
|> cursor_fields.()
|
|
|> then(fn {timestamp, id} -> encode(timestamp, id) end)
|
|
end
|
|
|
|
%Page{entries: entries, next_cursor: next_cursor}
|
|
end
|
|
|
|
def encode(%DateTime{} = timestamp, id) do
|
|
%{"at" => DateTime.to_iso8601(timestamp), "id" => to_string(id)}
|
|
|> Jason.encode!()
|
|
|> Base.url_encode64(padding: false)
|
|
end
|
|
|
|
def decode(nil), do: {:ok, nil}
|
|
def decode(""), do: {:ok, nil}
|
|
|
|
def decode(encoded) when is_binary(encoded) do
|
|
with {:ok, json} <- Base.url_decode64(encoded, padding: false),
|
|
{:ok, %{"at" => timestamp, "id" => id}} <- Jason.decode(json),
|
|
{:ok, timestamp, 0} <- DateTime.from_iso8601(timestamp),
|
|
{:ok, id} <- Ecto.UUID.cast(id) do
|
|
{:ok, {timestamp, id}}
|
|
else
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
def decode(_encoded), do: :error
|
|
|
|
defp normalize_limit(limit, _default)
|
|
when is_integer(limit) and limit > 0 and limit <= @maximum_limit,
|
|
do: limit
|
|
|
|
defp normalize_limit(_limit, default), do: min(default, @maximum_limit)
|
|
end
|