Packages
ash
0.4.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 false
# A filter expression in Ash.
# The way we represent filters may be strange, but its important to have it structured,
# as merging and checking filter subsets are used all through ash for things like
# authorization. The `ands` of a filter are not subject to its `ors`. The `not` of a filter
# is also *not* subject to its `ors`.
# For instance, if a filter `A` has two `ands`, `B` and `C` and two `ors`, `D` and `E`, and
# a `not` of F, the expression as can be represented as `(A or D or E) and NOT F and B and C`.
# The filters `attributes` and `relationships`, *are* subject to the `ors` of that filter.
# `<and_statements> AND NOT <not_statement> AND (<one_of_or_statements> OR <attributes + relationships>)`
# This probably needs to be refactored into something more representative of its behavior,
# like a series of nested boolean expression structs w/ a reference to the attribute/relationship
# it references. Maybe. This would be similar to Ecto's `BooleanExpr` structs.
alias Ash.Actions.PrimaryKeyHelpers
alias Ash.Engine
alias Ash.Engine.Request
alias Ash.Filter.{And, Eq, In, Merge, NotEq, NotIn, Or}
defstruct [
:api,
:resource,
:not,
ands: [],
ors: [],
attributes: %{},
relationships: %{},
requests: [],
path: [],
errors: [],
impossible?: false
]
@type t :: %__MODULE__{
api: Ash.api(),
resource: Ash.resource(),
ors: list(%__MODULE__{} | nil),
not: %__MODULE__{} | nil,
attributes: Keyword.t(),
relationships: map(),
path: list(atom),
impossible?: boolean,
errors: list(String.t()),
requests: list(Request.t())
}
@predicates %{
not_eq: NotEq,
not_in: NotIn,
eq: Eq,
in: In,
and: And,
or: Or
}
@spec parse(
Ash.resource(),
Keyword.t(),
Ash.api(),
relationship_path :: list(atom)
) :: t()
@doc """
Parse a filter from a filter expression
The only rason to pass `api` would be if you intend to leverage
any engine requests that would be generated by this filter.
"""
def parse(resource, filter, api, path \\ [])
def parse(resource, [], api, _),
do: %__MODULE__{
api: api,
resource: resource
}
def parse(_resource, %__MODULE__{} = filter, _, _) do
filter
end
def parse(resource, filter, api, path) do
parsed_filter = do_parse(filter, %Ash.Filter{resource: resource, api: api, path: path})
source =
case path do
[] -> "filter"
path -> "related #{Enum.join(path, ".")} filter"
end
if path == [] do
parsed_filter
else
query =
api
|> Ash.Query.new(resource)
|> Ash.Query.filter(parsed_filter)
request =
Request.new(
resource: resource,
api: api,
query: query,
path: [:filter, path],
skip_unless_authorize?: true,
data:
Request.resolve(
[[:filter, path, :query]],
fn %{filter: %{^path => %{query: query}}} ->
data_layer_query = Ash.DataLayer.resource_to_query(resource)
case Ash.DataLayer.filter(data_layer_query, query.filter, resource) do
{:ok, filtered_query} ->
Ash.DataLayer.run_query(filtered_query, resource)
{:error, error} ->
{:error, error}
end
end
),
action: Ash.primary_action!(resource, :read),
relationship: path,
name: source
)
add_request(
parsed_filter,
request
)
end
end
def optional_paths(filter) do
filter
|> do_optional_paths()
|> Enum.uniq()
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
if empty_filter?(filter) do
true
else
if empty_filter?(candidate) do
false
else
{filter, candidate} = cosimplify(filter, candidate)
Ash.SatSolver.strict_filter_subset(filter, candidate)
end
end
end
def strict_subset_of?(filter, candidate) do
strict_subset_of(filter, candidate) == true
end
def primary_key_filter?(nil), do: false
def primary_key_filter?(filter) do
cleared_pkey_filter =
filter.resource
|> Ash.primary_key()
|> Enum.map(fn key -> {key, nil} end)
case cleared_pkey_filter do
[] ->
false
cleared_pkey_filter ->
parsed_cleared_pkey_filter = parse(filter.resource, cleared_pkey_filter, filter.api)
cleared_candidate_filter = clear_equality_values(filter)
strict_subset_of?(parsed_cleared_pkey_filter, cleared_candidate_filter)
end
end
def get_pkeys(%{query: nil, resource: resource}, api, %_{} = item) do
pkey_filter =
item
|> Map.take(Ash.primary_key(resource))
|> Map.to_list()
api
|> Ash.Query.new(resource)
|> Ash.Query.filter(pkey_filter)
end
def get_pkeys(%{query: query}, _, %resource{} = item) do
pkey_filter =
item
|> Map.take(Ash.primary_key(resource))
|> Map.to_list()
Ash.Query.filter(query, pkey_filter)
end
def cosimplify(left, right) do
{new_left, new_right} = simplify_lists(left, right)
express_mutual_exclusion(new_left, new_right)
end
defp simplify_lists(left, right) do
values = get_all_values(left, get_all_values(right, %{}))
substitutions =
Enum.reduce(values, %{}, fn {key, values}, substitutions ->
value_substitutions = simplify_list_substitutions(values)
Map.put(substitutions, key, value_substitutions)
end)
{replace_values(left, substitutions), replace_values(right, substitutions)}
end
defp simplify_list_substitutions(values) do
Enum.reduce(values, %{}, fn value, substitutions ->
case do_simplify_list(value) do
{:ok, substitution} ->
Map.put(substitutions, value, substitution)
:error ->
substitutions
end
end)
end
defp do_simplify_list(%In{values: []}), do: :error
defp do_simplify_list(%In{values: [value]}) do
{:ok, %Eq{value: value}}
end
defp do_simplify_list(%In{values: [value | rest]}) do
{:ok,
Enum.reduce(rest, %Eq{value: value}, fn value, other_values ->
Or.prebuilt_new(%Eq{value: value}, other_values)
end)}
end
defp do_simplify_list(%NotIn{values: []}), do: :error
defp do_simplify_list(%NotIn{values: [value]}) do
{:ok, %NotEq{value: value}}
end
defp do_simplify_list(%NotIn{values: [value | rest]}) do
{:ok,
Enum.reduce(rest, %Eq{value: value}, fn value, other_values ->
And.prebuilt_new(%NotEq{value: value}, other_values)
end)}
end
defp do_simplify_list(_), do: :error
defp express_mutual_exclusion(left, right) do
values = get_all_values(left, get_all_values(right, %{}))
substitutions =
Enum.reduce(values, %{}, fn {key, values}, substitutions ->
value_substitutions = express_mutual_exclusion_substitutions(values)
Map.put(substitutions, key, value_substitutions)
end)
{replace_values(left, substitutions), replace_values(right, substitutions)}
end
defp express_mutual_exclusion_substitutions(values) do
Enum.reduce(values, %{}, fn value, substitutions ->
case do_express_mutual_exclusion(value, values) do
{:ok, substitution} ->
Map.put(substitutions, value, substitution)
:error ->
substitutions
end
end)
end
defp do_express_mutual_exclusion(%Eq{value: value} = eq_filter, values) do
values
|> Enum.filter(fn
%Eq{value: other_value} -> value != other_value
_ -> false
end)
|> case do
[] ->
:error
[%{value: other_value}] ->
{:ok, And.prebuilt_new(eq_filter, %NotEq{value: other_value})}
values ->
{:ok,
Enum.reduce(values, eq_filter, fn %{value: other_value}, expr ->
And.prebuilt_new(expr, %NotEq{value: other_value})
end)}
end
end
defp do_express_mutual_exclusion(_, _), do: :error
defp get_all_values(filter, state) do
state =
filter.attributes
|> Enum.reduce(state, fn {field, value}, state ->
state
|> Map.put_new([filter.path, field], [])
|> Map.update!([filter.path, field], fn values ->
value
|> do_get_values()
|> Enum.reduce(values, fn value, values ->
[value | values]
end)
|> Enum.uniq()
end)
end)
state =
Enum.reduce(filter.relationships, state, fn {_, relationship_filter}, new_state ->
get_all_values(relationship_filter, new_state)
end)
state =
if filter.not do
get_all_values(filter.not, state)
else
state
end
state =
Enum.reduce(filter.ors, state, fn or_filter, new_state ->
get_all_values(or_filter, new_state)
end)
Enum.reduce(filter.ands, state, fn and_filter, new_state ->
get_all_values(and_filter, new_state)
end)
end
defp do_get_values(%struct{left: left, right: right})
when struct in [And, Or] do
do_get_values(left) ++ do_get_values(right)
end
defp do_get_values(other), do: [other]
defp replace_values(filter, substitutions) do
new_attrs =
Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes ->
substitutions = Map.get(substitutions, [filter.path, field]) || %{}
Map.put(attributes, field, do_replace_value(value, substitutions))
end)
new_relationships =
Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships ->
new_relationship_filter = replace_values(related_filter, substitutions)
Map.put(relationships, relationship, new_relationship_filter)
end)
new_not =
if filter.not do
replace_values(filter.not, substitutions)
else
filter.not
end
new_ors =
Enum.reduce(filter.ors, [], fn or_filter, ors ->
new_or = replace_values(or_filter, substitutions)
[new_or | ors]
end)
new_ands =
Enum.reduce(filter.ands, [], fn and_filter, ands ->
new_and = replace_values(and_filter, substitutions)
[new_and | ands]
end)
%{
filter
| attributes: new_attrs,
relationships: new_relationships,
not: new_not,
ors: Enum.reverse(new_ors),
ands: Enum.reverse(new_ands)
}
end
defp do_replace_value(%struct{left: left, right: right} = compound, substitutions)
when struct in [And, Or] do
%{
compound
| left: do_replace_value(left, substitutions),
right: do_replace_value(right, substitutions)
}
end
defp do_replace_value(value, substitutions) do
case Map.fetch(substitutions, value) do
{:ok, new_value} ->
new_value
_ ->
value
end
end
defp clear_equality_values(filter) do
new_attrs =
Enum.reduce(filter.attributes, %{}, fn {field, value}, attributes ->
Map.put(attributes, field, do_clear_equality_value(value))
end)
new_relationships =
Enum.reduce(filter.relationships, %{}, fn {relationship, related_filter}, relationships ->
new_relationship_filter = clear_equality_values(related_filter)
Map.put(relationships, relationship, new_relationship_filter)
end)
new_not =
if filter.not do
clear_equality_values(filter)
else
filter.not
end
new_ors =
Enum.reduce(filter.ors, [], fn or_filter, ors ->
new_or = clear_equality_values(or_filter)
[new_or | ors]
end)
new_ands =
Enum.reduce(filter.ands, [], fn and_filter, ands ->
new_and = clear_equality_values(and_filter)
[new_and | ands]
end)
%{
filter
| attributes: new_attrs,
relationships: new_relationships,
not: new_not,
ors: Enum.reverse(new_ors),
ands: Enum.reverse(new_ands)
}
end
defp do_clear_equality_value(%struct{left: left, right: right} = compound)
when struct in [And, Or] do
%{
compound
| left: do_clear_equality_value(left),
right: do_clear_equality_value(right)
}
end
defp do_clear_equality_value(%Eq{value: _} = filter), do: %{filter | value: nil}
defp do_clear_equality_value(%In{values: _}), do: %Eq{value: nil}
defp do_clear_equality_value(other), do: other
defp do_optional_paths(%{relationships: relationships, requests: requests, ors: ors})
when relationships == %{} and ors in [[], nil] do
Enum.map(requests, fn request ->
request.path
end)
end
defp do_optional_paths(%{ors: [first | rest]} = filter) do
do_optional_paths(first) ++ do_optional_paths(%{filter | ors: rest})
end
defp do_optional_paths(%{relationships: relationships} = filter) when is_map(relationships) do
relationship_paths =
Enum.flat_map(relationships, fn {_, value} ->
do_optional_paths(value)
end)
relationship_paths ++ do_optional_paths(%{filter | relationships: %{}})
end
def request_filter_for_fetch(filter, data) do
filter
|> optional_paths()
|> paths_and_data(data)
|> most_specific_paths()
|> Enum.reduce(filter, fn {path, %{data: related_data}}, filter ->
[:filter, relationship_path] = path
filter
|> add_records_to_relationship_filter(
relationship_path,
List.wrap(related_data)
)
|> lift_impossibility()
end)
end
defp most_specific_paths(paths_and_data) do
Enum.reject(paths_and_data, fn {path, _} ->
Enum.any?(paths_and_data, &path_is_more_specific?(path, &1))
end)
end
# I don't think this is a possibility
defp path_is_more_specific?([], []), do: false
defp path_is_more_specific?(_, []), do: true
# first element of the search matches first element of candidate
defp path_is_more_specific?([part | rest], [part | candidate_rest]) do
path_is_more_specific?(rest, candidate_rest)
end
defp path_is_more_specific?(_, _), do: false
defp paths_and_data(paths, data) do
Enum.flat_map(paths, fn path ->
case Engine.fetch_nested_value(data, path) do
{:ok, related_data} -> [{path, related_data}]
:error -> []
end
end)
end
def empty_filter?(filter) do
filter.attributes == %{} and filter.relationships == %{} and filter.not == nil and
filter.ors in [[], nil] and filter.ands in [[], nil] and filter.impossible? == false
end
defp add_records_to_relationship_filter(filter, [], records) do
case PrimaryKeyHelpers.values_to_primary_key_filters(filter.resource, records) do
{:error, error} ->
add_error(filter, error)
{:ok, []} ->
if filter.ors in [[], nil] do
%{filter | impossible?: true}
else
filter
end
{:ok, [single]} ->
do_parse(single, filter)
{:ok, many} ->
do_parse([or: many], filter)
end
end
defp add_records_to_relationship_filter(filter, [relationship | rest] = path, records) do
filter
|> Map.update!(:relationships, fn relationships ->
case Map.fetch(relationships, relationship) do
{:ok, related_filter} ->
Map.put(
relationships,
relationship,
add_records_to_relationship_filter(related_filter, rest, records)
)
:error ->
relationships
end
end)
|> Map.update!(:ors, fn ors ->
Enum.map(ors, &add_records_to_relationship_filter(&1, path, records))
end)
end
defp lift_impossibility(filter) do
filter =
filter
|> Map.update!(:relationships, fn relationships ->
Enum.reduce(relationships, relationships, fn {key, filter}, relationships ->
Map.put(relationships, key, lift_impossibility(filter))
end)
end)
|> Map.update!(:ands, fn ands ->
Enum.map(ands, &lift_impossibility/1)
end)
|> Map.update!(:ors, fn ors ->
Enum.map(ors, &lift_impossibility/1)
end)
with_related_impossibility =
if Enum.any?(filter.relationships || %{}, fn {_, val} -> Map.get(val, :impossible?) end) do
Map.put(filter, :impossible?, true)
else
filter
end
if Enum.any?(with_related_impossibility.ands, &Map.get(&1, :impossible?)) do
Map.put(with_related_impossibility, :impossible?, true)
else
with_related_impossibility
end
end
defp add_not_filter_info(filter) do
case filter.not do
nil ->
filter
not_filter ->
filter
|> add_request(not_filter.requests)
|> add_error(not_filter.errors)
end
end
def predicate_strict_subset_of?(attribute, %left_struct{} = left, right) do
left_struct.strict_subset_of?(attribute, left, right)
end
def add_to_filter(filter, %__MODULE__{} = addition) do
cond do
empty_filter?(filter) ->
addition
empty_filter?(addition) ->
filter
true ->
%{addition | ands: [filter | addition.ands]}
|> lift_impossibility()
|> lift_if_empty()
|> add_not_filter_info()
end
end
def add_to_filter(filter, additions) do
parsed = parse(filter.resource, additions, filter.api)
add_to_filter(filter, parsed)
end
defp do_parse(filter_statement, %{resource: resource} = filter) do
Enum.reduce(filter_statement, filter, fn
{key, value}, filter ->
cond do
key == :__impossible__ && value == true ->
%{filter | impossible?: true}
key == :and ->
add_and_to_filter(filter, value)
key == :or ->
add_or_to_filter(filter, value)
key == :not ->
add_to_not_filter(filter, value)
attr = Ash.attribute(resource, key) ->
add_attribute_filter(filter, attr, value)
rel = Ash.relationship(resource, key) ->
add_relationship_filter(filter, rel, value)
true ->
add_error(
filter,
"Attempted to filter on #{key} which is neither a relationship, nor a field of #{
inspect(resource)
}"
)
end
end)
|> lift_impossibility()
|> lift_if_empty()
|> add_not_filter_info()
end
defp add_and_to_filter(filter, value) do
if Keyword.keyword?(value) do
%{filter | ands: [parse(filter.resource, value, filter.api) | filter.ands]}
else
empty_filter = parse(filter.resource, [], filter.api)
filter_with_ands = %{
empty_filter
| ands: Enum.map(value, &parse(filter.resource, &1, filter.api))
}
%{filter | ands: [filter_with_ands | filter.ands]}
end
end
defp add_or_to_filter(filter, value) do
if Keyword.keyword?(value) do
%{filter | ors: [parse(filter.resource, value, filter.api) | filter.ors]}
else
[first_or | rest_ors] = Enum.map(value, &parse(filter.resource, &1, filter.api))
or_filter =
filter.resource
|> parse(first_or, filter.api)
|> Map.update!(:ors, &Kernel.++(&1, rest_ors))
%{filter | ands: [or_filter | filter.ands]}
end
end
defp add_to_not_filter(filter, value) do
Map.update!(filter, :not, fn not_filter ->
if not_filter do
add_to_filter(not_filter, value)
else
parse(filter.resource, value, filter.api)
end
end)
end
defp lift_if_empty(%{
ors: [],
ands: [and_filter | rest],
attributes: attrs,
relationships: rels,
not: nil,
errors: errors
})
when attrs == %{} and rels == %{} do
and_filter
|> Map.update!(:ands, &Kernel.++(&1, rest))
|> lift_if_empty()
|> Map.update!(:errors, &Kernel.++(&1, errors))
end
defp lift_if_empty(%{
ands: [],
ors: [or_filter | rest],
attributes: attrs,
relationships: rels,
not: nil,
errors: errors
})
when attrs == %{} and rels == %{} do
or_filter
|> Map.update!(:ors, &Kernel.++(&1, rest))
|> lift_if_empty()
|> Map.update!(:errors, &Kernel.++(&1, errors))
end
defp lift_if_empty(filter) do
filter
end
defp add_attribute_filter(filter, attr, value) do
if Keyword.keyword?(value) do
Enum.reduce(value, filter, fn
{predicate_name, value}, filter ->
do_add_attribute_filter(filter, attr, predicate_name, value)
end)
else
add_attribute_filter(filter, attr, eq: value)
end
end
defp do_add_attribute_filter(
%{attributes: attributes, resource: resource} = filter,
%{type: attr_type, name: attr_name},
predicate_name,
value
) do
case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do
{:ok, predicate} ->
new_attributes =
Map.update(
attributes,
attr_name,
predicate,
&Merge.merge(&1, predicate)
)
%{filter | attributes: new_attributes}
{:error, error} ->
add_error(filter, error)
end
end
def parse_predicates(resource, keyword, attr_name, attr_type) do
Enum.reduce(keyword, {:ok, nil}, fn {predicate_name, value}, {:ok, existing_predicate} ->
case parse_predicate(resource, predicate_name, attr_name, attr_type, value) do
{:ok, predicate} when is_nil(existing_predicate) ->
{:ok, predicate}
{:ok, predicate} ->
{:ok, Merge.merge(existing_predicate, predicate)}
{:error, error} ->
{:error, error}
end
end)
end
def count_of_clauses(nil), do: 0
def count_of_clauses(filter) do
relationship_clauses =
filter.relationships
|> Map.values()
|> Enum.map(fn related_filter ->
1 + count_of_clauses(related_filter)
end)
|> Enum.sum()
or_clauses =
filter.ors
|> Kernel.||([])
|> Enum.map(&count_of_clauses/1)
|> Enum.sum()
not_clauses = count_of_clauses(filter.not)
and_clauses =
filter.ands
|> Enum.map(&count_of_clauses/1)
|> Enum.sum()
Enum.count(filter.attributes) + relationship_clauses + or_clauses + not_clauses + and_clauses
end
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(all_predicates, predicate_name)},
{:type_can?, _, true} <-
{:type_can?, predicate_name,
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})},
{:predicate, _, {:ok, predicate}} <-
{:predicate, attr_name, predicate_type.new(resource, attr_name, attr_type, value)} do
{:ok, predicate}
else
{:predicate_type, :error} ->
{:error, :predicate_type, "No such filter type #{predicate_name}"}
{:predicate, attr_name, {:error, error}} ->
{:error, Map.put(error, :field, attr_name)}
{:type_can?, predicate_name, false} ->
{:error,
"Cannot use filter type #{inspect(predicate_name)} on type #{inspect(attr_type)}."}
{:data_layer_can?, predicate_name, false} ->
{:error, "data layer not capable of provided filter: #{predicate_name}"}
end
end
defp add_relationship_filter(
%{relationships: relationships} = filter,
%{destination: destination, name: name} = relationship,
value
) do
case parse_relationship_filter(value, relationship) do
{:ok, provided_filter} ->
related_filter = parse(destination, provided_filter, filter.api, [name | filter.path])
new_relationships =
Map.update(relationships, name, related_filter, &Merge.merge(&1, related_filter))
filter
|> Map.put(:relationships, new_relationships)
|> add_relationship_compatibility_error(relationship)
|> add_error(related_filter.errors)
|> add_request(related_filter.requests)
{:error, error} ->
add_error(filter, error)
end
end
defp parse_relationship_filter(value, %{destination: destination} = relationship) do
cond do
match?(%__MODULE__{}, value) ->
{:ok, value}
match?(%^destination{}, value) ->
PrimaryKeyHelpers.value_to_primary_key_filter(destination, value)
is_map(value) ->
{:ok, Map.to_list(value)}
Keyword.keyword?(value) ->
{:ok, value}
is_list(value) ->
parse_relationship_list_filter(value, relationship)
true ->
PrimaryKeyHelpers.value_to_primary_key_filter(destination, value)
end
end
defp parse_relationship_list_filter(value, relationship) do
Enum.reduce_while(value, {:ok, []}, fn item, items ->
case parse_relationship_filter(item, relationship) do
{:ok, item_filter} -> {:cont, {:ok, [item_filter | items]}}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp add_relationship_compatibility_error(%{resource: resource} = filter, %{
cardinality: cardinality,
destination: destination,
name: name
}) do
cond do
not Ash.data_layer_can?(resource, {:filter_related, cardinality}) ->
add_error(
filter,
"Cannot filter on relationship #{name}: #{inspect(Ash.data_layer(resource))} does not support it."
)
not (Ash.data_layer(destination) == Ash.data_layer(resource)) ->
add_error(
filter,
"Cannot filter on related entites unless they share a data layer, for now."
)
true ->
filter
end
end
defp add_request(filter, requests)
when is_list(requests),
do: %{filter | requests: filter.requests ++ requests}
defp add_request(%{requests: requests} = filter, request),
do: %{filter | requests: [request | requests]}
defp add_error(%{errors: errors} = filter, errors) when is_list(errors),
do: %{filter | errors: filter.errors ++ errors}
defp add_error(%{errors: errors} = filter, error), do: %{filter | errors: [error | errors]}
end
defimpl Inspect, for: Ash.Filter do
import Inspect.Algebra
import Ash.Filter.InspectHelpers
defguardp is_empty(val) when is_nil(val) or val == [] or val == %{}
def inspect(
%Ash.Filter{
not: not_filter,
ors: ors,
relationships: relationships,
attributes: attributes,
ands: ands
},
opts
)
when not is_nil(not_filter) and is_empty(ors) and is_empty(relationships) and
is_empty(attributes) and is_empty(ands) do
if root?(opts) do
concat(["#Filter<not ", to_doc(not_filter, make_non_root(opts)), ">"])
else
concat(["not ", to_doc(not_filter, make_non_root(opts))])
end
end
def inspect(%Ash.Filter{not: not_filter} = filter, opts) when not is_nil(not_filter) do
if root?(opts) do
concat([
"#Filter<not ",
to_doc(not_filter, make_non_root(opts)),
" and ",
to_doc(%{filter | not: nil}, make_non_root(opts)),
">"
])
else
concat([
"not ",
to_doc(not_filter, make_non_root(opts)),
" and ",
to_doc(%{filter | not: nil}, make_non_root(opts))
])
end
end
def inspect(
%Ash.Filter{ors: ors, relationships: relationships, attributes: attributes, ands: ands},
opts
)
when is_empty(ors) and is_empty(relationships) and is_empty(attributes) and is_empty(ands) do
if root?(opts) do
concat(["#Filter<", to_doc(nil, opts), ">"])
else
to_doc(nil, opts)
end
end
def inspect(filter, opts) do
rels = parse_relationships(filter, opts)
attrs = parse_attributes(filter, opts)
and_container =
case attrs ++ rels do
[] ->
empty()
[and_clause] ->
and_clause
and_clauses ->
Inspect.Algebra.container_doc("(", and_clauses, ")", opts, fn term, _ -> term end,
break: :flex,
separator: " and"
)
end
with_or_container =
case Map.get(filter, :ors) do
nil ->
and_container
[] ->
and_container
ors ->
inspected_ors = Enum.map(ors, fn filter -> to_doc(filter, make_non_root(opts)) end)
or_container =
Inspect.Algebra.container_doc(
"(",
inspected_ors,
")",
opts,
fn term, _ -> term end,
break: :strict,
separator: " or "
)
if Enum.empty?(attrs) && Enum.empty?(rels) do
or_container
else
concat(["(", and_container, " or ", or_container, ")"])
end
end
all_container =
case filter.ands do
[] ->
with_or_container
ands ->
docs = [with_or_container | Enum.map(ands, &Inspect.inspect(&1, make_non_root(opts)))]
Inspect.Algebra.container_doc(
"(",
docs,
")",
opts,
fn term, _ -> term end,
break: :strict,
separator: " and "
)
end
if root?(opts) do
concat(["#Filter<", all_container, ">"])
else
all_container
end
end
defp parse_relationships(%Ash.Filter{relationships: relationships}, _opts)
when relationships == %{},
do: []
defp parse_relationships(filter, opts) do
filter
|> Map.fetch!(:relationships)
|> Enum.map(fn {key, value} -> to_doc(value, add_to_path(opts, key)) end)
end
defp parse_attributes(%Ash.Filter{attributes: attributes}, _opts) when attributes == %{}, do: []
defp parse_attributes(filter, opts) do
filter
|> Map.fetch!(:attributes)
|> Enum.map(fn {key, value} -> to_doc(value, put_attr(opts, key)) end)
end
end