mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +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)
|
parse_predicates([eq: value], field, context)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp parse_predicates(%_{__predicate__: _} = values, field, context) do
|
||||||
|
parse_predicates([eq: values], field, context)
|
||||||
|
end
|
||||||
|
|
||||||
defp parse_predicates(values, attr, context) do
|
defp parse_predicates(values, attr, context) do
|
||||||
if is_struct(values) && Map.has_key?(values, :__predicate__) do
|
if is_map(values) || Keyword.keyword?(values) do
|
||||||
parse_predicates([eq: values], attr, context)
|
at_path =
|
||||||
else
|
if is_map(values) do
|
||||||
if is_map(values) || Keyword.keyword?(values) do
|
Map.get(values, :at_path) || Map.get(values, "at_path")
|
||||||
at_path =
|
else
|
||||||
|
Keyword.get(values, :at_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
{values, at_path} =
|
||||||
|
if is_list(at_path) do
|
||||||
if is_map(values) 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
|
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
|
end
|
||||||
|
|
||||||
{values, at_path} =
|
{key, value}, {:ok, expression} ->
|
||||||
if is_list(at_path) do
|
case get_operator(key) do
|
||||||
if is_map(values) do
|
nil ->
|
||||||
{Map.drop(values, [:at_path, "at_path"]), at_path}
|
case get_predicate_function(key, context.resource, context.public?) do
|
||||||
else
|
nil ->
|
||||||
{Keyword.delete(values, :at_path), at_path}
|
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
|
||||||
end
|
{:halt, {:error, error}}
|
||||||
else
|
|
||||||
{values, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
Enum.reduce_while(values, {:ok, true}, fn
|
function_module ->
|
||||||
{key, value}, {:ok, expression} when key in [:not, "not"] ->
|
left =
|
||||||
case parse_predicates(List.wrap(value), attr, context) do
|
if is_list(at_path) do
|
||||||
{:ok, not_expression} ->
|
%Call{
|
||||||
{:cont,
|
name: :get_path,
|
||||||
{:ok,
|
args: [
|
||||||
BooleanExpression.optimized_new(:and, expression, %Not{
|
%Ref{
|
||||||
expression: not_expression
|
attribute: attr,
|
||||||
})}}
|
relationship_path: context[:relationship_path] || [],
|
||||||
|
resource: context.resource,
|
||||||
{:error, error} ->
|
input?: true
|
||||||
{:halt, {:error, error}}
|
},
|
||||||
end
|
at_path
|
||||||
|
]
|
||||||
{key, value}, {:ok, expression} ->
|
}
|
||||||
case get_operator(key) do
|
else
|
||||||
nil ->
|
%Ref{
|
||||||
case get_predicate_function(key, context.resource, context.public?) do
|
attribute: attr,
|
||||||
nil ->
|
relationship_path: context[:relationship_path] || [],
|
||||||
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
|
resource: context.resource,
|
||||||
{:halt, {:error, error}}
|
input?: true
|
||||||
|
}
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
operator_module ->
|
with {:ok, args} <- hydrate_refs([left, value], context),
|
||||||
left =
|
refs <- list_refs(args),
|
||||||
if is_list(at_path) do
|
:ok <- validate_refs(refs, context.root_resource, {key, [left, value]}),
|
||||||
%Call{
|
{:ok, function} <- Function.new(function_module, args) do
|
||||||
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
|
|
||||||
if is_nil(context.resource) ||
|
if is_nil(context.resource) ||
|
||||||
Ash.DataLayer.data_layer_can?(
|
Ash.DataLayer.data_layer_can?(
|
||||||
context.resource,
|
context.resource,
|
||||||
{:filter_expr, operator}
|
{:filter_expr, function}
|
||||||
) do
|
) do
|
||||||
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
|
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, function)}}
|
||||||
else
|
else
|
||||||
{:halt,
|
{:halt,
|
||||||
{:error, "data layer does not support the operator #{inspect(operator)}"}}
|
{:error, "data layer does not support the function #{inspect(function)}"}}
|
||||||
end
|
end
|
||||||
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
|
else
|
||||||
{:error, error} -> {:halt, {:error, error}}
|
%Ref{
|
||||||
|
attribute: attr,
|
||||||
|
relationship_path: context[:relationship_path] || [],
|
||||||
|
resource: context.resource,
|
||||||
|
input?: true
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end)
|
with {:ok, [left, right]} <- hydrate_refs([left, value], context),
|
||||||
else
|
refs <- list_refs([left, right]),
|
||||||
error = InvalidFilterValue.exception(value: values)
|
:ok <- validate_refs(refs, context.root_resource, {attr, value}),
|
||||||
{:error, error}
|
{:ok, operator} <- Operator.new(operator_module, left, right) do
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ defmodule Ash.Test.QueryTest do
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
default_accept :*
|
default_accept :*
|
||||||
|
defaults create: :*, update: :*
|
||||||
|
|
||||||
read :read do
|
read :read do
|
||||||
primary? true
|
primary? true
|
||||||
|
@ -26,14 +27,13 @@ defmodule Ash.Test.QueryTest do
|
||||||
|
|
||||||
filter expr(id == ^arg(:id))
|
filter expr(id == ^arg(:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
create :create
|
|
||||||
update :update
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
attribute :list, {:array, :string}, public?: true
|
||||||
|
|
||||||
attribute :name, :string do
|
attribute :name, :string do
|
||||||
public?(true)
|
public?(true)
|
||||||
end
|
end
|
||||||
|
@ -85,4 +85,16 @@ defmodule Ash.Test.QueryTest do
|
||||||
|> Ash.Query.loading?(:best_friend)
|
|> Ash.Query.loading?(:best_friend)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue