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 end
_value -> _value ->
if opts[:on_match] == :destroy do
changeset =
Ash.Changeset.force_change_attribute(
changeset,
relationship.source_field,
nil
)
{:cont, {changeset, instructions}} {:cont, {changeset, instructions}}
else
{:cont, {changeset, instructions}}
end
end end
{:error, error} -> {:error, error} ->
@ -488,7 +499,7 @@ defmodule Ash.Actions.ManagedRelationships do
{:cont, {:ok, new_value, all_notifications ++ notifications, all_used ++ used}} {:cont, {:ok, new_value, all_notifications ++ notifications, all_used ++ used}}
{:error, %Ash.Error.Changes.InvalidRelationship{} = error} -> {:error, %Ash.Error.Changes.InvalidRelationship{} = error} ->
{:error, error} {:halt, {:error, error}}
{:error, error} -> {:error, error} ->
case Keyword.fetch(opts[:meta] || [], :inputs_was_list?) do case Keyword.fetch(opts[:meta] || [], :inputs_was_list?) do
@ -1048,6 +1059,25 @@ defmodule Ash.Actions.ManagedRelationships do
:missing -> :missing ->
{:ok, current_value, [], []} {: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} -> {:unrelate, action_name} ->
case unrelate_data( case unrelate_data(
source_record, source_record,
@ -1525,4 +1555,79 @@ defmodule Ash.Actions.ManagedRelationships do
) do ) do
{:ok, []} {:ok, []}
end 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 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}` - 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 * `{: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). 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 * `: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. * `: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: * `: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 ->
{:unrelate, nil} {: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 ->
other other
end) end)
@ -135,6 +143,24 @@ defmodule Ash.Changeset.ManagedRelationshipHelpers do
opts[:on_match] == :missing -> opts[:on_match] == :missing ->
on_missing_destination_actions(opts, relationship) 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 -> unwrap(opts[:on_match]) == :update ->
case opts[:on_match] do case opts[:on_match] do
:update -> :update ->