fix: ensure that manual action configurations are honored for bulk actions

This commit is contained in:
Zach Daniel 2024-07-10 11:24:48 -04:00
parent 06d8e98f7e
commit 426ee6671d
5 changed files with 72 additions and 7 deletions

View file

@ -98,6 +98,9 @@ defmodule Ash.Actions.Destroy.Bulk do
changeset = opts[:atomic_changeset] ->
changeset
query.action.manual ->
{:not_atomic, "Manual read actions cannot be updated atomically"}
Ash.DataLayer.data_layer_can?(query.resource, :destroy_query) ->
private_context = Map.new(Keyword.take(opts, [:actor, :tenant, :authorize]))
@ -529,7 +532,7 @@ defmodule Ash.Actions.Destroy.Bulk do
{results, []}
end
{results, errors, error_count} =
{results, errors, error_count, notifications} =
case load_data(
results,
atomic_changeset.domain,
@ -538,7 +541,36 @@ defmodule Ash.Actions.Destroy.Bulk do
opts
) do
{:ok, results} ->
{results, [], 0}
if Enum.empty?(atomic_changeset.after_action) do
{results, [], 0, notifications}
else
Enum.reduce(results, {[], [], 0, notifications}, fn result,
{results, errors,
error_count,
notifications} ->
case Ash.Changeset.run_after_actions(result, atomic_changeset, []) do
{:error, error} ->
if opts[:transaction] && opts[:rollback_on_error?] do
if Ash.DataLayer.in_transaction?(atomic_changeset.resource) do
Ash.DataLayer.rollback(
atomic_changeset.resource,
error
)
end
end
{results, errors ++ List.wrap(error),
error_count + Enum.count(List.wrap(error)), notifications}
{:ok, result, _changeset, %{notifications: more_new_notifications}} ->
{[result | results], errors, error_count,
notifications ++ more_new_notifications}
end
end)
|> then(fn {results, errors, error_count, notifications} ->
{Enum.reverse(results), errors, error_count, notifications}
end)
end
{:error, error} ->
{[], List.wrap(error), Enum.count(List.wrap(error))}
@ -708,6 +740,8 @@ defmodule Ash.Actions.Destroy.Bulk do
resource = opts[:resource]
opts = Ash.Actions.Helpers.set_opts(opts, domain)
read_action = get_read_action(resource, opts)
{context_cs, opts} =
Ash.Actions.Helpers.set_context_and_get_opts(domain, Ash.Changeset.new(resource), opts)
@ -722,6 +756,9 @@ defmodule Ash.Actions.Destroy.Bulk do
!Ash.Resource.Info.primary_action(resource, :read) ->
{:not_atomic, "cannot atomically destroy a stream without a primary read action"}
read_action.manual ->
{:not_atomic, "Manual read actions cannot be updated atomically"}
Ash.DataLayer.data_layer_can?(resource, :destroy_query) ->
opts =
Keyword.update(
@ -822,8 +859,10 @@ defmodule Ash.Actions.Destroy.Bulk do
fn batch ->
pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))]
read_action = get_read_action(resource, opts).name
resource
|> Ash.Query.for_read(Ash.Resource.Info.primary_action!(resource, :read).name, %{},
|> Ash.Query.for_read(read_action, %{},
actor: opts[:actor],
authorize?: false,
context: atomic_changeset.context,
@ -852,7 +891,9 @@ defmodule Ash.Actions.Destroy.Bulk do
resource: opts[:resource],
return_notifications?: opts[:return_notifications?],
notify?: opts[:notify?],
read_action: read_action,
return_records?: opts[:return_records?],
allow_stream_with: opts[:allow_stream_with],
strategy: [:atomic]
],
not_atomic_reason
@ -2319,4 +2360,14 @@ defmodule Ash.Actions.Destroy.Bulk do
context
)
end
defp get_read_action(resource, opts) do
case opts[:read_action] do
nil ->
Ash.Resource.Info.primary_action!(resource, :read)
action ->
Ash.Resource.Info.action(resource, action)
end
end
end

View file

@ -53,6 +53,9 @@ defmodule Ash.Actions.Update.Bulk do
:atomic not in opts[:strategy] ->
{:not_atomic, "Not in requested strategies"}
query.action.manual ->
{:not_atomic, "Manual read actions cannot be updated atomically"}
changeset = opts[:atomic_changeset] ->
changeset
@ -901,6 +904,9 @@ defmodule Ash.Actions.Update.Bulk do
!read_action ->
{:not_atomic, "cannot atomically update a stream without a primary read action"}
read_action.manual ->
{:not_atomic, "Manual read actions cannot be updated atomically"}
Ash.DataLayer.data_layer_can?(resource, :update_query) ->
opts =
Keyword.update(
@ -1007,8 +1013,10 @@ defmodule Ash.Actions.Update.Bulk do
fn batch ->
pkeys = [or: Enum.map(batch, &Map.take(&1, pkey))]
read_action = get_read_action(resource, opts).name
resource
|> Ash.Query.for_read(get_read_action(resource, opts).name, %{},
|> Ash.Query.for_read(read_action, %{},
actor: opts[:actor],
authorize?: false,
context: atomic_changeset.context,
@ -1036,6 +1044,8 @@ defmodule Ash.Actions.Update.Bulk do
return_notifications?: opts[:return_notifications?],
notify?: opts[:notify?],
return_records?: opts[:return_records?],
allow_stream_with: opts[:allow_stream_with],
read_action: read_action,
strategy: [:atomic]
)
|> case do

View file

@ -361,7 +361,7 @@ defmodule Ash.MixProject do
{:simple_sat, "~> 0.1 and >= 0.1.1", optional: true},
# Code Generators
{:igniter, "~> 0.2.7"},
{:igniter, "~> 0.2.12"},
# Dev/Test dependencies
{:eflame, "~> 1.0", only: [:dev, :test]},

View file

@ -22,7 +22,7 @@
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
"glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"igniter": {:hex, :igniter, "0.2.7", "75524d8bbdb67eb7ccf39e583e5781f7d46f48f06b3450811602854da078e978", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "2962a8ca961c96ef1e2a13c9bb42d44b6f74dd160d3bd4f0f23032ea8b199c43"},
"igniter": {:hex, :igniter, "0.2.12", "e2e8fbb15effecb433f4096edbb0754282553544c75c3130d06ca09bdaa1fb13", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "51f3487a13441cd3e6e0d559689f8b0ba2c716834f86802e8a6760fdd1a2e579"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},

View file

@ -11,7 +11,11 @@ defmodule Ash.Test.UUIDv7Test do
test "generate/1 is ordered" do
uuids =
for _ <- 1..10_000 do
Ash.UUIDv7.generate()
uuid = Ash.UUIDv7.generate()
# only guaranteed sorted if >= 1 nanosecond apart
# can't sleep for one nanoseond AFAIK, so sleep for 1 ms
:timer.sleep(1)
uuid
end
assert uuids == Enum.sort(uuids)