defmodule Ash.Sort do
@moduledoc """
Utilities and types for sorting.
## Important
Keyset pagination cannot currently be used in conjunction with aggregate and calculation sorting.
Combining them will result in an error on the query.
"""
@type sort_order ::
:asc | :desc | :asc_nils_first | :asc_nils_last | :desc_nils_first | :desc_nils_last
@type t :: list(atom | {atom, sort_order} | {atom, {sort_order, Keyword.t() | map}}) | atom
alias Ash.Error.Query.{InvalidSortOrder, NoSuchAttribute}
@doc """
A utility for parsing sorts provided from external input. Only allows sorting
on public attributes and aggregates.
The supported formats are:
### Sort Strings
A comma separated list of fields to sort on, each with an optional prefix.
The prefixes are:
* "+" - Same as no prefix. Sorts `:asc`.
* "++" - Sorts `:asc_nils_first`
* "-" - Sorts `:desc`
* "--" - Sorts `:desc_nils_last`
For example
"foo,-bar,++baz,--buz"
### A list of sort strings
Same prefix rules as above, but provided as a list.
For example:
["foo", "-bar", "++baz", "--buz"]
### A standard Ash sort
"""
@spec parse_input(
Ash.Resource.t(),
String.t()
| list(atom | String.t() | {atom, sort_order()} | list(String.t()))
| nil
) ::
{:ok, Ash.Sort.t()} | {:error, term}
def parse_input(resource, sort) when is_binary(sort) do
sort = String.split(sort, ",")
parse_input(resource, sort)
end
def parse_input(resource, sort) when is_list(sort) do
sort
|> Enum.reduce_while({:ok, []}, fn field, {:ok, sort} ->
case parse_sort(resource, field) do
{:ok, value} -> {:cont, {:ok, [value | sort]}}
{:error, error} -> {:halt, {:error, error}}
end
end)
|> case do
{:ok, values} -> {:ok, Enum.reverse(values)}
{:error, error} -> {:error, error}
end
end
def parse_input(_resource, nil), do: nil
def parse_sort(resource, {field, direction})
when direction in [
:asc,
:desc,
:asc_nils_first,
:asc_nils_last,
:desc_nils_first,
:desc_nils_last
] do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, direction}}
end
end
def parse_sort(_resource, {_field, order}) do
{:error, InvalidSortOrder.exception(order: order)}
end
def parse_sort(resource, "++" <> field) do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, :asc_nils_first}}
end
end
def parse_sort(resource, "--" <> field) do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, :desc_nils_last}}
end
end
def parse_sort(resource, "+" <> field) do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, :asc}}
end
end
def parse_sort(resource, "-" <> field) do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, :desc}}
end
end
def parse_sort(resource, field) do
case get_field(resource, field) do
nil -> {:error, NoSuchAttribute.exception(resource: resource, name: field)}
field -> {:ok, {field, :asc}}
end
end
defp get_field(resource, field) do
with nil <- Ash.Resource.Info.public_attribute(resource, field),
nil <- Ash.Resource.Info.public_aggregate(resource, field),
nil <- Ash.Resource.Info.public_calculation(resource, field) do
nil
else
%{name: name} -> name
end
end
def reverse(sort) when is_list(sort) do
Enum.map(sort, &reverse/1)
end
def reverse(sort) when is_atom(sort) do
reverse({sort, :asc})
end
def reverse({key, {order, args}}) do
{key, {reverse_order(order), args}}
end
def reverse({key, order}) do
{key, reverse_order(order)}
end
defp reverse_order(:asc), do: :desc
defp reverse_order(:desc), do: :asc
defp reverse_order(:asc_nils_last), do: :desc_nils_first
defp reverse_order(:asc_nils_first), do: :desc_nils_last
defp reverse_order(:desc_nils_first), do: :asc_nils_last
defp reverse_order(:desc_nils_last), do: :asc_nils_first
@doc """
A utility for sorting a list of records at runtime.
For example:
Ash.Sort.runtime_sort([record1, record2, record3], name: :asc, type: :desc_nils_last)
Keep in mind that it is unrealistic to expect this runtime sort to always
be exactly the same as a sort that may have been applied by your data layer.
This is especially true for strings. For example, `Postgres` strings have a
collation that affects their sorting, making it unpredictable from the perspective
of a tool using the database: https://www.postgresql.org/docs/current/collation.html
"""
defdelegate runtime_sort(results, sort), to: Ash.Actions.Sort
end