Packages

ash

2.5.3
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 managed_relationships.ex
Raw

lib/ash/actions/managed_relationships.ex

defmodule Ash.Actions.ManagedRelationships do
@moduledoc false
alias Ash.Error.Changes.InvalidRelationship
alias Ash.Error.Query.NotFound
require Ash.Query
@use_all_identities_in_manage_relationship Application.compile_env(
:ash,
:use_all_identities_in_manage_relationship?
)
if is_nil(@use_all_identities_in_manage_relationship) do
IO.warn("""
* IMPORTANT *
The configuration `use_all_identities_in_manage_relationship` was not set.
It is defaulting to `true` for backwards compatibility.
This configuration must now be manually set, and it should be set to `false`.
If you have just started a new project, or haven't used `manage_relationship` yet, just set the following config:
config :ash, :use_all_identities_in_manage_relationship?, false
If you are currently using `manage_relationship`, please read https://github.com/ash-project/ash/issues/469 before proceeding
""")
@use_all_identities_in_manage_relationship true
end
def load(_api, created, %{relationships: rels}, _) when rels == %{},
do: {:ok, created}
def load(_api, created, %{relationships: nil}, _), do: {:ok, created}
def load(api, created, changeset, engine_opts) do
Enum.reduce_while(changeset.relationships, {:ok, created}, fn {key, value}, {:ok, acc} ->
relationship = Ash.Resource.Info.relationship(changeset.resource, key)
must_load_opts = [
type: relationship.type,
action_type: changeset.action.type,
could_be_related_at_creation?:
engine_opts[:upsert?] || Map.get(relationship, :could_be_related_at_creation?, false)
]
case Enum.filter(value, fn {_, opts} ->
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
Ash.Changeset.ManagedRelationshipHelpers.must_load?(
opts,
must_load_opts
)
end) do
[] ->
{:cont, {:ok, acc}}
relationships ->
authorize? =
engine_opts[:authorize?] &&
Enum.any?(relationships, fn {_, opts} -> opts[:authorize?] end)
actor = engine_opts[:actor]
# In order to use `lazy?: true` here we need this feature: https://github.com/ash-project/ash/issues/438
case api.load(acc, key, authorize?: authorize?, actor: actor, lazy?: false) do
{:ok, loaded} -> {:cont, {:ok, loaded}}
{:error, error} -> {:halt, {:error, error}}
end
end
end)
end
def setup_managed_belongs_to_relationships(changeset, actor, engine_opts) do
changeset.relationships
|> Enum.map(fn {relationship, val} ->
{Ash.Resource.Info.relationship(changeset.resource, relationship), val}
end)
|> Enum.filter(fn {relationship, _val} ->
relationship.type == :belongs_to
end)
|> Enum.flat_map(fn {relationship, inputs} ->
inputs
|> Enum.reject(fn {_, opts} -> opts[:ignore?] || opts[:handled?] end)
|> Enum.with_index()
|> Enum.map(fn {{input, opts}, index} ->
{{relationship, {input, opts}}, index}
end)
end)
|> Enum.map(fn
{{relationship, {[input], opts}}, index} ->
{{relationship, {input, opts}}, index}
{{relationship, {other, opts}}, index} ->
{{relationship, {other, opts}}, index}
end)
|> Enum.sort_by(fn {{_rel, {_input, opts}}, _index} ->
opts[:meta][:order]
end)
|> Enum.reduce_while({changeset, %{notifications: []}}, fn {{relationship, {input, opts}},
index},
{changeset, instructions} ->
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
opts = Keyword.put(opts, :authorize?, !!(engine_opts[:authorize?] && opts[:authorize?]))
pkeys = pkeys(relationship, opts)
changeset =
if changeset.action.type == :update do
case changeset.api.load(changeset.data, relationship.name,
authorize?: opts[:authorize?],
actor: actor
) do
{:ok, result} ->
{:ok, %{changeset | data: result}}
{:error, error} ->
{:error, error}
end
else
{:ok, changeset}
end
case changeset do
{:ok, changeset} ->
changeset =
if input in [nil, []] && opts[:on_missing] != :ignore do
changeset
|> maybe_force_change_attribute(relationship, :source_attribute, nil)
|> Ash.Changeset.after_action(
fn _changeset, result ->
{:ok, Map.put(result, relationship.name, nil)}
end,
prepend?: true
)
else
changeset
end
current_value =
case Map.get(changeset.data, relationship.name) do
%Ash.NotLoaded{} ->
nil
other ->
other
end
input =
if is_list(input) do
Enum.at(input, 0)
else
input
end
match =
if input do
find_match(
List.wrap(current_value),
input,
pkeys,
relationship,
opts[:on_no_match] == :match
)
else
nil
end
case match do
nil ->
case opts[:on_lookup] do
_ when is_nil(input) ->
create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
)
:ignore ->
create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
)
{_key, _create_or_update, read} ->
if is_struct(input, relationship.destination) do
changeset =
changeset
|> Ash.Changeset.set_context(%{
private: %{
belongs_to_manage_found: %{relationship.name => %{index => input}}
}
})
|> maybe_force_change_attribute(
relationship,
:source_attribute,
Map.get(input, relationship.destination_attribute)
)
{:cont, {changeset, instructions}}
else
Enum.find_value(pkeys(relationship, opts), fn fields ->
if Enum.all?(fields, fn field ->
has_input_value?(input, field)
end) do
Map.new(fields, &{&1, get_input_value(input, &1)})
end
end)
|> case do
nil ->
create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
)
keys ->
relationship.destination
|> Ash.Query.for_read(read, input,
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Query.filter(^keys)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort, prepend?: true)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_tenant(changeset.tenant)
|> api(changeset, relationship).read_one()
|> case do
{:ok, nil} ->
create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
)
{:ok, found} ->
changeset =
changeset
|> Ash.Changeset.set_context(%{
private: %{
belongs_to_manage_found: %{
relationship.name => %{index => found}
}
}
})
|> maybe_force_change_attribute(
relationship,
:source_attribute,
Map.get(found, relationship.destination_attribute)
)
{:cont, {changeset, instructions}}
{:error, error} ->
{:halt,
{Ash.Changeset.add_error(changeset, error, [
opts[:meta][:id] || relationship.name
]), instructions}}
end
end
end
end
_value ->
if opts[:on_match] == :destroy do
changeset =
maybe_force_change_attribute(
changeset,
relationship,
:source_attribute,
nil
)
{:cont, {changeset, instructions}}
else
{:cont, {changeset, instructions}}
end
end
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> validate_required_belongs_to()
|> case do
{:error, error} ->
{:error, error}
{changeset, instructions} ->
changeset =
Map.update!(changeset, :relationships, fn relationships ->
Map.new(relationships, fn {rel, inputs} ->
{rel,
Enum.map(inputs, fn {input, config} ->
{input, Keyword.put(config, :handled?, true)}
end)}
end)
end)
{changeset, instructions}
end
end
defp has_input_value?(value, key) when is_map(value) do
Map.has_key?(value, key) || Map.has_key?(value, to_string(key))
end
defp has_input_value?(_, _), do: false
defp get_input_value(value, key) when is_map(value) do
case Map.fetch(value, key) do
{:ok, value} ->
value
:error ->
Map.get(value, to_string(key))
end
end
defp get_input_value(_, _), do: nil
defp api(changeset, relationship) do
relationship.api || changeset.api
end
defp maybe_force_change_attribute(changeset, %{no_attributes?: true}, _, _), do: changeset
defp maybe_force_change_attribute(changeset, relationship, key, value) do
Ash.Changeset.force_change_attribute(
changeset,
Map.get(relationship, key),
value
)
end
defp keep_relationships_to_validate(relationships, changeset, true) do
Enum.filter(relationships, fn relationship ->
changeset.relationships[relationship.name] &&
!changeset.context[:private][:error][relationship.name]
end)
end
defp keep_relationships_to_validate(relationships, changeset, false) do
Enum.filter(relationships, fn relationship ->
!changeset.context[:private][:error][relationship.name]
end)
end
def validate_required_belongs_to(changeset_instructions_or_error, preflight? \\ true)
def validate_required_belongs_to({:error, error}, _), do: {:error, error}
def validate_required_belongs_to({%{valid?: false} = changeset, instructions}, _) do
{changeset, instructions}
end
def validate_required_belongs_to({changeset, instructions}, preflight?) do
changeset.resource
|> Ash.Resource.Info.relationships()
|> Enum.filter(&(&1.type == :belongs_to && !&1.allow_nil?))
|> keep_relationships_to_validate(changeset, preflight?)
|> Enum.reduce({changeset, instructions}, fn required_relationship,
{changeset, instructions} ->
changeset =
case Ash.Changeset.get_attribute(changeset, required_relationship.source_attribute) do
nil ->
Ash.Changeset.add_error(
changeset,
Ash.Error.Changes.Required.exception(
field: required_relationship.name,
type: :relationship
)
)
_ ->
changeset
end
{changeset, instructions}
end)
end
defp create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
) do
if input in [nil, []] do
{:cont, {changeset, instructions}}
else
case opts[:on_no_match] do
ignore when ignore in [:ignore, :match] ->
{:cont, {changeset, instructions}}
:error ->
if opts[:on_lookup] != :ignore do
changeset =
changeset
|> Ash.Changeset.add_error(
NotFound.exception(
primary_key: input,
resource: relationship.destination
),
[opts[:meta][:id] || relationship.name]
)
|> Ash.Changeset.put_context(:private, %{
error: %{relationship.name => true}
})
{:halt, {changeset, instructions}}
else
changeset =
changeset
|> Ash.Changeset.add_error(
InvalidRelationship.exception(
relationship: relationship.name,
message: "Changes would create a new related record"
)
)
|> Ash.Changeset.put_context(:private, %{
error: %{relationship.name => true}
})
{:halt, {changeset, instructions}}
end
{:create, action_name} ->
do_create_belongs_to_record(
relationship,
action_name,
input,
changeset,
actor,
opts,
instructions,
index
)
end
end
end
defp do_create_belongs_to_record(
relationship,
action_name,
input,
changeset,
actor,
opts,
instructions,
index
) do
relationship.destination
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(return_notifications?: true)
|> case do
{:ok, created, notifications} ->
changeset =
changeset
|> Ash.Changeset.set_context(%{
private: %{
belongs_to_manage_created: %{relationship.name => %{index => created}}
}
})
|> maybe_force_change_attribute(
relationship,
:source_attribute,
Map.get(created, relationship.destination_attribute)
)
{:cont,
{changeset, %{instructions | notifications: instructions.notifications ++ notifications}}}
{:error, error} ->
{:halt,
{Ash.Changeset.add_error(changeset, error, [opts[:meta][:id] || relationship.name]),
instructions}}
end
end
def manage_relationships(record, changeset, actor, engine_opts) do
changeset.relationships
|> Enum.map(fn {relationship, val} ->
{Ash.Resource.Info.relationship(changeset.resource, relationship), val}
end)
|> Enum.flat_map(fn {key, batches} ->
batches
|> Enum.reject(fn {_, opts} -> opts[:ignore?] end)
|> Enum.with_index()
|> Enum.map(fn {{batch, opts}, index} ->
opts = Keyword.put(opts, :authorize?, !!(engine_opts[:authorize?] && opts[:authorize?]))
{key, batch, opts, index}
end)
end)
|> Enum.sort_by(fn {_key, _batch, opts, _index} ->
opts[:meta][:order]
end)
|> Enum.reduce_while({:ok, record, []}, fn {relationship, inputs, opts, index},
{:ok, record, all_notifications} ->
inputs =
if relationship.cardinality == :many do
List.wrap(inputs)
else
inputs
end
case manage_relationship(record, relationship, inputs, changeset, actor, index, opts) do
{:ok, record, notifications} ->
record =
if relationship.type == :many_to_many do
Map.put(
record,
relationship.join_relationship,
Map.get(record.__struct__.__struct__, relationship.join_relationship)
)
else
record
end
{:cont, {:ok, record, notifications ++ all_notifications}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
end
def pkeys(relationship, opts) do
use_identities =
case opts[:use_identities] do
nil ->
if @use_all_identities_in_manage_relationship do
[
:_primary_key
| relationship.destination
|> Ash.Resource.Info.identities()
|> Enum.map(& &1.name)
]
else
[:_primary_key]
end
use_identities ->
use_identities
end
use_identities =
if opts[:identity_priority] do
Enum.sort_by(use_identities, fn identity ->
Enum.find_index(opts[:identity_priority], &(&1 == identity))
end)
else
use_identities
end
use_identities
|> Enum.map(fn
:_primary_key ->
Ash.Resource.Info.primary_key(relationship.destination)
identity ->
Ash.Resource.Info.identity(relationship.destination, identity).keys
end)
end
defp manage_relationship(
record,
%{cardinality: :many} = relationship,
inputs,
changeset,
actor,
index,
opts
) do
inputs = List.wrap(inputs)
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
pkeys = pkeys(relationship, opts)
original_value =
case Map.get(record, relationship.name) do
%Ash.NotLoaded{} -> []
value -> value
end
inputs
|> Enum.with_index()
|> Enum.reduce_while(
{:ok, [], [], []},
fn {input, input_index}, {:ok, current_value, all_notifications, all_used} ->
case handle_input(
record,
current_value,
original_value,
relationship,
input,
pkeys,
changeset,
actor,
index,
opts
) do
{:ok, new_value, notifications, used} ->
{:cont, {:ok, new_value, all_notifications ++ notifications, all_used ++ used}}
{:error, %Ash.Error.Changes.InvalidRelationship{} = error} ->
{:halt, {:error, error}}
{:error, error} ->
case Keyword.fetch(opts[:meta] || [], :inputs_was_list?) do
{:ok, false} ->
{:halt,
{:error,
Ash.Error.set_path(
error,
opts[:error_path] ||
[
opts[:meta][:id] || relationship.name
]
)}}
_ ->
{:halt,
{:error,
Ash.Error.set_path(
error,
opts[:error_path] || [opts[:meta][:id] || relationship.name, input_index]
)}}
end
end
end
)
|> case do
{:ok, new_value, all_notifications, all_used} ->
case delete_unused(
record,
original_value,
relationship,
new_value,
all_used,
changeset,
actor,
opts
) do
{:ok, new_value, notifications} ->
new_value =
if is_list(new_value) do
Enum.reverse(new_value)
else
new_value
end
{:ok, Map.put(record, relationship.name, new_value),
all_notifications ++ notifications}
{:error, %Ash.Error.Changes.InvalidRelationship{} = error} ->
{:error, error}
{:error, error} ->
{:error,
Ash.Error.set_path(
error,
opts[:error_path] || [opts[:meta][:id] || relationship.name]
)}
end
{:error, error} ->
{:error, error}
end
end
defp manage_relationship(
record,
%{cardinality: :one} = relationship,
inputs,
changeset,
actor,
index,
opts
) do
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
identities =
relationship.destination
|> Ash.Resource.Info.identities()
|> Enum.map(& &1.keys)
pkeys = [Ash.Resource.Info.primary_key(relationship.destination) | identities]
original_value =
if relationship.type == :belongs_to do
Map.get(changeset.data, relationship.name)
else
Map.get(record, relationship.name)
end
original_value =
case original_value do
%Ash.NotLoaded{} -> []
value -> value
end
inputs = List.wrap(inputs)
inputs
|> Enum.reduce_while(
{:ok, original_value, [], []},
fn input, {:ok, current_value, all_notifications, all_used} ->
case handle_input(
record,
current_value,
original_value,
relationship,
input,
pkeys,
changeset,
actor,
index,
opts
) do
{:ok, new_value, notifications, used} ->
{:cont, {:ok, new_value, all_notifications ++ notifications, all_used ++ used}}
{:error, error} ->
{:halt, {:error, error}}
end
end
)
|> case do
{:ok, new_value, all_notifications, all_used} ->
case delete_unused(
record,
original_value,
relationship,
new_value,
all_used,
changeset,
actor,
opts
) do
{:ok, new_value, notifications} ->
{:ok, Map.put(record, relationship.name, Enum.at(List.wrap(new_value), 0)),
all_notifications ++ notifications}
{:error, %Ash.Error.Changes.InvalidRelationship{} = error} ->
{:error, error}
{:error, error} ->
{:error,
Ash.Error.set_path(
error,
opts[:error_path] || [opts[:meta][:id] || relationship.name]
)}
end
{:error, %Ash.Error.Changes.InvalidRelationship{} = error} ->
{:error, error}
{:error, error} ->
{:error,
Ash.Error.set_path(
error,
opts[:error_path] || [opts[:meta][:id] || relationship.name]
)}
end
end
defp handle_input(
record,
current_value,
original_value,
relationship,
input,
pkeys,
changeset,
actor,
index,
opts
) do
match =
find_match(
List.wrap(original_value),
input,
pkeys,
relationship,
opts[:on_no_match] == :match
)
if is_nil(match) || opts[:on_match] == :no_match do
case handle_create(
record,
current_value,
relationship,
input,
changeset,
actor,
index,
opts
) do
{:ok, current_value, notifications, used} ->
{:ok, current_value, notifications, used}
{:error, error} ->
{:error, error}
end
else
handle_update(record, current_value, relationship, match, input, changeset, actor, opts)
end
end
defp handle_create(record, current_value, relationship, input, changeset, actor, index, opts) do
case opts[:on_lookup] do
:ignore ->
do_handle_create(
record,
current_value,
relationship,
input,
changeset,
actor,
index,
opts
)
other ->
case Map.fetch(
changeset.context[:private][:belongs_to_manage_found][relationship.name] || %{},
index
) do
:error ->
{key, create_or_update, read, join_keys} =
case other do
{key, create_or_update, read} -> {key, create_or_update, read, []}
{key, create_or_update, read, keys} -> {key, create_or_update, read, keys}
end
case Ash.Filter.get_filter(relationship.destination, input) do
{:ok, keys} ->
if is_struct(input, relationship.destination) do
{:ok, input}
else
relationship.destination
|> Ash.Query.for_read(read, input, actor: actor, authorize?: opts[:authorize?])
|> Ash.Query.filter(^keys)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort, prepend?: true)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_tenant(changeset.tenant)
|> Ash.Query.limit(1)
|> api(changeset, relationship).read_one()
end
|> case do
{:ok, found} when not is_nil(found) ->
do_handle_found(
relationship,
join_keys,
input,
api(changeset, relationship),
opts,
found,
current_value,
create_or_update,
actor,
key,
record,
changeset
)
{:ok, _} ->
do_handle_create(
record,
current_value,
relationship,
input,
changeset,
actor,
index,
opts
)
{:error, error} ->
{:error, error}
end
{:error, _error} ->
do_handle_create(
record,
current_value,
relationship,
input,
changeset,
actor,
index,
opts
)
end
{:ok, found} ->
{:ok, [found | current_value], [], [found]}
end
end
end
defp do_handle_found(
relationship,
join_keys,
input,
api,
opts,
found,
current_value,
create_or_update,
actor,
key,
record,
changeset
) do
case relationship.type do
:many_to_many ->
input =
if is_map(input) do
input
else
Enum.into(input, %{})
end
{join_input, input} = split_join_keys(input, join_keys)
join_relationship =
Ash.Resource.Info.relationship(
relationship.source,
relationship.join_relationship
)
relationship.through
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(create_or_update, join_input,
actor: actor,
authorize?: opts[:authorize?],
require?: false
)
|> maybe_force_change_attribute(
relationship,
:source_attribute_on_join_resource,
Map.get(record, relationship.source_attribute)
)
|> maybe_force_change_attribute(
relationship,
:destination_attribute_on_join_resource,
Map.get(found, relationship.destination_attribute)
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.create(return_notifications?: true)
|> case do
{:ok, _created, notifications} ->
case key do
:relate ->
{:ok, [found | current_value], notifications, [found]}
:relate_and_update ->
case handle_update(
record,
current_value,
relationship,
found,
input,
changeset,
actor,
opts
) do
{:ok, new_value, update_notifications, used} ->
{:ok, new_value, update_notifications ++ notifications, used}
{:error, error} ->
{:error, error}
end
end
{:error, error} ->
{:error, error}
end
type when type in [:has_many, :has_one] ->
{found, input} =
cond do
is_struct(input, relationship.destination) ->
{input, %{}}
is_struct(input) ->
{found, Map.from_struct(input)}
true ->
{found, input}
end
found
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(create_or_update, input,
relationships: opts[:relationships] || [],
actor: actor,
authorize?: opts[:authorize?]
)
|> maybe_force_change_attribute(
relationship,
:destination_attribute,
Map.get(record, relationship.source_attribute)
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, notifications} ->
{:ok, [updated | current_value], notifications, [updated]}
{:error, error} ->
{:error, error}
end
:belongs_to ->
{:ok, [found | current_value], [], [found]}
end
end
defp do_handle_create(record, current_value, relationship, input, changeset, actor, index, opts) do
case opts[:on_no_match] do
:error ->
if opts[:on_lookup] != :ignore do
{:error,
NotFound.exception(
primary_key: input,
resource: relationship.destination
)}
else
{:error,
InvalidRelationship.exception(
relationship: relationship.name,
message: "Changes would create a new related record"
)}
end
{:create, action_name} ->
case changeset.context[:private][:belongs_to_manage_created][relationship.name][index] do
nil ->
created =
if is_struct(input, relationship.destination) do
{:ok, input, []}
else
input =
if is_struct(input) do
Map.from_struct(input)
else
input
end
relationship.destination
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships]
)
|> maybe_force_change_attribute(
relationship,
:destination_attribute,
Map.get(record, relationship.source_attribute)
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(return_notifications?: true)
end
case created do
{:ok, created, notifications} ->
{:ok, [created | current_value], notifications, [created]}
{:error, error} ->
{:error, error}
end
created ->
{:ok, [created | current_value], [], [created]}
end
{:create, action_name, join_action_name, params} ->
join_keys = params ++ Enum.map(params, &to_string/1)
input =
cond do
is_struct(input, relationship.destination) ->
input
is_struct(input) ->
Map.from_struct(input)
true ->
input
end
{join_params, regular_params} = split_join_keys(input, join_keys)
created =
if is_struct(input, relationship.destination) do
{:ok, input, [], [input]}
else
relationship.destination
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action_name, regular_params,
require?: false,
authorize?: opts[:authorize?],
relationships: opts[:relationships],
actor: actor
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(return_notifications?: true)
end
case created do
{:ok, created, regular_notifications} ->
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(join_action_name, join_params,
require?: false,
authorize?: opts[:authorize?],
actor: actor
)
|> maybe_force_change_attribute(
relationship,
:source_attribute_on_join_resource,
Map.get(record, relationship.source_attribute)
)
|> maybe_force_change_attribute(
relationship,
:destination_attribute_on_join_resource,
Map.get(created, relationship.destination_attribute)
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(return_notifications?: true)
|> case do
{:ok, _join_row, notifications} ->
{:ok, [created | current_value], regular_notifications ++ notifications,
[created]}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
ignore when ignore in [:ignore, :match] ->
{:ok, current_value, [], [input]}
end
end
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
defp handle_update(
source_record,
current_value,
relationship,
match,
input,
changeset,
actor,
opts
) do
api = api(changeset, relationship)
case opts[:on_match] do
# :create case is handled when determining updates/creates
:error ->
{:error,
InvalidRelationship.exception(
relationship: relationship.name,
message: "Changes would update a record"
)}
:ignore ->
{:ok, [match | current_value], [], [match]}
:missing ->
{:ok, current_value, [], []}
{:destroy, action_name} ->
case destroy_data(
source_record,
match,
api,
actor,
opts,
action_name,
changeset.tenant,
relationship
) do
{:ok, notifications} ->
{:ok, current_value, notifications, []}
{:error, error} ->
{:error, error}
end
{:unrelate, action_name} ->
case unrelate_data(
source_record,
match,
api,
actor,
opts,
action_name,
changeset.tenant,
relationship
) do
{:ok, notifications} ->
{:ok, current_value, notifications, []}
{:error, error} ->
{:error, error}
end
{:update, action_name} ->
{match, input} =
cond do
is_struct(input, relationship.destination) ->
{input, %{}}
is_struct(input) ->
Map.from_struct(input)
true ->
{match, input}
end
match
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, input,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, update_notifications} ->
{:ok, [updated | current_value], update_notifications, [match]}
{:error, error} ->
{:error, error}
end
{:update, action_name, join_action_name, params} ->
join_keys = params ++ Enum.map(params, &to_string/1)
{join_params, regular_params} = split_join_keys(input, join_keys)
{match, regular_params} =
cond do
is_struct(regular_params, relationship.destination) ->
{regular_params, %{}}
is_struct(regular_params) ->
Map.from_struct(regular_params)
true ->
{match, regular_params}
end
source_value = Map.get(source_record, relationship.source_attribute)
match
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, regular_params,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, update_notifications} ->
destination_value = Map.get(updated, relationship.destination_attribute)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Query.filter(
ref(^relationship.source_attribute_on_join_resource) == ^source_value
)
|> Ash.Query.filter(
ref(^relationship.destination_attribute_on_join_resource) == ^destination_value
)
|> Ash.Query.set_context(join_relationship.context)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(changeset.tenant)
|> api(changeset, relationship).read_one(
authorize?: opts[:authorize?],
actor: actor
)
|> case do
{:ok, result} ->
if join_params == %{} do
{:ok, [updated | current_value], update_notifications, [match]}
else
join_relationship =
Ash.Resource.Info.relationship(
relationship.source,
relationship.join_relationship
)
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(join_action_name, join_params,
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(return_notifications?: true)
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|> case do
{:ok, _updated_join, join_update_notifications} ->
{:ok, [updated | current_value],
update_notifications ++ join_update_notifications, [updated]}
{:error, error} ->
{:error, error}
end
end
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
end
defp find_match(current_value, input, pkeys, relationship \\ nil, force_has_one? \\ false)
defp find_match(%Ash.NotLoaded{}, _input, _pkeys, _relationship, _force_has_one?) do
nil
end
defp find_match([match], _input, _pkeys, %{cardinality: :one}, true) do
match
end
defp find_match(current_value, input, pkeys, relationship, _force_has_one?) do
Enum.find(current_value, fn current_value ->
Enum.any?(pkeys, fn pkey ->
matches?(current_value, input, pkey, relationship)
end)
end)
end
defp matches?(current_value, input, pkey, relationship) do
if relationship && relationship.type in [:has_one, :has_many] &&
relationship.destination_attribute in pkey do
Enum.all?(pkey, fn field ->
if field == relationship.destination_attribute do
if is_struct(input) do
do_matches?(current_value, input, field)
else
# We know that it will be the same as all other records in this relationship
# (because thats how has_one and has_many relationships work), so we
# can assume its the same as the current value
true
end
else
do_matches?(current_value, input, field)
end
end)
else
Enum.all?(pkey, fn field ->
do_matches?(current_value, input, field)
end)
end
end
defp do_matches?(current_value, input, field) do
with {:ok, current_val} when not is_nil(current_val) <- Map.fetch(current_value, field),
{:ok, input_val} when not is_nil(input_val) <- fetch_field(input, field) do
Comp.equal?(current_val, input_val)
else
_ ->
false
end
end
defp split_join_keys(%_{__metadata__: metadata} = input, _join_keys) do
{metadata[:join_keys] || %{}, input}
end
defp split_join_keys(input, :all) do
{input, %{}}
end
defp split_join_keys(input, join_keys) do
Map.split(input, join_keys ++ Enum.map(join_keys, &to_string/1))
end
defp fetch_field(input, field) do
case Map.fetch(input, field) do
{:ok, value} ->
{:ok, value}
:error ->
Map.fetch(input, to_string(field))
end
end
defp delete_unused(
source_record,
original_value,
relationship,
current_value,
all_used,
changeset,
actor,
opts
) do
api = api(changeset, relationship)
pkey = Ash.Resource.Info.primary_key(relationship.destination)
original_value
|> List.wrap()
|> Enum.reject(&find_match(all_used, &1, [pkey]))
|> Enum.reduce_while(
{:ok, current_value, []},
fn record, {:ok, current_value, all_notifications} ->
case opts[:on_missing] do
:ignore ->
{:cont, {:ok, [record | current_value], []}}
{:destroy, action_name, join_action_name} ->
source_value = Map.get(source_record, relationship.source_attribute)
destination_value = Map.get(record, relationship.destination_attribute)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Query.filter(
ref(^relationship.source_attribute_on_join_resource) == ^source_value
)
|> Ash.Query.filter(
ref(^relationship.destination_attribute_on_join_resource) == ^destination_value
)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(changeset.tenant)
|> Ash.Query.set_context(join_relationship.context)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort, prepend?: true)
|> api.read_one(
authorize?: opts[:authorize?],
actor: actor
)
|> case do
{:ok, result} ->
result
|> Ash.Changeset.for_destroy(
join_action_name,
%{},
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, join_notifications} ->
notifications = join_notifications ++ all_notifications
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{},
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(return_notifications?: true)
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|> case do
{:ok, destroy_destination_notifications} ->
{:cont,
{:ok, current_value,
notifications ++
all_notifications ++ destroy_destination_notifications}}
{:error, error} ->
{:halt, {:error, error}}
end
{:error, error} ->
{:halt, {:error, error}}
end
{:error, error} ->
{:halt, {:error, error}}
end
{:destroy, action_name} ->
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{},
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:cont, {:ok, current_value, notifications ++ all_notifications}}
{:error, error} ->
{:halt, {:error, error}}
end
:error ->
{:halt,
{:error,
InvalidRelationship.exception(
relationship: relationship.name,
message: "Changes would destroy a record"
)}}
{:unrelate, action_name} ->
case unrelate_data(
source_record,
record,
api,
actor,
opts,
action_name,
changeset.tenant,
relationship
) do
{:ok, notifications} ->
{:cont, {:ok, current_value, notifications}}
{:error, error} ->
{:halt, {:error, error}}
end
end
end
)
end
defp unrelate_data(
source_record,
record,
api,
actor,
opts,
action_name,
tenant,
%{type: :many_to_many} = relationship
) do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
source_value = Map.get(source_record, relationship.source_attribute)
destination_value = Map.get(record, relationship.destination_attribute)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Query.filter(ref(^relationship.source_attribute_on_join_resource) == ^source_value)
|> Ash.Query.filter(
ref(^relationship.destination_attribute_on_join_resource) == ^destination_value
)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(tenant)
|> Ash.Query.set_context(join_relationship.context)
|> api.read_one(authorize?: opts[:authorize?], actor: actor)
|> case do
{:ok, result} ->
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor, authorize?: opts[:authorize?])
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:ok, notifications}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
defp unrelate_data(
_source_record,
record,
api,
actor,
opts,
action_name,
tenant,
%{type: type} = relationship
)
when type in [:has_many, :has_one] do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
authorize?: opts[:authorize?],
actor: actor
)
|> maybe_force_change_attribute(relationship, :destination_attribute, nil)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.update(return_notifications?: true)
|> case do
{:ok, _unrelated, notifications} ->
{:ok, notifications}
{:error, error} ->
{:error, error}
end
end
defp unrelate_data(
_source_record,
_record,
_api,
_actor,
_opts,
_action_name,
_tenant,
%{type: :belongs_to}
) do
{:ok, []}
end
defp destroy_data(
source_record,
record,
api,
actor,
opts,
action_name,
tenant,
%{type: :many_to_many} = relationship
) do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
source_value = Map.get(source_record, relationship.source_attribute)
destination_value = Map.get(record, relationship.destination_attribute)
relationship.through
|> Ash.Query.filter(ref(^relationship.source_attribute_on_join_resource) == ^source_value)
|> Ash.Query.filter(
ref(^relationship.destination_attribute_on_join_resource) == ^destination_value
)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(tenant)
|> api.read_one(authorize?: opts[:authorize?], actor: actor)
|> case do
{:ok, result} ->
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor, authorize?: opts[:authorize?])
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:ok, notifications}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
defp destroy_data(
_source_record,
record,
api,
actor,
opts,
action_name,
tenant,
relationship
) do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.destination, :update).name
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(return_notifications?: true)
end
end