Skip to content

Commit

Permalink
improvement: support nested fields in input sorts
Browse files Browse the repository at this point in the history
For example: sort: "category,-org.type"
  • Loading branch information
zachdaniel committed Sep 21, 2024
1 parent e293baa commit 6eb2ea0
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 11 deletions.
74 changes: 68 additions & 6 deletions lib/ash/sort/sort.ex
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,55 @@ defmodule Ash.Sort do
defp get_field(resource, field, handler) do
case call_handler(field, handler) do
nil ->
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
{path, field} =
if is_binary(field) do
case Enum.reverse(String.split(field, ".", trim: true)) do
[] -> {[], ""}
[field | path] -> {Enum.reverse(path), field}
end
else
{[], field}
end

case path do
[] ->
case Ash.Resource.Info.public_field(resource, field) do
%{name: name} -> name
_ -> nil
end

path ->
case related_field(resource, path, field) do
{:ok, path, field} ->
case field do
%{name: name, type: type, constraints: constraints} ->
case Ash.Query.Calculation.new(
:__expr_sort__,
Ash.Resource.Calculation.Expression,
[expr: Ash.Expr.ref(path, name)],
type,
constraints
) do
{:ok, calc} -> calc
{:error, term} -> raise Ash.Error.to_ash_error(term)
end

%{name: name} ->
case Ash.Query.Calculation.new(
:__expr_sort__,
Ash.Resource.Calculation.Expression,
[expr: Ash.Expr.ref(path, name)],
nil,
[]
) do
{:ok, calc} -> calc
{:error, term} -> raise Ash.Error.to_ash_error(term)
end
end

:error ->
nil
end
end

value ->
Expand All @@ -247,6 +290,25 @@ defmodule Ash.Sort do

defp call_handler(_, _), do: nil

defp related_field(resource, path, field, acc \\ [])

defp related_field(resource, [], field, acc) do
case Ash.Resource.Info.public_field(resource, field) do
nil -> :error
field -> {:ok, Enum.reverse(acc), field}
end
end

defp related_field(resource, [first | rest], field, acc) do
case Ash.Resource.Info.public_relationship(resource, first) do
%{sortable?: true, destination: destination} ->
related_field(destination, rest, field, [first | acc])

_ ->
:error
end
end

@doc """
Reverses an Ash sort statement.
"""
Expand Down
60 changes: 55 additions & 5 deletions test/sort/sort_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,36 @@ defmodule Ash.Test.Sort.SortTest do

alias Ash.Test.Domain, as: Domain

defmodule Post do
defmodule Author do
@moduledoc false
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

attributes do
uuid_primary_key :id
attribute :name, :string, public?: true
attribute :private_name, :string
end

actions do
default_accept :*
read :read
defaults [:read, :create, :update]
end
end

create :create
defmodule Post do
@moduledoc false
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

ets do
private?(true)
end

update :update
actions do
default_accept :*
defaults [:read, :create, :update]
end

attributes do
Expand All @@ -36,6 +51,14 @@ defmodule Ash.Test.Sort.SortTest do

attribute :points, :integer
end

relationships do
belongs_to :author, Author do
public? true
end

belongs_to :private_author, Author
end
end

describe "sort input" do
Expand All @@ -56,6 +79,33 @@ defmodule Ash.Test.Sort.SortTest do
Ash.Sort.parse_input(Post, "+title,-contents")
end

test "a string sort can parse relationships" do
{:ok, [{%Ash.Query.Calculation{}, :asc}] = sort} =
Ash.Sort.parse_input(Post, "+author.name")

Post
|> Ash.Query.sort(sort)
|> Ash.read!()
end

test "a string sort honors private relationships" do
{:error,
%Ash.Error.Query.NoSuchField{
resource: Ash.Test.Sort.SortTest.Post,
field: "private_author.name"
}} =
Ash.Sort.parse_input(Post, "+private_author.name")
end

test "a string sort honors private fields" do
{:error,
%Ash.Error.Query.NoSuchField{
resource: Ash.Test.Sort.SortTest.Post,
field: "author.private_name"
}} =
Ash.Sort.parse_input(Post, "+author.private_name")
end

test "private attributes cannot be used" do
assert {:error, %Ash.Error.Query.NoSuchField{}} = Ash.Sort.parse_input(Post, "points")
end
Expand Down

0 comments on commit 6eb2ea0

Please sign in to comment.