From dcbbd21f0c053b6e7041590a9407e1615bc18497 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Wed, 30 Dec 2020 00:49:32 -0500 Subject: [PATCH] improvement: add `parse_input/3` to `Ash.Filter` --- lib/ash/filter/filter.ex | 93 ++++++++++++++++++++++++++++--------- test/filter/filter_test.exs | 15 ++++++ 2 files changed, 86 insertions(+), 22 deletions(-) diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index fec3c5f0..261db4c4 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -157,8 +157,16 @@ defmodule Ash.Filter do end end - def parse!(resource, statement, aggregates \\ %{}) do - case parse(resource, statement, aggregates) do + def parse_input(resource, statement, aggregates \\ %{}) do + parse(resource, statement, aggregates, true) + end + + def parse_input!(resource, statement, aggregates \\ %{}) do + parse!(resource, statement, aggregates, true) + end + + def parse!(resource, statement, aggregates \\ %{}, public? \\ false) do + case parse(resource, statement, aggregates, public?) do {:ok, filter} -> filter @@ -167,11 +175,12 @@ defmodule Ash.Filter do end end - def parse(resource, statement, aggregates \\ %{}) do + def parse(resource, statement, aggregates \\ %{}, public? \\ false) do context = %{ resource: resource, relationship_path: [], - aggregates: aggregates + aggregates: aggregates, + public?: public? } case parse_expression(statement, context) do @@ -964,6 +973,33 @@ defmodule Ash.Filter do defp do_relationship_paths(_, _), do: [] + defp attribute(%{public?: true, resource: resource}, attribute), + do: Ash.Resource.public_attribute(resource, attribute) + + defp attribute(%{public?: false, resource: resource}, attribute), + do: Ash.Resource.attribute(resource, attribute) + + defp relationship(%{public?: true, resource: resource}, relationship) do + Ash.Resource.public_relationship(resource, relationship) + end + + defp relationship(%{public?: false, resource: resource}, relationship) do + Ash.Resource.relationship(resource, relationship) + end + + defp related(context, relationship) when not is_list(relationship) do + related(context, [relationship]) + end + + defp related(context, []), do: context.resource + + defp related(context, [rel | rest]) do + case relationship(context, rel) do + %{destination: destination} -> related(%{context | resource: destination}, rest) + nil -> nil + end + end + defp parse_expression(%__MODULE__{expression: expression}, context), do: {:ok, add_to_predicate_path(expression, context)} @@ -1035,13 +1071,24 @@ defmodule Ash.Filter do end defp add_expression_part({%Ref{} = ref, nested_statement}, context, expression) do - new_context = %{ - relationship_path: ref.relationship_path, - resource: Ash.Resource.related(context.resource, ref.relationship_path), - aggregates: context.aggregates - } + case related(context, ref.relationship_path) do + nil -> + {:error, + NoSuchAttributeOrRelationship.exception( + attribute_or_relationship: List.first(ref.relationship_path), + resource: context.resource + )} - add_expression_part({ref.attribute.name, nested_statement}, new_context, expression) + related -> + new_context = %{ + relationship_path: ref.relationship_path, + resource: related, + aggregates: context.aggregates, + public?: context.public? + } + + add_expression_part({ref.attribute.name, nested_statement}, new_context, expression) + end end defp add_expression_part({field, nested_statement}, context, expression) @@ -1054,7 +1101,7 @@ defmodule Ash.Filter do cond do function_module = get_function(field, Ash.Resource.data_layer_functions(context.resource)) -> with {:ok, args} <- - hydrate_refs(List.wrap(nested_statement), context.resource, context.aggregates), + hydrate_refs(List.wrap(nested_statement), context), {:ok, function} <- Function.new( function_module, @@ -1067,7 +1114,7 @@ defmodule Ash.Filter do {:ok, Expression.optimized_new(:and, expression, function)} end - rel = Ash.Resource.relationship(context.resource, field) -> + rel = relationship(context, field) -> context = context |> Map.update!(:relationship_path, fn path -> path ++ [rel.name] end) @@ -1083,7 +1130,7 @@ defmodule Ash.Filter do end else with [field] <- Ash.Resource.primary_key(context.resource), - attribute <- Ash.Resource.attribute(context.resource, field), + attribute <- attribute(context, field), {:ok, casted} <- Ash.Type.cast_input(attribute.type, nested_statement) do add_expression_part({field, casted}, context, expression) @@ -1100,7 +1147,7 @@ defmodule Ash.Filter do end end - attr = Ash.Resource.attribute(context.resource, field) -> + attr = attribute(context, field) -> case parse_predicates(nested_statement, attr, context) do {:ok, nested_statement} -> {:ok, Expression.optimized_new(:and, expression, nested_statement)} @@ -1122,7 +1169,7 @@ defmodule Ash.Filter do (op_module = get_operator(field, Ash.Resource.data_layer_operators(context.resource))) && match?([_, _ | _], nested_statement) -> with {:ok, [left, right]} <- - hydrate_refs(nested_statement, context.resource, context.aggregates), + hydrate_refs(nested_statement, context), {:ok, operator} <- Operator.new(op_module, left, right) do {:ok, Expression.optimized_new(:and, expression, operator)} end @@ -1172,16 +1219,16 @@ defmodule Ash.Filter do {:error, InvalidFilterValue.exception(value: value)} end - defp hydrate_refs(list, resource, aggregates) do + defp hydrate_refs(list, context) do list |> Enum.reduce_while({:ok, []}, fn %Ref{attribute: attribute} = ref, {:ok, acc} when is_atom(attribute) -> - case Ash.Resource.related(resource, ref.relationship_path) do + case related(context, ref.relationship_path) do nil -> {:halt, {:error, "Invalid reference #{inspect(ref)}"}} related -> - do_hydrate_ref(ref, attribute, related, aggregates, acc) + do_hydrate_ref(ref, attribute, related, context, acc) end other, {:ok, acc} -> @@ -1193,15 +1240,17 @@ defmodule Ash.Filter do end end - defp do_hydrate_ref(ref, field, related, aggregates, acc) do + defp do_hydrate_ref(ref, field, related, %{aggregates: aggregates} = context, acc) do + context = %{context | resource: related} + cond do Map.has_key?(aggregates, field) -> {:cont, {:ok, [%{ref | attribute: Map.get(aggregates, field)} | acc]}} - attribute = Ash.Resource.attribute(related, field) -> + attribute = attribute(context, field) -> {:cont, {:ok, [%{ref | attribute: attribute} | acc]}} - relationship = Ash.Resource.relationship(related, field) -> + relationship = relationship(context, field) -> case Ash.Resource.primary_key(relationship.destination) do [key] -> new_ref = %{ @@ -1351,7 +1400,7 @@ defmodule Ash.Filter do } with {:ok, [left, right]} <- - hydrate_refs([left, value], context.resource, context.aggregates), + hydrate_refs([left, value], context), {:ok, operator} <- Operator.new(operator_module, left, right) do if is_boolean(operator) do {:cont, {:ok, operator}} diff --git a/test/filter/filter_test.exs b/test/filter/filter_test.exs index 8da579d0..f95d5caf 100644 --- a/test/filter/filter_test.exs +++ b/test/filter/filter_test.exs @@ -23,6 +23,7 @@ defmodule Ash.Test.Filter.FilterTest do attributes do attribute :id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0 attribute :bio, :string + attribute :private, :string, private?: true end relationships do @@ -424,6 +425,20 @@ defmodule Ash.Test.Filter.FilterTest do end end + describe "parse_input" do + test "parse_input works when no private attributes are used" do + Ash.Filter.parse_input!(Profile, bio: "foo") + end + + test "parse_input fails when a private attribute is used" do + Ash.Filter.parse!(Profile, private: "private") + + assert_raise(Ash.Error.Query.NoSuchAttributeOrRelationship, fn -> + Ash.Filter.parse_input!(Profile, private: "private") + end) + end + end + describe "base_filter" do test "resources that apply to the base filter are returned" do %{id: id} =