mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: do not perform atomic upgrade on destroy actions
fix: correct atomic implementation of `present` validation fix: track keys that are set to `nil` in changesets, for use in atomic upgrade
This commit is contained in:
parent
9b88628b07
commit
f19fa6c6c0
4 changed files with 118 additions and 194 deletions
|
@ -24,135 +24,6 @@ defmodule Ash.Actions.Destroy do
|
|||
end
|
||||
|
||||
def run(api, changeset, action, opts) do
|
||||
primary_read = Ash.Resource.Info.primary_action(changeset.resource, :read)
|
||||
|
||||
{fully_atomic_changeset, params} =
|
||||
cond do
|
||||
!Ash.DataLayer.data_layer_can?(changeset.resource, :expr_error) && opts[:authorize?] ->
|
||||
{{:not_atomic, "data layer does not support adding errors to a query"}, nil}
|
||||
|
||||
!Ash.DataLayer.data_layer_can?(changeset.resource, :destroy_query) ->
|
||||
{{:not_atomic, "data layer does not support updating a query"}, nil}
|
||||
|
||||
!primary_read ->
|
||||
{{:not_atomic, "cannot atomically destroy a record without a primary read action"}, nil}
|
||||
|
||||
true ->
|
||||
params =
|
||||
changeset.attributes
|
||||
|> Map.merge(changeset.casted_attributes)
|
||||
|> Map.merge(changeset.arguments)
|
||||
|> Map.merge(changeset.casted_arguments)
|
||||
|
||||
res =
|
||||
Ash.Changeset.fully_atomic_changeset(
|
||||
changeset.resource,
|
||||
action,
|
||||
params,
|
||||
opts
|
||||
|> Keyword.merge(
|
||||
assume_casted?: true,
|
||||
notify?: true,
|
||||
atomics: changeset.atomics || [],
|
||||
tenant: changeset.tenant
|
||||
)
|
||||
)
|
||||
|
||||
{res, params}
|
||||
end
|
||||
|
||||
case fully_atomic_changeset do
|
||||
%Ash.Changeset{} = atomic_changeset ->
|
||||
atomic_changeset =
|
||||
%{atomic_changeset | data: changeset.data}
|
||||
|> Ash.Changeset.set_context(%{data_layer: %{use_atomic_destroy_data?: true}})
|
||||
|> Map.put(:load, changeset.load)
|
||||
|> Map.put(:select, changeset.select)
|
||||
|> Ash.Changeset.set_context(changeset.context)
|
||||
|
||||
{atomic_changeset, opts} =
|
||||
Ash.Actions.Helpers.add_process_context(api, atomic_changeset, opts)
|
||||
|
||||
opts =
|
||||
Keyword.merge(opts,
|
||||
atomic_changeset: atomic_changeset,
|
||||
return_records?: true,
|
||||
notify?: true,
|
||||
return_notifications?: opts[:return_notifications?],
|
||||
return_errors?: true
|
||||
)
|
||||
|
||||
primary_key = Ash.Resource.Info.primary_key(atomic_changeset.resource)
|
||||
primary_key_filter = changeset.data |> Map.take(primary_key) |> Map.to_list()
|
||||
|
||||
query =
|
||||
atomic_changeset.resource
|
||||
|> Ash.Query.for_read(primary_read.name, %{},
|
||||
actor: opts[:actor],
|
||||
authorize?: false,
|
||||
context: atomic_changeset.context,
|
||||
tenant: atomic_changeset.tenant,
|
||||
tracer: opts[:tracer]
|
||||
)
|
||||
|> Ash.Query.set_context(%{private: %{internal?: true}})
|
||||
|> Ash.Query.do_filter(primary_key_filter)
|
||||
|
||||
case Ash.Actions.Destroy.Bulk.run(
|
||||
api,
|
||||
query,
|
||||
fully_atomic_changeset.action,
|
||||
params,
|
||||
Keyword.merge(opts,
|
||||
strategy: [:atomic],
|
||||
authorize_query?: false,
|
||||
atomic_changeset: atomic_changeset,
|
||||
authorize_changeset_with: :error,
|
||||
return_records?: true
|
||||
)
|
||||
) do
|
||||
%Ash.BulkResult{status: :success, records: [record], notifications: notifications} ->
|
||||
if opts[:return_notifications?] do
|
||||
if opts[:return_destroyed?] do
|
||||
{:ok, record, List.wrap(notifications)}
|
||||
else
|
||||
{:ok, List.wrap(notifications)}
|
||||
end
|
||||
else
|
||||
if opts[:return_destroyed?] do
|
||||
{:ok, record}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
%Ash.BulkResult{status: :success, records: []} ->
|
||||
primary_key = Ash.Resource.Info.primary_key(atomic_changeset.resource)
|
||||
|
||||
{:error,
|
||||
Ash.Error.to_error_class(
|
||||
Ash.Error.Changes.StaleRecord.exception(
|
||||
resource: fully_atomic_changeset.resource,
|
||||
filters: Map.take(changeset.data, primary_key)
|
||||
)
|
||||
)}
|
||||
|
||||
%Ash.BulkResult{status: :error, errors: errors} ->
|
||||
{:error, Ash.Error.to_error_class(errors)}
|
||||
end
|
||||
|
||||
other ->
|
||||
if Ash.DataLayer.data_layer_can?(changeset.resource, :destroy_query) &&
|
||||
action.require_atomic? &&
|
||||
match?({:not_atomic, _reason}, other) do
|
||||
{:not_atomic, reason} = other
|
||||
|
||||
{:error,
|
||||
Ash.Error.Framework.MustBeAtomic.exception(
|
||||
resource: changeset.resource,
|
||||
action: action.name,
|
||||
reason: reason
|
||||
)}
|
||||
else
|
||||
{changeset, opts} = Ash.Actions.Helpers.add_process_context(api, changeset, opts)
|
||||
|
||||
Ash.Tracer.span :action,
|
||||
|
@ -202,8 +73,6 @@ defmodule Ash.Actions.Destroy do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
reraise Ash.Error.to_error_class(e, changeset: changeset, stacktrace: __STACKTRACE__),
|
||||
|
|
|
@ -37,6 +37,20 @@ defmodule Ash.Actions.Update do
|
|||
!Enum.empty?(changeset.relationships) ->
|
||||
{{:not_atomic, "cannot atomically manage relationships"}, nil}
|
||||
|
||||
!Enum.empty?(changeset.before_action) ->
|
||||
{{:not_atomic, "cannot atomically run a changeset with a before_action hook"}, nil}
|
||||
|
||||
!Enum.empty?(changeset.before_transaction) ->
|
||||
{{:not_atomic, "cannot atomically run a changeset with a before_transaction hook"},
|
||||
nil}
|
||||
|
||||
!Enum.empty?(changeset.around_action) ->
|
||||
{{:not_atomic, "cannot atomically run a changeset with an around_action hook"}, nil}
|
||||
|
||||
!Enum.empty?(changeset.around_transaction) ->
|
||||
{{:not_atomic, "cannot atomically run a changeset with an around_transaction hook"},
|
||||
nil}
|
||||
|
||||
!primary_read ->
|
||||
{{:not_atomic, "cannot atomically update a record without a primary read action"},
|
||||
nil}
|
||||
|
@ -48,6 +62,9 @@ defmodule Ash.Actions.Update do
|
|||
|> Map.merge(changeset.arguments)
|
||||
|> Map.merge(changeset.casted_arguments)
|
||||
|
||||
params =
|
||||
Enum.reduce(changeset.nil_inputs, params, &Map.put(&2, &1, nil))
|
||||
|
||||
res =
|
||||
Ash.Changeset.fully_atomic_changeset(
|
||||
changeset.resource,
|
||||
|
|
|
@ -42,6 +42,7 @@ defmodule Ash.Changeset do
|
|||
phase: :validate,
|
||||
relationships: %{},
|
||||
select: nil,
|
||||
nil_inputs: [],
|
||||
load: [],
|
||||
valid?: true
|
||||
]
|
||||
|
@ -4217,15 +4218,25 @@ defmodule Ash.Changeset do
|
|||
%{
|
||||
changeset
|
||||
| attributes: Map.delete(changeset.attributes, attribute.name),
|
||||
nil_inputs: [attribute.name | changeset.nil_inputs],
|
||||
defaults: changeset.defaults -- [attribute.name]
|
||||
}
|
||||
|
||||
Ash.Type.equal?(attribute.type, casted, data_value) ->
|
||||
if is_nil(casted) do
|
||||
%{
|
||||
changeset
|
||||
| attributes: Map.delete(changeset.attributes, attribute.name),
|
||||
defaults: changeset.defaults -- [attribute.name],
|
||||
nil_inputs: [attribute.name | changeset.nil_inputs]
|
||||
}
|
||||
else
|
||||
%{
|
||||
changeset
|
||||
| attributes: Map.delete(changeset.attributes, attribute.name),
|
||||
defaults: changeset.defaults -- [attribute.name]
|
||||
}
|
||||
end
|
||||
|
||||
true ->
|
||||
%{
|
||||
|
|
|
@ -92,42 +92,69 @@ defmodule Ash.Resource.Validation.Present do
|
|||
|> Keyword.delete(:attributes)
|
||||
|> Enum.map(fn
|
||||
{:exactly, exactly} ->
|
||||
attribute_count = length(opts[:attributes])
|
||||
|
||||
message =
|
||||
cond do
|
||||
exactly == 0 -> "must be absent"
|
||||
length(opts[:attributes]) == 1 -> "must be present"
|
||||
attribute_count == 1 -> "must be present"
|
||||
true -> "exactly %{exactly} of %{keys} must be present"
|
||||
end
|
||||
|
||||
{:atomic, [opts[:attribute]], expr(^nil_count == ^exactly),
|
||||
if attribute_count == 1 do
|
||||
attribute = Enum.at(opts[:attributes], 0)
|
||||
|
||||
condition =
|
||||
if exactly == 0 do
|
||||
expr(not is_nil(^atomic_ref(attribute)))
|
||||
else
|
||||
expr(is_nil(^atomic_ref(attribute)))
|
||||
end
|
||||
|
||||
{:atomic, opts[:attributes], condition,
|
||||
expr(
|
||||
error(^InvalidAttribute, %{
|
||||
field: ^opts[:attribute],
|
||||
value: ^atomic_ref(opts[:attribute]),
|
||||
field: ^Enum.at(opts[:attributes], 0),
|
||||
value: ^atomic_ref(Enum.at(opts[:attributes], 0)),
|
||||
message: ^message,
|
||||
vars: %{exactly: ^exactly, keys: ^values}
|
||||
vars: %{exactly: ^exactly, keys: ^Enum.join(opts[:attributes], ", ")}
|
||||
})
|
||||
)}
|
||||
|
||||
{:at_least, at_least} ->
|
||||
{:atomic, [opts[:attribute]], expr(count_nils(^atomic_ref(opts[:attribute])) < ^at_least),
|
||||
else
|
||||
{:atomic, opts[:attributes], expr(^nil_count == ^exactly),
|
||||
expr(
|
||||
error(^InvalidAttribute, %{
|
||||
field: ^opts[:attribute],
|
||||
value: ^atomic_ref(opts[:attribute]),
|
||||
field: ^Enum.at(opts[:attributes], 0),
|
||||
value: ^atomic_ref(Enum.at(opts[:attributes], 0)),
|
||||
message: ^message,
|
||||
vars: %{exactly: ^exactly, keys: ^Enum.join(opts[:attributes], ", ")}
|
||||
})
|
||||
)}
|
||||
end
|
||||
|
||||
{:at_least, at_least} ->
|
||||
attributes = Enum.map(opts[:attributes], fn attr -> expr(^atomic_ref(attr)) end)
|
||||
|
||||
{:atomic, opts[:attributes], expr(count_nils(^attributes) < ^at_least),
|
||||
expr(
|
||||
error(^InvalidAttribute, %{
|
||||
field: ^Enum.at(opts[:attributes], 0),
|
||||
value: ^atomic_ref(Enum.at(opts[:attributes], 0)),
|
||||
message: "at least %{at_least} of %{keys} must be present",
|
||||
vars: %{at_least: ^at_least, keys: ^values}
|
||||
vars: %{at_least: ^at_least, keys: ^Enum.join(opts[:attributes], ", ")}
|
||||
})
|
||||
)}
|
||||
|
||||
{:at_most, at_most} ->
|
||||
{:atomic, [opts[:attribute]], expr(count_nils(^atomic_ref(opts[:attribute])) > ^at_most),
|
||||
attributes = Enum.map(opts[:attributes], fn attr -> expr(^atomic_ref(attr)) end)
|
||||
|
||||
{:atomic, opts[:attributes], expr(count_nils(^attributes) > ^at_most),
|
||||
expr(
|
||||
error(^InvalidAttribute, %{
|
||||
field: ^opts[:attribute],
|
||||
value: ^atomic_ref(opts[:attribute]),
|
||||
field: ^Enum.at(opts[:attributes], 0),
|
||||
value: ^atomic_ref(Enum.at(opts[:attributes], 0)),
|
||||
message: "at most %{at_most} of %{keys} must be present",
|
||||
vars: %{at_most: ^at_most, keys: ^values}
|
||||
vars: %{at_most: ^at_most, keys: ^Enum.join(opts[:attributes], ", ")}
|
||||
})
|
||||
)}
|
||||
end)
|
||||
|
|
Loading…
Reference in a new issue