Packages

ash

1.35.0
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash actions read.ex
Raw

lib/ash/actions/read.ex

defmodule Ash.Actions.Read do
@moduledoc false
require Logger
require Ash.Query
alias Ash.Actions.{Helpers, SideLoad}
alias Ash.Engine
alias Ash.Engine.Request
alias Ash.Error.Invalid.{LimitRequired, PaginationRequired}
alias Ash.Error.Query.NoReadAction
alias Ash.Filter
alias Ash.Query.Aggregate
def unpaginated_read(query, action \\ nil, opts \\ []) do
action = action || Ash.Resource.Info.primary_action(query.resource, :read)
cond do
!action ->
{:error, NoReadAction.exception(resource: query.resource, when: "reading")}
action.pagination ->
opts = Keyword.put(opts, :page, false)
run(query, %{action | pagination: %{action.pagination | required?: false}}, opts)
true ->
run(query, action, opts)
end
end
@spec run(Ash.Query.t(), Ash.Resource.Actions.action(), Keyword.t()) ::
{:ok, Ash.Page.page() | list(Ash.Resource.record())}
| {:ok, Ash.Page.page() | list(Ash.Resource.record()), Ash.Query.t()}
| {:error, term}
def run(query, action, opts \\ []) do
authorize? =
if opts[:authorize?] == false do
false
else
opts[:authorize?] || Keyword.has_key?(opts, :actor)
end
opts = Keyword.put(opts, :authorize?, authorize?)
opts = Keyword.merge(opts, Map.get(query.context, :override_api_params) || [])
engine_opts = Keyword.take(opts, [:verbose?, :actor, :authorize?])
query = %{query | action: action}
original_query = query
initial_offset = query.offset
initial_limit = query.limit
query =
if query.__validated_for_action__ == action.name do
query
else
Ash.Query.for_read(query, action.name, %{}, actor: engine_opts[:actor])
end
query = Ash.Query.cast_arguments(query, action)
with %{valid?: true} <- query,
:ok <- validate_multitenancy(query, opts),
%{errors: []} = query <- query_with_initial_data(query, opts),
{:ok, filter_requests} <- filter_requests(query, opts),
{:ok, query, page_opts, count_request} <-
paginate(query, action, filter_requests, initial_offset, initial_limit, opts),
page_opts <- page_opts && Keyword.delete(page_opts, :filter),
{:ok, requests} <- requests(query, action, filter_requests, opts),
side_load_requests <- SideLoad.requests(query),
{:ok, %{data: %{data: data} = all_data}} <-
Engine.run(
requests ++ side_load_requests ++ List.wrap(count_request),
query.api,
engine_opts
),
data_with_side_loads <- SideLoad.attach_side_loads(data, all_data),
data_with_aggregates <-
add_aggregate_values(
data_with_side_loads,
query.aggregates,
query.resource,
Map.get(all_data, :aggregate_values, %{})
) do
data_with_aggregates
|> add_tenant(query)
|> add_page(
action,
Map.get(all_data, :count),
query.sort,
original_query,
Keyword.put(opts, :page, page_opts)
)
|> add_query(Map.get(all_data, :ultimate_query), opts)
else
%Ash.Query{errors: errors} = query ->
{:error, Ash.Error.to_error_class(errors, query: query)}
{:error, %Ash.Engine.Runner{errors: errors}} ->
{:error, Ash.Error.to_error_class(errors, query: query)}
{:error, error} ->
{:error, Ash.Error.to_error_class(error, query: query)}
end
end
defp validate_multitenancy(query, opts) do
if is_nil(Ash.Resource.Info.multitenancy_strategy(query.resource)) ||
Ash.Resource.Info.multitenancy_global?(query.resource) || query.tenant ||
opts[:initial_data] do
:ok
else
{:error,
"Queries against the #{inspect(query.resource)} resource require a tenant to be specified"}
end
end
defp add_tenant(data, query) do
if Ash.Resource.Info.multitenancy_strategy(query.resource) do
Enum.map(data, fn item ->
%{item | __metadata__: Map.put(item.__metadata__, :tenant, query.tenant)}
end)
else
data
end
end
defp add_query(result, query, opts) do
if opts[:return_query?] do
{:ok, result, query}
else
{:ok, result}
end
end
defp add_page(data, action, count, sort, original_query, opts) do
if opts[:page] && action.pagination do
to_page(data, action, count, sort, original_query, opts)
else
data
end
end
defp to_page(data, action, count, sort, original_query, opts) do
page_opts = opts[:page]
{data, rest} =
if opts[:page][:limit] do
Enum.split(data, opts[:page][:limit])
else
{data, []}
end
more? = not Enum.empty?(rest)
if page_opts[:offset] do
if action.pagination.keyset? do
data
|> Ash.Page.Keyset.data_with_keyset(sort)
|> Ash.Page.Offset.new(count, original_query, more?, opts)
else
Ash.Page.Offset.new(data, count, original_query, more?, opts)
end
else
cond do
action.pagination.offset? && action.pagination.keyset? ->
data
|> Ash.Page.Keyset.data_with_keyset(sort)
|> Ash.Page.Offset.new(count, original_query, more?, opts)
action.pagination.offset? ->
Ash.Page.Offset.new(data, count, original_query, more?, opts)
true ->
Ash.Page.Keyset.new(data, count, sort, original_query, more?, opts)
end
end
end
defp filter_requests(query, opts) do
authorizing? =
if opts[:authorize?] == false do
false
else
Keyword.has_key?(opts, :actor) || opts[:authorize?]
end
if not Keyword.has_key?(opts, :initial_data) &&
authorizing? do
Filter.read_requests(query.api, query.filter)
else
{:ok, []}
end
end
defp query_with_initial_data(query, opts) do
case Keyword.fetch(opts, :initial_data) do
:error ->
query
{:ok, nil} ->
Ash.Query.filter(query, false)
{:ok, []} ->
Ash.Query.filter(query, false)
{:ok, [record]} ->
pkey_value =
record |> Map.take(Ash.Resource.Info.primary_key(query.resource)) |> Map.to_list()
Ash.Query.filter(query, ^pkey_value)
{:ok, %{} = record} ->
pkey_value =
record |> Map.take(Ash.Resource.Info.primary_key(query.resource)) |> Map.to_list()
Ash.Query.filter(query, ^pkey_value)
{:ok, records} when is_list(records) ->
pkey = Ash.Resource.Info.primary_key(query.resource)
pkey_value = Enum.map(records, fn record -> record |> Map.take(pkey) |> Map.to_list() end)
filter = [or: pkey_value]
Ash.Query.filter(query, ^filter)
end
end
defp requests(query, action, filter_requests, opts) do
authorizing? =
if opts[:authorize?] == false do
false
else
Keyword.has_key?(opts, :actor) || opts[:authorize?]
end
can_be_in_query? = not Keyword.has_key?(opts, :initial_data)
{aggregate_auth_requests, aggregate_value_requests, aggregates_in_query} =
Aggregate.requests(query, can_be_in_query?, authorizing?)
request =
Request.new(
resource: query.resource,
api: query.api,
query:
Request.resolve([], fn _ ->
case Filter.run_other_data_layer_filters(
query.api,
query.resource,
query.filter
) do
{:ok, filter} ->
{:ok, %{query | filter: filter}}
{:error, error} ->
{:error, error}
end
end),
action: action,
authorize?: not Keyword.has_key?(opts, :initial_data),
data:
data_field(
opts,
filter_requests,
aggregate_auth_requests,
aggregates_in_query,
query
),
path: [:data],
name: "#{action.type} - `#{action.name}`"
)
{:ok, [request | filter_requests] ++ aggregate_auth_requests ++ aggregate_value_requests}
end
defp data_field(
params,
filter_requests,
aggregate_auth_requests,
aggregates_in_query,
initial_query
) do
if params[:initial_data] do
List.wrap(params[:initial_data])
else
relationship_filter_paths =
Enum.map(filter_requests, fn request ->
request.path ++ [:authorization_filter]
end)
aggregate_auth_paths =
Enum.map(aggregate_auth_requests, fn request ->
request.path ++ [:authorization_filter]
end)
deps = [
[:data, :query]
| relationship_filter_paths ++ aggregate_auth_paths
]
Request.resolve(
deps,
fn %{data: %{query: ash_query}} = data ->
multitenancy_attribute = Ash.Resource.Info.multitenancy_attribute(ash_query.resource)
ash_query =
if multitenancy_attribute && ash_query.tenant do
{m, f, a} = Ash.Resource.Info.multitenancy_parse_attribute(ash_query.resource)
attribute_value = apply(m, f, [ash_query.tenant | a])
Ash.Query.filter(ash_query, [{multitenancy_attribute, attribute_value}])
else
ash_query
end
|> run_before_action()
query =
initial_query
|> Ash.Query.unset([:filter, :aggregates, :sort])
|> Ash.Query.data_layer_query(only_validate_filter?: true)
with %{valid?: true} <- ash_query,
{:ok, query} <- query,
{:ok, filter} <-
filter_with_related(relationship_filter_paths, ash_query, data),
{:ok, query} <-
add_aggregates(
query,
ash_query,
aggregates_in_query,
Map.get(data, :aggregate, %{})
),
{:ok, query} <-
Ash.DataLayer.filter(query, filter, ash_query.resource),
{:ok, query} <-
Ash.DataLayer.sort(query, ash_query.sort, ash_query.resource),
{:ok, query} <-
Ash.DataLayer.set_context(ash_query.resource, query, ash_query.context),
{:ok, query} <- set_tenant(query, ash_query),
{:ok, results} <- run_query(ash_query, query),
{:ok, results} <- run_after_action(initial_query, results),
{:ok, with_calculations} <-
add_calculation_values(ash_query, results, ash_query.calculations) do
if params[:return_query?] do
ultimate_query =
ash_query
|> Ash.Query.unset(:filter)
|> Ash.Query.filter(filter)
{:ok, with_calculations, %{extra_data: %{ultimate_query: ultimate_query}}}
else
{:ok, with_calculations}
end
else
%{valid?: false} = query ->
{:error, query}
other ->
other
end
end
)
end
end
defp run_before_action(query) do
Enum.reduce(query.before_action, query, & &1.(&2))
end
defp run_after_action(query, results) do
query.after_action
|> Enum.reduce_while({query, {:ok, results}}, fn after_action, {query, {:ok, results}} ->
case after_action.(query, results) do
{:ok, results} -> {:cont, {query, {:ok, results}}}
{:error, error} -> {:halt, {query, {:error, error}}}
end
end)
|> elem(1)
end
defp set_tenant(query, ash_query) do
if Ash.Resource.Info.multitenancy_strategy(ash_query.resource) == :context && ash_query.tenant do
Ash.DataLayer.set_tenant(ash_query.resource, query, ash_query.tenant)
else
{:ok, query}
end
end
defp page_opts(action, opts) do
cond do
!(action.pagination && action.pagination.default_limit) ->
opts[:page]
Keyword.keyword?(opts[:page]) && !Keyword.has_key?(opts[:page], :limit) ->
Keyword.put(opts[:page], :limit, action.pagination.default_limit)
is_nil(opts[:page]) ->
[limit: action.pagination.default_limit]
true ->
opts[:page]
end
end
defp paginate(starting_query, action, filter_requests, initial_offset, initial_limit, opts) do
page_opts = page_opts(action, opts)
cond do
action.pagination == false && page_opts ->
{:error, "Pagination is not supported"}
action.pagination == false ->
{:ok, starting_query, opts[:page], nil}
page_opts == false ->
if action.pagination.required? do
{:error, PaginationRequired.exception([])}
else
{:ok, starting_query, false, nil}
end
page_opts[:limit] || is_nil(page_opts) || page_opts == [] ->
case do_paginate(starting_query, action.pagination, opts) do
{:ok, query} ->
count_request =
count_request(
starting_query,
action,
filter_requests,
initial_offset,
initial_limit,
opts
)
{:ok, query, page_opts, count_request}
{:error, error} ->
{:error, error}
end
action.pagination.required? ->
{:error, LimitRequired.exception([])}
true ->
{:error, LimitRequired.exception([])}
end
end
defp do_paginate(query, pagination, opts) do
paginated =
cond do
opts[:page][:before] || opts[:page][:after] ->
keyset_pagination(query, opts[:page])
opts[:page][:offset] ->
limit_offset_pagination(query, opts[:page])
pagination.offset? && pagination.keyset? ->
keyset_pagination(query, opts[:page])
pagination.offset? ->
limit_offset_pagination(query, opts[:page])
true ->
keyset_pagination(query, opts[:page])
end
case paginated do
{:ok, paginated} ->
if opts[:page][:filter] do
{:ok, Ash.Query.filter(paginated, ^opts[:page][:filter])}
else
{:ok, paginated}
end
{:error, error} ->
{:error, error}
end
end
defp keyset_pagination(query, opts) do
sorted =
if Ash.Actions.Sort.sorting_on_identity?(query) do
query
else
Ash.Query.sort(query, Ash.Resource.Info.primary_key(query.resource))
end
limited =
cond do
opts[:limit] && sorted.limit ->
Ash.Query.limit(sorted, min(opts[:limit], sorted.limit) + 1)
opts[:limit] ->
Ash.Query.limit(sorted, opts[:limit] + 1)
true ->
sorted
end
if opts[:before] || opts[:after] do
after_or_before =
if opts[:before] do
:before
else
:after
end
case Ash.Page.Keyset.filter(
opts[:before] || opts[:after],
sorted.sort,
after_or_before
) do
{:ok, filter} ->
{:ok, Ash.Query.filter(limited, ^filter)}
{:error, error} ->
{:error, error}
end
else
{:ok, limited}
end
end
defp limit_offset_pagination(query, opts) do
limited =
cond do
opts[:limit] && query.limit ->
Ash.Query.limit(query, min(opts[:limit], query.limit) + 1)
opts[:limit] ->
Ash.Query.limit(query, opts[:limit] + 1)
true ->
query
end
with_offset =
cond do
opts[:offset] && query.offset ->
Ash.Query.offset(limited, max(opts[:offset], query.offset))
opts[:offset] ->
Ash.Query.offset(limited, opts[:offset])
true ->
limited
end
{:ok, with_offset}
end
defp count_request(_, %{pagination: %{countable: false}}, _, _, _, _), do: nil
defp count_request(initial_query, action, filter_requests, initial_offset, initial_limit, opts) do
if opts[:page][:count] == true ||
(opts[:page][:count] != false and action.pagination.countable == :by_default) do
relationship_filter_paths =
Enum.map(filter_requests, fn request ->
request.path ++ [:authorization_filter]
end)
Request.new(
resource: initial_query.resource,
api: initial_query.api,
query: initial_query,
action: action,
authorize?: false,
data:
Request.resolve(
[[:data, :authorization_filter]] ++ relationship_filter_paths,
fn %{
data: %{
authorization_filter: auth_filter
}
} = data ->
query =
initial_query
|> Ash.Query.unset([:filter, :aggregates, :sort, :limit, :offset])
|> Ash.Query.limit(initial_limit)
|> Ash.Query.offset(initial_offset)
|> Ash.Query.filter(^auth_filter)
|> Ash.Query.data_layer_query(only_validate_filter?: true)
with {:ok, query} <- query,
{:ok, filter} <-
filter_with_related(relationship_filter_paths, initial_query, data),
{:ok, query} <-
Ash.DataLayer.filter(query, filter, initial_query.resource),
{:ok, query} <-
Ash.DataLayer.sort(query, initial_query.sort, initial_query.resource),
{:ok, %{count: count}} <- run_count_query(initial_query, query) do
{:ok, count}
end
end
),
path: [:count],
name: "#{action.type} - `#{action.name}`"
)
end
end
defp run_query(
%{
resource: destination_resource,
context: %{
data_layer: %{
lateral_join_source: {root_data, resource, source, destination}
}
}
} = ash_query,
query
) do
query
|> Ash.DataLayer.run_query_with_lateral_join(
root_data,
resource,
destination_resource,
source,
destination
)
|> Helpers.select(ash_query)
end
defp run_query(%{resource: resource} = ash_query, query) do
query
|> Ash.DataLayer.run_query(resource)
|> Helpers.select(ash_query)
end
defp run_count_query(
%{
resource: destination_resource,
context: %{
data_layer: %{lateral_join_source: {root_data, resource, source, destination}}
}
},
query
) do
case Ash.Query.Aggregate.new(destination_resource, :count, :count, [], nil, nil) do
{:ok, aggregate} ->
Ash.DataLayer.run_aggregate_query_with_lateral_join(
query,
[aggregate],
root_data,
resource,
destination_resource,
source,
destination
)
{:error, error} ->
{:error, error}
end
end
defp run_count_query(ash_query, query) do
case Ash.Query.Aggregate.new(ash_query.resource, :count, :count, [], nil, nil) do
{:ok, aggregate} ->
Ash.DataLayer.run_aggregate_query(query, [aggregate], ash_query.resource)
{:error, error} ->
{:error, error}
end
end
defp add_calculation_values(query, results, calculations) do
calculations
|> Enum.reduce_while({:ok, %{}}, fn {_name, calculation}, {:ok, calculation_results} ->
context = Map.put(calculation.context, :context, query.context)
case calculation.module.calculate(results, calculation.opts, context) do
results when is_list(results) ->
{:cont, {:ok, Map.put(calculation_results, calculation, results)}}
{:ok, results} ->
{:cont, {:ok, Map.put(calculation_results, calculation, results)}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, calculation_results} ->
{:ok,
Enum.reduce(calculation_results, results, fn {calculation, values}, records ->
if calculation.load do
:lists.zipwith(
fn record, value -> Map.put(record, calculation.name, value) end,
records,
values
)
else
:lists.zipwith(
fn record, value ->
%{record | calculations: Map.put(record.calculations, calculation.name, value)}
end,
records,
values
)
end
end)}
{:error, error} ->
{:error, error}
end
end
defp add_aggregate_values(results, _aggregates, _resource, aggregate_values)
when aggregate_values == %{},
do: Enum.map(results, &Map.update!(&1, :aggregates, fn agg -> agg || %{} end))
defp add_aggregate_values(results, aggregates, resource, aggregate_values) do
keys_to_aggregates =
Enum.reduce(aggregate_values, %{}, fn {_name, keys_to_values}, acc ->
Enum.reduce(keys_to_values, acc, fn {pkey, values}, acc ->
Map.update(acc, pkey, values, &Map.merge(&1, values))
end)
end)
pkey = Ash.Resource.Info.primary_key(resource)
loaded =
aggregates
|> Enum.map(fn {_, aggregate} -> aggregate.load end)
|> Enum.reject(&is_nil/1)
Enum.map(results, fn result ->
aggregate_values = Map.get(keys_to_aggregates, Map.take(result, pkey), %{})
{top_level, nested} = Map.split(aggregate_values || %{}, loaded)
Map.merge(%{result | aggregates: Map.merge(result.aggregates, nested)}, top_level)
end)
end
defp add_aggregates(data_layer_query, query, aggregates_to_add, aggregate_filters) do
aggregates_to_add =
Enum.into(aggregates_to_add, %{}, fn aggregate ->
{aggregate.name, aggregate}
end)
aggregates_to_add
|> Enum.reduce({aggregates_to_add, aggregate_filters}, fn {name, aggregate},
{aggregates, aggregate_filters} ->
case Map.fetch(aggregate_filters, aggregate.relationship_path) do
{:ok, %{authorization_filter: filter}} ->
{Map.put(aggregates, name, %{aggregate | authorization_filter: filter}),
Map.delete(aggregates, aggregate.relationship_path)}
:error ->
{aggregates, aggregate_filters}
end
end)
|> elem(0)
|> Enum.reduce_while({:ok, data_layer_query}, fn {_name, aggregate},
{:ok, data_layer_query} ->
case Ash.DataLayer.add_aggregate(data_layer_query, aggregate, query.resource) do
{:ok, new_query} -> {:cont, {:ok, new_query}}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp filter_with_related(relationship_filter_paths, ash_query, data) do
Enum.reduce_while(relationship_filter_paths, {:ok, ash_query.filter}, fn path,
{:ok, filter} ->
case get_in(data, path) do
nil ->
{:cont, {:ok, filter}}
authorization_filter ->
add_authorization_filter(filter, authorization_filter)
end
end)
end
defp add_authorization_filter(filter, authorization_filter) do
case Ash.Filter.add_to_filter(filter, authorization_filter) do
{:ok, new_filter} ->
{:cont, {:ok, new_filter}}
{:error, error} ->
{:halt, {:error, error}}
end
end
end