From 0ea5ce64b6d1a223a3b5d405d17d7c5e0e783e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Wed, 11 Sep 2024 21:28:08 +0200 Subject: [PATCH] fix: Handle Ash.Query.filter for array values (#1452) * Refactor Ash.Filter.parse_predicates/3 * Handle Ash.Query.filter for array values --- lib/ash/filter/filter.ex | 238 +++++++++++++++++++-------------------- test/query_test.exs | 18 ++- 2 files changed, 133 insertions(+), 123 deletions(-) diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 080c18d3..abe6b474 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -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 diff --git a/test/query_test.exs b/test/query_test.exs index 015e116a..e3529b3b 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -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