Packages

ash

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

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash embeddable_type.ex
Raw

lib/ash/embeddable_type.ex

defmodule Ash.EmbeddableType do
@moduledoc false
@include_source_by_default Application.compile_env(
:ash,
:include_embedded_source_by_default?,
true
)
@embedded_resource_array_constraints [
sort: [
type: :any,
doc: """
A sort to be applied when casting the data.
Only relevant for a type of {:array, `EmbeddedResource}`
The sort is not applied when reading the data, so if the sort changes you will
need to fix it in your database or wait for the data to be written again, at which
point it will be sorted when casting.
"""
],
load: [
type: {:list, :atom},
doc: """
A list of calculations to load on the resource.
Only relevant for a type of {:array, `EmbeddedResource}`
Aggregates are not supported on embedded resources.
"""
],
domain: [
type: {:spark, Ash.Domain},
doc:
"The domain to use when interacting with the resource. Defaults to the domain of the source changeset."
],
min_length: [
type: :non_neg_integer,
doc: "A minimum length for the items"
],
max_length: [
type: :non_neg_integer,
doc: "A maximum length for the items"
],
create_action: [
type: :atom,
doc:
"The action to use on the resource when creating an embed. The primary is used by default."
],
update_action: [
type: :atom,
doc:
"The action to use on the resource when updating an embed. The primary is used by default."
],
destroy_action: [
type: :atom,
doc:
"The action to use on the resource when destroying an embed. The primary is used by default."
],
read_action: [
type: :atom,
doc: "The read action to use when reading the embed. The primary is used by default."
],
include_source?: [
type: :boolean,
default: @include_source_by_default,
doc:
"Whether to include the source changeset in the context. Defaults to the value of `config :ash, :include_embedded_source_by_default`, or `true`. In 4.x, the default will be `false`."
],
__source__: [
type: :any,
hide: true,
doc:
"This is hidden in the documentation, but this is used to add the `__source__` context to the changeset."
]
]
defmodule ShadowDomain do
@moduledoc false
use Ash.Domain, validate_config_inclusion?: false
resources do
allow_unregistered?(true)
end
execution do
timeout(:infinity)
end
end
@doc false
def embedded_resource_array_constraints, do: @embedded_resource_array_constraints
@doc false
def handle_errors(errors) do
errors
|> do_handle_errors()
|> List.wrap()
|> Ash.Helpers.flatten_preserving_keywords()
end
defp do_handle_errors(errors) when is_list(errors) do
if Keyword.keyword?(errors) do
main_fields = Keyword.take(errors, [:message, :field, :fields, :path])
vars = Keyword.merge(main_fields, Keyword.get(errors, :vars, []))
main_fields
|> Keyword.put(:vars, vars)
|> Enum.into(%{})
|> do_handle_errors()
else
Enum.map(errors, &do_handle_errors/1)
end
end
defp do_handle_errors(%{errors: errors}) do
errors
|> List.wrap()
|> do_handle_errors()
end
defp do_handle_errors(%Ash.Error.Changes.InvalidAttribute{
message: message,
field: field,
vars: vars
}) do
vars
|> Keyword.put(:field, field)
|> Keyword.put(:message, message)
end
defp do_handle_errors(%{message: message, vars: vars, field: field}) do
vars
|> Keyword.put(:message, message)
|> Keyword.put(:field, field)
end
defp do_handle_errors(%{message: message, vars: vars}) do
vars
|> Keyword.put(:message, message)
end
defp do_handle_errors(error) when is_binary(error) do
[message: error]
end
defp do_handle_errors(error) when is_exception(error) do
[error]
end
defp do_handle_errors(_error) do
[message: "Something went wrong"]
end
defmacro single_embed_implementation(opts) do
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
quote generated: true, location: :keep, bind_quoted: [opts: opts] do
@include_source_by_default Application.compile_env(
:ash,
:include_embedded_source_by_default?,
true
)
alias Ash.EmbeddableType.ShadowDomain
def storage_type(_), do: :map
def cast_atomic(value, constraints) do
with :ok <- check_atomic(value, constraints) do
case cast_input(value, constraints) do
{:ok, value} ->
{:atomic, value}
{:error, error} ->
{:error, error}
end
end
end
def cast_input(%{__struct__: __MODULE__} = input, _constraints), do: {:ok, input}
def cast_input(value, constraints) when is_map(value) do
action =
constraints[:create_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :create).name
__MODULE__
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_create(action, value,
domain: ShadowDomain,
skip_unknown_inputs: skip_unknown_inputs(constraints)
)
|> Ash.create()
|> case do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, Ash.EmbeddableType.handle_errors(error)}
end
end
def cast_input(nil, _), do: {:ok, nil}
def cast_input(_, _), do: :error
def cast_input_array(value, constraints) when is_list(value) do
action =
case constraints[:create_action] do
nil ->
Ash.Resource.Info.primary_action!(__MODULE__, :create)
action ->
Ash.Resource.Info.action(__MODULE__, action)
end
{structs, values} =
value
|> Stream.with_index()
|> Enum.split_with(&is_struct(elem(&1, 0), __MODULE__))
if Enum.empty?(values) do
{:ok, Enum.map(structs, &elem(&1, 0))}
else
skip_unknown_inputs = List.wrap(constraints[:skip_unknown_inputs])
# This is a simplified, tight-loop version of resource creation
if Enum.empty?(action.changes) &&
Enum.empty?(Ash.Resource.Info.changes(__MODULE__, action.type)) &&
Enum.empty?(Ash.Resource.Info.validations(__MODULE__, action.type)) &&
Enum.empty?(Ash.Resource.Info.notifiers(__MODULE__)) &&
Enum.empty?(Ash.Resource.Info.relationships(__MODULE__)) do
has_structs? = not Enum.empty?(structs)
attributes = Ash.Resource.Info.attributes(__MODULE__)
base =
attributes
|> Enum.filter(&(not is_nil(&1.default) && &1.match_other_defaults?))
|> Enum.reduce(struct(__MODULE__), fn attribute, acc ->
value =
case attribute.default do
{mod, func, args} -> apply(mod, func, args)
function when is_function(function, 0) -> function.()
value -> value
end
Map.put(acc, attribute.name, value)
end)
Enum.reduce_while(values, {:ok, []}, fn {value, index}, {:ok, results} ->
base =
attributes
|> Enum.filter(&(not is_nil(&1.default) && !&1.match_other_defaults?))
|> Enum.reduce(struct(__MODULE__), fn attribute, acc ->
value =
case attribute.default do
{mod, func, args} -> apply(mod, func, args)
function when is_function(function, 0) -> function.()
value -> value
end
Map.put(acc, attribute.name, value)
end)
Enum.reduce_while(value, {:ok, index, base}, fn {key, value}, {:ok, index, acc} ->
case Ash.Resource.Info.attribute(__MODULE__, key) do
nil ->
{:cont, {:ok, index, acc}}
attribute ->
if attribute.name in action.accept do
with value <- Ash.Type.Helpers.handle_indexed_maps(attribute.type, value),
{:ok, casted} <-
Ash.Type.cast_input(attribute.type, value, attribute.constraints),
{:ok, casted} <-
Ash.Type.apply_constraints(
attribute.type,
casted,
attribute.constraints
) do
{:cont, {:ok, index, Map.put(acc, attribute.name, casted)}}
else
{:error, error} ->
error =
Ash.Error.set_path(Ash.Error.to_ash_error(error), attribute.name)
{:halt, {:error, index, error}}
end
else
if Enum.any?(skip_unknown_inputs, &(&1 == :* || &1 == key)) do
{:cont, {:ok, index, acc}}
else
{:halt,
{:error, index,
Ash.Error.Invalid.NoSuchInput.exception(
resource: __MODULE__,
action: action.name,
input: key,
inputs: Ash.Resource.Info.action_inputs(__MODULE__, action.name)
)}}
end
end
end
end)
|> case do
{:ok, index, result} ->
attributes
|> Stream.filter(
&(&1.name in action.accept && &1.name not in action.allow_nil_input &&
(&1.allow_nil? == false || &1.name in action.require_attributes))
)
|> Enum.reduce_while({:ok, result}, fn attr, {:ok, result} ->
if is_nil(Map.get(result, attr.name)) do
{:halt,
{:error,
Ash.Error.Changes.Required.exception(
resource: __MODULE__,
field: attr.name,
type: :attribute
)}}
else
{:cont, {:ok, result}}
end
end)
|> case do
{:ok, result} ->
if has_structs? do
{:cont, {:ok, [{result, index} | results]}}
else
{:cont, {:ok, [result | results]}}
end
{:error, error} ->
{:halt, {:error, index, error}}
end
{:error, index, error} ->
{:halt, {:error, index, error}}
end
end)
|> case do
{:ok, values} ->
if has_structs? do
values =
values
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
Enum.reduce(structs, {:ok, values}, fn {struct, index}, {:ok, values} ->
{:ok, List.insert_at(values, index, struct)}
end)
else
{:ok, Enum.reverse(values)}
end
{:error, index, error} ->
{:error, Ash.Error.set_path(Ash.Error.to_ash_error(error), index)}
end
else
{context, opts} =
case constraints[:__source__] do
%Ash.Changeset{context: context} = source ->
{Map.put(context, :__source__, source),
Ash.Context.to_opts(context[:private] || %{})}
_ ->
{%{}, []}
end
values
|> Stream.map(&elem(&1, 0))
|> Ash.bulk_create(
__MODULE__,
action.name,
Keyword.merge(opts,
domain: ShadowDomain,
context: context,
sorted?: true,
skip_unknown_inputs: skip_unknown_inputs(constraints),
return_records?: true,
return_errors?: true,
batch_size: 1_000_000_000
)
)
|> case do
%{status: :success, records: records} when structs == [] ->
{:ok, records}
%{status: :success, records: records} ->
Enum.reduce(structs, {:ok, records}, fn {struct, index}, {:ok, records} ->
{:ok, List.insert_at(records, index, struct)}
end)
%{errors: errors} ->
errors =
Enum.map(errors, fn
%Ash.Changeset{context: %{bulk_create: %{index: index}}, errors: errors} ->
Ash.Error.set_path(Ash.Error.to_ash_error(errors), index)
other ->
Ash.Error.to_ash_error(other)
end)
{:error, Ash.Error.to_ash_error(errors)}
end
end
end
end
def cast_input_array(nil, _), do: {:ok, nil}
def cast_input_array(_, _), do: :error
def cast_stored(value, constraints) when is_map(value) do
__MODULE__
|> Ash.Resource.Info.attributes()
|> Enum.reduce_while({:ok, struct(__MODULE__)}, fn attr, {:ok, struct} ->
with {:fetch, {:ok, value}} <- {:fetch, fetch_key(value, attr.source)},
{:ok, casted} <- Ash.Type.cast_stored(attr.type, value, attr.constraints) do
{:cont, {:ok, Map.put(struct, attr.name, casted)}}
else
{:fetch, :error} ->
{:cont, {:ok, struct}}
other ->
{:halt, other}
end
end)
|> case do
{:ok, casted} ->
case constraints[:load] do
empty when empty in [nil, []] ->
{:ok, casted}
load ->
action =
constraints[:read_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :read).name
__MODULE__
|> Ash.DataLayer.Simple.set_data([casted])
|> Ash.Query.load(load)
|> Ash.Query.for_read(action, %{}, domain: ShadowDomain)
|> Ash.read()
|> case do
{:ok, [casted]} ->
{:ok, casted}
{:error, errors} ->
{:error, Ash.EmbeddableType.handle_errors(errors)}
end
end
other ->
other
end
end
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(_other, _) do
:error
end
def fetch_key(map, atom) do
case Map.fetch(map, atom) do
{:ok, value} ->
{:ok, value}
:error ->
Map.fetch(map, to_string(atom))
end
end
def dump_to_native(value, _) when is_map(value) do
attributes = Ash.Resource.Info.attributes(__MODULE__)
Enum.reduce_while(attributes, {:ok, %{}}, fn attribute, {:ok, acc} ->
case Map.fetch(value, attribute.name) do
:error ->
{:cont, {:ok, acc}}
{:ok, value} ->
value =
case value do
%Ash.ForbiddenField{original_value: original_value} ->
original_value
other ->
other
end
case Ash.Type.dump_to_embedded(
attribute.type,
value,
Map.get(attribute, :constraints) || []
) do
:error ->
{:halt, :error}
{:ok, nil} when unquote(!opts[:embed_nil_values?]) ->
{:cont, {:ok, acc}}
{:ok, dumped} ->
{:cont, {:ok, Map.put(acc, attribute.source, dumped)}}
end
end
end)
end
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(_, _), do: :error
def constraints do
array_constraints()
|> Keyword.take([
:load,
:create_action,
:destroy_action,
:update_action,
:domain,
:skip_unknown_inputs,
:__source__
])
|> Keyword.put(:on_update,
type: {:one_of, [:update, :replace, :update_on_match]},
default: :update_on_match,
doc:
"Whether or not setting the value should update the existing value, replace it, or update it if the primary key's match. If there is no primary key, `update_on_match` is the same as `replace`."
)
end
def apply_constraints(nil, _), do: {:ok, nil}
def apply_constraints(term, constraints) do
Ash.load(term, constraints[:load] || [], lazy?: true, domain: ShadowDomain)
end
def handle_change(nil, new_value, _constraints) do
{:ok, new_value}
end
def handle_change(old_value, nil, constraints) do
action =
constraints[:destroy_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :destroy).name
old_value
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_destroy(action, %{}, domain: ShadowDomain)
|> Ash.destroy()
|> case do
:ok -> {:ok, nil}
{:error, error} -> {:error, Ash.EmbeddableType.handle_errors(error)}
end
end
def handle_change(old_value, new_value, constraints) do
pkey_fields = Ash.Resource.Info.primary_key(__MODULE__)
if Enum.empty?(pkey_fields) ||
Enum.all?(pkey_fields, fn pkey_field ->
!Ash.Resource.Info.attribute(__MODULE__, pkey_field).public?
end) do
{:ok, new_value}
else
pkey = Map.take(old_value, pkey_fields)
if Map.take(new_value, pkey_fields) == pkey do
{:ok, new_value}
else
action =
constraints[:destroy_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :destroy).name
old_value
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_destroy(action, %{}, domain: ShadowDomain)
|> Ash.destroy()
|> case do
:ok ->
{:ok, new_value}
{:error, error} ->
{:error, Ash.EmbeddableType.handle_errors(error)}
end
end
end
end
defp skip_unknown_inputs(constraints) do
case Keyword.fetch(constraints, :__union_tag__) do
{:ok, union_tag} when is_atom(union_tag) ->
[union_tag, to_string(union_tag)]
{:ok, union_tag} ->
List.wrap(union_tag)
_ ->
[]
end
|> Enum.concat(
Enum.flat_map(Ash.Resource.Info.primary_key(__MODULE__), &[&1, to_string(&1)])
)
|> Enum.concat(List.wrap(constraints[:skip_unknown_inputs]))
|> Enum.concat(List.wrap(constraints[:items][:skip_unknown_inputs]))
end
def prepare_change(old_value, "", constraints) do
prepare_change(old_value, nil, constraints)
end
def prepare_change(_old_value, nil, _constraints) do
{:ok, nil}
end
def prepare_change(_old_value, %{__struct__: __MODULE__} = new_value, _constraints) do
{:ok, new_value}
end
def prepare_change(nil, new_value, _constraints) do
{:ok, new_value}
end
def prepare_change(old_value, new_uncasted_value, constraints) do
pkey_fields = Ash.Resource.Info.primary_key(__MODULE__)
if Enum.empty?(pkey_fields) ||
Enum.all?(pkey_fields, fn pkey_field ->
!Ash.Resource.Info.attribute(__MODULE__, pkey_field).public?
end) do
action =
constraints[:update_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :update).name
old_value
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_update(action, new_uncasted_value,
domain: ShadowDomain,
skip_unknown_inputs: skip_unknown_inputs(constraints)
)
|> Ash.update()
|> case do
{:ok, value} -> {:ok, value}
{:error, error} -> {:error, Ash.EmbeddableType.handle_errors(error)}
end
else
pkey =
Enum.into(pkey_fields, %{}, fn pkey_field ->
with {:ok, value} <- fetch_key(new_uncasted_value, pkey_field),
attribute <- Ash.Resource.Info.attribute(__MODULE__, pkey_field),
{:ok, casted} <-
Ash.Type.cast_input(attribute.type, value, attribute.constraints) do
{pkey_field, casted}
else
_ -> {pkey_field, :error}
end
end)
if Enum.any?(Map.values(pkey), &(&1 == :error)) do
{:ok, new_uncasted_value}
else
old_pkey = Map.take(old_value, pkey_fields)
if old_pkey == pkey do
action =
constraints[:update_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :update).name
old_value
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_update(action, new_uncasted_value,
domain: ShadowDomain,
skip_unknown_inputs: skip_unknown_inputs(constraints)
)
|> Ash.update()
|> case do
{:ok, value} -> {:ok, value}
{:error, error} -> {:error, Ash.EmbeddableType.handle_errors(error)}
end
else
{:ok, new_uncasted_value}
end
end
end
end
end
end
defmacro array_embed_implementation do
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
quote location: :keep do
alias Ash.EmbeddableType.ShadowDomain
def cast_atomic_array(value, constraints) do
with :ok <- check_atomic(value, constraints) do
case cast_input_array(value, constraints) do
{:ok, value} ->
{:atomic, value}
{:error, error} ->
{:error, error}
end
end
end
def check_atomic(value, constraints) do
if Ash.Expr.expr?(value) do
{:not_atomic,
"Embedded attributes do not support atomic updates with expressions, only literal values."}
else
if Enum.empty?(Ash.Resource.Info.primary_key(__MODULE__)) ||
constraints[:on_update] == :replace ||
update_action_allows_atomics?(constraints) do
:ok
else
{:not_atomic,
"""
Embedded attributes do not support atomic updates unless they have no primary key, or `constraints[:on_update]` is set to `:replace`, or the update action accepts all public attributes and has no changes.
"""}
end
end
end
defp update_action_allows_atomics?(constraints) do
action =
case constraints[:update_action] do
nil -> Ash.Resource.Info.primary_action!(__MODULE__, :update)
action -> Ash.Resource.Info.action(__MODULE__, action)
end
if Enum.empty?(action.changes) &&
Enum.empty?(Ash.Resource.Info.changes(__MODULE__, action.type)) &&
Enum.empty?(Ash.Resource.Info.validations(__MODULE__, action.type)) &&
Enum.empty?(Ash.Resource.Info.notifiers(__MODULE__)) &&
Enum.empty?(Ash.Resource.Info.relationships(__MODULE__)) do
__MODULE__
|> Ash.Resource.Info.public_attributes()
|> Enum.all?(&(&1.name in action.accept))
|> if do
:ok
else
:error
end
else
:error
end
|> case do
:ok ->
:ok
:error ->
{:not_atomic,
"""
Embedded attributes do not support atomic updates unless one of the following is true:
- it has no primary key
- `constraints[:on_update]` is set to `:replace`
Or all of the following are true:
- the update action accepts all public attributes and has no changes
- there are no changes or validations on the action
- there are no notifiers, or relationships defined on the resource \"""}
"""}
end
end
def loaded?(record, path_to_load, _constraints, opts) do
Ash.Resource.loaded?(record, path_to_load, opts)
end
def load(record, load, _constraints, %{domain: domain} = context) do
opts = Ash.Context.to_opts(context, domain: domain)
attribute_loads =
__MODULE__ |> Ash.Resource.Info.public_attributes() |> Enum.map(& &1.name)
load =
case load do
%Ash.Query{} ->
Ash.Query.ensure_selected(load, attribute_loads)
load_statement ->
__MODULE__ |> Ash.Query.select(attribute_loads) |> Ash.Query.load(load_statement)
end
Ash.load(record, load, opts)
end
def merge_load(left, right, constraints, context) do
right = Ash.Query.load(__MODULE__, right)
__MODULE__
|> Ash.Query.new()
|> Ash.Query.load(left)
|> case do
%{valid?: true} = left ->
{:ok, Ash.Query.merge_query_load(left, right, context)}
query ->
{:error, Ash.Error.to_ash_error(query.errors)}
end
end
def get_rewrites(merged_load, calculation, path, _) do
merged_load = Ash.Query.load(__MODULE__, merged_load)
Ash.Actions.Read.Calculations.get_all_rewrites(merged_load, calculation, path)
end
def rewrite(value, rewrites, _constraints) do
Ash.Actions.Read.Calculations.rewrite(rewrites, value)
end
def array_constraints, do: Ash.EmbeddableType.embedded_resource_array_constraints()
def apply_constraints_array(term, constraints) do
case find_duplicates(
term,
__MODULE__ |> Ash.Resource.Info.unique_keys()
) do
nil ->
if constraints[:sort] do
query =
__MODULE__
|> Ash.DataLayer.Simple.set_data(term)
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Query.load(constraints[:load] || [])
query =
if constraints[:sort] do
Ash.Query.sort(query, constraints[:sort])
else
query
end
case Ash.read(query, domain: ShadowDomain) do
{:ok, result} ->
case Ash.Type.list_constraint_errors(result, constraints) do
[] ->
{:ok, result}
errors ->
{:error, errors}
end
end
else
if constraints[:load] do
query =
__MODULE__
|> Ash.DataLayer.Simple.set_data(term)
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Query.load(constraints[:load] || [])
Ash.load(term, query, domain: ShadowDomain)
else
{:ok, term}
end
end
%{type: type, keys: keys, nils_distinct?: nils_distinct?} ->
type =
case type do
:primary_key -> "primary key"
other -> to_string(other)
end
nils_distinct? =
if nils_distinct? do
" with `nil` values considered distinct"
end
{:error,
message: "items must be unique on #{type} with keys %{keys}#{nils_distinct?}",
keys: Enum.join(keys, ",")}
end
end
defp find_duplicates([], _), do: nil
defp find_duplicates([_item], _), do: nil
defp find_duplicates(list, unique_key_configs) do
Enum.find(unique_key_configs, fn unique_key_config ->
unique_key = unique_key_config[:keys]
attributes = Enum.map(unique_key, &Ash.Resource.Info.attribute(__MODULE__, &1))
if Enum.all?(attributes, &Ash.Type.simple_equality?(&1.type)) do
list
|> Enum.flat_map(fn item ->
value =
item
|> Map.take(unique_key)
|> Map.values()
if unique_key_config.nils_distinct? && Enum.any?(value, &is_nil/1) do
[]
else
[value]
end
end)
|> Enum.reduce_while({false, MapSet.new()}, fn item, {false, acc} ->
if MapSet.member?(acc, item) do
{:halt, {true, acc}}
else
{:cont, {false, MapSet.put(acc, item)}}
end
end)
|> elem(0)
else
Enum.reduce_while(list, list, fn
_term, [_] ->
{:halt, false}
this, [_ | rest] ->
has_duplicate? =
Enum.any?(rest, fn other ->
Enum.all?(attributes, fn attribute ->
this_value = Map.get(this, attribute.name)
other_value = Map.get(other, attribute.name)
if unique_key_config.nils_distinct? do
not is_nil(this_value) and not is_nil(other_value) and
Ash.Type.equal?(attribute.type, this_value, other_value)
else
Ash.Type.equal?(attribute.type, this_value, other_value)
end
end)
end)
if has_duplicate? do
{:halt, true}
else
{:cont, rest}
end
end)
end
end)
end
def handle_change_array(nil, new_values, constraints) do
handle_change_array([], new_values, constraints)
end
def handle_change_array(old_values, nil, constraints) do
case handle_change_array(old_values, [], constraints) do
{:ok, []} -> {:ok, nil}
other -> other
end
end
def handle_change_array(old_values, new_values, constraints) do
pkey_fields = Ash.Resource.Info.primary_key(__MODULE__)
old_values
|> List.wrap()
|> then(fn list ->
if Enum.empty?(pkey_fields) do
list
else
list
|> Enum.reject(fn old_value ->
pkey = Map.take(old_value, pkey_fields)
Enum.any?(new_values, fn new_value ->
Map.take(new_value, pkey_fields) == pkey
end)
end)
end
end)
|> case do
[] ->
{:ok, new_values}
to_destroy ->
destroy_action =
constraints[:destroy_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :destroy).name
{context, opts} =
case constraints[:__source__] do
%Ash.Changeset{context: context} = source ->
{Map.put(context, :__source__, source),
Ash.Context.to_opts(context[:private] || %{})}
_ ->
{%{}, []}
end
case Ash.bulk_destroy(
to_destroy,
destroy_action,
%{},
Keyword.merge(opts,
domain: ShadowDomain,
context: context,
sorted?: true,
skip_unknown_inputs: skip_unknown_inputs(constraints),
return_records?: true,
return_errors?: true,
batch_size: 1_000_000_000
)
) do
%Ash.BulkResult{status: :success} ->
{:ok, new_values}
%Ash.BulkResult{errors: errors} ->
{:error, Ash.EmbeddableType.handle_errors(errors)}
end
end
end
def include_source(constraints, changeset) do
if Keyword.get(constraints, :include_source?, @include_source_by_default) do
Keyword.put(constraints, :__source__, changeset)
else
constraints
end
end
def prepare_change_array(_old_values, nil, _constraints) do
{:ok, nil}
end
def prepare_change_array(old_values, new_uncasted_values, constraints) do
pkey_fields = Ash.Resource.Info.primary_key(__MODULE__)
if Enum.empty?(pkey_fields) ||
Enum.all?(pkey_fields, fn pkey_field ->
!Ash.Resource.Info.attribute(__MODULE__, pkey_field).public?
end) do
{:ok, new_uncasted_values}
else
pkey_attributes =
Enum.into(pkey_fields, %{}, fn field ->
{field, Ash.Resource.Info.attribute(__MODULE__, field)}
end)
action =
constraints[:update_action] ||
Ash.Resource.Info.primary_action!(__MODULE__, :update).name
new_uncasted_values
|> Enum.with_index()
|> Enum.reduce_while(
{:ok, []},
fn
{new, _index}, {:ok, new_uncasted_values} when is_struct(new, __MODULE__) ->
{:cont, {:ok, [new | new_uncasted_values]}}
{new, index}, {:ok, new_uncasted_values} ->
pkey =
Enum.into(pkey_fields, %{}, fn pkey_field ->
case fetch_key(new, pkey_field) do
:error ->
{pkey_field, :error}
{:ok, value} ->
attr = Map.get(pkey_attributes, pkey_field)
case Ash.Type.cast_input(attr.type, value, attr.constraints) do
{:ok, casted} ->
{pkey_field, casted}
_ ->
{pkey_field, :error}
end
end
end)
if Enum.any?(Map.values(pkey), &(&1 == :error)) do
{:cont, {:ok, [new | new_uncasted_values]}}
else
value_updating_from =
old_values
|> List.wrap()
|> Enum.find(fn old_value ->
Map.take(old_value, pkey_fields) ==
pkey
end)
if value_updating_from do
value_updating_from
|> Ash.Changeset.new()
|> Ash.EmbeddableType.copy_source(constraints)
|> Ash.Changeset.for_update(action, new,
domain: ShadowDomain,
skip_unknown_inputs: skip_unknown_inputs(constraints)
)
|> Ash.update()
|> case do
{:ok, value} ->
{:cont, {:ok, [value | new_uncasted_values]}}
{:error, error} ->
errors =
error
|> Ash.EmbeddableType.handle_errors()
|> Enum.map(fn error ->
cond do
is_exception(error) ->
Ash.Error.set_path(error, [index])
Keyword.keyword?(error) ->
error
|> Keyword.put(:index, index)
|> Keyword.update(:path, [index], &[index | &1])
true ->
error
end
end)
{:halt, {:error, errors}}
end
else
{:cont, {:ok, [new | new_uncasted_values]}}
end
end
end
)
|> case do
{:ok, values} -> {:ok, Enum.reverse(values)}
{:error, error} -> {:error, error}
end
end
end
end
end
defmacro define_embeddable_type(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
use Ash.Type, embedded?: true
Ash.EmbeddableType.single_embed_implementation(opts)
Ash.EmbeddableType.array_embed_implementation()
end
end
def copy_source(changeset, opts) do
changeset =
if source = opts[:__source__] do
changeset
|> Ash.Changeset.set_tenant(source.tenant)
|> Ash.Changeset.set_context(source.context)
|> Ash.Changeset.set_context(%{__source__: source})
|> Map.put(:domain, source.domain)
else
changeset
end
if domain = opts[:domain] do
Map.put(changeset, :domain, domain)
else
changeset
end
end
end