mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
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:
parent
672f16c527
commit
0ea5ce64b6
2 changed files with 133 additions and 123 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue