diff --git a/lib/ash/actions/managed_relationships.ex b/lib/ash/actions/managed_relationships.ex index 5ed16d43..64b23e9f 100644 --- a/lib/ash/actions/managed_relationships.ex +++ b/lib/ash/actions/managed_relationships.ex @@ -228,7 +228,18 @@ defmodule Ash.Actions.ManagedRelationships do end _value -> - {:cont, {changeset, instructions}} + if opts[:on_match] == :destroy do + changeset = + Ash.Changeset.force_change_attribute( + changeset, + relationship.source_field, + nil + ) + + {:cont, {changeset, instructions}} + else + {:cont, {changeset, instructions}} + end end {:error, error} -> @@ -488,7 +499,7 @@ defmodule Ash.Actions.ManagedRelationships do {:cont, {:ok, new_value, all_notifications ++ notifications, all_used ++ used}} {:error, %Ash.Error.Changes.InvalidRelationship{} = error} -> - {:error, error} + {:halt, {:error, error}} {:error, error} -> case Keyword.fetch(opts[:meta] || [], :inputs_was_list?) do @@ -1048,6 +1059,25 @@ defmodule Ash.Actions.ManagedRelationships do :missing -> {:ok, current_value, [], []} + {:destroy, action_name} -> + case destroy_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 + {:unrelate, action_name} -> case unrelate_data( source_record, @@ -1525,4 +1555,79 @@ defmodule Ash.Actions.ManagedRelationships do ) do {:ok, []} end + + defp destroy_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 destroy_data( + _source_record, + record, + api, + actor, + opts, + action_name, + tenant, + relationship, + changeset + ) 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_destroy(action_name, %{}, + relationships: opts[:relationships] || [], + actor: actor + ) + |> Ash.Changeset.set_context(relationship.context) + |> Ash.Changeset.set_tenant(tenant) + |> api.destroy(return_notifications?: true, actor: actor, authorize?: opts[:authorize?]) + end end diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 6c463f17..a838e17b 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -1263,6 +1263,11 @@ defmodule Ash.Changeset do * `{:update, :action_name}` - the record is updated using the specified action on the destination resource * `{:update, :action_name, :join_table_action_name, [:list, :of, :params]}` - Same as `{:update, :action_name}` but takes the list of params specified out and applies them as an update to the join table row (only valid for many to many). + * `{:destroy, :action_name}` - the record is destroyed using the specified action on the destination resource. The action should be: + * many_to_many - a destroy action on the join table + * has_many - a destroy action on the destination resource + * has_one - a destroy action on the destination resource + * belongs_to - a destroy action on the destination resource * `:error` - an eror is returned indicating that a record would have been updated * `:no_match` - ignores the primary key match and follows the on_no_match instructions with these records instead. * `:unrelate` - the related item is not destroyed, but the data is "unrelated", making this behave like `remove_from_relationship/3`. The action should be: diff --git a/lib/ash/changeset/managed_relationship_helpers.ex b/lib/ash/changeset/managed_relationship_helpers.ex index a1cdcc61..2ee17834 100644 --- a/lib/ash/changeset/managed_relationship_helpers.ex +++ b/lib/ash/changeset/managed_relationship_helpers.ex @@ -80,6 +80,14 @@ defmodule Ash.Changeset.ManagedRelationshipHelpers do :unrelate -> {:unrelate, nil} + :destroy when relationship.type == :many_to_many -> + action = Ash.Resource.Info.primary_action!(relationship.through, :destroy) + {:destroy, action.name} + + :destroy -> + action = Ash.Resource.Info.primary_action!(relationship.destination, :destroy) + {:destroy, action.name} + other -> other end) @@ -135,6 +143,24 @@ defmodule Ash.Changeset.ManagedRelationshipHelpers do opts[:on_match] == :missing -> on_missing_destination_actions(opts, relationship) + unwrap(opts[:on_match]) == :destroy && relationship.type == :many_to_many -> + case opts[:on_match] do + :destroy -> + all(join(primary_action_name(relationship.through, :destroy), :all)) + + {:destroy, action_name} -> + all(join(action_name, :all)) + end + + unwrap(opts[:on_match]) == :destroy -> + case opts[:on_match] do + :destroy -> + all(destination(primary_action_name(relationship.destination, :destroy))) + + {:destroy, action_name} -> + all(destination(action_name)) + end + unwrap(opts[:on_match]) == :update -> case opts[:on_match] do :update ->