improvement: add on_match: :destroy option

This commit is contained in:
Zach Daniel 2021-08-27 01:45:13 -04:00
parent fed720bb59
commit d786aadec7
3 changed files with 138 additions and 2 deletions

View file

@ -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

View file

@ -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:

View file

@ -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 ->