Packages
ash
1.9.0
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/filter/filter.ex
defmodule Ash.Filter do
@moduledoc """
The representation of a filter in Ash.
Ash filters are stored as nested `Ash.Filter.Expression{}` and `%Ash.Filter.Not{}` structs,
terminating in a `%Ash.Filter.Predicate{}` struct. An expression is simply a boolean operator
and the left and right hand side of that operator.
"""
alias Ash.Actions.SideLoad
alias Ash.Engine.Request
alias Ash.Error.Query.{
AggregatesNotSupported,
InvalidFilterValue,
NoSuchAttributeOrRelationship,
NoSuchFilterPredicate,
ReadActionRequired
}
alias Ash.Filter.Predicate.{Eq, GreaterThan, In, IsNil, LessThan}
alias Ash.Filter.{Expression, Not, Predicate}
alias Ash.Query.Aggregate
@built_in_predicates [
eq: Eq,
equals: Eq,
in: In,
lt: LessThan,
gt: GreaterThan,
less_than: LessThan,
greater_than: GreaterThan,
is_nil: IsNil
]
@string_builtin_predicates Enum.into(@built_in_predicates, %{}, fn {key, value} ->
{to_string(key), value}
end)
defstruct [:resource, :expression]
@type t :: %__MODULE__{}
defmodule Simple do
@moduledoc "Represents a simplified filter, with a simple list of predicates"
defstruct [:resource, :predicates]
defmodule Not do
@moduledoc "A negated predicate"
defstruct [:predicate]
end
end
def parse!(resource, statement, aggregates \\ %{}) do
case parse(resource, statement, aggregates) do
{:ok, filter} ->
filter
{:error, error} ->
raise error
end
end
def parse(resource, statement, aggregates \\ %{}) do
context = %{
resource: resource,
relationship_path: [],
aggregates: aggregates
}
case parse_expression(statement, context) do
{:ok, expression} ->
{:ok, %__MODULE__{expression: expression, resource: resource}}
{:error, error} ->
{:error, error}
end
end
def to_simple_filter(%{resource: resource, expression: expression}) do
predicates = get_predicates(expression)
%Simple{resource: resource, predicates: predicates}
end
defp get_predicates(expr, acc \\ [])
defp get_predicates(true, acc), do: acc
defp get_predicates(false, _), do: false
defp get_predicates(_, false), do: false
defp get_predicates(%Expression{op: :and, left: left, right: right}, acc) do
acc = get_predicates(left, acc)
get_predicates(right, acc)
end
defp get_predicates(%Not{expression: expression}, acc) do
expression
|> get_predicates()
|> Enum.reduce(acc, fn predicate, acc ->
[%Simple.Not{predicate: predicate} | acc]
end)
end
defp get_predicates(%Predicate{} = predicate, acc), do: [predicate | acc]
def used_aggregates(filter) do
reduce(filter, [], fn
%Predicate{attribute: %Aggregate{} = aggregate}, acc ->
[aggregate | acc]
_, acc ->
acc
end)
end
def run_other_data_layer_filters(api, resource, filter) do
reduce(filter, {:ok, filter}, fn
%Expression{op: :or}, {:ok, filter} ->
{:halt, {:ok, filter}}
%Predicate{} = expression, {:ok, filter} ->
expression
|> relationship_paths(:ands_only)
|> filter_paths_that_change_data_layers(resource)
|> Enum.reduce_while({:halt, {:ok, filter}}, fn path, {:halt, {:ok, filter}} ->
{for_path, without_path} = split_expression_by_relationship_path(filter, path)
relationship = Ash.Resource.relationship(resource, path)
query =
relationship.destination
|> Ash.Query.new(api)
|> Map.put(:filter, for_path)
add_other_datalayer_read_results(query, relationship, path, without_path)
end)
%Expression{op: :and} = expression, {:ok, filter} ->
expression
|> relationship_paths(:ands_only)
|> filter_paths_that_change_data_layers(resource)
|> Enum.reduce_while({:halt, {:ok, filter}}, fn path, {:halt, {:ok, filter}} ->
{for_path, without_path} = split_expression_by_relationship_path(filter, path)
relationship = Ash.Resource.relationship(resource, path)
query =
relationship.destination
|> Ash.Query.new(api)
|> Map.put(:filter, for_path)
add_other_datalayer_read_results(query, relationship, path, without_path)
end)
_, {:ok, filter} ->
{:ok, filter}
end)
end
defp add_other_datalayer_read_results(query, relationship, path, filter_without_path) do
case query.api.read(query) do
{:ok, results} ->
new_filter =
case relationship.type do
:many_to_many ->
many_to_many_read_results(results, relationship, query, path)
_ ->
results
|> Enum.map(&Map.get(&1, relationship.destination_field))
|> Enum.reject(&is_nil/1)
|> record_filters_or_false(relationship)
|> put_at_path(:lists.droplast(path))
end
case add_to_filter(filter_without_path, new_filter) do
{:ok, filter} -> {:cont, {:halt, {:ok, filter}}}
{:error, error} -> {:halt, {:return, {:error, error}}}
end
{:error, error} ->
{:halt, {:return, {:error, error}}}
end
end
defp record_filters_or_false(records, relationship) do
case records do
[] ->
false
[value] ->
[{relationship.source_field, value}]
values ->
[{relationship.source_field, [in: values]}]
end
end
defp many_to_many_read_results(results, relationship, query, path) do
destination_values =
results
|> Enum.map(&Map.get(&1, relationship.destination_field))
|> Enum.reject(&is_nil/1)
join_query =
relationship.through
|> Ash.Query.new(query.api)
|> Ash.Query.filter([
{relationship.destination_field_on_join_table, [in: destination_values]}
])
case query.api.read(join_query) do
{:ok, results} ->
results
|> Enum.map(&Map.get(&1, relationship.source_field_on_join_table))
|> Enum.reject(&is_nil/1)
|> case do
[] ->
false
[value] ->
[{relationship.source_field, value}]
values ->
[{relationship.source_field, [in: values]}]
end
|> put_at_path(:lists.droplast(path))
{:error, error} ->
{:error, error}
end
end
defp filter_paths_that_change_data_layers(paths, resource, acc \\ [])
defp filter_paths_that_change_data_layers([], _resource, acc), do: acc
defp filter_paths_that_change_data_layers([path | rest], resource, acc) do
case shortest_path_to_changed_data_layer(resource, path) do
{:ok, path} ->
new_rest = Enum.reject(rest, &List.starts_with?(&1, path))
filter_paths_that_change_data_layers(new_rest, resource, [path | acc])
:error ->
filter_paths_that_change_data_layers(rest, resource, acc)
end
end
defp shortest_path_to_changed_data_layer(resource, path, acc \\ [])
defp shortest_path_to_changed_data_layer(_resource, [], _acc), do: :error
defp shortest_path_to_changed_data_layer(resource, [relationship | rest], acc) do
relationship = Ash.Resource.relationship(resource, relationship)
data_layer = Ash.Resource.data_layer(relationship.destination)
if relationship.type == :many_to_many do
if data_layer == Ash.Resource.data_layer(resource) &&
data_layer == Ash.Resource.data_layer(relationship.through) &&
Ash.Resource.data_layer_can?(resource, :join) do
shortest_path_to_changed_data_layer(relationship.destination, rest, [
relationship.name | acc
])
else
{:ok, Enum.reverse([relationship.name | acc])}
end
else
if data_layer == Ash.Resource.data_layer(resource) &&
Ash.Resource.data_layer_can?(resource, :join) do
shortest_path_to_changed_data_layer(relationship.destination, rest, [
relationship.name | acc
])
else
{:ok, Enum.reverse([relationship.name | acc])}
end
end
end
def put_at_path(value, []), do: value
def put_at_path(value, [key | rest]), do: [{key, put_at_path(value, rest)}]
def relationship_paths(filter_or_expression, kind \\ :all)
def relationship_paths(nil, _), do: []
def relationship_paths(%{expression: nil}, _), do: []
def relationship_paths(%__MODULE__{expression: expression}, kind),
do: relationship_paths(expression, kind)
def relationship_paths(expression, kind) do
expression
|> do_relationship_paths(kind)
|> List.wrap()
|> List.flatten()
|> Enum.uniq()
|> Enum.map(fn {path} -> path end)
end
def add_to_filter!(base, addition, op \\ :and, aggregates \\ %{}) do
case add_to_filter(base, addition, op, aggregates) do
{:ok, value} ->
value
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
end
def add_to_filter(base, addition, op \\ :and, aggregates \\ %{})
def add_to_filter(nil, %__MODULE__{} = addition, _, _), do: {:ok, addition}
def add_to_filter(
%__MODULE__{resource: resource} = base,
%__MODULE__{resource: resource} = addition,
op,
_
) do
{:ok, %{base | expression: Expression.new(op, base.expression, addition.expression)}}
end
def add_to_filter(%__MODULE__{} = base, %__MODULE__{} = addition, _, _) do
{:error,
"Cannot add filter for resource #{inspect(addition.resource)} to filter with resource #{
inspect(base.resource)
}"}
end
def add_to_filter(%__MODULE__{} = base, statement, op, aggregates) do
case parse(base.resource, statement, aggregates) do
{:ok, filter} -> add_to_filter(base, filter, op, aggregates)
{:error, error} -> {:error, error}
end
end
@doc """
Returns true if the second argument is a strict subset (always returns the same or less data) of the first
"""
def strict_subset_of(nil, _), do: true
def strict_subset_of(_, nil), do: false
def strict_subset_of(%{resource: resource}, %{resource: other_resource})
when resource != other_resource,
do: false
def strict_subset_of(filter, candidate) do
Ash.SatSolver.strict_filter_subset(filter, candidate)
end
def strict_subset_of?(filter, candidate) do
strict_subset_of(filter, candidate) == true
end
def relationship_filter_request_paths(filter) do
filter
|> relationship_paths()
|> Enum.map(&[:filter, &1])
end
def read_requests(_, nil), do: {:ok, []}
def read_requests(api, filter) do
filter
|> Ash.Filter.relationship_paths()
|> Enum.map(fn path ->
{path, filter_expression_by_relationship_path(filter, path, true)}
end)
|> Enum.reduce_while({:ok, []}, fn {path, scoped_filter}, {:ok, requests} ->
%{resource: resource} = scoped_filter
with %{errors: []} = query <- Ash.Query.new(resource, api),
%{errors: []} = query <- Ash.Query.filter(query, scoped_filter),
{:action, action} when not is_nil(action) <-
{:action, Ash.Resource.primary_action(resource, :read)} do
request =
Request.new(
resource: resource,
api: api,
query:
Request.resolve(
[[:data, :authorization_filter]],
fn %{
data: %{
authorization_filter: authorization_filter
}
} ->
if authorization_filter do
relationship =
Ash.Resource.relationship(
resource,
List.first(path)
)
case SideLoad.reverse_relationship_path(
relationship,
tl(path)
) do
:error ->
{:ok, query}
{:ok, reverse_relationship} ->
filter = put_at_path(authorization_filter, reverse_relationship)
{:ok, Ash.Query.filter(query, filter)}
end
else
{:ok, query}
end
end
),
async?: false,
path: [:filter, path],
strict_check_only?: true,
action: action,
name: "authorize filter #{Enum.join(path, ".")}",
data: []
)
{:cont, {:ok, [request | requests]}}
else
{:error, error} -> {:halt, {:error, error}}
%{errors: errors} -> {:halt, {:error, errors}}
{:action, nil} -> {:halt, {:error, ReadActionRequired.exception(resource: resource)}}
end
end)
end
def map(%__MODULE__{expression: nil} = filter, _) do
filter
end
def map(%__MODULE__{expression: expression} = filter, func) do
%{filter | expression: do_map(func.(expression), func)}
end
def map(expression, func) do
do_map(func.(expression), func)
end
def do_map(expression, func) do
case expression do
{:halt, expr} ->
expr
%Expression{left: left, right: right} = expr ->
%{expr | left: do_map(left, func), right: do_map(right, func)}
%Not{expression: not_expr} = expr ->
%{expr | expression: do_map(not_expr, func)}
other ->
func.(other)
end
end
def reduce(filter, acc \\ nil, func)
def reduce(%__MODULE__{expression: nil}, acc, _), do: acc
def reduce(%__MODULE__{expression: expression}, acc, func) do
case func.(expression, acc) do
{:halt, acc} ->
acc
{:return, value} ->
value
acc ->
case do_reduce(expression, acc, func) do
{:halt, acc} -> acc
{:return, value} -> value
acc -> acc
end
end
end
def reduce(expression, acc, func) do
case func.(expression, acc) do
{:halt, acc} ->
acc
{:return, value} ->
value
acc ->
case do_reduce(expression, acc, func) do
{:halt, acc} -> acc
{:return, value} -> value
acc -> acc
end
end
end
def do_reduce(expression, acc, func) do
case expression do
%Expression{} = expression ->
do_reduce_expression(expression, acc, func)
%Not{expression: not_expr} ->
case func.(not_expr, acc) do
{:halt, acc} ->
acc
{:return, value} ->
{:return, value}
acc ->
do_reduce(not_expr, acc, func)
end
{:return, value} ->
{:return, value}
{:halt, value} ->
{:halt, value}
other ->
func.(other, acc)
end
end
defp do_reduce_expression(%Expression{left: left, right: right}, acc, func) do
case func.(right, acc) do
{:halt, acc} ->
case func.(left, acc) do
{:return, value} ->
{:return, value}
{:halt, acc} ->
acc
acc ->
do_reduce(left, acc, func)
end
{:return, value} ->
{:return, value}
acc ->
continue_reduce(left, right, acc, func)
end
end
defp continue_reduce(left, right, acc, func) do
case func.(left, acc) do
{:halt, acc} ->
do_reduce(right, acc, func)
{:return, value} ->
{:return, value}
acc ->
case do_reduce(left, acc, func) do
{:halt, acc} ->
{:halt, acc}
{:return, acc} ->
{:return, acc}
acc ->
do_reduce(right, acc, func)
end
end
end
defp split_expression_by_relationship_path(%{expression: expression} = filter, _path)
when expression in [nil, true, false] do
{filter, filter}
end
defp split_expression_by_relationship_path(filter, path) do
{for_path, without_path} = do_split_expression_by_relationship_path(filter.expression, path)
{%__MODULE__{
resource: Ash.Resource.related(filter.resource, path),
expression: for_path
},
%__MODULE__{
resource: filter.resource,
expression: without_path
}}
end
def filter_expression_by_relationship_path(filter, path, scope? \\ false) do
%__MODULE__{
resource: Ash.Resource.related(filter.resource, path),
expression: do_filter_expression_by_relationship_path(filter.expression, path, scope?)
}
end
defp do_split_expression_by_relationship_path(
%Expression{op: op, left: left, right: right},
path
) do
{new_for_path_left, new_without_path_left} =
do_split_expression_by_relationship_path(left, path)
{new_for_path_right, new_without_path_right} =
do_split_expression_by_relationship_path(right, path)
{Expression.new(op, new_for_path_left, new_for_path_right),
Expression.new(op, new_without_path_left, new_without_path_right)}
end
defp do_split_expression_by_relationship_path(%Not{expression: expression}, path) do
{new_for_path, new_without_path} = do_split_expression_by_relationship_path(expression, path)
{Not.new(new_for_path), Not.new(new_without_path)}
end
defp do_split_expression_by_relationship_path(
%Predicate{relationship_path: predicate_path} = predicate,
path
) do
if List.starts_with?(predicate_path, path) do
{%{predicate | relationship_path: Enum.drop(predicate_path, length(path))}, nil}
else
{nil, predicate}
end
end
defp do_filter_expression_by_relationship_path(
%Expression{op: op, left: left, right: right},
path,
scope?
) do
new_left = do_filter_expression_by_relationship_path(left, path, scope?)
new_right = do_filter_expression_by_relationship_path(right, path, scope?)
Expression.new(op, new_left, new_right)
end
defp do_filter_expression_by_relationship_path(%Not{expression: expression}, path, scope?) do
new_expression = do_filter_expression_by_relationship_path(expression, path, scope?)
Not.new(new_expression)
end
defp do_filter_expression_by_relationship_path(
%Predicate{relationship_path: predicate_path} = predicate,
path,
scope?
) do
if List.starts_with?(predicate_path, path) do
if scope? do
%{predicate | relationship_path: Enum.drop(predicate_path, Enum.count(path))}
else
predicate
end
else
nil
end
end
def remove_aggregates(%__MODULE__{expression: expression} = filter) do
%{filter | expression: remove_aggregates(expression)}
end
def remove_aggregates(%Expression{op: op, left: left, right: right}) do
Expression.new(op, remove_aggregates(left), remove_aggregates(right))
end
def remove_aggregates(%Not{expression: expression}) do
Not.new(remove_aggregates(expression))
end
def remove_aggregates(%Predicate{attribute: %Ash.Query.Aggregate{}}), do: nil
def remove_aggregates(other), do: other
defp do_relationship_paths(%Predicate{relationship_path: []}, _) do
[]
end
defp do_relationship_paths(%Predicate{relationship_path: path}, _) do
{path}
end
defp do_relationship_paths(%Expression{op: :or}, :ands_only) do
[]
end
defp do_relationship_paths(%Expression{left: left, right: right}, kind) do
[do_relationship_paths(left, kind), do_relationship_paths(right, kind)]
end
defp do_relationship_paths(%Not{expression: expression}, kind) do
do_relationship_paths(expression, kind)
end
defp do_relationship_paths(_, _), do: []
defp parse_expression(%__MODULE__{expression: expression}, context),
do: {:ok, add_to_predicate_path(expression, context)}
defp parse_expression(statement, context) when is_map(statement) or is_list(statement) do
Enum.reduce_while(statement, {:ok, nil}, fn expression_part, {:ok, expression} ->
case add_expression_part(expression_part, context, expression) do
{:ok, new_expression} ->
{:cont, {:ok, new_expression}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
end
defp parse_expression(statement, context) do
parse_expression([statement], context)
end
defp add_expression_part(boolean, _context, expression) when is_boolean(boolean),
do: {:ok, Expression.new(:and, expression, boolean)}
defp add_expression_part(%__MODULE__{expression: adding_expression}, context, expression) do
{:ok, Expression.new(:and, expression, add_to_predicate_path(adding_expression, context))}
end
defp add_expression_part(%resource{} = record, context, expression) do
if resource == context.resource do
pkey_filter = record |> Map.take(Ash.Resource.primary_key(resource)) |> Map.to_list()
add_expression_part(pkey_filter, context, expression)
else
{:error,
InvalidFilterValue.exception(
value: record,
message: "Records must match the resource being filtered"
)}
end
end
defp add_expression_part({:not, nested_statement}, context, expression) do
case parse_expression(nested_statement, context) do
{:ok, nested_expression} ->
{:ok, Expression.new(:and, expression, Not.new(nested_expression))}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part({:or, nested_statements}, context, expression) do
with {:ok, nested_expression} <- parse_and_join(nested_statements, :or, context),
:ok <- validate_datalayers_support_boolean_filters(nested_expression) do
{:ok, Expression.new(:and, expression, nested_expression)}
end
end
defp add_expression_part({:and, nested_statements}, context, expression) do
case parse_and_join(nested_statements, :and, context) do
{:ok, nested_expression} ->
{:ok, Expression.new(:and, expression, nested_expression)}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part({field, nested_statement}, context, expression)
when is_atom(field) or is_binary(field) do
aggregates =
Enum.flat_map(context.aggregates, fn {key, _} ->
[key, to_string(key)]
end)
cond do
attr = Ash.Resource.attribute(context.resource, field) ->
case parse_predicates(nested_statement, attr, context) do
{:ok, nested_statement} ->
{:ok, Expression.new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
rel = Ash.Resource.relationship(context.resource, field) ->
context =
context
|> Map.update!(:relationship_path, fn path -> path ++ [rel.name] end)
|> Map.put(:resource, rel.destination)
if is_list(nested_statement) || is_map(nested_statement) do
case parse_expression(nested_statement, context) do
{:ok, nested_expression} ->
{:ok, Expression.new(:and, expression, nested_expression)}
{:error, error} ->
{:error, error}
end
else
with [field] <- Ash.Resource.primary_key(context.resource),
attribute <- Ash.Resource.attribute(context.resource, field),
{:ok, casted} <-
Ash.Type.cast_input(attribute.type, nested_statement) do
add_expression_part({field, casted}, context, expression)
else
_other ->
{:error,
InvalidFilterValue.exception(
value: inspect(nested_statement),
message:
"A single value must be castable to the primary key of the resource: #{
inspect(context.resource)
}"
)}
end
end
field in aggregates ->
field =
if is_binary(field) do
String.to_existing_atom(field)
else
field
end
add_aggregate_expression(context, nested_statement, field, expression)
true ->
{:error,
NoSuchAttributeOrRelationship.exception(
attribute_or_relationship: field,
resource: context.resource
)}
end
end
defp add_expression_part(value, context, expression) when is_map(value) do
# Can't call `parse_expression/2` here because it will loop
value
|> Map.to_list()
|> Enum.reduce_while({:ok, nil}, fn {key, value}, {:ok, expression} ->
case add_expression_part({key, value}, context, expression) do
{:ok, new_expression} ->
{:cont, {:ok, new_expression}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, new_expression} -> {:ok, Expression.new(:and, expression, new_expression)}
{:error, error} -> {:error, error}
end
end
defp add_expression_part(value, _, _) do
{:error, InvalidFilterValue.exception(value: value)}
end
defp add_aggregate_expression(context, nested_statement, field, expression) do
if Ash.Resource.data_layer_can?(context.resource, :join) &&
Ash.Resource.data_layer_can?(context.resource, :aggregate_filter) do
case parse_predicates(nested_statement, Map.get(context.aggregates, field), context) do
{:ok, nested_statement} ->
{:ok, Expression.new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
else
{:error, AggregatesNotSupported.exception(resource: context.resource, feature: "filtering")}
end
end
defp validate_datalayers_support_boolean_filters(%Expression{op: :or, left: left, right: right}) do
left_predicates =
left
|> reduce([], fn
%Predicate{} = pred, acc ->
[{pred.relationship_path, pred.resource} | acc]
_, acc ->
acc
end)
|> Enum.uniq()
right_predicates =
right
|> reduce([], fn
%Predicate{} = pred, acc ->
[{pred.relationship_path, pred.resource} | acc]
_, acc ->
acc
end)
|> Enum.uniq()
Enum.reduce_while(left_predicates, :ok, fn {path, resource}, :ok ->
check_predicate_compatibility(resource, path, right_predicates)
end)
end
defp validate_datalayers_support_boolean_filters(_), do: :ok
defp check_predicate_compatibility(resource, path, right_predicates) do
can_join? = Ash.Resource.data_layer_can?(resource, :join)
can_boolean_filter? = Ash.Resource.data_layer_can?(resource, :boolean_filter)
cond do
can_join? and can_boolean_filter? ->
{:cont, :ok}
can_join? ->
check_when_cant_boolean_filter(right_predicates, resource, path)
can_boolean_filter? ->
check_when_cant_join(right_predicates, resource, path)
true ->
check_when_cant_join_or_boolean_filter(right_predicates, resource)
end
end
defp check_when_cant_join_or_boolean_filter(right_predicates, resource) do
data_layer = Ash.Resource.data_layer(resource)
if Enum.any?(right_predicates, fn {_, right_resource} ->
Ash.Resource.data_layer(right_resource) == data_layer
end) do
{:halt,
{:error,
"Data layer #{inspect(Ash.Resource.data_layer(resource))} does not support joins or boolean filters, which are necessary for this query"}}
else
{:cont, :ok}
end
end
defp check_when_cant_join(right_predicates, resource, path) do
data_layer = Ash.Resource.data_layer(resource)
right_predicates
|> Enum.filter(fn {_right_path, right_resource} ->
Ash.Resource.data_layer(right_resource) == data_layer
end)
|> Enum.all?(fn {right_path, _} ->
right_path == path
end)
|> case do
true ->
{:cont, :ok}
false ->
{:halt,
{:error,
"Data layer #{inspect(Ash.Resource.data_layer(resource))} does not support joins, which are necessary for this query"}}
end
end
defp check_when_cant_boolean_filter(right_predicates, resource, path) do
if Enum.any?(right_predicates, fn {right_path, right_resource} ->
right_resource == resource && right_path == path
end) do
{:halt,
{:error,
"Data layer #{inspect(Ash.Resource.data_layer(resource))} does not support boolean filters"}}
else
{:cont, :ok}
end
end
defp add_to_predicate_path(expression, context) do
case expression do
%Not{expression: expression} = not_expr ->
%{not_expr | expression: add_to_predicate_path(expression, context)}
%Expression{left: left, right: right} = expression ->
%{
expression
| left: add_to_predicate_path(left, context),
right: add_to_predicate_path(right, context)
}
%Predicate{relationship_path: relationship_path} = pred ->
%{pred | relationship_path: context.relationship_path ++ relationship_path}
other ->
other
end
end
defp parse_and_join(statements, op, context) do
Enum.reduce_while(statements, {:ok, nil}, fn statement, {:ok, expression} ->
case parse_expression(statement, context) do
{:ok, nested_expression} ->
{:cont, {:ok, Expression.new(op, expression, nested_expression)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
end
defp parse_predicates(value, field, context) when not is_list(value) and not is_map(value) do
parse_predicates([eq: value], field, context)
end
defp parse_predicates(values, attr, context) do
data_layer_predicates =
Map.get(
Ash.Resource.data_layer_filters(context.resource),
Ash.Type.storage_type(attr.type),
[]
)
if is_map(values) || Keyword.keyword?(values) do
Enum.reduce_while(values, {:ok, nil}, fn {key, value}, {:ok, expression} ->
case get_predicate(key, data_layer_predicates) do
value when value in [nil, []] ->
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
{:halt, {:error, error}}
predicate_module ->
case Predicate.new(
context.resource,
attr,
predicate_module,
value,
context.relationship_path
) do
{:ok, predicate} ->
{:cont, {:ok, Expression.new(:and, expression, predicate)}}
{:error, error} ->
{:halt, {:error, error}}
end
end
end)
else
error = InvalidFilterValue.exception(value: values)
{:halt, error}
end
end
defp get_predicate(key, data_layer_predicates) when is_atom(key) do
@built_in_predicates[key] || data_layer_predicates[key]
end
defp get_predicate(key, data_layer_predicates) when is_binary(key) do
Map.get(@string_builtin_predicates, key) ||
Enum.find_value(data_layer_predicates, fn {pred, value} ->
if to_string(pred) == key do
value
else
false
end
end)
end
defp get_predicate(_, _), do: nil
defimpl Inspect do
import Inspect.Algebra
@custom_colors [
number: :cyan
]
def inspect(
%{expression: expression},
opts
) do
opts = %{opts | syntax_colors: Keyword.merge(opts.syntax_colors, @custom_colors)}
concat(["#Ash.Filter<", to_doc(expression, opts), ">"])
end
end
end