Packages

ash

3.4.54
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 helpers.ex
Raw

lib/ash/actions/helpers.ex

defmodule Ash.Actions.Helpers do
@moduledoc false
require Logger
require Ash.Flags
def rollback_if_in_transaction(
{:error, %Ash.Error.Changes.StaleRecord{} = error},
_resource,
_changeset
) do
{:error, error}
end
def rollback_if_in_transaction({:error, error}, resource, changeset) do
error = Ash.Error.to_ash_error(error)
if Ash.DataLayer.in_transaction?(resource) do
case changeset do
%Ash.Changeset{} = changeset ->
Ash.DataLayer.rollback(resource, Ash.Changeset.add_error(changeset, error))
%Ash.Query{} = query ->
Ash.DataLayer.rollback(resource, Ash.Query.add_error(query, error))
%Ash.ActionInput{} = action_input ->
Ash.DataLayer.rollback(resource, Ash.ActionInput.add_error(action_input, error))
_ ->
Ash.DataLayer.rollback(resource, Ash.Error.to_error_class(error))
end
else
{:error, error}
end
end
def rollback_if_in_transaction({:error, :no_rollback, error}, _, _changeset) do
{:error, error}
end
def rollback_if_in_transaction(success, _, _), do: success
def validate_calculation_load!(%Ash.Query{}, module) do
raise """
`#{inspect(module)}.load/3` returned a query.
Returning a query from the `load/3` callback of a calculation is now deprecated.
Instead, return the load statement itself, i.e instead of `Ash.Query.load(query, [...])`,
just return `[...]`. This is so that Ash can examine the requirements of just this single
calculation to ensure that all required values are present
"""
end
def validate_calculation_load!(other, _), do: List.wrap(other)
defp set_context(%Ash.Changeset{} = changeset, context),
do: Ash.Changeset.set_context(changeset, context)
defp set_context(%Ash.Query{} = query, context),
do: Ash.Query.set_context(query, context)
defp set_context(%Ash.ActionInput{} = action_input, context),
do: Ash.ActionInput.set_context(action_input, context)
defp set_skip_unknown_opts(opts, %{action: %{skip_unknown_inputs: skip_unknown_inputs}}) do
Keyword.update(
opts,
:skip_unknown_inputs,
skip_unknown_inputs,
&Enum.concat(List.wrap(&1), skip_unknown_inputs)
)
end
defp set_skip_unknown_opts(opts, _query_or_changeset) do
opts
end
def maybe_embedded_domain(resource) do
if Ash.Resource.Info.embedded?(resource) do
Ash.EmbeddableType.ShadowDomain
end
end
def set_context_and_get_opts(domain, query_or_changeset, opts) do
opts = transform_tenant(opts)
opts = set_skip_unknown_opts(opts, query_or_changeset)
query_or_changeset = set_context(query_or_changeset, opts[:context] || %{})
domain =
Ash.Resource.Info.domain(query_or_changeset.resource) || opts[:domain] || domain ||
query_or_changeset.domain || maybe_embedded_domain(query_or_changeset.resource)
opts =
case query_or_changeset.context do
%{
private: %{
actor: actor
}
} ->
Keyword.put_new(opts, :actor, actor)
_ ->
opts
end
opts =
if tenant = query_or_changeset.tenant do
Keyword.put_new(opts, :tenant, tenant)
else
opts
end
opts =
case query_or_changeset.context do
%{
private: %{
authorize?: authorize?
}
}
when not is_nil(authorize?) ->
Keyword.put_new(opts, :authorize?, authorize?)
_ ->
opts
end
opts =
case query_or_changeset.context do
%{
private: %{
tracer: tracer
}
} ->
do_add_tracer(opts, tracer)
_ ->
opts
end
opts = set_opts(opts, domain, query_or_changeset)
query_or_changeset = add_context(query_or_changeset, opts)
query_or_changeset = %{query_or_changeset | domain: domain}
{query_or_changeset, opts}
end
def set_opts(opts, domain, query_or_changeset \\ nil) do
opts
|> add_actor(query_or_changeset, domain)
|> add_authorize?(query_or_changeset, domain)
|> add_tracer()
end
def add_context(query_or_changeset, opts) do
private_context = Map.new(Keyword.take(opts, [:actor, :authorize?, :tracer]))
case query_or_changeset do
%Ash.ActionInput{} ->
query_or_changeset
|> Ash.ActionInput.set_context(%{private: private_context})
|> Ash.ActionInput.set_tenant(query_or_changeset.tenant || opts[:tenant])
%Ash.Query{} ->
query_or_changeset
|> Ash.Query.set_context(%{private: private_context})
|> Ash.Query.set_tenant(query_or_changeset.tenant || opts[:tenant])
%Ash.Changeset{} ->
query_or_changeset
|> Ash.Changeset.set_context(%{
private: private_context
})
|> Ash.Changeset.set_tenant(query_or_changeset.tenant || opts[:tenant])
end
end
defp transform_tenant(opts) do
case Keyword.fetch(opts, :tenant) do
{:ok, tenant} ->
Keyword.put(opts, :tenant, tenant)
:error ->
opts
end
end
defp add_actor(opts, query_or_changeset, domain) do
if !domain do
raise Ash.Error.Framework.AssumptionFailed,
message: "Could not determine domain for action."
end
if !skip_requiring_actor?(query_or_changeset) && !internal?(query_or_changeset) &&
!Keyword.has_key?(opts, :actor) &&
Ash.Domain.Info.require_actor?(domain) do
raise Ash.Error.to_error_class(
Ash.Error.Forbidden.DomainRequiresActor.exception(domain: domain)
)
end
opts
end
defp internal?(%{context: %{private: %{internal?: true}}}), do: true
defp internal?(_), do: false
defp skip_requiring_actor?(%{context: %{private: %{require_actor?: false}}}), do: true
defp skip_requiring_actor?(_), do: false
defp add_authorize?(opts, query_or_changeset, domain) do
if !domain do
raise Ash.Error.Framework.AssumptionFailed,
message: "Could not determine domain for action."
end
case Ash.Domain.Info.authorize(domain) do
:always ->
if opts[:authorize?] == false && internal?(query_or_changeset) do
opts
else
if opts[:authorize?] == false do
raise Ash.Error.Forbidden.DomainRequiresAuthorization, domain: domain
end
Keyword.put(opts, :authorize?, true)
end
:by_default ->
Keyword.put_new(opts, :authorize?, true)
:when_requested ->
if Keyword.has_key?(opts, :actor) do
Keyword.put_new(opts, :authorize?, true)
else
Keyword.put(opts, :authorize?, opts[:authorize?] || Keyword.has_key?(opts, :actor))
end
end
end
defp add_tracer(opts) do
case Application.get_env(:ash, :tracer) do
nil ->
opts
tracer ->
do_add_tracer(opts, tracer)
end
end
defp do_add_tracer(opts, tracer) do
tracer = List.wrap(tracer)
Keyword.update(opts, :tracer, tracer, fn existing_tracer ->
if is_list(existing_tracer) do
Enum.uniq(tracer ++ existing_tracer)
else
if is_nil(existing_tracer) do
tracer
else
Enum.uniq(tracer ++ existing_tracer)
end
end
end)
end
@doc false
def notify({:ok, record, instructions}, changeset, opts) do
resource_notification = resource_notification(changeset, record, opts)
if opts[:return_notifications?] do
{:ok, record,
Map.update(
instructions,
:notifications,
[resource_notification],
&[resource_notification | &1]
)}
else
if Process.get(:ash_started_transaction?) do
current_notifications = Process.get(:ash_notifications, [])
Process.put(
:ash_notifications,
[resource_notification | current_notifications]
)
else
unsent_notifications = Ash.Notifier.notify([resource_notification])
warn_missed!(changeset.resource, changeset.action, %{
resource_notifications: unsent_notifications
})
end
{:ok, record, instructions}
end
end
def notify(other, _changeset, _opts), do: other
defp resource_notification(changeset, result, opts) do
# This gives notifications a view of what actually changed
changeset =
Enum.reduce(changeset.atomics, changeset, fn {key, value}, changeset ->
%{changeset | attributes: Map.put(changeset.attributes, key, value)}
end)
%Ash.Notifier.Notification{
resource: changeset.resource,
domain: changeset.domain,
actor: changeset.context[:private][:actor],
action: changeset.action,
for: Ash.Resource.Info.notifiers(changeset.resource) ++ changeset.action.notifiers,
data: result,
changeset: changeset,
metadata: opts[:notification_metadata] || %{}
}
end
def warn_missed!(resource, action, result) do
case Map.get(result, :resource_notifications, Map.get(result, :notifications, [])) do
empty when empty in [nil, []] ->
:ok
missed ->
case Application.get_env(:ash, :missed_notifications, :warn) do
:ignore ->
:ok
:raise ->
raise """
Missed #{Enum.count(missed)} notifications in action #{inspect(resource)}.#{action.name}.
This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
To ignore these in all cases:
config :ash, :missed_notifications, :ignore
To turn this into warnings:
config :ash, :missed_notifications, :warn
"""
:warn ->
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
Logger.warning("""
Missed #{Enum.count(missed)} notifications in action #{inspect(resource)}.#{action.name}.
This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
#{Exception.format_stacktrace(stacktrace)}
While you should likely leave this setting on, you can ignore these or turn them into errors.
To ignore these in all cases:
config :ash, :missed_notifications, :ignore
To turn this into raised errors:
config :ash, :missed_notifications, :raise
""")
end
end
end
def process_errors(changeset, [error]) do
%{changeset | errors: []}
|> Ash.Changeset.add_error(error)
|> Map.get(:errors)
|> case do
[error] ->
error
errors ->
errors
end
end
def process_errors(changeset, errors) when is_list(errors) do
%{changeset | errors: []}
|> Ash.Changeset.add_error(errors)
|> Map.get(:errors)
end
def process_errors(changeset, error), do: process_errors(changeset, [error])
def templated_opts({:templated, opts}, _actor, _arguments, _context), do: opts
def templated_opts(opts, actor, arguments, context) do
Ash.Expr.fill_template(
opts,
actor,
arguments,
context
)
end
def load_runtime_types({:ok, results}, query, attributes?) do
load_runtime_types(results, query, attributes?)
end
def load_runtime_types({:error, error}, _query, _attributes?) do
{:error, error}
end
def load_runtime_types(results, query, attributes?) when is_list(results) do
attributes = runtime_attributes(query, attributes?)
calcs = runtime_calculations(query)
if Enum.empty?(attributes) && Enum.empty?(calcs) do
{:ok, results}
else
Enum.reduce_while(results, {:ok, []}, fn result, {:ok, results} ->
case do_load_runtime_types(result, attributes, calcs) do
{:ok, result} ->
{:cont, {:ok, [result | results]}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> case do
{:ok, results} -> {:ok, Enum.reverse(results)}
{:error, error} -> {:error, error}
end
end
end
def load_runtime_types(nil, _, _attributes?), do: {:ok, nil}
def load_runtime_types(result, query, attributes?) do
do_load_runtime_types(
result,
runtime_attributes(query, attributes?),
runtime_calculations(query)
)
end
defp runtime_attributes(query, true) do
case query.select do
nil ->
Ash.Resource.Info.attributes(query.resource)
select ->
Enum.map(select, &Ash.Resource.Info.attribute(query.resource, &1))
end
|> Enum.reject(fn %{type: type, constraints: constraints} ->
Ash.Type.cast_in_query?(type, constraints)
end)
end
defp runtime_attributes(_, _), do: []
defp runtime_calculations(query) do
query.calculations
|> Kernel.||(%{})
|> Enum.filter(fn {_name, calc} ->
calc.type
end)
|> Enum.reject(fn {_name, calc} ->
constraints = Map.get(calc, :constraints, [])
if function_exported?(Ash.Type, :cast_in_query?, 2) do
Ash.Type.cast_in_query?(calc.type, constraints)
else
Ash.Type.cast_in_query?(calc.type)
end
end)
end
defp do_load_runtime_types(record, select, calculations) do
select
|> Enum.reduce_while({:ok, record}, fn attr, {:ok, record} ->
case Map.get(record, attr.name) do
nil ->
{:cont, {:ok, record}}
%Ash.NotLoaded{} ->
{:cont, {:ok, record}}
%Ash.ForbiddenField{} ->
{:cont, {:ok, record}}
value ->
case Ash.Type.cast_stored(
attr.type,
value,
attr.constraints
) do
{:ok, value} ->
{:cont, {:ok, Map.put(record, attr.name, value)}}
:error ->
{:halt, {:error, message: "is invalid", field: attr.name}}
end
end
end)
|> case do
{:ok, record} ->
Enum.reduce_while(calculations, {:ok, record}, fn {name, calc}, {:ok, record} ->
case calc.load do
nil ->
case Map.get(record.calculations || %{}, calc.name) do
nil ->
{:cont, {:ok, record}}
value ->
case Ash.Type.cast_stored(
calc.type,
value,
Map.get(calc, :constraints, [])
) do
{:ok, value} ->
{:cont,
{:ok, Map.update!(record, :calculations, &Map.put(&1, name, value))}}
:error ->
{:halt, {:error, message: "is invalid", field: calc.name}}
end
end
load ->
case Map.get(record, load) do
nil ->
{:cont, {:ok, record}}
value ->
case Ash.Type.cast_stored(
calc.type,
value,
Map.get(calc, :constraints, [])
) do
{:ok, casted} ->
{:cont, {:ok, Map.put(record, load, casted)}}
:error ->
{:halt, {:error, message: "is invalid", field: calc.name}}
end
end
end
end)
other ->
other
end
end
def apply_opts_load(%Ash.Changeset{} = changeset, opts) do
if opts[:load] do
Ash.Changeset.load(changeset, opts[:load])
else
changeset
end
end
def apply_opts_load(%Ash.Query{} = query, opts) do
if opts[:load] do
Ash.Query.load(query, opts[:load], Keyword.take(opts, [:strict?]))
else
query
end
end
def load({:ok, result, instructions}, changeset, domain, opts) do
if changeset.load in [nil, []] do
{:ok, result, instructions}
else
query =
changeset.resource
|> Ash.Query.load(changeset.load)
|> Ash.Query.set_context(changeset.context)
|> select_selected(result)
case Ash.load(result, query, Keyword.put(opts, :domain, domain)) do
{:ok, result} ->
{:ok, result, instructions}
{:error, error} ->
{:error, error}
end
end
end
def load({:ok, result}, changeset, domain, opts) do
if changeset.load in [nil, []] do
{:ok, result, %{}}
else
query =
changeset.resource
|> Ash.Query.load(changeset.load)
|> Ash.Query.set_context(changeset.context)
|> select_selected(result)
case Ash.load(result, query, Keyword.put(opts, :domain, domain)) do
{:ok, result} ->
{:ok, result, %{}}
{:error, error} ->
{:error, error}
end
end
end
def load(other, _, _, _), do: other
defp select_selected(query, result) do
select =
query.resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(&Ash.Resource.selected?(result, &1.name))
|> Enum.map(& &1.name)
Ash.Query.ensure_selected(query, select)
end
def restrict_field_access(result, %Ash.Query{
context: %{private: %{loading_relationship?: true}}
}) do
result
end
def restrict_field_access({:ok, record, instructions}, query_or_changeset) do
{:ok, restrict_field_access(record, query_or_changeset), instructions}
end
def restrict_field_access({:ok, record}, query_or_changeset) do
{:ok, restrict_field_access(record, query_or_changeset)}
end
def restrict_field_access({:error, error}, _), do: {:error, error}
def restrict_field_access(records, query_or_changeset) when is_list(records) do
Enum.map(records, &restrict_field_access(&1, query_or_changeset))
end
def restrict_field_access(%struct{results: results} = page, query_or_changeset)
when struct in [Ash.Page.Keyset, Ash.Page.Offset] do
%{page | results: restrict_field_access(results, query_or_changeset)}
end
def restrict_field_access(%Ash.NotLoaded{} = not_loaded, _query_or_changeset) do
not_loaded
end
def restrict_field_access(%Ash.ForbiddenField{} = forbidden_field, _query_or_changeset) do
forbidden_field
end
def restrict_field_access(%_{} = record, query_or_changeset) do
embedded? = Ash.Resource.Info.embedded?(query_or_changeset.resource)
if internal?(query_or_changeset) ||
(embedded? && !query_or_changeset.context[:private][:cleaning_up_field_auth?]) do
record
else
record.calculations
|> Enum.reduce(record, fn
{{:__ash_fields_are_visible__, fields}, value}, record ->
if value do
record
else
Enum.reduce(fields, record, fn field, record ->
type =
case Ash.Resource.Info.field(query_or_changeset.resource, field) do
%Ash.Resource.Aggregate{} -> :aggregate
%Ash.Resource.Attribute{} -> :attribute
%Ash.Resource.Calculation{} -> :calculation
end
forbidden_field =
if embedded? && type == :attribute do
%Ash.ForbiddenField{
field: field,
type: type,
original_value: Map.get(record, field)
}
else
%Ash.ForbiddenField{field: field, type: type}
end
record
|> Map.put(field, forbidden_field)
|> replace_dynamic_loads(field, type, query_or_changeset)
end)
end
|> Map.update!(
:calculations,
&Map.delete(&1, {:__ash_fields_are_visible__, fields})
)
_, record ->
record
end)
end
end
defp replace_dynamic_loads(record, _, :aggregate, _), do: record
defp replace_dynamic_loads(record, field, type, %Ash.Changeset{} = changeset)
when type in [:attribute, :calculation] do
query =
changeset.resource
|> Ash.Query.new()
|> Ash.Query.load(changeset.load)
replace_dynamic_loads(record, field, type, query)
end
defp replace_dynamic_loads(record, field, type, query)
when type in [:attribute, :calculation] do
Enum.reduce(
query.calculations,
record,
fn
{key, %{module: Ash.Resource.Calculation.LoadAttribute, opts: opts, load: load}},
record ->
if type == :attribute && opts[:attribute] == field do
if load do
Map.put(record, load, %Ash.ForbiddenField{field: load, type: type})
else
Map.update!(
record,
:calculations,
&Map.put(&1, key, %Ash.ForbiddenField{field: field, type: type})
)
end
else
record
end
{key, %{calc_name: calc_name, load: load}}, record ->
if calc_name == field and type == :calculation do
if load do
Map.put(record, load, %Ash.ForbiddenField{field: load, type: type})
else
Map.update!(
record,
:calculations,
&Map.put(&1, key, %Ash.ForbiddenField{field: field, type: type})
)
end
else
record
end
_, record ->
record
end
)
end
def select({:ok, results, instructions}, query) do
{:ok, select(results, query), instructions}
end
def select({:ok, results}, query) do
{:ok, select(results, query)}
end
def select({:error, error}, _query) do
{:error, error}
end
def select(nil, _), do: nil
def select(result, %{select: nil}) do
result
end
def select(result, nil) do
result
end
def select(%resource{} = result, %{select: select, resource: resource} = query) do
select_mask = select_mask(query)
result
|> Map.merge(select_mask)
|> Ash.Resource.put_metadata(:selected, select)
end
def select(:ok, _query), do: :ok
def select(results, %{select: select} = query) do
if Enumerable.impl_for(results) do
select_mask = select_mask(query)
Enum.map(results, fn result ->
result
|> Map.merge(select_mask)
|> Ash.Resource.put_metadata(:selected, select)
end)
else
results
end
end
defp select_mask(%{select: select, resource: resource}) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(fn attribute ->
if is_nil(select) do
attribute.select_by_default?
else
attribute.always_select? || attribute.primary_key? || attribute.name in select
end
end)
|> Map.new(fn attribute ->
{attribute.name, %Ash.NotLoaded{field: attribute.name, type: :attribute}}
end)
end
end