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