Packages

ash

1.44.10
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
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)
case Enum.filter(value, fn {_, opts} ->
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
Ash.Changeset.ManagedRelationshipHelpers.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) 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?] end)
|> Enum.with_index()
|> Enum.map(fn {{input, opts}, index} ->
{{relationship, {input, opts}}, index}
end)
end)
|> Enum.reject(fn {{_relationship, {input, _opts}}, _index} ->
is_nil(input) || input == []
end)
|> Enum.map(fn
{{relationship, {[input], opts}}, index} ->
{{relationship, {input, opts}}, index}
{{relationship, {other, opts}}, index} ->
{{relationship, {other, opts}}, index}
end)
|> Enum.reduce_while({changeset, %{notifications: []}}, fn {{relationship, {input, opts}},
index},
{changeset, instructions} ->
pkeys = pkeys(relationship)
opts = Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
opts = Keyword.put(opts, :authorize?, engine_opts[:authorize?] && opts[:authorize?])
current_value =
case Map.get(changeset.data, relationship.name) do
%Ash.NotLoaded{} ->
case relationship.cardinality do
:many -> []
:one -> nil
end
other ->
other
end
case find_match(
List.wrap(current_value),
input,
pkeys,
relationship,
opts[:on_no_match] == :match
) do
nil ->
case opts[:on_lookup] do
: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(%{
belongs_to_manage_found: %{relationship.name => %{index => input}}
})
|> Ash.Changeset.force_change_attribute(
relationship.source_field,
Map.get(input, relationship.destination_field)
)
{:cont, {changeset, instructions}}
else
case Ash.Filter.get_filter(relationship.destination, input) do
{:ok, keys} ->
relationship.destination
|> Ash.Query.for_read(read, input, actor: actor)
|> Ash.Query.filter(^keys)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(changeset.tenant)
|> changeset.api.read_one(
authorize?: opts[:authorize?],
actor: actor
)
|> 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}}
}
})
|> Ash.Changeset.force_change_attribute(
relationship.source_field,
Map.get(found, relationship.destination_field)
)
{:cont, {changeset, instructions}}
{:error, error} ->
{:halt, {Ash.Changeset.add_error(changeset, error), instructions}}
end
_ ->
create_belongs_to_record(
changeset,
instructions,
relationship,
input,
actor,
index,
opts
)
end
end
end
_value ->
{:cont, {changeset, instructions}}
end
end)
|> validate_required_belongs_to()
end
defp validate_required_belongs_to({changeset, instructions}) do
changeset.resource
|> Ash.Resource.Info.relationships()
|> Enum.filter(&(&1.type == :belongs_to))
|> Enum.filter(& &1.required?)
|> Enum.reject(fn relationship ->
changeset.context[:private][:error][relationship.name]
end)
|> Enum.reduce({changeset, instructions}, fn required_relationship,
{changeset, instructions} ->
changeset =
case Ash.Changeset.get_attribute(changeset, required_relationship.source_field) 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
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
)
)
|> 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
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,
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create(
actor: actor,
authorize?: opts[:authorize?],
return_notifications?: true
)
|> case do
{:ok, created, notifications} ->
changeset =
changeset
|> Ash.Changeset.set_context(%{
private: %{
belongs_to_manage_created: %{relationship.name => %{index => created}}
}
})
|> Ash.Changeset.force_change_attribute(
relationship.source_field,
Map.get(created, relationship.destination_field)
)
{:cont,
{changeset, %{instructions | notifications: instructions.notifications ++ notifications}}}
{:error, error} ->
{:halt, {Ash.Changeset.add_error(changeset, error), 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.reject(fn {relationship, _} ->
relationship.type == :belongs_to
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.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
defp pkeys(relationship) do
identities =
relationship.destination
|> Ash.Resource.Info.identities()
|> Enum.map(& &1.keys)
[Ash.Resource.Info.primary_key(relationship.destination) | 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)
original_value =
case Map.get(record, relationship.name) do
%Ash.NotLoaded{} -> []
value -> value
end
inputs
|> Enum.reduce_while(
{:ok, [], [], []},
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, new_value),
all_notifications ++ notifications}
{:error, error} ->
{:error, error}
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 =
case Map.get(record, relationship.name) 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, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
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
api = changeset.api
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)
|> 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)
|> changeset.api.read_one(
authorize?: opts[:authorize?],
actor: actor
)
end
|> case do
{:ok, found} when not is_nil(found) ->
do_handle_found(
relationship,
join_keys,
input,
api,
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)
|> Ash.Changeset.force_change_attribute(
relationship.source_field_on_join_table,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.force_change_attribute(
relationship.destination_field_on_join_table,
Map.get(found, relationship.destination_field)
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> 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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_update(create_or_update, input,
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute(
relationship.destination_field,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> 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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
relationships: opts[:relationships]
)
|> Ash.Changeset.force_change_attribute(
relationship.destination_field,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
end
case created do
{:ok, created, notifications} ->
{:ok, [created | current_value], notifications, []}
{:error, error} ->
{:error, error}
end
created ->
{:ok, [created | current_value], [], []}
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, []}
else
relationship.destination
|> Ash.Changeset.new()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_create(action_name, regular_params,
require?: false,
relationships: opts[:relationships],
actor: actor
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
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,
actor: actor
)
|> Ash.Changeset.force_change_attribute(
relationship.source_field_on_join_table,
Map.get(record, relationship.source_field)
)
|> Ash.Changeset.force_change_attribute(
relationship.destination_field_on_join_table,
Map.get(created, relationship.destination_field)
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> changeset.api.create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> case do
{:ok, _join_row, notifications} ->
{:ok, [created | current_value], regular_notifications ++ notifications, []}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
ignore when ignore in [:ignore, :match] ->
{:ok, current_value, [], []}
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 = changeset.api
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, [], []}
{:unrelate, action_name} ->
case unrelate_data(
source_record,
match,
api,
actor,
opts,
action_name,
changeset.tenant,
relationship,
changeset
) 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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_update(action_name, input,
actor: actor,
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], 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_field)
match
|> Ash.Changeset.new()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_update(action_name, regular_params,
actor: actor,
relationships: opts[:relationships]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
|> case do
{:ok, updated, update_notifications} ->
destination_value = Map.get(updated, relationship.destination_field)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Query.filter(ref(^relationship.source_field_on_join_table) == ^source_value)
|> Ash.Query.filter(
ref(^relationship.destination_field_on_join_table) == ^destination_value
)
|> Ash.Query.set_context(join_relationship.context)
|> Ash.Query.limit(1)
|> Ash.Query.set_tenant(changeset.tenant)
|> changeset.api.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)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
# 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_field in pkey do
Enum.all?(pkey, fn field ->
if field == relationship.destination_field 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 set_source_context(changeset, {relationship, original_changeset}) do
case changeset.data.__metadata__[:manage_relationship_source] ||
original_changeset.context[:manage_relationship_source] do
nil ->
Ash.Changeset.set_context(changeset, %{
manage_relationship_source: [
{relationship.source, relationship.name, original_changeset}
]
})
value ->
Ash.Changeset.set_context(changeset, %{
manage_relationship_source:
value ++ [{relationship.source, relationship.name, original_changeset}]
})
end
|> Ash.Changeset.after_action(fn changeset, record ->
{:ok,
Ash.Resource.Info.put_metadata(
record,
:manage_relationship_source,
changeset.context[:manage_relationship_source]
)}
end)
end
defp delete_unused(
source_record,
original_value,
relationship,
current_value,
all_used,
changeset,
actor,
opts
) do
api = changeset.api
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_field)
destination_value = Map.get(record, relationship.destination_field)
join_relationship =
Ash.Resource.Info.relationship(relationship.source, relationship.join_relationship)
relationship.through
|> Ash.Query.filter(ref(^relationship.source_field_on_join_table) == ^source_value)
|> Ash.Query.filter(
ref(^relationship.destination_field_on_join_table) == ^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} ->
join_relationship =
Ash.Resource.Info.relationship(
relationship.source,
relationship.join_relationship
)
result
|> Ash.Changeset.for_destroy(
join_action_name,
%{},
actor: actor
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> case do
{:ok, join_notifications} ->
notifications = join_notifications ++ all_notifications
record
|> Ash.Changeset.new()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
# 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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
authorize?: opts[:authorize?],
actor: actor,
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,
changeset
) 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,
changeset
) do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.through, :destroy).name
source_value = Map.get(source_record, relationship.source_field)
destination_value = Map.get(record, relationship.destination_field)
relationship.through
|> Ash.Query.filter(ref(^relationship.source_field_on_join_table) == ^source_value)
|> Ash.Query.filter(ref(^relationship.destination_field_on_join_table) == ^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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> 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,
changeset
)
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()
|> set_source_context({relationship, changeset})
|> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute(relationship.destination_field, nil)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.update(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|> 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} = relationship,
_changeset
) do
action_name =
action_name || Ash.Resource.Info.primary_action(relationship.source, :update).name
source_record
|> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor
)
|> Ash.Changeset.force_change_attribute(relationship.source_field, nil)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.update(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|> case do
{:ok, _unrelated, notifications} ->
{:ok, notifications}
{:error, error} ->
{:error, error}
end
end
end