Packages
ash
1.36.2
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/query/query.ex
defmodule Ash.Query do
@moduledoc """
Utilties around constructing/manipulating ash queries.
Ash queries are used for read actions and side loads, and ultimately
map to queries to a resource's data layer.
Queries are run by calling `read` on an API that contains the resource in question
Examples:
```elixir
MyApp.Post
|> Query.filter(likes > 10)
|> Query.sort([:title])
|> MyApp.Api.read!()
MyApp.Author
|> Query.aggregate(:published_post_count, :posts, filter: [published: true])
|> Query.sort(published_post_count: :desc)
|> Query.limit(10)
|> MyApp.Api.read!()
MyApp.Author
|> Query.load([:post_count, :comment_count])
|> Query.load(posts: [:comments])
|> MyApp.Api.read!()
```
"""
defstruct [
:api,
:resource,
:filter,
:tenant,
:action,
:__validated_for_action__,
params: %{},
arguments: %{},
aggregates: %{},
side_load: [],
calculations: %{},
context: %{},
select: nil,
sort: [],
limit: nil,
offset: 0,
errors: [],
action_failed?: false,
before_action: [],
after_action: [],
valid?: true
]
@type t :: %__MODULE__{}
defimpl Inspect do
import Inspect.Algebra
def inspect(query, opts) do
sort? = query.sort != []
side_load? = query.side_load != []
aggregates? = query.aggregates != %{}
calculations? = query.calculations != %{}
limit? = not is_nil(query.limit)
offset? = not (is_nil(query.offset) || query.offset == 0)
filter? = not is_nil(query.filter)
errors? = not Enum.empty?(query.errors)
tenant? = not is_nil(query.tenant)
select? = query.select not in [[], nil]
container_doc(
"#Ash.Query<",
[
concat("resource: ", inspect(query.resource)),
or_empty(concat("tenant: ", to_doc(query.tenant, opts)), tenant?),
arguments(query, opts),
or_empty(concat("filter: ", to_doc(query.filter, opts)), filter?),
or_empty(concat("sort: ", to_doc(query.sort, opts)), sort?),
or_empty(concat("limit: ", to_doc(query.limit, opts)), limit?),
or_empty(concat("offset: ", to_doc(query.offset, opts)), offset?),
or_empty(concat("side_load: ", to_doc(query.side_load, opts)), side_load?),
or_empty(concat("aggregates: ", to_doc(query.aggregates, opts)), aggregates?),
or_empty(concat("calculations: ", to_doc(query.calculations, opts)), calculations?),
or_empty(concat("errors: ", to_doc(query.errors, opts)), errors?),
or_empty(concat("select: ", to_doc(query.select, opts)), select?)
],
">",
opts,
fn str, _ -> str end
)
end
defp arguments(query, opts) do
if query.action do
action = Ash.Resource.Info.action(query.resource, query.action, :read)
if is_nil(action) || Enum.empty?(action.arguments) do
empty()
else
arg_string =
action.arguments
|> Map.new(fn argument ->
if argument.sensitive? do
{argument.name, "**redacted**"}
else
{argument.name, Ash.Query.get_argument(query, argument.name)}
end
end)
|> to_doc(opts)
concat(["arguments: ", arg_string])
end
else
empty()
end
end
defp or_empty(value, true), do: value
defp or_empty(_, false), do: empty()
end
alias Ash.Actions.Sort
alias Ash.Error.Query.{
AggregatesNotSupported,
InvalidArgument,
InvalidLimit,
InvalidOffset,
NoReadAction,
Required
}
alias Ash.Error.SideLoad.{InvalidQuery, NoSuchRelationship}
alias Ash.Query.{Aggregate, BooleanExpression, Calculation}
@doc """
Attach a filter statement to the query.
The filter is applied as an "and" to any filters currently on the query.
For more information on writing filters, see: `Ash.Filter`.
"""
defmacro filter(query, %Ash.Filter{} = filter) do
quote do
Ash.Query.do_filter(unquote(query), unquote(filter))
end
end
defmacro filter(query, nil), do: query
defmacro filter(query, true), do: query
defmacro filter(query, false) do
quote do
Ash.Query.do_filter(unquote(query), false)
end
end
defmacro filter(query, do: body) do
quote do
Ash.Query.do_filter(unquote(query), unquote(body))
end
end
defmacro filter(query, expression) do
if Keyword.keyword?(expression) do
quote do
Ash.Query.do_filter(unquote(query), unquote(expression))
end
else
expr = do_expr(expression)
quote do
Ash.Query.do_filter(unquote(query), List.wrap(unquote(expr)))
end
end
end
@doc "Create a new query"
def new(resource, api \\ nil)
def new(%__MODULE__{} = query, _), do: query
def new(resource, api) when is_atom(resource) do
query = %__MODULE__{
api: api,
filter: nil,
resource: resource
}
query =
case Ash.Resource.Info.base_filter(resource) do
nil ->
query
filter ->
filter =
resource
|> Ash.Filter.parse!(filter)
|> Ash.Filter.embed_predicates()
do_filter(query, filter)
end
case Ash.Resource.Info.default_context(resource) do
nil ->
query
context ->
Ash.Query.set_context(query, context)
end
end
@for_read_opts [
actor: [
type: :any,
doc:
"set the actor, which can be used in any `Ash.Resource.Change`s configured on the action. (in the `context` argument)"
],
tenant: [
type: :any,
doc: "set the tenant on the query"
],
defaults: [
type: :any,
doc:
"A list of arguments to apply defaults for. Defaults to: []. Any unset defaults are set when the action is called."
]
]
def for_read_opts, do: @for_read_opts
@doc """
Creates a query for a given read action and prepares it.
Multitenancy is *not* validated until an action is called. This allows you to avoid specifying a tenant until just before calling
the api action.
### Arguments
Provide a map or keyword list of arguments for the read action
### Opts
#{Ash.OptionsHelpers.docs(@for_read_opts)}
"""
def for_read(query, action_name, args \\ %{}, opts \\ []) do
query = to_query(query)
query = %{query | params: Map.merge(query.params || %{}, Enum.into(args, %{}))}
action = Ash.Resource.Info.action(query.resource, action_name, :read)
if action do
query = Map.put(query, :action, action.name)
query
|> Ash.Query.set_tenant(opts[:tenant] || query.tenant)
|> Map.put(:action, action)
|> Map.put(:__validated_for_action__, action_name)
|> cast_params(action, args)
|> run_preparations(action, opts[:actor])
|> add_action_filters(action, opts[:actor])
|> cast_arguments(action, opts[:defaults], true)
else
add_error(query, :action, "No such action #{action_name}")
end
end
defp cast_params(query, action, args) do
Enum.reduce(args, query, fn {name, value}, query ->
if has_argument?(action, name) do
set_argument(query, name, value)
else
query
end
end)
end
defp has_argument?(action, name) when is_atom(name) do
Enum.any?(action.arguments, &(&1.private? == false && &1.name == name))
end
defp has_argument?(action, name) when is_binary(name) do
Enum.any?(action.arguments, &(&1.private? == false && to_string(&1.name) == name))
end
defp run_preparations(query, action, actor) do
Enum.reduce(action.preparations || [], query, fn %{preparation: {module, opts}}, query ->
module.prepare(query, opts, %{actor: actor})
end)
end
@spec before_action(
t(),
(t() -> t())
) ::
t()
def before_action(query, func) do
query = to_query(query)
%{query | before_action: [func | query.before_action]}
end
@spec after_action(
t(),
(t(), [Ash.Resource.record()] ->
{:ok, [Ash.Resource.record()]}
| {:ok, [Ash.Resource.record()]}
| {:error, term})
) :: t()
def after_action(query, func) do
query = to_query(query)
%{query | after_action: [func | query.after_action]}
end
defp add_action_filters(query, %{filter: nil}, _actor), do: query
defp add_action_filters(query, action, actor) do
if Ash.Filter.template_references_actor?(action.filter) and is_nil(actor) do
{:error, "Read action requires actor"}
else
built_filter =
Ash.Filter.build_filter_from_template(
action.filter,
actor,
query.arguments,
query.context
)
do_filter(query, built_filter)
end
end
defmacro expr(do: body) do
quote do
Ash.Query.expr(unquote(body))
end
end
defmacro expr({var, _, context} = binding) when is_atom(var) and is_atom(context) do
quote do
unquote(binding)
end
end
defmacro expr(body) do
if Keyword.keyword?(body) do
quote do
unquote(body)
end
else
expr = do_expr(body)
quote do
unquote(expr)
end
end
end
@operator_symbols Ash.Query.Operator.operator_symbols()
defp do_expr(expr, escape? \\ true)
defp do_expr({:^, _, [value]}, _escape?) do
value
end
defp do_expr({{:., _, [_, _]} = left, _, _}, escape?) do
do_expr(left, escape?)
end
defp do_expr({:ref, _, [field, path]}, escape?) do
ref =
case do_expr(path, false) do
%Ash.Query.Ref{attribute: head_attr, relationship_path: head_path} ->
case do_expr(field) do
%Ash.Query.Ref{attribute: tail_attribute, relationship_path: tail_relationship_path} ->
%Ash.Query.Ref{
relationship_path: head_path ++ [head_attr] ++ tail_relationship_path,
attribute: tail_attribute
}
other ->
%Ash.Query.Ref{relationship_path: head_path ++ [head_attr], attribute: other}
end
other ->
case do_expr(field, false) do
%Ash.Query.Ref{attribute: attribute, relationship_path: relationship_path} ->
%Ash.Query.Ref{
attribute: attribute,
relationship_path: List.wrap(other) ++ List.wrap(relationship_path)
}
other_field ->
%Ash.Query.Ref{attribute: other_field, relationship_path: other}
end
end
soft_escape(ref, escape?)
end
defp do_expr({:ref, _, [field]}, escape?) do
ref =
case do_expr(field, false) do
%Ash.Query.Ref{} = ref ->
ref
other ->
%Ash.Query.Ref{attribute: other, relationship_path: []}
end
soft_escape(ref, escape?)
end
defp do_expr({:., _, [left, right]} = ref, escape?) when is_atom(right) do
case do_ref(left, right) do
%Ash.Query.Ref{} = ref ->
soft_escape(ref, escape?)
:error ->
raise "Invalid reference! #{Macro.to_string(ref)}"
end
end
defp do_expr({op, _, nil}, escape?) when is_atom(op) do
soft_escape(%Ash.Query.Ref{relationship_path: [], attribute: op}, escape?)
end
defp do_expr({op, _, args}, escape?) when op in [:and, :or] do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(BooleanExpression.optimized_new(op, Enum.at(args, 0), Enum.at(args, 1)), escape?)
end
defp do_expr({op, _, [_, _] = args}, escape?)
when is_atom(op) and op in @operator_symbols do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: true}, escape?)
end
defp do_expr({op, _, args}, escape?) when is_atom(op) and is_list(args) do
args = Enum.map(args, &do_expr(&1, false))
soft_escape(%Ash.Query.Call{name: op, args: args, operator?: false}, escape?)
end
defp do_expr({left, _, _}, escape?) when is_tuple(left), do: do_expr(left, escape?)
defp do_expr(other, _), do: other
defp soft_escape(%_{} = val, _) do
{:%{}, [], Map.to_list(val)}
end
defp soft_escape(other, _), do: other
defp do_ref({left, _, nil}, right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref({{:., _, [_, _]} = left, _, _}, right) do
do_ref(left, right)
end
defp do_ref({:., _, [left, right]}, far_right) do
case do_ref(left, right) do
%Ash.Query.Ref{relationship_path: path, attribute: attribute} = ref ->
%{ref | relationship_path: path ++ [attribute], attribute: far_right}
:error ->
:error
end
end
defp do_ref({left, _, _}, right) when is_atom(left) and is_atom(right) do
%Ash.Query.Ref{relationship_path: [left], attribute: right}
end
defp do_ref(_left, _right) do
:error
end
@doc """
Ensure that only the specified attributes are present in the results.
The first call to `select/2` will replace the default behavior of selecting
all attributes. Subsequent calls to `select/2` will combine the provided
fields unless the `replace?` option is provided with a value of `true`.
If a field has been deselected, selecting it again will override that (because a single list of fields is tracked for selection)
Primary key attributes and private attributes are always selected and cannot be deselected.
When attempting to load a relationship (or manage it with `Ash.Changeset.manage_relationship/3`),
if the source field is not selected on the query/provided data an error will be produced. If loading
a relationship with a query, an error is produced if the query does not select the destination field
of the relationship.
"""
def select(query, fields, opts \\ []) do
query = to_query(query)
if opts[:replace?] do
%{query | select: Enum.uniq(List.wrap(fields))}
else
%{query | select: Enum.uniq(List.wrap(fields) ++ (query.select || []))}
end
end
@doc """
Ensure the the specified attributes are `nil` in the query results.
"""
def deselect(query, fields) do
query = to_query(query)
select =
if query.select do
query.select
else
query.resource
|> Ash.Resource.Info.attributes()
|> Enum.map(& &1.name)
end
select = select -- List.wrap(fields)
select(query, select, replace?: true)
end
def selecting?(query, field) do
case query.select do
nil ->
not is_nil(Ash.Resource.Info.attribute(query.resource, field))
select ->
if field in select do
true
else
attribute = Ash.Resource.Info.attribute(query.resource, field)
attribute && (attribute.primary_key? || attribute.private?)
end
end
end
@doc """
Loads named calculations or aggregates on the resource.
Currently, loading attributes has no effects, as all attributes are returned.
Before long, we will have the default list to load as the attributes, but if you say
`load(query, [:attribute1])`, that will be the only field filled in. This will let
data layers make more intelligent "select" statements as well.
"""
@spec load(t() | Ash.Resource.t(), atom | list(atom) | Keyword.t()) :: t()
def load(query, fields) when not is_list(fields) do
load(query, List.wrap(fields))
end
def load(query, fields) do
query = to_query(query)
Enum.reduce(fields, query, fn
{field, %__MODULE__{} = nested}, query ->
side_load(query, [{field, nested}])
{field, rest}, query ->
cond do
rel = Ash.Resource.Info.relationship(query.resource, field) ->
nested_query = load(rel.destination, rest)
side_load(query, [{field, nested_query}])
calculation = Ash.Resource.Info.calculation(query.resource, field) ->
{module, opts} = module_and_opts(calculation.calculation)
with {:ok, args} <- validate_arguments(calculation, rest),
{:ok, calculation} <-
Calculation.new(
calculation.name,
module,
opts,
args
) do
calculation = %{calculation | load: field}
%{query | calculations: Map.put(query.calculations, field, calculation)}
end
true ->
add_error(query, :load, "Invalid load #{inspect(field)}")
end
field, query ->
do_load(query, field)
end)
end
defp module_and_opts({module, opts}), do: {module, opts}
defp module_and_opts(module), do: {module, []}
defp do_load(query, field) do
cond do
Ash.Resource.Info.attribute(query.resource, field) ->
query
Ash.Resource.Info.relationship(query.resource, field) ->
side_load(query, field)
aggregate = Ash.Resource.Info.aggregate(query.resource, field) ->
related = Ash.Resource.Info.related(query.resource, aggregate.relationship_path)
with %{valid?: true} = aggregate_query <-
build(related, filter: aggregate.filter, sort: aggregate.sort),
{:ok, query_aggregate} <-
Aggregate.new(
query.resource,
aggregate.name,
aggregate.kind,
aggregate.relationship_path,
aggregate_query,
aggregate.field
) do
query_aggregate = %{query_aggregate | load: field}
new_aggregates = Map.put(query.aggregates, aggregate.name, query_aggregate)
%{query | aggregates: new_aggregates}
else
%{errors: errors} ->
add_error(query, :aggregates, Ash.Error.to_ash_error(errors))
{:error, error} ->
add_error(query, :aggregates, Ash.Error.to_ash_error(error))
end
calculation = Ash.Resource.Info.calculation(query.resource, field) ->
{module, opts} =
case calculation.calculation do
{module, opts} -> {module, opts}
module -> {module, []}
end
with {:ok, args} <- validate_arguments(calculation, %{}),
{:ok, calculation} <-
Calculation.new(calculation.name, module, opts, args) do
calculation = %{calculation | load: field}
%{query | calculations: Map.put(query.calculations, field, calculation)}
else
{:error, error} ->
add_error(query, :load, error)
end
true ->
add_error(query, :load, "Could not load #{inspect(field)}")
end
end
defp validate_arguments(calculation, args) do
Enum.reduce_while(calculation.arguments, {:ok, %{}}, fn argument, {:ok, arg_values} ->
value = default(Map.get(args, argument.name), argument.default)
if is_nil(value) do
if argument.allow_nil? do
{:cont, {:ok, Map.put(arg_values, argument.name, nil)}}
else
{:halt, {:error, "Argument #{argument.name} is required"}}
end
else
with {:ok, casted} <- Ash.Type.cast_input(argument.type, value, argument.constraints),
{:ok, casted} <-
Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
{:cont, {:ok, Map.put(arg_values, argument.name, casted)}}
else
{:error, error} ->
{:halt, {:error, error}}
end
end
end)
end
defp default(nil, {module, function, args}), do: apply(module, function, args)
defp default(nil, value) when is_function(value, 0), do: value.()
defp default(nil, value), do: value
defp default(value, _), do: value
@doc """
Sets a specific context key to a specific value
See `set_context/2` for more information.
"""
@spec put_context(t(), atom, term) :: t()
def put_context(query, key, value) do
query = to_query(query)
set_context(query, %{key => value})
end
@doc """
Merge a map of values into the query context
Not much uses this currently.
"""
@spec set_context(t(), map | nil) :: t()
def set_context(query, nil), do: query
def set_context(query, map) do
query = to_query(query)
%{query | context: Ash.Helpers.deep_merge_maps(query.context, map)}
end
@doc "Gets the value of an argument provided to the query"
@spec get_argument(t, atom) :: term
def get_argument(query, argument) when is_atom(argument) do
Map.get(query.arguments, argument) || Map.get(query.arguments, to_string(argument))
end
@doc "fetches the value of an argument provided to the query or `:error`"
@spec fetch_argument(t, atom) :: {:ok, term} | :error
def fetch_argument(query, argument) when is_atom(argument) do
case Map.fetch(query.arguments, argument) do
{:ok, value} ->
{:ok, value}
:error ->
case Map.fetch(query.arguments, to_string(argument)) do
{:ok, value} -> {:ok, value}
:error -> :error
end
end
end
@doc """
Add an argument to the query, which can be used in filter templates on actions
"""
def set_argument(query, argument, value) do
query = to_query(query)
if query.action do
with {:arg, argument} when not is_nil(argument) <-
{:arg,
Enum.find(
query.action.arguments,
&(&1.name == argument || to_string(&1.name) == argument)
)},
{:ok, casted} <- Ash.Changeset.cast_input(argument.type, value, argument.constraints),
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
{:constrained,
Ash.Type.apply_constraints(argument.type, casted, argument.constraints),
argument} do
%{query | arguments: Map.put(query.arguments, argument.name, casted)}
else
{:arg, nil} ->
query
{:constrained, {:ok, nil}, _argument} ->
query
{:constrained, {:error, error}, argument} ->
add_invalid_errors(query, argument, error)
{:error, error} ->
add_invalid_errors(query, argument, error)
:error ->
nil
end
else
%{query | arguments: Map.put(query.arguments, argument, value)}
end
end
defp add_invalid_errors(query, argument, error) do
messages =
if Keyword.keyword?(error) do
[error]
else
List.wrap(error)
end
messages
|> Enum.reduce(query, fn message, query ->
opts = Ash.Changeset.error_to_exception_opts(message, argument)
add_error(query, InvalidArgument.exception(opts))
end)
end
@doc """
Remove an argument from the query
"""
def delete_argument(query, argument_or_arguments) do
query = to_query(query)
argument_or_arguments
|> List.wrap()
|> Enum.reduce(query, fn argument, query ->
%{query | arguments: Map.delete(query.arguments, argument)}
end)
end
@doc """
Merge a map of arguments to the arguments list
"""
def set_arguments(query, map) do
query = to_query(query)
%{query | arguments: Map.merge(query.arguments, map)}
end
@doc false
def cast_arguments(query, action, defaults \\ :all, only_supplied? \\ false) do
action.arguments
|> Enum.reject(& &1.private?)
|> Enum.reject(&(only_supplied? && match?({:ok, _}, fetch_argument(query, &1.name))))
|> Enum.reduce(query, fn argument, new_query ->
{ignore_nil_check, value} =
if defaults == :all || argument.name in (defaults || []) do
{false, get_argument(query, argument.name) || argument_default(argument.default)}
else
value = get_argument(query, argument.name)
if argument_default(argument.default) do
{true, value}
else
{false, value}
end
end
cond do
!ignore_nil_check && is_nil(value) && !argument.allow_nil? ->
add_error(
query,
:arguments,
Required.exception(field: argument.name, type: :argument)
)
is_nil(value) ->
new_query
true ->
val =
case fetch_argument(query, argument.name) do
:error ->
if argument.default do
{:ok, argument_default(argument.default)}
else
:error
end
{:ok, val} ->
{:ok, val}
end
with {:found, {:ok, value}} <- {:found, val},
{:ok, casted} <- Ash.Type.cast_input(argument.type, value, argument.constraints),
{:ok, casted} <-
Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
if casted do
%{new_query | arguments: Map.put(new_query.arguments, argument.name, casted)}
else
new_query
end
else
{:error, _error} ->
add_error(
query,
:arguments,
InvalidArgument.exception(field: argument.name)
)
{:found, :error} ->
query
end
end
end)
end
defp argument_default(value) when is_function(value, 0), do: value.()
defp argument_default(value), do: value
def struct?(%_{}), do: true
def struct?(_), do: false
@spec set_tenant(t() | Ash.Resource.t(), String.t()) :: t()
def set_tenant(query, tenant) do
query = to_query(query)
%{query | tenant: tenant}
end
@doc "Removes a field from the list of fields to load"
@spec unload(t(), list(atom)) :: t()
def unload(query, fields) do
query = to_query(query)
Enum.reduce(fields, query, fn field, query ->
case field do
{field, rest} ->
new_side_loads = do_unload_side_load(query.side_load, {field, rest})
%{query | side_load: new_side_loads}
field ->
do_unload(query, field)
end
end)
end
defp do_unload(query, field) do
cond do
Ash.Resource.Info.attribute(query.resource, field) ->
query
Ash.Resource.Info.relationship(query.resource, field) ->
%{query | side_load: Keyword.delete(query.side_load, field)}
Ash.Resource.Info.aggregate(query.resource, field) ->
new_aggregates =
Enum.reduce(query.aggregates, %{}, fn
{_field, %{load: ^field}}, acc ->
acc
{field, aggregate}, acc ->
Map.put(acc, field, aggregate)
end)
%{query | aggregates: new_aggregates}
end
end
defp do_unload_side_load(%__MODULE__{} = query, unload) do
%{query | side_load: do_unload_side_load(query.side_load, unload)}
end
defp do_unload_side_load(side_loads, {field, rest}) do
Enum.reduce(side_loads, [], fn
^field, acc ->
acc
{^field, value}, acc ->
new_value =
rest
|> List.wrap()
|> Enum.reduce(value, &do_unload_side_load(&2, &1))
[{field, new_value} | acc]
value, acc ->
[value | acc]
end)
|> Enum.reverse()
end
defp do_unload_side_load(side_loads, field) do
do_unload_side_load(side_loads, {field, []})
end
@doc """
Builds a query from a keyword list.
This is used by certain query constructs like aggregates. It can also be used to manipulate a data structure
before passing it to an ash query. It allows for building an entire query struct using only a keyword list.
For example:
```elixir
Ash.Query.build(MyResource, filter: [name: "fred"], sort: [name: :asc], load: [:foo, :bar], offset: 10)
```
If you want to use the expression style filters, you can use `expr/1`. Be sure to `require Ash.Query` first,
or import it. Consider importing only the `expr/1` macro if you do that
For example:
```elixir
import Ash.Query, only: [expr: 1]
Ash.Query.build(Myresource, filter: expr(name == "marge"))
```
Supported keys:
* `filter` - filter keyword/expr or `%Ash.Filter{}`
* `sort` - sort keyword
* `limit` - integer limit
* `offset` - integer offset
* `load` - keyword/list of atoms to load
* `aggregate` - `{name, type, relationship}`
* `aggregate` - `{name, type, relationship, query_in_build_format}`
* `calculate` - `{name, module_and_opts}`
* `calculate` - `{name, module_and_opts, context}`
* `context: %{key: value}`
"""
@spec build(Ash.Resource.t(), Ash.Api.t() | nil, Keyword.t()) :: t()
def build(resource, api \\ nil, keyword) do
Enum.reduce(keyword, new(resource, api), fn
{:filter, value}, query ->
do_filter(query, value)
{:sort, value}, query ->
sort(query, value)
{:limit, value}, query ->
limit(query, value)
{:offset, value}, query ->
offset(query, value)
{:load, value}, query ->
load(query, value)
{:aggregate, {name, type, relationship}}, query ->
aggregate(query, name, type, relationship)
{:aggregate, {name, type, relationship, agg_query}}, query ->
aggregate(query, name, type, relationship, agg_query)
{:calculate, {name, module_and_opts}}, query ->
calculate(query, name, module_and_opts)
{:calculate, {name, module_and_opts, context}}, query ->
calculate(query, name, module_and_opts, context)
{:context, context}, query ->
set_context(query, context)
end)
end
@doc "Set the query's api, and any side loaded query's api"
def set_api(query, api) do
query = to_query(query)
%{query | api: api, side_load: set_side_load_api(query.side_load, api)}
end
@doc """
Adds an aggregation to the query.
Aggregations are made available on the `aggregates` field of the records returned
The filter option accepts either a filter or a keyword list of options to supply to build a limiting query for that aggregate.
See the DSL docs for each aggregate type in `Ash.Resource.Dsl` for more information.
"""
@spec aggregate(
t() | Ash.Resource.t(),
atom(),
Ash.Query.Aggregate.kind(),
atom | list(atom),
Keyword.t() | nil
) :: t()
def aggregate(query, name, type, relationship, agg_query \\ nil) do
{field, agg_query} = Keyword.pop(agg_query || [], :field)
query = to_query(query)
relationship = List.wrap(relationship)
if Ash.DataLayer.data_layer_can?(query.resource, {:aggregate, type}) do
agg_query =
case agg_query do
[] ->
nil
options when is_list(options) ->
build(Ash.Resource.Info.related(query.resource, relationship), options)
end
case Aggregate.new(query.resource, name, type, relationship, agg_query, field) do
{:ok, aggregate} ->
new_aggregates = Map.put(query.aggregates, aggregate.name, aggregate)
%{query | aggregates: new_aggregates}
{:error, error} ->
add_error(query, :aggregate, error)
end
else
add_error(
query,
:aggregate,
AggregatesNotSupported.exception(resource: query.resource, feature: "using")
)
end
end
@doc """
Adds a calculation to the query.
Calculations are made available on the `calculations` field of the records returned
The `module_and_opts` argument accepts either a `module` or a `{module, opts}`. For more information
on what that module should look like, see `Ash.Calculation`.
More features for calculations, like passing anonymous functions, will be supported in the future.
"""
def calculate(query, name, module_and_opts, context \\ %{}) do
query = to_query(query)
{module, opts} =
case module_and_opts do
{module, opts} -> {module, opts}
module -> {module, []}
end
case Calculation.new(name, module, opts, context) do
{:ok, calculation} ->
%{query | calculations: Map.put(query.calculations, name, calculation)}
{:error, error} ->
add_error(query, :calculations, error)
end
end
@doc "Limit the results returned from the query"
@spec limit(t() | Ash.Resource.t(), nil | integer()) :: t()
def limit(query, nil), do: to_query(query)
def limit(query, limit) when is_integer(limit) do
query = to_query(query)
if Ash.DataLayer.data_layer_can?(query.resource, :limit) do
query
|> Map.put(:limit, max(0, limit))
else
add_error(query, :limit, "Data layer does not support limits")
end
end
def limit(query, limit) do
add_error(query, :offset, InvalidLimit.exception(limit: limit))
end
@doc "Skip the first n records"
@spec offset(t() | Ash.Resource.t(), nil | integer()) :: t()
def offset(query, nil), do: to_query(query)
def offset(query, offset) when is_integer(offset) do
query = to_query(query)
if Ash.DataLayer.data_layer_can?(query.resource, :offset) do
query
|> Map.put(:offset, max(0, offset))
else
add_error(query, :offset, "Data layer does not support offset")
end
end
def offset(query, offset) do
query
|> to_query()
|> add_error(:offset, InvalidOffset.exception(offset: offset))
end
defp side_load(query, statement) do
query = to_query(query)
with sanitized_statement <- List.wrap(sanitize_side_loads(statement)),
:ok <-
validate_side_load(query, sanitized_statement),
new_side_loads <- merge_side_load(query.side_load, sanitized_statement) do
%{query | side_load: new_side_loads}
else
{:error, errors} ->
Enum.reduce(errors, query, &add_error(&2, :side_load, &1))
end
end
@doc false
def validate_side_load(query, side_loads, path \\ []) do
case do_validate_side_load(query, side_loads, path) do
[] -> :ok
errors -> {:error, errors}
end
end
defp do_validate_side_load(_query, %Ash.Query{} = side_load_query, path) do
case side_load_query.errors do
[] ->
[]
_errors ->
[
{:error,
InvalidQuery.exception(
query: side_load_query,
side_load_path: Enum.reverse(path)
)}
]
end
end
defp do_validate_side_load(query, {atom, _} = tuple, path) when is_atom(atom) do
do_validate_side_load(query, [tuple], path)
end
defp do_validate_side_load(query, side_loads, path) when is_list(side_loads) do
side_loads
|> List.wrap()
|> Enum.flat_map(fn
{key, value} ->
case Ash.Resource.Info.relationship(query.resource, key) do
nil ->
[
{:error,
NoSuchRelationship.exception(
resource: query.resource,
relationship: key,
side_load_path: Enum.reverse(path)
)}
]
relationship ->
cond do
!selecting?(query, relationship.source_field) ->
[
{:error,
"Cannot side load a relationship if you are not selecting the source field of that relationship"}
]
!Ash.Resource.Info.primary_action(relationship.destination, :read) ->
[
{:error,
NoReadAction.exception(
resource: relationship.destination,
when: "loading relationship #{relationship.name}"
)}
]
relationship.type == :many_to_many &&
!Ash.Resource.Info.primary_action(relationship.through, :read) ->
[
{:error,
NoReadAction.exception(
resource: relationship.destination,
when: "loading relationship #{relationship.name}"
)}
]
match?(%Ash.Query{}, value) && selecting?(value, relationship.destination_field) ->
validate_matching_query_and_continue(
value,
query.resource,
key,
path,
relationship
)
match?(%Ash.Query{}, value) ->
[
{:error,
"Cannot side load a relationship with a query unless the destination field of that query is selected"}
]
true ->
validate_matching_query_and_continue(
value,
query.resource,
key,
path,
relationship
)
end
end
end)
end
@doc false
def do_filter(query, %Ash.Filter{} = filter) do
query = to_query(query)
if Ash.DataLayer.data_layer_can?(query.resource, :filter) do
new_filter =
case query.filter do
nil ->
{:ok, filter}
existing_filter ->
Ash.Filter.add_to_filter(existing_filter, filter, :and, query.aggregates)
end
case new_filter do
{:ok, filter} ->
%{query | filter: filter}
{:error, error} ->
add_error(query, :filter, error)
end
else
add_error(query, :filter, "Data layer does not support filtering")
end
end
def do_filter(query, statement) do
query = to_query(query)
if Ash.DataLayer.data_layer_can?(query.resource, :filter) do
filter =
if query.filter do
Ash.Filter.add_to_filter(query.filter, statement, :and, query.aggregates)
else
Ash.Filter.parse(query.resource, statement, query.aggregates)
end
case filter do
{:ok, filter} ->
query
|> Map.put(:filter, filter)
{:error, error} ->
add_error(query, :filter, error)
end
else
add_error(query, :filter, "Data layer does not support filtering")
end
end
@doc """
Sort the results based on attributes or aggregates (calculations are not yet supported)
Takes a list of fields to sort on, or a keyword list/mixed keyword list of fields and sort directions.
The default sort direction is `:asc`.
Examples:
```
Ash.Query.sort(query, [:foo, :bar])
Ash.Query.sort(query, [:foo, bar: :desc])
Ash.Query.sort(query, [foo: :desc, bar: :asc])
```
"""
@spec sort(t() | Ash.Resource.t(), Ash.Sort.t()) :: t()
def sort(query, sorts) do
query = to_query(query)
if Ash.DataLayer.data_layer_can?(query.resource, :sort) do
sorts
|> List.wrap()
|> Enum.reduce(query, fn
{sort, direction}, query ->
%{query | sort: query.sort ++ [{sort, direction}]}
sort, query ->
%{query | sort: query.sort ++ [{sort, :asc}]}
end)
|> validate_sort()
else
add_error(query, :sort, "Data layer does not support sorting")
end
end
@spec unset(Ash.Resource.t() | t(), atom | [atom]) :: t()
def unset(query, keys) when is_list(keys) do
query = to_query(query)
keys
|> Enum.reduce(query, fn key, query ->
if key in [:api, :resource] do
query
else
struct(query, [{key, Map.get(%__MODULE__{}, key)}])
end
end)
end
def unset(query, key) do
if key in [:api, :resource] do
to_query(query)
else
query
|> to_query()
|> struct([{key, Map.get(%__MODULE__{}, key)}])
end
end
@doc "Return the underlying data layer query for an ash query"
def data_layer_query(%{resource: resource, api: api} = ash_query, opts \\ []) do
query = Ash.DataLayer.resource_to_query(resource, api)
filter_aggregates =
if ash_query.filter do
Ash.Filter.used_aggregates(ash_query.filter)
else
[]
end
sort_aggregates =
Enum.flat_map(ash_query.sort, fn {field, _} ->
case Map.fetch(ash_query.aggregates, field) do
:error ->
[]
{:ok, agg} ->
[agg]
end
end)
aggregates = Enum.uniq_by(filter_aggregates ++ sort_aggregates, & &1.name)
with {:ok, query} <-
add_aggregates(query, ash_query, aggregates),
{:ok, query} <-
Ash.DataLayer.sort(query, ash_query.sort, resource),
{:ok, query} <- maybe_filter(query, ash_query, opts),
{:ok, query} <- add_tenant(query, ash_query),
{:ok, query} <-
Ash.DataLayer.limit(query, ash_query.limit, resource),
{:ok, query} <-
Ash.DataLayer.offset(query, ash_query.offset, resource) do
Ash.DataLayer.set_context(resource, query, ash_query.context)
else
{:error, error} -> {:error, error}
end
end
defp add_tenant(query, ash_query) do
with :context <- Ash.Resource.Info.multitenancy_strategy(ash_query.resource),
tenant when not is_nil(tenant) <- ash_query.tenant,
{:ok, query} <- Ash.DataLayer.set_tenant(ash_query.resource, query, tenant) do
{:ok, query}
else
{:error, error} -> {:error, error}
_ -> {:ok, query}
end
end
defp add_aggregates(query, ash_query, aggregates) do
resource = ash_query.resource
aggregates
|> Enum.map(&add_tenant_to_aggregate_query(&1, ash_query))
|> Enum.reduce_while({:ok, query}, fn aggregate, {:ok, query} ->
case Ash.DataLayer.add_aggregate(query, aggregate, resource) do
{:ok, query} -> {:cont, {:ok, query}}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp add_tenant_to_aggregate_query(aggregate, %{tenant: nil}), do: aggregate
defp add_tenant_to_aggregate_query(%{query: nil} = aggregate, ash_query) do
aggregate_with_query = %{aggregate | query: Ash.Query.new(aggregate.resource)}
add_tenant_to_aggregate_query(aggregate_with_query, ash_query)
end
defp add_tenant_to_aggregate_query(aggregate, ash_query) do
case Ash.Resource.Info.multitenancy_strategy(aggregate.resource) do
nil ->
aggregate
:attribute ->
attribute = Ash.Resource.Info.multitenancy_attribute(aggregate.resource)
{m, f, a} = Ash.Resource.Info.multitenancy_parse_attribute(ash_query.resource)
attribute_value = apply(m, f, [ash_query.tenant | a])
%{aggregate | query: filter(aggregate.query, ^[{attribute, attribute_value}])}
:context ->
%{aggregate | query: set_tenant(aggregate.query, ash_query.tenant)}
end
end
defp validate_sort(%{resource: resource, sort: sort} = query) do
case Sort.process(resource, sort, query.aggregates) do
{:ok, new_sort} -> %{query | sort: new_sort}
{:error, error} -> add_error(query, :sort, error)
end
end
def add_error(query, keys \\ [], message) do
keys = List.wrap(keys)
query = to_query(query)
message =
if is_binary(message) do
string_path =
case keys do
[key] -> to_string(key)
keys -> Enum.join(keys, ".")
end
"#{string_path}: #{message}"
else
message
end
error =
message
|> Ash.Error.to_ash_error()
|> Map.update(:path, keys, &(keys ++ List.wrap(&1)))
%{query | errors: [error | query.errors], valid?: false}
end
defp validate_matching_query_and_continue(value, resource, key, path, relationship) do
%{destination: relationship_resource} = relationship
case value do
%__MODULE__{resource: query_resource} = destination_query
when query_resource != relationship_resource ->
[
InvalidQuery.exception(
resource: resource,
relationship: key,
query: destination_query,
side_load_path: Enum.reverse(path)
)
]
other ->
do_validate_side_load(relationship.destination, other, [key | path])
end
end
defp maybe_filter(query, %{filter: nil}, _) do
{:ok, query}
end
defp maybe_filter(query, ash_query, opts) do
case Ash.DataLayer.filter(query, ash_query.filter, ash_query.resource) do
{:ok, filtered} ->
if Keyword.get(opts, :only_validate_filter?, false) do
{:ok, query}
else
{:ok, filtered}
end
{:error, error} ->
{:error, error}
end
end
defp set_side_load_api(nil, _), do: nil
defp set_side_load_api([], _), do: []
defp set_side_load_api(%__MODULE__{} = query, api) do
set_api(query, api)
end
defp set_side_load_api(side_loads, api) do
Enum.map(side_loads, fn {key, further} ->
{key, set_side_load_api(further, api)}
end)
end
defp to_query(%__MODULE__{} = query), do: query
defp to_query(resource) do
resource
|> new()
|> Ash.DataLayer.transform_query()
end
defp merge_side_load([], right), do: sanitize_side_loads(right)
defp merge_side_load(left, []), do: sanitize_side_loads(left)
defp merge_side_load(
%__MODULE__{side_load: left_side_loads, tenant: left_tenant},
%__MODULE__{side_load: right_side_loads} = query
) do
%{query | side_load: merge_side_load(left_side_loads, right_side_loads)}
|> set_tenant(query.tenant || left_tenant)
end
defp merge_side_load(%__MODULE__{} = query, right) when is_list(right) do
side_load(query, right)
end
defp merge_side_load(left, %Ash.Query{} = query) when is_list(left) do
side_load(query, left)
end
defp merge_side_load(left, right) when is_atom(left), do: merge_side_load([{left, []}], right)
defp merge_side_load(left, right) when is_atom(right), do: merge_side_load(left, [{right, []}])
defp merge_side_load(left, right) when is_list(left) and is_list(right) do
right
|> sanitize_side_loads()
|> Enum.reduce(sanitize_side_loads(left), fn {rel, rest}, acc ->
Keyword.update(acc, rel, rest, &merge_side_load(&1, rest))
end)
end
defp sanitize_side_loads(side_load) when is_atom(side_load), do: {side_load, []}
defp sanitize_side_loads(%Ash.Query{} = query) do
Map.update!(query, :side_load, &sanitize_side_loads/1)
end
defp sanitize_side_loads(side_loads) do
side_loads
|> List.wrap()
|> Enum.map(fn
{key, value} ->
{key, sanitize_side_loads(value)}
side_load_part ->
cond do
is_atom(side_load_part) -> {side_load_part, []}
is_list(side_load_part) -> sanitize_side_loads(side_load_part)
true -> side_load_part
end
end)
end
end