Packages
ash
2.17.8
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
# credo:disable-for-this-file Credo.Check.Readability.StrictModuleLayout
@dialyzer {:nowarn_function, do_map: 2, map: 2}
require Logger
require Ash.Expr
alias Ash.Engine.Request
alias Ash.Error.Query.{
InvalidFilterValue,
NoSuchAttributeOrRelationship,
NoSuchFilterPredicate,
NoSuchFunction,
NoSuchOperator
}
alias Ash.Error.Invalid.InvalidPrimaryKey
alias Ash.Query.Function.{
Ago,
At,
CompositeType,
Contains,
DateAdd,
DateTimeAdd,
FromNow,
GetPath,
If,
IsNil,
Length,
Minus,
Now,
Round,
StringJoin,
StringSplit,
Today,
Type
}
alias Ash.Query.Operator.{
Eq,
GreaterThan,
GreaterThanOrEqual,
In,
LessThan,
LessThanOrEqual,
NotEq
}
alias Ash.Query.{BooleanExpression, Call, Not, Ref}
alias Ash.Query.{Aggregate, Calculation, Function, Operator}
@functions [
Ago,
At,
CompositeType,
Contains,
DateAdd,
DateTimeAdd,
FromNow,
GetPath,
IsNil,
If,
Length,
Minus,
Now,
Round,
Today,
Type,
StringJoin,
StringSplit
]
@inline_aggregates [:count, :first, :sum, :list, :max, :min, :avg, :custom_aggregate]
@operators [
Ash.Query.Operator.IsNil,
Eq,
NotEq,
In,
LessThan,
GreaterThan,
LessThanOrEqual,
GreaterThanOrEqual
] ++ Ash.Query.Operator.Basic.operator_modules()
@builtins @functions ++ @operators
@operators_with_aliases @operators |> Enum.reject(&(&1.name() == &1.operator()))
@operator_aliases [
equals: Eq,
not_equals: NotEq,
gt: GreaterThan,
lt: LessThan,
gte: GreaterThanOrEqual,
lte: LessThanOrEqual
] ++ Enum.map(@operators_with_aliases, &{&1.name(), &1})
@moduledoc """
The representation of a filter in Ash.
## Security Concerns
If you are using a map with string keys, it is likely that you are parsing
input. It is important to note that, instead of passing a filter supplied from
an external source directly to `Ash.Query.filter/2`, you should call
`Ash.Filter.parse_input/2`. This ensures that the filter only uses public
attributes, relationships, aggregates and calculations, honors field policies
and any policies on related resources.
## Filter Templates
To see the available templates, see `Ash.Filter.TemplateHelpers`. You can
pass a filter template to `build_filter_from_template/2` with an actor, and it
will return the new result
Additionally, you can ask if the filter template contains an actor reference
via `template_references_actor?/1`
## Writing a filter
### Built In Predicates
#{Enum.map_join(@operators, "\n", &"* `#{&1.operator()}`")}
#{Enum.map_join(@operator_aliases, "\n", fn {key, val} -> "* `#{key}` (alias
for `#{val.operator()}`)" end)}
### BooleanExpression syntax
The expression syntax ultimately just builds the keyword list style filter,
but with lots of conveniences that would be very annoying to do manually.
Examples
```elixir
Ash.Query.filter(resource, name == "Zardoz")
Ash.Query.filter(resource, first_name == "Zar" and last_name == "Doz")
Ash.Query.filter(resource, first_name == "Zar" and last_name in ["Doz", "Daz"] and high_score > 10)
Ash.Query.filter(resource, first_name == "Zar" or last_name == "Doz" or (high_score > 10 and high_score < -10))
```
### Keyword list syntax
A filter is a nested keyword list (with some exceptions, like `true` for
everything and `false` for nothing).
The key is the "predicate" (or "condition") and the value is the parameter.
You can use `and` and `or` to create nested filters. Data layers can expose
custom predicates. Eventually, you will be able to define your own custom
predicates, which will be a mechanism for you to attach complex filters
supported by the data layer to your queries.
** Important ** In a given keyword list, all predicates are considered to be
"ands". So `[or: [first_name: "Tom", last_name: "Bombadil"]]` doesn't mean
'First name == "tom" or last_name == "bombadil"'. To say that, you want to
provide a list of filters, like so: `[or: [[first_name: "Tom"], [last_name:
"Bombadil"]]]`
Some example filters:
```elixir
Ash.Query.filter(resource, [name: "Zardoz"])
Ash.Query.filter(resource, [first_name: "Zar", last_name: "Doz"])
Ash.Query.filter(resource, [first_name: "Zar", last_name: [in: ["Doz", "Daz"]], high_score: [greater_than: 10]])
Ash.Query.filter(resource, [or: [
[first_name: "Zar"],
[last_name: "Doz"],
[or: [
[high_score: [greater_than: 10]]],
[high_score: [less_than: -10]]
]
]])
```
### Other formats
Maps are also accepted, as are maps with string keys. Technically, a list of
`[{"string_key", value}]` would also work.
"""
@builtin_operators Enum.map(@operators, &{&1.operator(), &1}) ++ @operator_aliases
@builtin_functions Enum.map(@functions, &{&1.name(), &1})
@string_builtin_operators Enum.into(@builtin_operators, %{}, fn {key, value} ->
{to_string(key), value}
end)
@string_builtin_functions Enum.into(@builtin_functions, %{}, fn {key, value} ->
{to_string(key), value}
end)
defstruct [:resource, :expression]
@type t :: %__MODULE__{}
def builtins, do: @builtins
def builtin_functions, do: @functions
def builtin_operators, do: @operators
def builtin_predicate_operators, do: Enum.filter(@operators, & &1.predicate?())
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
# Used for fetching related data in filters, which will have already had authorization rules applied
defmodule ShadowApi do
@moduledoc false
use Ash.Api, validate_config_inclusion?: false
resources do
allow_unregistered?(true)
end
end
@doc """
Parses a filter statement, accepting only public attributes/relationships,
honoring field policies & related resource policies.
See `parse/2` for more
"""
def parse_input(
resource,
statement,
_aggregates \\ %{},
_calculations \\ %{},
context \\ %{}
) do
context =
Map.merge(
%{
resource: resource,
root_resource: resource,
relationship_path: [],
public?: true,
input?: true,
data_layer: Ash.DataLayer.data_layer(resource)
},
context
)
with {:ok, expression} <- parse_expression(statement, context),
{:ok, expression} <- hydrate_refs(expression, context),
:ok <- validate_references(expression, resource) do
{:ok, %__MODULE__{expression: expression, resource: resource}}
end
end
@doc """
Parses a filter statement, accepting only public attributes/relationships,
honoring field policies & related resource policies, raising on errors.
See `parse_input/2` for more
"""
def parse_input!(resource, statement, aggregates \\ %{}, calculations \\ %{}, context \\ %{}) do
case parse_input(resource, statement, aggregates, calculations, context) do
{:ok, filter} ->
filter
{:error, error} ->
raise error
end
end
@doc """
Parses a filter statement
See `parse/2` for more
"""
def parse!(resource, statement, _aggregates \\ %{}, _calculations \\ %{}, context \\ %{}) do
case parse(resource, statement, %{}, %{}, context) do
{:ok, filter} ->
filter
{:error, error} ->
raise Ash.Error.to_error_class(error,
error_context: parse_error_context(resource, statement, context)
)
end
end
@doc """
Parses a filter statement
See the module documentation for more information on the supported formats for filter
statements.
### Important
If you are trying to validate a filter supplied from an external/untrusted source,
be sure to use `parse_input/2` instead! The only difference is that it only accepts
filters over public attributes/relationships.
"""
def parse(resource, statement, aggregates \\ %{}, calculations \\ %{}, context \\ %{})
def parse(_resource, nil, _aggregates, _calculations, _context) do
{:ok, nil}
end
def parse(resource, statement, _aggregates, _calculations, original_context) do
context =
Map.merge(
%{
resource: resource,
relationship_path: [],
public?: false,
input?: false,
root_resource: resource,
data_layer: Ash.DataLayer.data_layer(resource)
},
original_context
)
with {:ok, expression} <- parse_expression(statement, context),
{:ok, expression} <- hydrate_refs(expression, context),
:ok <- validate_references(expression, resource) do
{:ok, %__MODULE__{expression: expression, resource: resource}}
end
end
defp validate_references(expression, resource) do
refs =
expression
|> list_refs()
|> Enum.map(fn ref ->
field =
case ref.attribute do
field when is_atom(field) or is_binary(field) ->
case Ash.Resource.Info.field(resource, field) do
nil ->
field
field ->
field
end
field ->
field
end
%{ref | attribute: field}
end)
errors =
refs
|> Enum.flat_map(fn
%{attribute: attribute, relationship_path: relationship_path}
when is_atom(attribute) or is_binary(attribute) ->
[
NoSuchAttributeOrRelationship.exception(
attribute_or_relationship: attribute,
resource: Ash.Resource.Info.related(resource, relationship_path)
)
]
ref ->
field = ref.attribute
# This handles manually added calculations and aggregates
case Map.fetch(field, :filterable?) do
:error ->
[]
{:ok, true} ->
[]
{:ok, false} ->
[Ash.Error.Query.InvalidFilterReference.exception(field: field.name)]
{:ok, :simple_equality} ->
if ref.simple_equality? do
[]
else
[
Ash.Error.Query.InvalidFilterReference.exception(
field: field.name,
simple_equality?: true
)
]
end
end
end)
multiple_filter_errors =
refs
|> Enum.filter(fn ref ->
is_map(ref.attribute) &&
Map.fetch(ref.attribute, :filterable?) == {:ok, :simple_equality}
end)
|> Enum.group_by(& &1.attribute.name)
|> Enum.flat_map(fn
{_, []} ->
[]
{_, [_]} ->
[]
{name, _} ->
[
Ash.Error.Query.InvalidFilterReference.exception(
field: name,
simple_equality?: true
)
]
end)
case Enum.concat(errors, multiple_filter_errors) do
[] ->
:ok
errors ->
{:error, Enum.uniq(errors)}
end
end
@doc """
Returns a filter statement that would find a single record based on the input.
For example:
iex> get_filter(MyApp.Post, 1)
{:ok, %{id: 1}} #using primary key
iex> get_filter(MyApp.Post, id: 1)
{:ok, %{id: 1}} #using primary key
iex> get_filter(MyApp.Post, author_id: 1, publication_id: 2, first_name: "fred")
{:ok, %{author_id: 1, publication_id: 1}} # using a unique identity
iex> get_filter(MyApp.Post, first_name: "fred")
:error # not enough information
"""
def get_filter(resource, id) do
primary_key = Ash.Resource.Info.primary_key(resource)
keyval? = Keyword.keyword?(id) || is_map(id)
case {primary_key, id} do
{[field], [{field, value}]} ->
case cast_value(resource, field, value, id) do
{:ok, value} ->
{:ok, %{field => value}}
{:error, error} ->
{:error, error}
end
{[field], value} when not keyval? ->
case cast_value(resource, field, value, id) do
{:ok, value} ->
{:ok, %{field => value}}
{:error, error} ->
{:error, error}
end
{fields, value} ->
if keyval? do
with :error <- get_keys(value, fields, resource),
:error <- get_identity_filter(resource, id) do
{:error, InvalidPrimaryKey.exception(resource: resource, value: id)}
end
else
{:error, InvalidPrimaryKey.exception(resource: resource, value: id)}
end
end
end
defp get_keys(value, fields, resource) do
original_value = value
Enum.reduce_while(fields, {:ok, %{}}, fn field, {:ok, vals} ->
case fetch(value, field) do
{:ok, value} ->
case cast_value(resource, field, value, original_value) do
{:ok, value} ->
{:cont, {:ok, Map.put(vals, field, value)}}
{:error, error} ->
{:halt, {:error, error}}
end
:error ->
case fetch(value, to_string(field)) do
{:ok, value} ->
case cast_value(resource, field, value, original_value) do
{:ok, value} ->
{:cont, {:ok, Map.put(vals, field, value)}}
{:error, error} ->
{:error, error}
end
:error ->
{:halt, :error}
end
end
end)
end
defp cast_value(resource, field, value, id) do
attribute = Ash.Resource.Info.attribute(resource, field)
if attribute do
case Ash.Type.cast_input(attribute.type, value, attribute.constraints) do
{:ok, value} ->
{:ok, value}
_ ->
{:error, InvalidPrimaryKey.exception(resource: resource, value: id)}
end
else
{:error, InvalidPrimaryKey.exception(resource: resource, value: id)}
end
end
defp fetch(val, key) when is_map(val), do: Map.fetch(val, key)
defp fetch(val, key) when is_list(val) and is_atom(key), do: Keyword.fetch(val, key)
defp fetch(_, _), do: :error
defp get_identity_filter(resource, id) do
resource
|> Ash.Resource.Info.identities()
|> Enum.find_value(
:error,
fn identity ->
case get_keys(id, identity.keys, resource) do
{:ok, key} ->
{:ok, key}
_ ->
nil
end
end
)
end
@to_simple_filter_options [
skip_invalid?: [
type: :boolean,
default: false,
doc:
"If an invalid filter expression is reached that can't be used with a simple filter (like an `or` statement, or a non-predicate expression), it will be ignored instead of raising an error."
]
]
@doc """
Transform an expression based filter to a simple filter, which is just a list of predicates
Options:
- skip_invalid?:
"""
def to_simple_filter(%{resource: resource, expression: expression}, opts \\ []) do
opts = NimbleOptions.validate!(opts, @to_simple_filter_options)
predicates = get_predicates(expression, opts[:skip_invalid?])
%Simple{resource: resource, predicates: predicates}
end
@doc "Replace any actor value references in a template with the values from a given actor"
def build_filter_from_template(template, actor \\ nil, args \\ %{}, context \\ %{}) do
walk_filter_template(template, fn
{:_actor, :_primary_key} ->
if actor do
Map.take(actor, Ash.Resource.Info.primary_key(actor.__struct__))
end
{:_actor, field} when is_atom(field) or is_binary(field) ->
Map.get(actor || %{}, field)
{:_actor, path} when is_list(path) ->
get_path(actor || %{}, path)
{:_arg, field} ->
case Map.fetch(args, field) do
:error ->
Map.get(args, to_string(field))
{:ok, value} ->
value
end
{:_context, fields} when is_list(fields) ->
get_path(context, fields)
{:_context, field} ->
Map.get(context, field)
{:_ref, path, name} ->
%Ref{
attribute: build_filter_from_template(name, actor, args, context),
relationship_path: build_filter_from_template(path, actor, args, context)
}
other ->
other
end)
end
defp get_path(map, [key]) when is_struct(map) do
Map.get(map, key)
end
defp get_path(map, [key]) when is_map(map) do
Map.get(map, key)
end
defp get_path(map, [key | rest]) when is_map(map) do
get_path(get_path(map, [key]), rest)
end
defp get_path(_, _), do: nil
def template_references_actor?(template) do
template_references?(template, fn
{:_actor, _} -> true
_ -> false
end)
end
def template_references_argument?(template) do
template_references?(template, fn
{:_arg, _} -> true
_ -> false
end)
end
def template_references_context?(template) do
template_references?(template, fn
{:_context, _} -> true
_ -> false
end)
end
@doc "Whether or not a given template contains an actor reference"
def template_references?(%BooleanExpression{op: :and, left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%Not{expression: expression}, pred) do
template_references?(expression, pred)
end
def template_references?(%Ash.Query.Exists{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(%Ash.Query.Parent{expr: expr}, pred) do
template_references?(expr, pred)
end
def template_references?(%{left: left, right: right}, pred) do
template_references?(left, pred) || template_references?(right, pred)
end
def template_references?(%{arguments: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(%Ash.Query.Call{args: args}, pred) do
Enum.any?(args, &template_references?(&1, pred))
end
def template_references?(list, pred) when is_list(list) do
Enum.any?(list, &template_references?(&1, pred))
end
def template_references?(map, pred) when is_map(map) and not is_struct(map) do
Enum.any?(map, &template_references?(&1, pred))
end
def template_references?(tuple, pred) when is_tuple(tuple) do
pred.(tuple) ||
tuple
|> Tuple.to_list()
|> Enum.any?(&template_references?(&1, pred))
end
def template_references?(thing, pred), do: pred.(thing)
@doc false
def walk_filter_template(filter, mapper) when is_list(filter) do
case mapper.(filter) do
^filter ->
Enum.map(filter, &walk_filter_template(&1, mapper))
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%BooleanExpression{left: left, right: right} = expr, mapper) do
case mapper.(expr) do
^expr ->
%{
expr
| left: walk_filter_template(left, mapper),
right: walk_filter_template(right, mapper)
}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%Not{expression: expression} = not_expr, mapper) do
case mapper.(not_expr) do
^not_expr ->
%{not_expr | expression: walk_filter_template(expression, mapper)}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%Ash.Query.Parent{expr: expr} = this_expr, mapper) do
case mapper.(this_expr) do
^this_expr ->
%{this_expr | expr: walk_filter_template(expr, mapper)}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%Ash.Query.Exists{expr: expr} = exists_expr, mapper) do
case mapper.(exists_expr) do
^exists_expr ->
%{exists_expr | expr: walk_filter_template(expr, mapper)}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%{__predicate__?: _, left: left, right: right} = pred, mapper) do
case mapper.(pred) do
^pred ->
%{
pred
| left: walk_filter_template(left, mapper),
right: walk_filter_template(right, mapper)
}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%{__predicate__?: _, arguments: arguments} = func, mapper) do
case mapper.(func) do
^func ->
%{
func
| arguments: Enum.map(arguments, &walk_filter_template(&1, mapper))
}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(%Call{args: args} = call, mapper) do
case mapper.(call) do
^call ->
%{
call
| args: Enum.map(args, &walk_filter_template(&1, mapper))
}
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(filter, mapper) when is_map(filter) do
if Map.has_key?(filter, :__struct__) do
filter
else
case mapper.(filter) do
^filter ->
Enum.into(filter, %{}, &walk_filter_template(&1, mapper))
other ->
walk_filter_template(other, mapper)
end
end
end
def walk_filter_template(tuple, mapper) when is_tuple(tuple) do
case mapper.(tuple) do
^tuple ->
tuple
|> Tuple.to_list()
|> Enum.map(&walk_filter_template(&1, mapper))
|> List.to_tuple()
other ->
walk_filter_template(other, mapper)
end
end
def walk_filter_template(value, mapper), do: mapper.(value)
@doc """
Can be used to find a simple equality predicate on an attribute
Use this when your attribute is configured with `filterable? :simple_equality`, and you want to
to find the value that it is being filtered on with (if any).
"""
def find_simple_equality_predicate(expression, attribute) do
expression
|> find(&simple_eq?(&1, attribute), false)
|> case do
nil ->
nil
%{right: right, left: left} ->
Enum.find([right, left], fn value ->
!Ash.Filter.TemplateHelpers.expr?(value)
end)
end
end
defp simple_eq?(%Eq{left: %Ref{}, right: %Ref{}}, _), do: false
defp simple_eq?(%Eq{right: %Ref{}} = eq, attribute) do
simple_eq?(%{eq | left: eq.right, right: eq.left}, attribute)
end
defp simple_eq?(%Eq{left: %Ref{attribute: attribute}}, attribute), do: true
defp simple_eq?(%Eq{left: %Ref{attribute: %{name: attribute}}}, attribute), do: true
defp simple_eq?(_, _), do: false
def find_value(expr, pred) do
do_find(expr, pred, true, true, true)
end
@doc "Find an expression inside of a filter that matches the provided predicate"
def find(expr, pred, ors? \\ true, ands? \\ true) do
do_find(expr, pred, false, ors?, ands?)
end
defp do_find(expr, pred, value?, ors?, ands?) do
if value = pred.(expr) do
if value? do
value
else
expr
end
else
case expr do
%__MODULE__{expression: expression} ->
find(expression, pred, ors?, ands?)
%Not{expression: expression} ->
find(expression, pred, ors?, ands?)
%BooleanExpression{op: op, left: left, right: right} ->
cond do
op == :ors && !ors? ->
nil
op == :ands && !ands? ->
nil
true ->
find(left, pred, ors?, ands?) || find(right, pred, ors?, ands?)
end
%Call{args: arguments} ->
Enum.find(arguments, &find(&1, pred, ors?, ands?))
%{__operator__?: true, left: left, right: right} ->
find(left, pred, ors?, ands?) || find(right, pred, ors?, ands?)
%{__function__?: true, arguments: arguments} ->
Enum.find(arguments, &find(&1, pred, ors?, ands?))
_ ->
nil
end
end
end
defp get_predicates(expr, skip_invalid?, acc \\ [])
defp get_predicates(true, _skip_invalid?, acc), do: acc
defp get_predicates(false, _, _), do: false
defp get_predicates(_, _, false), do: false
defp get_predicates(%BooleanExpression{op: :and, left: left, right: right}, skip_invalid?, acc) do
acc = get_predicates(left, skip_invalid?, acc)
get_predicates(right, skip_invalid?, acc)
end
defp get_predicates(%Not{expression: expression}, skip_invalid?, acc) do
expression
|> get_predicates(skip_invalid?)
|> Enum.reduce(acc, fn predicate, acc ->
[%Simple.Not{predicate: predicate} | acc]
end)
end
defp get_predicates(%{__predicate__?: true} = predicate, _skip_invalid?, acc),
do: [predicate | acc]
defp get_predicates(_invalid, true, acc), do: acc
defp get_predicates(invalid, false, _acc) do
raise "Invalid filter statement provided: #{inspect(invalid)} while constructing a simple filter. To skip invalid statements, use `skip_invalid?: true`."
end
def used_calculations(
filter,
resource,
relationship_path \\ [],
_calculations \\ %{},
_aggregates \\ %{}
) do
filter
|> list_refs()
|> Enum.filter(fn
%Ref{attribute: %Calculation{}, relationship_path: ref_relationship_path} ->
(relationship_path in [nil, []] and ref_relationship_path in [nil, []]) ||
relationship_path == ref_relationship_path
_ ->
false
end)
|> Enum.map(& &1.attribute)
|> calculations_used_by_calculations(resource, relationship_path)
end
defp calculations_used_by_calculations(
used_calculations,
resource,
relationship_path
) do
used_calculations
|> Enum.flat_map(fn calculation ->
expression = calculation.module.expression(calculation.opts, calculation.context)
case hydrate_refs(expression, %{
resource: resource,
relationship_path: [],
public?: false
}) do
{:ok, expression} ->
with_recursive_used =
calculations_used_by_calculations(
used_calculations(
expression,
resource,
relationship_path
),
resource,
relationship_path
)
[calculation | with_recursive_used]
_ ->
[calculation]
end
end)
end
def used_aggregates(filter, relationship_path \\ [], return_refs? \\ false) do
refs =
filter
|> list_refs()
|> Enum.filter(fn
%Ref{attribute: %Aggregate{}, relationship_path: ref_relationship_path} ->
relationship_path == :all ||
(relationship_path in [nil, []] and ref_relationship_path in [nil, []]) ||
relationship_path == ref_relationship_path
_ref ->
false
end)
if return_refs? do
refs
else
Enum.map(refs, & &1.attribute)
end
|> Enum.uniq()
end
def put_at_path(value, []), do: value
def put_at_path(value, [key | rest]), do: [{key, put_at_path(value, rest)}]
def add_to_filter!(
base,
addition,
op \\ :and,
aggregates \\ %{},
calculations \\ %{},
context \\ %{}
) do
case add_to_filter(base, addition, op, aggregates, calculations, context) do
{:ok, value} ->
value
{:error, error} ->
raise Ash.Error.to_ash_error(error, nil,
error_context: parse_error_context(base.resource, addition, context)
)
end
end
def add_to_filter(
base,
addition,
op \\ :and,
aggregates \\ %{},
calculations \\ %{},
context \\ %{}
)
def add_to_filter(nil, %__MODULE__{} = addition, _, _, _, _), do: {:ok, addition}
def add_to_filter(
%__MODULE__{} = base,
%__MODULE__{} = addition,
op,
_,
_,
_
) do
{:ok,
%{
base
| expression: BooleanExpression.optimized_new(op, base.expression, addition.expression)
}}
end
def add_to_filter(%__MODULE__{} = base, statement, op, aggregates, calculations, context) do
case parse(base.resource, statement, %{}, %{}, context) do
{:ok, filter} -> add_to_filter(base, filter, op, aggregates, calculations)
{:error, error} -> {:error, error}
end
end
defp parse_error_context(%{resource: resource} = _filter, addition, context) do
parse_error_context(resource, addition, context)
end
defp parse_error_context(resource, addition, context) do
context_str = if context, do: ", given context: #{inspect(context)}", else: ""
"parsing addition of filter statement: #{inspect(addition)}, to resource: #{inspect(resource)}" <>
context_str
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 read_requests(_, nil, _, _, _), do: {:ok, []}
def read_requests(api, %{resource: resource} = filter, request_path, actor, tenant) do
paths_with_refs =
filter
|> relationship_paths(true, true)
|> Enum.map(fn {path, refs} ->
{path, Enum.filter(refs, &(&1 && &1.input?))}
end)
|> Enum.reject(fn {path, refs} -> path == [] || refs == [] end)
refs = group_refs_by_all_paths(paths_with_refs)
paths_with_refs
|> Enum.map(&elem(&1, 0))
|> Enum.reduce_while({:ok, []}, fn path, {:ok, requests} ->
last_relationship =
Enum.reduce(path, nil, fn
relationship, nil ->
Ash.Resource.Info.relationship(resource, relationship)
relationship, acc ->
Ash.Resource.Info.relationship(acc.destination, relationship)
end)
case relationship_query(resource, path, actor, tenant) do
%{errors: []} = query ->
request =
Request.new(
resource: query.resource,
api: api,
query:
query
|> Ash.Query.set_context(%{
accessing_from: %{
source: last_relationship.source,
name: last_relationship.name
}
})
|> Ash.Query.set_context(%{
filter_only?: true,
filter_references: refs[path] || []
})
|> Ash.Query.select([]),
async?: false,
path: request_path ++ [:filter, path],
strict_check_only?: true,
action: query.action,
name: "authorize filter #{Enum.join(path, ".")}",
data: []
)
{:cont, {:ok, [request | requests]}}
%{errors: errors} ->
{:halt, {:error, errors}}
end
end)
end
defp relationship_query(resource, [last], actor, tenant) do
relationship = Ash.Resource.Info.relationship(resource, last)
action =
relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.destination, :read).name
relationship.destination
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.sort(relationship.sort, prepend?: true)
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|> Ash.Query.for_read(action, %{},
actor: actor,
authorize?: true,
tenant: tenant
)
end
defp relationship_query(resource, [next | rest], actor, tenant) do
resource
|> Ash.Resource.Info.related(next)
|> relationship_query(rest, actor, tenant)
end
defp group_refs_by_all_paths(paths_with_refs) do
all_paths_with_refs =
paths_with_refs
|> Enum.flat_map(fn {path, refs} ->
Enum.map(refs, fn ref -> {path, ref} end)
end)
|> Enum.uniq()
acc = %{
[] => Enum.map(all_paths_with_refs, &elem(&1, 1))
}
Enum.reduce(all_paths_with_refs, acc, &add_ref_to_relevant_paths/2)
end
defp add_ref_to_relevant_paths(path_ref, acc, trail \\ [])
defp add_ref_to_relevant_paths({[], ref}, acc, trail) do
Map.update(acc, trail, [ref], &[ref | &1])
end
defp add_ref_to_relevant_paths({[next | rest], ref}, acc, trail) do
new_trail = trail ++ [next]
new_acc = Map.update(acc, new_trail, [ref], &[ref | &1])
add_ref_to_relevant_paths({rest, ref}, new_acc, new_trail)
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
defp do_map(expression, func) do
case expression do
{:halt, expr} ->
expr
%BooleanExpression{left: left, right: right} = expr ->
%{expr | left: map(left, func), right: map(right, func)}
%Not{expression: not_expr} = expr ->
%{expr | expression: map(not_expr, func)}
%Ash.Query.Parent{} = this ->
# you have to map over the internals of this yourself
func.(this)
%Ash.Query.Exists{} = expr ->
# you have to map over the internals of exists yourself
func.(expr)
%{__operator__?: true, left: left, right: right} = op ->
%{op | left: map(left, func), right: map(right, func)}
%{__function__?: true, arguments: arguments} = function ->
%{
function
| arguments:
Enum.map(arguments, fn
{key, arg} when is_atom(key) ->
{key, map(arg, func)}
arg ->
map(arg, func)
end)
}
other ->
func.(other)
end
end
def flat_map(%__MODULE__{expression: nil}, _) do
[]
end
def flat_map(%__MODULE__{expression: expression}, func) do
flat_map(expression, func)
end
def flat_map(expression, func) do
do_flat_map(expression, func)
end
defp do_flat_map(expression, func) do
case expression do
%BooleanExpression{left: left, right: right} ->
func.(expression) ++ flat_map(left, func) ++ flat_map(right, func)
%Not{expression: not_expr} ->
func.(expression) ++ flat_map(not_expr, func)
%Ash.Query.Parent{} = this ->
# you have to flat_map over the internals of this yourself
func.(this)
%Ash.Query.Exists{} = expr ->
# you have to flat_map over the internals of exists yourself
func.(expr)
%{__operator__?: true, left: left, right: right} = op ->
func.(op) ++ flat_map(left, func) ++ flat_map(right, func)
%{__function__?: true, arguments: arguments} = function ->
func.(function) ++ Enum.flat_map(arguments, &flat_map(&1, func))
other ->
func.(other)
end
end
def update_aggregates(%__MODULE__{expression: expression} = filter, mapper) do
%{filter | expression: update_aggregates(expression, mapper)}
end
def update_aggregates(expression, mapper) do
case expression do
{key, value} when is_atom(key) ->
{key, update_aggregates(value, mapper)}
%Not{expression: expression} = not_expr ->
%{not_expr | expression: update_aggregates(expression, mapper)}
%BooleanExpression{left: left, right: right} = expression ->
%{
expression
| left: update_aggregates(left, mapper),
right: update_aggregates(right, mapper)
}
%{__operator__?: true, left: left, right: right} = op ->
left = update_aggregates(left, mapper)
right = update_aggregates(right, mapper)
%{op | left: left, right: right}
%{__function__?: true, arguments: args} = func ->
%{func | arguments: Enum.map(args, &update_aggregates(&1, mapper))}
%Ref{attribute: %Aggregate{} = agg} = ref ->
%{ref | attribute: mapper.(agg, ref)}
other ->
other
end
end
def run_other_data_layer_filters(api, resource, %{expression: expression} = filter, data) do
case do_run_other_data_layer_filters(expression, api, resource, data) do
{:filter_requests, requests} -> {:filter_requests, requests}
{:ok, new_expression} -> {:ok, %{filter | expression: new_expression}}
{:error, error} -> {:error, error}
end
end
def run_other_data_layer_filters(_, _, filter, _data) when filter in [nil, true, false],
do: {:ok, filter}
defp do_run_other_data_layer_filters(
%BooleanExpression{op: op, left: left, right: right},
api,
resource,
data
) do
left_result = do_run_other_data_layer_filters(left, api, resource, data)
right_result = do_run_other_data_layer_filters(right, api, resource, data)
case {left_result, right_result} do
{{:ok, left}, {:ok, right}} ->
{:ok, BooleanExpression.optimized_new(op, left, right)}
{{:error, error}, _} ->
{:error, error}
{_, {:error, error}} ->
{:error, error}
{{:filter_requests, left_filter_requests}, {:filter_requests, right_filter_requests}} ->
{:filter_requests, left_filter_requests ++ right_filter_requests}
{{:filter_requests, left_filter_requests}, _} ->
{:filter_requests, left_filter_requests}
{_, {:filter_requests, right_filter_requests}} ->
{:filter_requests, right_filter_requests}
end
end
defp do_run_other_data_layer_filters(%Not{expression: expression}, api, resource, data) do
case do_run_other_data_layer_filters(expression, api, resource, data) do
{:ok, expr} -> {:ok, Not.new(expr)}
{:filter_requests, requests} -> {:filter_requests, requests}
{:error, error} -> {:error, error}
end
end
defp do_run_other_data_layer_filters(
%Ash.Query.Exists{path: path, expr: expr, at_path: at_path} = exists,
api,
resource,
{request_path, tenant, data}
) do
case shortest_path_to_changed_data_layer(resource, at_path ++ path) do
{:ok, shortest_path} ->
request_path = request_path ++ [:other_data_layer_filter_exists, at_path] ++ path
related = Ash.Resource.Info.related(resource, shortest_path)
case get_in(data, request_path ++ [:data]) do
%{data: data} ->
pkey = Ash.Resource.Info.primary_key(related)
expr =
Enum.reduce(data, nil, fn item, expr ->
new_expr =
Enum.reduce(pkey, nil, fn key, expr ->
{:ok, new_expr} =
Ash.Query.Operator.new(
Ash.Query.Operator.Eq,
%Ash.Query.Ref{
attribute: key,
relationship_path: at_path
},
Map.get(item, key)
)
if expr do
Ash.Query.BooleanExpression.new(:and, expr, new_expr)
else
new_expr
end
end)
if expr do
Ash.Query.BooleanExpression.new(:or, expr, new_expr)
else
new_expr
end
end)
{:ok, expr}
nil ->
{context, action} = last_relationship_context_and_action(resource, at_path ++ path)
query =
related
|> Ash.Query.do_filter(expr)
|> Ash.Query.set_context(context)
{:filter_requests,
Ash.Actions.Read.as_requests(
request_path,
query.resource,
api,
action,
query: query,
page: false,
tenant: tenant
)
|> Enum.map(fn request ->
# By returning the request and a key, we register a dependency on that key
{request, :data}
end)}
end
:error ->
case do_run_other_data_layer_filters(
expr,
api,
Ash.Resource.Info.related(resource, at_path ++ path),
data
) do
{:ok, new_nested} ->
{:ok, %{exists | expr: new_nested}}
{:error, error} ->
{:error, error}
{:filter_requests, requests} ->
{:filter_requests, requests}
end
end
end
defp do_run_other_data_layer_filters(%{__predicate__?: _} = predicate, api, resource, data) do
predicate
|> relationship_paths()
|> filter_paths_that_change_data_layers(resource)
|> Enum.find_value(fn path ->
case split_expression_by_relationship_path(predicate, path) do
{nil, _} ->
nil
{for_path, nil} ->
{path, for_path}
end
end)
|> case do
nil ->
{:ok, predicate}
{path, new_predicate} ->
relationship = Ash.Resource.Info.relationship(resource, path)
fetch_related_data(resource, path, new_predicate, api, relationship, data)
end
end
defp do_run_other_data_layer_filters(other, _api, _resource, _data), do: {:ok, other}
defp last_relationship_context_and_action(resource, [name]) do
relationship = Ash.Resource.Info.relationship(resource, name)
{relationship.context,
relationship.read_action ||
Ash.Resource.Info.primary_action!(relationship.destination, :read)}
end
defp last_relationship_context_and_action(resource, path) do
second_to_last = Ash.Resource.Info.related(resource, :lists.droplast(path))
relationship = Ash.Resource.Info.relationship(second_to_last, List.last(path))
{relationship.context, relationship.read_action}
end
defp split_expression_by_relationship_path(%{expression: expression}, path) do
split_expression_by_relationship_path(expression, path)
end
defp split_expression_by_relationship_path(
%BooleanExpression{op: op, left: left, right: right},
path
) do
{new_for_path_left, new_without_path_left} = split_expression_by_relationship_path(left, path)
{new_for_path_right, new_without_path_right} =
split_expression_by_relationship_path(right, path)
{BooleanExpression.optimized_new(op, new_for_path_left, new_for_path_right),
BooleanExpression.optimized_new(op, new_without_path_left, new_without_path_right)}
end
defp split_expression_by_relationship_path(%Not{expression: expression}, path) do
{new_for_path, new_without_path} = split_expression_by_relationship_path(expression, path)
{Not.new(new_for_path), Not.new(new_without_path)}
end
defp split_expression_by_relationship_path(
%{
__predicate__?: _,
left: left,
right: right
} = predicate,
path
) do
refs = list_refs([left, right])
if Enum.any?(refs, &List.starts_with?(&1.relationship_path, path)) do
if Enum.all?(refs, &List.starts_with?(&1.relationship_path, path)) do
{scope_refs(predicate, path), nil}
else
{scope_refs(predicate, path), predicate}
end
else
{nil, predicate}
end
end
defp split_expression_by_relationship_path(
%{__predicate__?: _, arguments: args} = predicate,
path
) do
refs = list_refs(args)
if Enum.any?(refs, &List.starts_with?(&1.relationship_path, path)) do
if Enum.all?(refs, &List.starts_with?(&1.relationship_path, path)) do
{scope_refs(predicate, path), nil}
else
{scope_refs(predicate, path), predicate}
end
else
{nil, predicate}
end
end
defp scope_refs(%BooleanExpression{left: left, right: right} = expr, path) do
%{expr | left: scope_refs(left, path), right: scope_refs(right, path)}
end
defp scope_refs(%Not{expression: expression} = expr, path) do
%{expr | expression: scope_refs(expression, path)}
end
defp scope_refs(%{__predicate__?: _, left: left, right: right} = pred, path) do
%{pred | left: scope_refs(left, path), right: scope_refs(right, path)}
end
defp scope_refs(%{__predicate__?: _, arguments: arguments} = pred, path) do
%{pred | args: Enum.map(arguments, &scope_refs(&1, path))}
end
defp scope_refs({key, value}, path) do
{key, scope_refs(value, path)}
end
defp scope_refs(%Ref{relationship_path: ref_path} = ref, path) do
if List.starts_with?(ref_path, path) do
%{ref | relationship_path: Enum.drop(ref_path, Enum.count(path))}
else
ref
end
end
defp scope_refs(other, _), do: other
def prefix_refs(%BooleanExpression{left: left, right: right} = expr, path) do
%{expr | left: prefix_refs(left, path), right: prefix_refs(right, path)}
end
def prefix_refs(%Not{expression: expression} = expr, path) do
%{expr | expression: prefix_refs(expression, path)}
end
def prefix_refs(%{__predicate__?: _, left: left, right: right} = pred, path) do
%{pred | left: prefix_refs(left, path), right: prefix_refs(right, path)}
end
def prefix_refs(%{__predicate__?: _, argsuments: arguments} = pred, path) do
%{pred | args: Enum.map(arguments, &prefix_refs(&1, path))}
end
def prefix_refs(%Ref{relationship_path: ref_path} = ref, path) do
if List.starts_with?(ref_path, path) do
%{ref | relationship_path: path ++ ref_path}
else
ref
end
end
def prefix_refs(other, _), do: other
defp fetch_related_data(
resource,
path,
new_predicate,
api,
%{type: :many_to_many, join_relationship: join_relationship, through: through} =
relationship,
{_, _, context} = data
) do
if Ash.DataLayer.data_layer(through) == Ash.DataLayer.data_layer(resource) &&
Ash.DataLayer.data_layer_can?(resource, {:join, through}) do
filter = %__MODULE__{
resource: relationship.destination,
expression: new_predicate
}
relationship.destination
|> Ash.Query.new(api)
|> Ash.Query.do_filter(filter)
|> filter_related_in(
relationship,
:lists.droplast(path) ++ [join_relationship],
api,
data
)
else
filter = %__MODULE__{
resource: through,
expression: new_predicate
}
relationship.destination
|> Ash.Query.new(ShadowApi)
|> Ash.Query.do_filter(filter)
|> Ash.Actions.Read.unpaginated_read(
authorize?: context[:authorize?],
actor: context[:actor]
)
|> case do
{:ok, results} ->
relationship.through
|> Ash.Query.new(api)
|> Ash.Query.do_filter([
{relationship.destination_attribute_on_join_resource,
in: Enum.map(results, &Map.get(&1, relationship.destination_attribute))}
])
|> filter_related_in(
Ash.Resource.Info.relationship(resource, join_relationship),
:lists.droplast(path),
api,
data
)
{:error, error} ->
{:error, error}
end
end
end
defp fetch_related_data(
_resource,
path,
new_predicate,
api,
relationship,
data
) do
filter = %__MODULE__{
resource: relationship.destination,
expression: new_predicate
}
relationship.destination
|> Ash.Query.new(api)
|> Ash.Query.do_filter(filter)
|> Ash.Query.do_filter(relationship.filter, parent_stack: relationship.source)
|> Ash.Query.sort(relationship.sort, prepend?: true)
|> Ash.Query.set_context(relationship.context)
|> filter_related_in(relationship, :lists.droplast(path), api, data)
end
defp filter_related_in(
query,
relationship,
path,
api,
{request_path, tenant, data}
) do
query = Ash.Query.set_tenant(query, tenant)
request_path = request_path ++ [:other_data_layer_filter, path ++ [relationship.name], query]
case get_in(data, request_path ++ [:data]) do
%{data: records} ->
records_to_expression(
records,
relationship,
path
)
_ ->
action =
Ash.Resource.Info.action(relationship.destination, relationship.read_action) ||
Ash.Resource.Info.primary_action!(relationship.destination, :read)
action = %{action | pagination: false}
{:filter_requests,
Ash.Actions.Read.as_requests(request_path, query.resource, api, action,
query: query,
page: false,
tenant: tenant
)
|> Enum.map(fn request ->
# By returning the request and a key, we register a dependency on that key
{request, :data}
end)}
end
end
defp records_to_expression([], _, _), do: {:ok, false}
defp records_to_expression([single_record], relationship, path) do
Ash.Query.Operator.new(
Eq,
%Ref{
relationship_path: path,
resource: relationship.source,
attribute: Ash.Resource.Info.attribute(relationship.source, relationship.source_attribute)
},
Map.get(single_record, relationship.destination_attribute)
)
end
defp records_to_expression(records, relationship, path) do
Enum.reduce_while(records, {:ok, true}, fn record, {:ok, expression} ->
case records_to_expression([record], relationship, path) do
{:ok, operator} ->
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
{:error, error} ->
{:halt, {:error, error}}
end
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.Info.relationship(resource, relationship)
if relationship.type == :many_to_many do
if Ash.DataLayer.data_layer_can?(resource, {:join, relationship.through}) do
shortest_path_to_changed_data_layer(relationship.destination, rest, [
relationship.name | acc
])
else
{:ok, Enum.reverse([relationship.name | acc])}
end
else
if Ash.DataLayer.data_layer_can?(resource, {:join, relationship.destination}) do
shortest_path_to_changed_data_layer(relationship.destination, rest, [
relationship.name | acc
])
else
{:ok, Enum.reverse([relationship.name | acc])}
end
end
end
def relationship_paths(filter_or_expression, include_exists? \\ false, with_reference? \\ false)
def relationship_paths(nil, _, _), do: []
def relationship_paths(%{expression: nil}, _, _), do: []
def relationship_paths(%__MODULE__{expression: expression}, include_exists?, with_reference?),
do: relationship_paths(expression, include_exists?, with_reference?)
def relationship_paths(expression, include_exists?, with_reference?) do
paths =
expression
|> do_relationship_paths(include_exists?, with_reference?)
|> List.wrap()
|> List.flatten()
if with_reference? do
paths
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
|> Map.new(fn {key, values} ->
{key, Enum.uniq(values)}
end)
else
paths
|> Enum.uniq()
|> Enum.map(fn {path} -> path end)
end
end
defp do_relationship_paths(
%Ref{
relationship_path: path,
resource: resource,
attribute: %Ash.Query.Calculation{module: module, opts: opts, context: context}
} = ref,
include_exists?,
with_references?
) do
if function_exported?(module, :expression, 2) do
expression = module.expression(opts, context)
case hydrate_refs(expression, %{
resource: resource,
relationship_path: [],
public?: false
}) do
{:ok, expression} ->
path_and_ref =
if with_references? do
{path, ref}
else
{path}
end
nested =
expression
|> do_relationship_paths(include_exists?, with_references?)
|> List.wrap()
|> List.flatten()
# This validation needs to be added back in at some point
# it was removed because we currently can't tell the difference between relationship paths
# that appear inside of the path to `exists`, i.e `exists(to_many, ....)`.
# nested
# |> Enum.map(fn
# {nested_path, _ref} ->
# nested_path
# {nested_path} ->
# nested_path
# end)
# |> Enum.find(fn path ->
# not to_one_path?(path, resource)
# end)
# |> case do
# nil ->
# :ok
# path ->
# raise """
# Only to-one relationship references are allowed in a calculation reference paths.
# Got: #{inspect(Enum.join(path, "."))} in #{inspect(ref)}
# To extract a single value from a to_many relationship or path that includes a
# to_many relationship, use a `first` aggregate.
# """
# end
nested =
Enum.map(nested, fn
{nested_path, ref} ->
{path ++ nested_path, ref}
{nested_path} ->
{path ++ nested_path}
end)
[path_and_ref | nested]
_ ->
if with_references? do
[{path, ref}]
else
[{path}]
end
end
else
if with_references? do
[{path, ref}]
else
[{path}]
end
end
end
defp do_relationship_paths(%Ref{relationship_path: path} = ref, _, true) do
[{path, ref}]
end
defp do_relationship_paths(%Ref{relationship_path: path}, _, false) do
[{path}]
end
defp do_relationship_paths(
%BooleanExpression{left: left, right: right},
include_exists?,
with_reference?
) do
[
do_relationship_paths(left, include_exists?, with_reference?),
do_relationship_paths(right, include_exists?, with_reference?)
]
end
defp do_relationship_paths(%Not{expression: expression}, include_exists?, with_reference?) do
do_relationship_paths(expression, include_exists?, with_reference?)
end
defp do_relationship_paths(%Ash.Query.Exists{at_path: at_path}, false, with_reference?) do
if with_reference? do
[{at_path, nil}]
else
[{at_path}]
end
end
defp do_relationship_paths(
%Ash.Query.Exists{path: path, expr: expression, at_path: at_path},
include_exists?,
false
) do
expression
|> do_relationship_paths(include_exists?, false)
|> List.flatten()
|> Enum.flat_map(fn {rel_path} ->
[{at_path}, {at_path ++ path ++ rel_path}]
end)
|> Kernel.++(parent_relationship_paths(expression, at_path, include_exists?, false))
end
defp do_relationship_paths(
%Ash.Query.Exists{path: path, expr: expression, at_path: at_path},
include_exists?,
true
) do
expression
|> do_relationship_paths(include_exists?, true)
|> List.flatten()
|> Enum.flat_map(fn {rel_path, ref} ->
[{at_path, nil}, {at_path ++ path ++ rel_path, ref}]
end)
|> Kernel.++(parent_relationship_paths(expression, at_path, include_exists?, true))
end
defp do_relationship_paths(
%{__operator__?: true, left: left, right: right},
include_exists?,
with_reference?
) do
Enum.flat_map([left, right], &do_relationship_paths(&1, include_exists?, with_reference?))
end
defp do_relationship_paths({key, value}, include_exists?, with_reference?) when is_atom(key) do
do_relationship_paths(value, include_exists?, with_reference?)
end
defp do_relationship_paths(
%{__function__?: true, arguments: arguments},
include_exists?,
with_reference?
) do
Enum.flat_map(arguments, &do_relationship_paths(&1, include_exists?, with_reference?))
end
defp do_relationship_paths(value, include_exists?, with_reference?) when is_list(value) do
Enum.flat_map(value, &do_relationship_paths(&1, include_exists?, with_reference?))
end
defp do_relationship_paths(_, _, _), do: []
# defp to_one_path?([], _resource), do: true
# defp to_one_path?([next | rest], resource) do
# case Ash.Resource.Info.relationship(resource, next) do
# nil ->
# false
# %{cardinality: :one, destination: destination} ->
# to_one_path?(rest, destination)
# _ ->
# false
# end
# end
defp parent_relationship_paths(expression, at_path, include_exists?, with_reference?) do
expression
|> flat_map(fn
%Ash.Query.Parent{expr: expr} ->
expr
|> do_relationship_paths(include_exists?, with_reference?)
|> Enum.flat_map(fn
{rel_path, ref} ->
[{at_path ++ rel_path, ref}]
{rel_path} ->
[{at_path ++ rel_path}]
end)
_ ->
[]
end)
end
@doc false
def embed_predicates(nil), do: nil
def embed_predicates(%__MODULE__{expression: expression} = filter) do
%{filter | expression: embed_predicates(expression)}
end
def embed_predicates(%Not{expression: expression} = not_expr) do
%{not_expr | expression: embed_predicates(expression)}
end
def embed_predicates(%BooleanExpression{left: left, right: right} = expr) do
%{expr | left: embed_predicates(left), right: embed_predicates(right)}
end
def embed_predicates(%Call{args: args} = call) do
%{call | args: embed_predicates(args)}
end
def embed_predicates(%{__predicate__?: true} = pred) do
%{pred | embedded?: true}
end
def embed_predicates(list) when is_list(list) do
Enum.map(list, &embed_predicates(&1))
end
def embed_predicates(other), do: other
def list_refs(expression, no_longer_simple? \\ false, in_an_eq? \\ false) do
expression
|> do_list_refs(no_longer_simple?, in_an_eq?)
|> Enum.uniq()
end
defp do_list_refs(list, no_longer_simple?, in_an_eq? \\ false)
defp do_list_refs(list, no_longer_simple?, in_an_eq?) when is_list(list) do
Enum.flat_map(list, &do_list_refs(&1, no_longer_simple?, in_an_eq?))
end
defp do_list_refs({key, value}, no_longer_simple?, in_an_eq?) when is_atom(key),
do: do_list_refs(value, no_longer_simple?, in_an_eq?)
defp do_list_refs(%__MODULE__{expression: expression}, no_longer_simple?, in_an_eq?) do
do_list_refs(expression, no_longer_simple?, in_an_eq?)
end
defp do_list_refs(expression, no_longer_simple?, in_an_eq?) do
case expression do
%BooleanExpression{left: left, right: right, op: op} ->
no_longer_simple? = no_longer_simple? || op == :or
do_list_refs(left, no_longer_simple?) ++ do_list_refs(right, no_longer_simple?)
%Not{expression: not_expr} ->
do_list_refs(not_expr, true)
%struct{__predicate__?: _, left: left, right: right} ->
in_an_eq? = struct == Ash.Query.Operator.Eq
do_list_refs(left, no_longer_simple?, in_an_eq?) ++
do_list_refs(right, no_longer_simple?, in_an_eq?)
%{__predicate__?: _, arguments: args} ->
Enum.flat_map(args, &do_list_refs(&1, true))
value when is_list(value) ->
Enum.flat_map(value, &do_list_refs(&1, true))
value when is_map(value) and not is_struct(value) ->
Enum.flat_map(value, fn {key, value} ->
do_list_refs(key, true) ++ do_list_refs(value, true)
end)
%Ash.Query.Exists{at_path: at_path, path: path, expr: expr} ->
parent_refs_inside_of_exists =
flat_map(expr, fn
%Ash.Query.Parent{expr: expr} ->
expr
|> do_list_refs(true)
|> Enum.map(&%{&1 | relationship_path: at_path ++ &1.relationship_path})
_ ->
[]
end)
expr
|> do_list_refs(true)
|> Enum.map(&%{&1 | relationship_path: at_path ++ path ++ &1.relationship_path})
|> Enum.concat(parent_refs_inside_of_exists)
%Call{args: args, relationship_path: relationship_path} ->
args
|> Enum.flat_map(&do_list_refs(&1, true))
|> Enum.map(&%{&1 | relationship_path: relationship_path ++ &1.relationship_path})
%Ref{} = ref ->
[%{ref | simple_equality?: !no_longer_simple? && in_an_eq?}]
_ ->
[]
end
end
def list_predicates(%__MODULE__{expression: expression}) do
list_predicates(expression)
end
def list_predicates(expression) do
case expression do
%BooleanExpression{left: left, right: right} ->
list_predicates(left) ++ list_predicates(right)
%Not{expression: not_expr} ->
list_predicates(not_expr)
%{__predicate__?: true} = pred ->
[pred]
%Ash.Query.Exists{} = exists ->
exists
_ ->
[]
end
end
defp attribute(%{public?: true, resource: resource}, attribute) when not is_nil(resource),
do: Ash.Resource.Info.public_attribute(resource, attribute)
defp attribute(%{public?: false, resource: resource}, attribute) when not is_nil(resource) do
Ash.Resource.Info.attribute(resource, attribute)
end
defp attribute(_, _), do: nil
defp aggregate(%{public?: true, resource: resource}, aggregate) when not is_nil(resource),
do: Ash.Resource.Info.public_aggregate(resource, aggregate)
defp aggregate(%{public?: false, resource: resource}, aggregate) when not is_nil(resource),
do: Ash.Resource.Info.aggregate(resource, aggregate)
defp aggregate(_, _), do: nil
defp calculation(%{public?: true, resource: resource}, calculation) when not is_nil(resource),
do: Ash.Resource.Info.public_calculation(resource, calculation)
defp calculation(%{public?: false, resource: resource}, calculation) when not is_nil(resource),
do: Ash.Resource.Info.calculation(resource, calculation)
defp calculation(_, _), do: nil
defp relationship(%{public?: true, resource: resource}, relationship)
when not is_nil(resource) do
Ash.Resource.Info.public_relationship(resource, relationship)
end
defp relationship(%{public?: false, resource: resource}, relationship)
when not is_nil(resource) do
Ash.Resource.Info.relationship(resource, relationship)
end
defp relationship(_, _), do: nil
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, move_to_relationship_path(expression, context[:relationship_path] || [])}
defp parse_expression(statement, context) when is_list(statement) do
Enum.reduce_while(statement, {:ok, true}, 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, nil) do
add_expression_part(boolean, context, true)
end
defp add_expression_part(boolean, _context, expression) when is_boolean(boolean) do
{:ok, BooleanExpression.optimized_new(:and, expression, boolean)}
end
defp add_expression_part(%__MODULE__{expression: adding_expression}, context, expression) do
{:ok,
BooleanExpression.optimized_new(
:and,
expression,
move_to_relationship_path(adding_expression, context[:relationship_path] || [])
)}
end
defp add_expression_part({not_key, nested_statement}, context, expression)
when not_key in [:not, "not"] do
case parse_expression(nested_statement, context) do
{:ok, nested_expression} ->
{:ok, BooleanExpression.optimized_new(:and, expression, Not.new(nested_expression))}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part({or_key, nested_statements}, context, expression)
when or_key in [:or, "or"] do
with {:ok, nested_expression} <- parse_and_join(nested_statements, :or, context),
:ok <- validate_data_layers_support_boolean_filters(nested_expression) do
{:ok, BooleanExpression.optimized_new(:and, expression, nested_expression)}
end
end
defp add_expression_part({and_key, nested_statements}, context, expression)
when and_key in [:and, "and"] do
case parse_and_join(nested_statements, :and, context) do
{:ok, nested_expression} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_expression)}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(%Call{} = call, context, expression) do
case resolve_call(call, context) do
{:ok, result} ->
{:ok, BooleanExpression.optimized_new(:and, expression, result)}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(%Ash.Query.Parent{expr: expr} = this, context, expression) do
case parse_expression(expr, %{context | resource: context.root_resource}) do
{:ok, result} ->
{:ok, BooleanExpression.optimized_new(:and, expression, %{this | expr: result})}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(
%Ash.Query.Exists{at_path: at_path, path: path, expr: exists_expression} = exists,
context,
expression
) do
related = related(context, at_path ++ path)
if !related do
raise """
Could not determine related resource for `exists/2` expression.
Context Resource: #{inspect(context)}
Context Relationship Path: #{inspect(context[:relationship_path])}
At Path: #{inspect(at_path)}
Path: #{inspect(path)}
Related: #{inspect(related)}
Expression: #{inspect(exists)}
"""
end
case parse_expression(
exists_expression,
%{
context
| resource: related,
root_resource: related
}
|> Map.update(
:parent_stack,
[context[:root_resource]],
&[context[:root_resource] | &1]
)
) do
{:ok, result} ->
{:ok, BooleanExpression.optimized_new(:and, expression, %{exists | expr: result})}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(%Ref{} = ref, _context, _expression) do
{:ok, %{ref | bare?: true}}
end
defp add_expression_part({%Ref{} = ref, nested_statement}, context, expression) do
case related(context, ref.relationship_path) do
nil ->
{:error,
NoSuchAttributeOrRelationship.exception(
attribute_or_relationship: List.first(ref.relationship_path),
resource: context.resource
)}
related ->
new_context = %{
relationship_path: ref.relationship_path,
resource: related,
root_resource: context.root_resource,
public?: context.public?
}
add_expression_part({ref.attribute.name, nested_statement}, new_context, expression)
end
end
defp add_expression_part(
%BooleanExpression{op: op, left: left, right: right},
context,
expression
) do
add_expression_part({op, [left, right]}, context, expression)
end
defp add_expression_part(%Not{expression: not_expression}, context, expression) do
add_expression_part({:not, not_expression}, context, expression)
end
defp add_expression_part(%_{__predicate__?: _} = pred, context, expression) do
{:ok,
BooleanExpression.optimized_new(
:and,
expression,
move_to_relationship_path(pred, context[:relationship_path] || [])
)}
end
defp add_expression_part(%_{} = record, context, expression) do
pkey_filter =
record
|> Map.take(Ash.Resource.Info.primary_key(context.resource))
|> Map.to_list()
add_expression_part(pkey_filter, context, expression)
end
defp add_expression_part({:is_nil, attribute}, context, expression)
when is_atom(attribute) or is_binary(attribute) do
add_expression_part({attribute, [is_nil: true]}, context, expression)
end
defp add_expression_part({:fragment, _}, _context, _expression) do
raise "Cannot use fragment outside of expression syntax"
end
defp add_expression_part({function, args}, context, expression)
when is_tuple(args) and (is_atom(function) or is_binary(function)) do
case get_function(function, context.resource, context.public?) do
nil ->
case calculation(context, function) do
nil ->
add_expression_part({function, [args]}, context, expression)
resource_calculation ->
{module, opts} = resource_calculation.calculation
{args, nested_statement} =
case args do
{input, nested} ->
{input || %{}, nested}
args ->
{args, []}
end
with {:ok, args} <-
Ash.Query.validate_calculation_arguments(resource_calculation, args),
{:ok, calculation} <-
Calculation.new(
resource_calculation.name,
module,
opts,
{resource_calculation.type, resource_calculation.constraints},
args,
resource_calculation.filterable?,
resource_calculation.load
) do
case parse_predicates(nested_statement, calculation, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
else
{:error, error} ->
{:error, error}
end
end
function_module ->
nested_statement = Tuple.to_list(args)
with {:ok, args} <-
hydrate_refs(List.wrap(nested_statement), context),
refs <- list_refs(args),
:ok <-
validate_refs(
refs,
context.root_resource,
{function, nested_statement}
),
{:ok, function} <-
Function.new(
function_module,
args
) do
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, BooleanExpression.optimized_new(:and, expression, function)}
else
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
end
end
defp add_expression_part({field, nested_statement}, context, expression)
when is_atom(field) or is_binary(field) do
cond do
rel = relationship(context, 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, BooleanExpression.optimized_new(:and, expression, nested_expression)}
{:error, error} ->
{:error, error}
end
else
with [field] <- Ash.Resource.Info.primary_key(context.resource),
attribute <- attribute(context, field),
{:ok, casted} <-
Ash.Type.cast_input(attribute.type, nested_statement, attribute.constraints) 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
attr = attribute(context, field) ->
case parse_predicates(nested_statement, attr, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
aggregate = aggregate(context, field) ->
related = Ash.Resource.Info.related(context.resource, aggregate.relationship_path)
read_action =
aggregate.read_action || Ash.Resource.Info.primary_action!(related, :read).name
with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action),
%{valid?: true} = aggregate_query <-
Ash.Query.build(aggregate_query, filter: aggregate.filter, sort: aggregate.sort),
{:ok, query_aggregate} <-
Aggregate.new(
context.resource,
aggregate.name,
aggregate.kind,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
type: aggregate.type,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action:
aggregate.read_action ||
Ash.Resource.Info.primary_action!(
Ash.Resource.Info.related(context.resource, aggregate.relationship_path),
:read
).name,
authorize?: aggregate.authorize?
) do
case parse_predicates(nested_statement, query_aggregate, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
else
%{valid?: false, errors: errors} ->
{:error, errors}
{:error, error} ->
{:error, error}
end
resource_calculation = calculation(context, field) ->
{module, opts} = resource_calculation.calculation
{input, nested_statement} =
case nested_statement do
{input, nested} ->
{input || %{}, nested}
nested ->
{%{}, nested}
end
with {:ok, args} <-
Ash.Query.validate_calculation_arguments(
resource_calculation,
input
),
{:ok, calculation} <-
Calculation.new(
resource_calculation.name,
module,
opts,
{resource_calculation.type, resource_calculation.constraints},
args,
resource_calculation.filterable?,
resource_calculation.load
) do
case parse_predicates(nested_statement, calculation, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
else
{:error, error} ->
{:error, error}
end
op_module = get_operator(field) && match?([_, _ | _], nested_statement) ->
with {:ok, [left, right]} <-
hydrate_refs(nested_statement, context),
refs <- list_refs([left, right]),
:ok <-
validate_refs(
refs,
context.root_resource,
{field, nested_statement}
),
{:ok, operator} <- Operator.new(op_module, left, right) do
if is_boolean(operator) do
{:ok, BooleanExpression.optimized_new(:and, expression, operator)}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, operator}) do
{:ok, BooleanExpression.optimized_new(:and, expression, operator)}
else
{:error, "data layer does not support the operator #{inspect(operator)}"}
end
end
end
true ->
{:error,
NoSuchAttributeOrRelationship.exception(
attribute_or_relationship: field,
resource: context.resource
)}
end
end
defp add_expression_part({%Ash.Query.Calculation{} = calc, rest}, context, expression) do
case parse_predicates(rest, calc, context) do
{:ok, nested_statement} ->
{:ok, BooleanExpression.optimized_new(:and, expression, nested_statement)}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(%Ash.Query.Calculation{} = calc, _context, expression) do
{:ok, BooleanExpression.optimized_new(:and, calc, expression)}
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, true}, 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, BooleanExpression.optimized_new(:and, expression, new_expression)}
{:error, error} ->
{:error, error}
end
end
defp add_expression_part(value, context, expression) when is_list(value) do
Enum.reduce_while(value, {:ok, expression}, fn value, {:ok, expression} ->
case add_expression_part(value, context, expression) do
{:ok, expression} -> {:cont, {:ok, expression}}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp add_expression_part(value, _, _) do
{:error, InvalidFilterValue.exception(value: value)}
end
defp each_related(_resource, []), do: []
defp each_related(resource, [item | rest]) do
relationship = Ash.Resource.Info.relationship(resource, item)
if relationship do
if relationship.type == :many_to_many do
[
relationship.through,
relationship.destination | each_related(relationship.destination, rest)
]
else
[relationship.destination | each_related(relationship.destination, rest)]
end
else
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
Logger.warning(
"Failed to detect relationship #{inspect(resource)} | #{inspect([item | rest])}\n#{Exception.format_stacktrace(stacktrace)}"
)
[]
end
end
defp validate_refs(refs, resource, expr) do
with :ok <- validate_filterable_relationship_paths(refs, resource) do
validate_not_crossing_data_layer_boundaries(refs, resource, expr)
end
end
defp validate_filterable_relationship_paths(refs, resource) do
Enum.find_value(
refs,
:ok,
fn ref ->
case check_filterable(resource, ref.relationship_path) do
:ok ->
false
{:error, error} ->
{:error, error}
end
end
)
end
defp check_filterable(_resource, []), do: :ok
defp check_filterable(resource, [relationship | rest]) do
relationship = Ash.Resource.Info.relationship(resource, relationship)
if relationship.filterable? do
if Ash.DataLayer.data_layer_can?(resource, {:filter_relationship, relationship}) do
check_filterable(relationship.destination, rest)
else
{:error, "#{inspect(resource)}.#{relationship.name} is not filterable"}
end
else
{:error,
"#{inspect(resource)}.#{relationship.name} has been configured as filterable?: false"}
end
end
defp validate_not_crossing_data_layer_boundaries(refs, resource, expr) do
refs
|> Enum.flat_map(&each_related(resource, &1.relationship_path))
|> Enum.filter(& &1)
|> Enum.group_by(&Ash.DataLayer.data_layer/1)
|> Map.to_list()
|> case do
[] ->
:ok
[{_data_layer, resources}] ->
can_join? =
Enum.all?(resources, fn resource ->
resources
|> Kernel.--([resource])
|> Enum.all?(fn other_resource ->
Ash.DataLayer.data_layer_can?(resource, {:join, other_resource})
end)
end)
if can_join? do
:ok
else
{:error,
Ash.Error.Query.InvalidExpression.exception(
expression: expr,
message:
"cannot access multiple resources for a data layer that can't be joined from within a single expression"
)}
end
[_ | _] ->
{:error,
Ash.Error.Query.InvalidExpression.exception(
expression: expr,
message: "cannot access multiple data layers within a single expression"
)}
end
end
defp resolve_call(
%Call{name: name, args: args, operator?: true, relationship_path: relationship_path} =
call,
context
) do
with :ok <- validate_datalayer_supports_nested_expressions(args, context.resource),
{:op, op_module} when not is_nil(op_module) <-
{:op, get_operator(name)},
context <-
Map.merge(context, %{
resource: Ash.Resource.Info.related(context.resource, relationship_path),
relationship_path: []
}),
{:ok, [left, right]} <-
hydrate_refs(args, context),
refs <- list_refs([left, right]),
:ok <-
validate_refs(refs, context.root_resource, call),
{:ok, operator} <- Operator.new(op_module, left, right) do
if is_boolean(operator) do
{:ok, operator}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, operator}) do
{:ok, operator}
else
{:error, "data layer does not support the operator #{inspect(operator)}"}
end
end
else
{:op, nil} ->
{:error, NoSuchOperator.exception(name: name)}
other ->
other
end
end
defp resolve_call(
%Call{name: :parent, args: [arg], relationship_path: []},
context
) do
do_hydrate_refs(%Ash.Query.Parent{expr: arg}, context)
end
defp resolve_call(%Call{name: name, args: args} = call, context)
when name in @inline_aggregates do
resource = Ash.Resource.Info.related(context.resource, call.relationship_path)
path = refs_to_path(Enum.at(args, 0))
related = Ash.Resource.Info.related(resource, path)
opts = Enum.at(args, 1) || []
if Keyword.keyword?(opts) do
kind =
if name == :custom_aggregate do
:custom
else
name
end
field =
if opts[:field] do
opts[:field]
else
unless kind == :custom do
List.first(Ash.Resource.Info.primary_key(related))
end
end
opts =
if field && kind != :custom do
attribute = Ash.Resource.Info.attribute(related, field)
if attribute do
{:ok, type, constraints} =
Ash.Query.Aggregate.kind_to_type(kind, attribute.type, attribute.constraints)
opts
|> Keyword.put(:type, type)
|> Keyword.put(:constraints, constraints)
else
opts
end
else
opts
end
opts = Keyword.put(opts, :path, path)
with {:ok, agg} <-
Ash.Query.Aggregate.new(
resource,
agg_name(kind, opts),
kind,
opts
) do
{:ok,
%Ref{
relationship_path: call.relationship_path,
attribute: agg
}}
end
else
{:error, "Aggregate options must be keyword list. In: #{inspect(call)}"}
end
end
defp resolve_call(%Call{name: name, args: args} = call, context) do
could_be_calculation? = Enum.count_until(args, 2) == 1 && Keyword.keyword?(Enum.at(args, 0))
resource = Ash.Resource.Info.related(context.resource, call.relationship_path)
context =
Map.merge(context, %{
resource: resource,
relationship_path: []
})
case {calculation(%{context | resource: resource}, name), could_be_calculation?} do
{resource_calculation, true} when not is_nil(resource_calculation) ->
{module, opts} = resource_calculation.calculation
with {:ok, args} <-
Ash.Query.validate_calculation_arguments(
resource_calculation,
Map.new(Enum.at(args, 0) || [])
),
{:ok, calculation} <-
Calculation.new(
resource_calculation.name,
module,
opts,
{resource_calculation.type, resource_calculation.constraints},
args,
resource_calculation.filterable?,
resource_calculation.load
) do
{:ok,
%Ref{
attribute: calculation,
relationship_path:
Map.get(context, :relationship_path, []) ++ call.relationship_path,
resource: resource
}}
else
{:error, error} ->
{:error, error}
end
_ ->
with :ok <- validate_datalayer_supports_nested_expressions(args, context.resource),
{:ok, args} <-
hydrate_refs(args, context),
refs <- list_refs(args),
:ok <- validate_refs(refs, context.root_resource, call),
{:func, function_module} when not is_nil(function_module) <-
{:func, get_function(name, context.resource, context.public?)},
{:ok, function} <-
Function.new(
function_module,
args
) do
if is_boolean(function) do
{:ok, function}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, function}
else
case function_module.evaluate(function) do
{:known, result} ->
{:ok, result}
_ ->
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
end
else
{:func, nil} ->
{:error, NoSuchFunction.exception(name: name, resource: context.resource)}
other ->
other
end
end
end
# This kind of sucks, but anonymous aggregates need consistent names currently.
# We may want to move this into the data layer to be responsible for setting these
# names
defp agg_name(kind, opts) do
opts_string =
opts
|> Keyword.take([
:query,
:field,
:default,
:filterable?,
:type,
:constraints,
:implementation,
:read_action,
:uniq?,
:authorize?
])
|> Keyword.reject(fn {_key, value} -> is_nil(value) end)
|> case do
[] ->
""
opts ->
inspect(opts)
end
"#{kind}(#{Enum.join(opts[:path] || [], ".")}#{opts_string})"
end
defp refs_to_path(%Ref{relationship_path: relationship_path, attribute: attribute}) do
attribute =
case attribute do
%{name: name} ->
name
name ->
name
end
relationship_path ++ [attribute]
end
defp refs_to_path(list) when is_list(list) do
Enum.flat_map(list, fn item -> refs_to_path(item) end)
end
defp refs_to_path(item), do: [item]
defp validate_datalayer_supports_nested_expressions(args, resource) do
if resource && Enum.any?(args, &Ash.Filter.TemplateHelpers.expr?/1) &&
!Ash.DataLayer.data_layer_can?(resource, :nested_expressions) do
{:error, "Datalayer does not support nested expressions"}
else
:ok
end
end
def hydrate_refs(value, context) do
context =
context
|> Map.put_new(:resource, nil)
|> Map.put_new(:root_resource, context[:resource])
|> Map.put_new(:public?, false)
do_hydrate_refs(value, context)
end
def do_hydrate_refs({:ref, value}, context) do
do_hydrate_refs(
%Ash.Query.Ref{
attribute: value,
relationship_path: [],
input?: context.input?,
resource: context.root_resource
},
context
)
end
def do_hydrate_refs({key, value}, context) when is_atom(key) or is_binary(key) do
case do_hydrate_refs(value, context) do
{:ok, hydrated} ->
{:ok, {key, hydrated}}
other ->
other
end
end
def do_hydrate_refs(
%Ref{relationship_path: relationship_path, resource: nil} = ref,
%{resource: resource} = context
)
when not is_nil(resource) do
case Ash.Resource.Info.related(resource, relationship_path || []) do
nil ->
{:error, "Invalid reference #{inspect(ref)}"}
related ->
do_hydrate_refs(
%{ref | resource: related},
context
)
end
end
def do_hydrate_refs(
%Ref{attribute: attribute} = ref,
context
)
when is_atom(attribute) or is_binary(attribute) do
ref = %{ref | input?: ref.input? || context[:input?] || false}
case related(context, ref.relationship_path) do
nil ->
{:error,
"Invalid reference #{inspect(ref)} at relationship_path #{inspect(ref.relationship_path)}"}
related ->
context = %{context | resource: related}
cond do
attribute = attribute(context, attribute) ->
{:ok, %{ref | attribute: attribute, resource: related}}
resource_calculation = calculation(context, attribute) ->
{module, opts} = resource_calculation.calculation
with {:ok, args} <-
Ash.Query.validate_calculation_arguments(resource_calculation, %{}),
{:ok, calculation} <-
Calculation.new(
resource_calculation.name,
module,
opts,
{resource_calculation.type, resource_calculation.constraints},
args,
resource_calculation.filterable?,
resource_calculation.load
) do
{:ok, %{ref | attribute: calculation, resource: related}}
else
{:error, error} ->
{:error, error}
end
aggregate = aggregate(context, attribute) ->
agg_related = Ash.Resource.Info.related(related, aggregate.relationship_path)
read_action =
aggregate.read_action || Ash.Resource.Info.primary_action!(agg_related, :read).name
with %{valid?: true} = aggregate_query <-
Ash.Query.for_read(agg_related, read_action),
%{valid?: true} = aggregate_query <-
Ash.Query.build(aggregate_query,
filter: aggregate.filter,
sort: aggregate.sort
),
{:ok, query_aggregate} <-
Aggregate.new(
related,
aggregate.name,
aggregate.kind,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
type: aggregate.type,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action: aggregate.read_action,
authorize?: aggregate.authorize?
) do
{:ok, %{ref | attribute: query_aggregate, resource: related}}
else
%{valid?: false, errors: errors} ->
{:error, errors}
{:error, error} ->
{:error, error}
end
relationship = relationship(context, attribute) ->
case Ash.Resource.Info.primary_key(relationship.destination) do
[key] ->
new_ref = %{
ref
| relationship_path: ref.relationship_path ++ [relationship.name],
attribute: attribute(%{context | resource: relationship.destination}, key),
resource: relationship.destination
}
{:ok, new_ref}
_ ->
{:error,
"Invalid reference #{inspect(ref)} when hydrating relationship ref for #{inspect(ref.relationship_path ++ [relationship.name])}. Require single attribute primary key."}
end
true ->
{:error, "Invalid reference #{inspect(ref)}"}
end
end
end
def do_hydrate_refs(%Ref{relationship_path: relationship_path, resource: nil} = ref, context) do
ref = %{ref | input?: ref.input? || context[:input?] || false}
{:ok, %{ref | resource: Ash.Resource.Info.related(context.resource, relationship_path)}}
end
def do_hydrate_refs(%BooleanExpression{left: left, right: right} = expr, context) do
with {:ok, left} <- do_hydrate_refs(left, context),
{:ok, right} <- do_hydrate_refs(right, context) do
{:ok, %{expr | left: left, right: right}}
else
other ->
other
end
end
def do_hydrate_refs(%Not{expression: expression} = expr, context) do
with {:ok, expression} <- do_hydrate_refs(expression, context) do
{:ok, %{expr | expression: expression}}
end
end
def do_hydrate_refs(%Call{} = call, context) do
resolve_call(call, context)
end
def do_hydrate_refs(%{__predicate__?: _, left: left, right: right} = expr, context) do
with {:ok, left} <- do_hydrate_refs(left, context),
{:ok, right} <- do_hydrate_refs(right, context) do
{:ok, %{expr | left: left, right: right}}
else
other ->
other
end
end
def do_hydrate_refs(%{__predicate__?: _, arguments: arguments} = expr, context) do
case do_hydrate_refs(arguments, context) do
{:ok, args} ->
{:ok, %{expr | arguments: args}}
other ->
other
end
end
def do_hydrate_refs(%Ash.Query.Parent{expr: expr} = this, context) do
if !Map.has_key?(context, :parent_stack) || context.parent_stack in [[], nil] do
{:ok, this}
else
context =
%{
context
| resource: hd(context.parent_stack),
root_resource: hd(context.parent_stack),
parent_stack: tl(context.parent_stack)
}
|> Map.put(:relationship_path, [])
case do_hydrate_refs(expr, context) do
{:ok, expr} ->
{:ok, %{this | expr: expr}}
other ->
other
end
end
end
def do_hydrate_refs(
%Ash.Query.Exists{expr: expr, at_path: at_path, path: path} = exists,
context
) do
new_resource = Ash.Resource.Info.related(context[:resource], at_path ++ path)
context = %{
resource: new_resource,
root_resource: new_resource,
parent_stack: [context[:root_resource] | context[:parent_stack] || []],
relationship_path: [],
public?: context[:public?],
input?: context[:input?],
data_layer: Ash.DataLayer.data_layer(new_resource)
}
case do_hydrate_refs(expr, context) do
{:ok, expr} ->
{:ok, %{exists | expr: expr}}
other ->
other
end
end
def do_hydrate_refs(%__MODULE__{expression: expression} = filter, context) do
case do_hydrate_refs(expression, context) do
{:ok, expression} ->
{:ok, %{filter | expression: expression}}
{:error, error} ->
{:error, error}
end
end
def do_hydrate_refs(list, context) when is_list(list) do
list
|> Enum.reduce_while({:ok, []}, fn val, {:ok, acc} ->
case do_hydrate_refs(val, context) do
{:ok, value} ->
{:cont, {:ok, [value | acc]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, value} -> {:ok, Enum.reverse(value)}
{:error, error} -> {:error, error}
end
end
def do_hydrate_refs(map, context) when is_map(map) and not is_struct(map) do
map
|> Enum.reduce_while({:ok, %{}}, fn {key, value}, {:ok, acc} ->
with {:ok, key} <- do_hydrate_refs(key, context),
{:ok, value} <- do_hydrate_refs(value, context) do
{:cont, {:ok, Map.put(acc, key, value)}}
else
{:error, error} ->
{:halt, {:error, error}}
end
end)
end
def do_hydrate_refs(%Ref{} = ref, context) do
{:ok, %{ref | input?: ref.input? || context[:input?] || false}}
end
def do_hydrate_refs(val, _context) do
{:ok, val}
end
defp validate_data_layers_support_boolean_filters(%BooleanExpression{
op: :or,
left: left,
right: right
}) do
left_resources =
left
|> map(fn
%Ref{} = ref ->
[ref.resource]
_ ->
[]
end)
|> List.flatten()
|> Enum.uniq()
right_resources =
right
|> map(fn
%Ref{} = ref ->
[ref.resource]
_ ->
[]
end)
|> List.flatten()
|> Enum.uniq()
left_resources
|> Enum.filter(&(&1 in right_resources))
|> Enum.reduce_while(:ok, fn resource, :ok ->
if Ash.DataLayer.data_layer_can?(resource, :boolean_filter) do
{:cont, :ok}
else
{:halt, {:error, "Data layer for #{resource} does not support boolean filters"}}
end
end)
end
defp validate_data_layers_support_boolean_filters(_), do: :ok
def move_exprs_to_relationship_path(refs, []), do: refs
def move_exprs_to_relationship_path(refs, path) do
Enum.map(refs, &move_to_relationship_path(&1, path))
end
def move_to_relationship_path(expression, []), do: expression
def move_to_relationship_path(expression, relationship_path) do
case expression do
{key, value} when is_atom(key) ->
{key, move_to_relationship_path(value, relationship_path)}
%Not{expression: expression} = not_expr ->
%{not_expr | expression: move_to_relationship_path(expression, relationship_path)}
%BooleanExpression{left: left, right: right} = expression ->
%{
expression
| left: move_to_relationship_path(left, relationship_path),
right: move_to_relationship_path(right, relationship_path)
}
%{__operator__?: true, left: left, right: right} = op ->
left = move_to_relationship_path(left, relationship_path)
right = move_to_relationship_path(right, relationship_path)
%{op | left: left, right: right}
%Ref{} = ref ->
add_to_ref_path(ref, relationship_path)
%{__function__?: true, arguments: args} = func ->
%{func | arguments: Enum.map(args, &move_to_relationship_path(&1, relationship_path))}
%Ash.Query.Exists{} = exists ->
%{exists | at_path: relationship_path ++ exists.at_path}
%Call{relationship_path: call_path} = call ->
%{call | relationship_path: relationship_path ++ call_path}
%__MODULE__{expression: expression} = filter ->
%{filter | expression: move_to_relationship_path(expression, relationship_path)}
other ->
other
end
end
defp add_to_ref_path(%Ref{relationship_path: relationship_path} = ref, to_add) do
%{ref | relationship_path: to_add ++ relationship_path}
end
defp add_to_ref_path(other, _), do: other
defp parse_and_join([statement | statements], op, context) do
case parse_expression(statement, context) do
{:ok, nested_expression} ->
Enum.reduce_while(statements, {:ok, nested_expression}, fn statement, {:ok, expression} ->
case parse_expression(statement, context) do
{:ok, nested_expression} ->
{:cont, {:ok, BooleanExpression.optimized_new(op, expression, nested_expression)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
{:error, error} ->
{:error, error}
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(%struct{} = value, field, context)
when struct not in [Not, BooleanExpression, Ref, Call] do
parse_predicates([eq: value], field, context)
end
defp parse_predicates(values, attr, context) do
if is_struct(values) && Map.has_key?(values, :__predicate__) do
parse_predicates([eq: values], attr, context)
else
if is_map(values) || Keyword.keyword?(values) do
Enum.reduce_while(values, {:ok, true}, fn
{:not, value}, {:ok, expression} ->
case parse_predicates(List.wrap(value), attr, context) do
{:ok, not_expression} ->
{:cont,
{:ok,
BooleanExpression.optimized_new(:and, expression, %Not{
expression: not_expression
})}}
{:error, error} ->
{:halt, {:error, error}}
end
{key, value}, {:ok, expression} ->
case get_operator(key) do
nil ->
error = NoSuchFilterPredicate.exception(key: key, resource: context.resource)
{:halt, {:error, error}}
operator_module ->
left = %Ref{
attribute: attr,
relationship_path: context[:relationship_path] || [],
resource: context.resource
}
with {:ok, [left, right]} <-
hydrate_refs([left, value], context),
refs <- list_refs([left, right]),
:ok <-
validate_refs(
refs,
context.root_resource,
{attr, value}
),
{:ok, operator} <- Operator.new(operator_module, left, right) do
if is_boolean(operator) do
{:cont, {:ok, operator}}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, operator}) do
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
else
{:halt,
{:error, "data layer does not support the operator #{inspect(operator)}"}}
end
end
else
{:error, error} -> {:halt, {:error, error}}
end
end
end)
else
error = InvalidFilterValue.exception(value: values)
{:error, error}
end
end
end
def get_function(key, resource, public?) when is_atom(key) do
function = @builtin_functions[key]
function =
if function do
function
else
if resource do
Enum.find(Ash.DataLayer.data_layer_functions(resource), &(&1.name() == key))
end
end
if public? && function && function.private?() do
nil
else
function
end
end
def get_function(key, resource, public?) when is_binary(key) do
function =
Map.get(@string_builtin_functions, key) ||
Enum.find(Ash.DataLayer.data_layer_functions(resource), &(&1.name() == key))
if public? && function && function.private?() do
nil
else
function
end
end
def get_function(_, _, _), do: nil
def get_operator(key) when is_atom(key) do
@builtin_operators[key]
end
def get_operator(key) when is_binary(key) do
Map.get(@string_builtin_operators, key)
end
def get_operator(_), 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)}
expression = sanitize(expression)
concat(["#Ash.Filter<", to_doc(expression, opts), ">"])
end
defp sanitize(%BooleanExpression{left: left, right: right} = expr) do
%{expr | left: sanitize(left), right: sanitize(right)}
end
defp sanitize(%Not{expression: expression} = not_expr) do
%{not_expr | expression: sanitize(expression)}
end
defp sanitize(%{__operator__?: true, left: left, right: right} = op) do
[left, right] = poison_exprs([left, right])
%{op | left: left, right: right}
end
defp sanitize(%{__function__?: true, arguments: arguments} = func) do
%{func | arguments: poison_exprs(arguments)}
end
defp sanitize(%Call{args: arguments} = call) do
%{call | args: poison_exprs(arguments)}
end
defp sanitize(%Ash.Query.Parent{expr: expr} = exists) do
%{exists | expr: sanitize(expr)}
end
defp sanitize(%Ash.Query.Exists{expr: expr} = exists) do
%{exists | expr: sanitize(expr)}
end
defp sanitize(other) do
other
end
defp poison_exprs(values) do
if Enum.any?(values, &refers_to_sensitive?/1) do
Enum.map(values, &scrub_values/1)
else
values
end
end
defp scrub_values(%BooleanExpression{left: left, right: right} = expr) do
%{expr | left: scrub_values(left), right: scrub_values(right)}
end
defp scrub_values(%Not{expression: expression} = not_expr) do
%{not_expr | expression: scrub_values(expression)}
end
defp scrub_values(%{__operator__?: true, left: left, right: right} = op) do
[left, right] = poison_exprs([left, right])
%{op | left: left, right: right}
end
defp scrub_values(%{__function__?: true, arguments: arguments} = func) do
%{func | arguments: poison_exprs(arguments)}
end
defp scrub_values(%Call{args: arguments} = call) do
%{call | args: poison_exprs(arguments)}
end
defp scrub_values(%Ref{} = ref), do: ref
defp scrub_values(_other) do
"**redacted**"
end
defp refers_to_sensitive?(%BooleanExpression{left: left, right: right}) do
Enum.any?([left, right], &refers_to_sensitive?/1)
end
defp refers_to_sensitive?(%Not{expression: expression}) do
refers_to_sensitive?(expression)
end
defp refers_to_sensitive?(%{__operator__?: true, left: left, right: right}) do
Enum.any?([left, right], &refers_to_sensitive?/1)
end
defp refers_to_sensitive?(%{__function__?: true, arguments: arguments}) do
Enum.any?(arguments, &refers_to_sensitive?/1)
end
defp refers_to_sensitive?(%Call{args: arguments}) do
Enum.any?(arguments, &refers_to_sensitive?/1)
end
defp refers_to_sensitive?(%Ref{attribute: %{sensitive?: true}}), do: true
defp refers_to_sensitive?(_other) do
false
end
end
end