From 377319e8812f6d2ddb09979a3e1d2fc996f8a4a4 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sun, 31 May 2020 01:52:00 -0400 Subject: [PATCH] feat: add data layer custom filters --- README.md | 22 ++++++++++++++++++++++ lib/ash.ex | 5 +++++ lib/ash/data_layer/data_layer.ex | 12 ++++++++++++ lib/ash/filter/filter.ex | 15 ++++++++++++--- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8620552a..d58dc9ec 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,28 @@ Filters - lift `or` filters over the same field equaling a value into a single `in` filter, for performance (potentially) - make runtime filtering of a list of records + an ash filter a public interface. At first, we can just run a query with that filter, and filter for matches. Eventually it can be optimized. +This is from a conversation about how we might support on-demand calculated attributes +that would allow doing something like getting the trigram similarity to a piece of text +and filtering on it in the same request + +```elixir + :representatives + |> Api.query() + |> Ash.Query.filter(first_name: [trigram: [text: "Geoff", similarity: [greater_than: 0.1]]]) + |> Api.read!() + + calculated_attributes do + on_demand :trigram, [:first_name, :last_name] + # calculated(:name_similarity, :float, calculate: [trigram_similarity: {:input, :text}]) + end + + :representatives + |> Api.query() + |> Ash.Query.calculate(first_name_similarity: [first_name: [trigram: [text: "Geoff"]]]) + |> Ash.Query.filter(first_name_similarity: [greater_than: 0.1]) + |> Api.read!() +``` + Actions - all actions need to be performed in a transaction diff --git a/lib/ash.ex b/lib/ash.ex index 9a5d2fd2..1cd370d8 100644 --- a/lib/ash.ex +++ b/lib/ash.ex @@ -87,6 +87,11 @@ defmodule Ash do data_layer && data_layer.can?(resource, feature) end + @spec data_layer_filters(resource) :: map + def data_layer_filters(resource) do + Ash.DataLayer.custom_filters(resource) + end + @spec resources(api) :: list(resource()) def resources(api) do api.resources() diff --git a/lib/ash/data_layer/data_layer.ex b/lib/ash/data_layer/data_layer.ex index f77f76f7..e57cd283 100644 --- a/lib/ash/data_layer/data_layer.ex +++ b/lib/ash/data_layer/data_layer.ex @@ -7,6 +7,7 @@ defmodule Ash.DataLayer do | {:filter_related, Ash.relationship_cardinality()} | :upsert + @callback custom_filters(Ash.resource()) :: map() @callback filter(Ash.data_layer_query(), Ash.filter(), resource :: Ash.resource()) :: {:ok, Ash.data_layer_query()} | {:error, Ash.error()} @callback sort(Ash.data_layer_query(), Ash.sort(), resource :: Ash.resource()) :: @@ -34,6 +35,7 @@ defmodule Ash.DataLayer do @optional_callbacks transaction: 2 @optional_callbacks upsert: 2 + @optional_callbacks custom_filters: 1 @spec resource_to_query(Ash.resource()) :: Ash.data_layer_query() def resource_to_query(resource) do @@ -112,4 +114,14 @@ defmodule Ash.DataLayer do {:ok, func.()} end end + + def custom_filters(resource) do + data_layer = Ash.data_layer(resource) + + if :erlang.function_exported(data_layer, :custom_filters, 1) do + data_layer.custom_filters(resource) + else + %{} + end + end end diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 843fde8d..e5e7e6d2 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -868,11 +868,20 @@ defmodule Ash.Filter do defp parse_predicate(resource, predicate_name, attr_name, attr_type, value) do data_layer = Ash.data_layer(resource) + data_layer_predicates = + Map.get(Ash.data_layer_filters(resource), Ash.Type.storage_type(attr_type), []) + + all_predicates = + Enum.reduce(data_layer_predicates, @predicates, fn {name, module}, all_predicates -> + Map.put(all_predicates, name, module) + end) + with {:predicate_type, {:ok, predicate_type}} <- - {:predicate_type, Map.fetch(@predicates, predicate_name)}, + {:predicate_type, Map.fetch(all_predicates, predicate_name)}, {:type_can?, _, true} <- {:type_can?, predicate_name, - Ash.Type.supports_filter?(resource, attr_type, predicate_name, data_layer)}, + Keyword.has_key?(data_layer_predicates, predicate_name) or + Ash.Type.supports_filter?(resource, attr_type, predicate_name, data_layer)}, {:data_layer_can?, _, true} <- {:data_layer_can?, predicate_name, Ash.data_layer_can?(resource, {:filter, predicate_name})}, @@ -881,7 +890,7 @@ defmodule Ash.Filter do {:ok, predicate} else {:predicate_type, :error} -> - {:error, "No such filter type #{predicate_name}"} + {:error, :predicate_type, "No such filter type #{predicate_name}"} {:predicate, attr_name, {:error, error}} -> {:error, Map.put(error, :field, attr_name)}