feat: add data layer custom filters

This commit is contained in:
Zach Daniel 2020-05-31 01:52:00 -04:00
parent 7820adeebe
commit 377319e881
No known key found for this signature in database
GPG key ID: C377365383138D4B
4 changed files with 51 additions and 3 deletions

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)}