fix: we cannot assume that after_action/1 can be done atomically

The reason for this is that a given change may access `changeset.data`
which will not be available. This may introduce compile errors for some users
unfortunately, there is nothing we can do about this. They will need to change
to writing custom changes w/ the `atomic/3` callback that adds an after action
hook

improvement: honor a `_union_type` type param when casting unions
This commit is contained in:
Zach Daniel 2024-09-11 09:57:22 -04:00
parent 5b89286854
commit cb3facb519
5 changed files with 59 additions and 6 deletions

View file

@ -1739,7 +1739,7 @@ Declares a `destroy` action. For calling this action, see the `Ash.Domain` docum
### Examples ### Examples
``` ```
destroy :soft_delete do destroy :destroy do
primary? true primary? true
end end

View file

@ -14,8 +14,10 @@ defmodule Ash.Resource.Change.AfterAction do
) )
end end
@impl true # we had to remove this. We can't be sure that the change function won't access
def atomic(changeset, opts, context) do # `changeset.data`, so cannot do this automatically
{:ok, change(changeset, opts, context)} # @impl true
end # def atomic(changeset, opts, context) do
# {:ok, change(changeset, opts, context)}
# end
end end

View file

@ -679,7 +679,7 @@ defmodule Ash.Resource.Dsl do
""", """,
examples: [ examples: [
""" """
destroy :soft_delete do destroy :destroy do
primary? true primary? true
end end
""" """

View file

@ -406,6 +406,18 @@ defmodule Ash.Type.Union do
end end
end end
def cast_input(%{"_union_type" => union_type} = value, constraints) do
case Enum.find(constraints[:types], fn {key, _} ->
to_string(key) == union_type
end) do
{type, _config} ->
cast_input(%Ash.Union{value: Map.delete(value, "_union_type"), type: type}, constraints)
_ ->
cast_input(Map.delete(value, "_union_type"), constraints)
end
end
def cast_input(value, constraints) do def cast_input(value, constraints) do
types = constraints[:types] || [] types = constraints[:types] || []

View file

@ -60,6 +60,21 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
end end
defmodule ChangeTitleBeforeAction do
use Ash.Resource.Change
@impl Ash.Resource.Change
def change(changeset, _opts, _context) do
Ash.Changeset.before_action(changeset, fn changeset ->
Ash.Changeset.force_change_attribute(
changeset,
:title,
"before_" <> Ash.Changeset.get_attribute(changeset, :title)
)
end)
end
end
defmodule ChangeMessage do defmodule ChangeMessage do
use Ash.Resource.Change use Ash.Resource.Change
@ -206,6 +221,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
default_accept :* default_accept :*
defaults [:read, :destroy, create: :*, update: :*] defaults [:read, :destroy, create: :*, update: :*]
create :create_with_before_action do
accept [:title]
change ChangeTitleBeforeAction
end
create :create_with_related_posts do create :create_with_related_posts do
argument :related_post_ids, {:array, :uuid} do argument :related_post_ids, {:array, :uuid} do
allow_nil? false allow_nil? false
@ -547,6 +567,25 @@ defmodule Ash.Test.Actions.BulkCreateTest do
) )
end end
test "runs before action hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "before_title1"}, %{title: "before_title2"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_before_action,
return_records?: true,
return_errors?: true,
authorize?: false,
sorted?: true,
tenant: org.id
)
end
test "runs changes" do test "runs changes" do
org = org =
Org Org