fix: Handle Ash.Query.filter for array values (#1452)

* Refactor Ash.Filter.parse_predicates/3

* Handle Ash.Query.filter for array values
This commit is contained in:
Jonatan Männchen 2024-09-11 21:28:08 +02:00 committed by GitHub
parent 672f16c527
commit 0ea5ce64b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 133 additions and 123 deletions

View file

@ -3898,144 +3898,142 @@ defmodule Ash.Filter do
parse_predicates([eq: value], field, context)
end
defp parse_predicates(%_{__predicate__: _} = values, field, context) do
parse_predicates([eq: values], field, context)
end
defp parse_predicates(values, attr, context) do
if is_struct(values) && Map.has_key?(values, :__predicate__) do
parse_predicates([eq: values], attr, context)
else
if is_map(values) || Keyword.keyword?(values) do
at_path =
if is_map(values) || Keyword.keyword?(values) do
at_path =
if is_map(values) do
Map.get(values, :at_path) || Map.get(values, "at_path")
else
Keyword.get(values, :at_path)
end
{values, at_path} =
if is_list(at_path) do
if is_map(values) do
Map.get(values, :at_path) || Map.get(values, "at_path")
{Map.drop(values, [:at_path, "at_path"]), at_path}
else
Keyword.get(values, :at_path)
{Keyword.delete(values, :at_path), at_path}
end
else
{values, nil}
end
Enum.reduce_while(values, {:ok, true}, fn
{key, value}, {:ok, expression} when key in [:not, "not"] ->
case parse_predicates(List.wrap(value), attr, context) do
{:ok, not_expression} ->
{:cont,
{:ok,
BooleanExpression.optimized_new(:and, expression, %Not{
expression: not_expression
})}}
{:error, error} ->
{:halt, {:error, error}}
end
{values, at_path} =
if is_list(at_path) do
if is_map(values) do
{Map.drop(values, [:at_path, "at_path"]), at_path}
else
{Keyword.delete(values, :at_path), at_path}
end
else
{values, nil}
end
{key, value}, {:ok, expression} ->
case get_operator(key) do
nil ->
case get_predicate_function(key, context.resource, context.public?) do
nil ->
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
{:halt, {:error, error}}
Enum.reduce_while(values, {:ok, true}, fn
{key, value}, {:ok, expression} when key in [:not, "not"] ->
case parse_predicates(List.wrap(value), attr, context) do
{:ok, not_expression} ->
{:cont,
{:ok,
BooleanExpression.optimized_new(:and, expression, %Not{
expression: not_expression
})}}
{:error, error} ->
{:halt, {:error, error}}
end
{key, value}, {:ok, expression} ->
case get_operator(key) do
nil ->
case get_predicate_function(key, context.resource, context.public?) do
nil ->
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
{:halt, {:error, error}}
function_module ->
left =
if is_list(at_path) do
%Call{
name: :get_path,
args: [
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
},
at_path
]
}
else
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
}
end
with {:ok, args} <- hydrate_refs([left, value], context),
refs <- list_refs(args),
:ok <- validate_refs(refs, context.root_resource, {key, [left, value]}),
{:ok, function} <- Function.new(function_module, args) do
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(
context.resource,
{:filter_expr, function}
) do
{:cont,
{:ok, BooleanExpression.optimized_new(:and, expression, function)}}
else
{:halt,
{:error, "data layer does not support the function #{inspect(function)}"}}
end
function_module ->
left =
if is_list(at_path) do
%Call{
name: :get_path,
args: [
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
},
at_path
]
}
else
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
}
end
end
operator_module ->
left =
if is_list(at_path) do
%Call{
name: :get_path,
args: [
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
},
at_path
]
}
else
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
}
end
with {:ok, [left, right]} <- hydrate_refs([left, value], context),
refs <- list_refs([left, right]),
:ok <- validate_refs(refs, context.root_resource, {attr, value}),
{:ok, operator} <- Operator.new(operator_module, left, right) do
if is_boolean(operator) do
{:cont, {:ok, operator}}
else
with {:ok, args} <- hydrate_refs([left, value], context),
refs <- list_refs(args),
:ok <- validate_refs(refs, context.root_resource, {key, [left, value]}),
{:ok, function} <- Function.new(function_module, args) do
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(
context.resource,
{:filter_expr, operator}
{:filter_expr, function}
) do
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, function)}}
else
{:halt,
{:error, "data layer does not support the operator #{inspect(operator)}"}}
{:error, "data layer does not support the function #{inspect(function)}"}}
end
end
end
operator_module ->
left =
if is_list(at_path) do
%Call{
name: :get_path,
args: [
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
},
at_path
]
}
else
{:error, error} -> {:halt, {:error, error}}
%Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource,
input?: true
}
end
end
end)
else
error = InvalidFilterValue.exception(value: values)
{:error, error}
end
with {:ok, [left, right]} <- hydrate_refs([left, value], context),
refs <- list_refs([left, right]),
:ok <- validate_refs(refs, context.root_resource, {attr, value}),
{:ok, operator} <- Operator.new(operator_module, left, right) do
if is_boolean(operator) do
{:cont, {:ok, operator}}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(
context.resource,
{:filter_expr, operator}
) do
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
else
{:halt,
{:error, "data layer does not support the operator #{inspect(operator)}"}}
end
end
else
{:error, error} -> {:halt, {:error, error}}
end
end
end)
else
parse_predicates([eq: values], attr, context)
end
end

View file

@ -16,6 +16,7 @@ defmodule Ash.Test.QueryTest do
actions do
default_accept :*
defaults create: :*, update: :*
read :read do
primary? true
@ -26,14 +27,13 @@ defmodule Ash.Test.QueryTest do
filter expr(id == ^arg(:id))
end
create :create
update :update
end
attributes do
uuid_primary_key :id
attribute :list, {:array, :string}, public?: true
attribute :name, :string do
public?(true)
end
@ -85,4 +85,16 @@ defmodule Ash.Test.QueryTest do
|> Ash.Query.loading?(:best_friend)
end
end
describe "filter" do
test "can filter by list" do
list = ["a", "b", "c"]
Ash.create!(User, %{list: list})
assert User
|> Ash.Query.filter(list: list)
|> Ash.read_one!()
end
end
end