improvement: add to_filter_map/1 to filter_form

This commit is contained in:
Zach Daniel 2022-12-21 03:07:07 -05:00
parent cb9fc8758b
commit ecdce6e894
2 changed files with 117 additions and 0 deletions

View file

@ -118,6 +118,93 @@ defmodule AshPhoenix.FilterForm do
|> set_validity()
end
@doc """
Returns a filter map that can be provided to `Ash.Filter.parse`
This allows for things like saving a stored filter. Does not currently support parameterizing calculations or functions.
"""
def to_filter_map(form) do
if form.valid? do
case do_to_filter_map(form, form.resource) do
{:ok, expr} ->
{:ok, expr}
{:error, %__MODULE__{} = form} ->
{:error, form}
{:error, error} ->
{:error, %{form | errors: List.wrap(error)}}
end
else
{:error, form}
end
end
defp do_to_filter_map(%__MODULE__{components: []}, _), do: {:ok, true}
defp do_to_filter_map(
%__MODULE__{components: components, operator: operator, negated?: negated?} = form,
resource
) do
{filters, components, errors?} =
Enum.reduce(components, {[], [], false}, fn component, {filters, components, errors?} ->
case do_to_filter_map(component, resource) do
{:ok, component_filter} ->
{filters ++ [component_filter], components ++ [component], errors?}
{:error, component} ->
{filters, components ++ [component], true}
end
end)
if errors? do
{:error, %{form | components: components}}
else
expr = %{to_string(operator) => filters}
if negated? do
{:ok, %{"not" => expr}}
else
{:ok, expr}
end
end
end
defp do_to_filter_map(
%Predicate{
field: field,
value: value,
operator: operator,
negated?: negated?,
path: path
},
_resource
) do
expr =
put_at_path(%{}, Enum.map(path, &to_string/1), %{
to_string(field) => %{to_string(operator) => value}
})
if negated? do
{:ok, %{"not" => expr}}
else
{:ok, expr}
end
end
defp put_at_path(_, [], value), do: value
defp put_at_path(map, [key], value) do
Map.put(map || %{}, key, value)
end
defp put_at_path(map, [key | rest], value) do
map
|> Kernel.||(%{})
|> Map.put_new(key, %{})
|> Map.update!(key, &put_at_path(&1, rest, value))
end
@doc """
Returns a filter expression that can be provided to Ash.Query.filter/2

View file

@ -107,6 +107,8 @@ defmodule AshPhoenix.FilterFormTest do
test "An empty form returns the filter `true`" do
form = FilterForm.new(Post)
assert FilterForm.to_filter_map(form) == {:ok, true}
assert Ash.Query.equivalent_to?(
FilterForm.filter!(Post, form),
true
@ -122,6 +124,9 @@ defmodule AshPhoenix.FilterFormTest do
}
)
assert FilterForm.to_filter_map(form) ==
{:ok, %{"and" => [%{"title" => %{"eq" => "new post"}}]}}
assert Ash.Query.equivalent_to?(
FilterForm.filter!(Post, form),
title == "new post"
@ -185,12 +190,37 @@ defmodule AshPhoenix.FilterFormTest do
}
)
assert {:ok, %{"and" => [%{"comments" => %{"text" => %{"contains" => "new"}}}]} = filter} =
FilterForm.to_filter_map(form)
Ash.Filter.parse!(Post, filter) |> IO.inspect()
assert Ash.Query.equivalent_to?(
FilterForm.filter!(Post, form),
contains(comments.text, "new")
)
end
test "predicates can reference paths for to_filter_map" do
form =
FilterForm.new(Post,
params: %{
field: :text,
operator: :eq,
path: "comments",
value: "new"
}
)
assert {:ok, %{"and" => [%{"comments" => %{"text" => %{"eq" => "new"}}}]} = filter} =
FilterForm.to_filter_map(form)
assert Ash.Query.equivalent_to?(
Ash.Query.filter(Post, ^Ash.Filter.parse!(Post, filter)),
comments.text == "new"
)
end
test "predicates with fields that refer to a relationship will be appended to the path" do
form =
FilterForm.new(Post,