fix: run changesets w/ after_transaction hooks through standard logic

This commit is contained in:
Zach Daniel 2024-05-02 00:06:31 -04:00
parent 8a7a4763fd
commit c1354d503e
13 changed files with 321 additions and 71 deletions

View file

@ -144,7 +144,6 @@ defmodule Ash.Actions.Create.Bulk do
argument_names argument_names
) )
) )
|> reject_and_maybe_store_errors(ref, opts)
|> handle_batch( |> handle_batch(
domain, domain,
resource, resource,
@ -357,8 +356,38 @@ defmodule Ash.Actions.Create.Bulk do
opts[:tenant] opts[:tenant]
) )
batch = {batch, must_be_simple} =
Enum.map(batch, &Ash.Changeset.run_before_transaction_hooks/1) batch
|> Stream.map(fn changeset ->
{changeset, _} =
Ash.Actions.ManagedRelationships.validate_required_belongs_to({changeset, []})
changeset
end)
|> Enum.reduce({[], []}, fn changeset, {batch, must_be_simple} ->
if changeset.after_transaction in [[], nil] do
changeset = Ash.Changeset.run_before_transaction_hooks(changeset)
{[changeset | batch], must_be_simple}
else
{batch, [%{changeset | __validated_for_action__: action.name} | must_be_simple]}
end
end)
must_be_simple_results =
Enum.flat_map(must_be_simple, fn changeset ->
case Ash.Actions.Create.run(domain, changeset, action, opts) do
{:ok, result} ->
[
Ash.Resource.set_metadata(result, %{
bulk_create_index: changeset.context.bulk_create.index
})
]
{:error, error} ->
store_error(ref, error, opts)
[]
end
end)
if opts[:transaction] == :batch && if opts[:transaction] == :batch &&
Ash.DataLayer.data_layer_can?(resource, :transact) do Ash.DataLayer.data_layer_can?(resource, :transact) do
@ -390,7 +419,8 @@ defmodule Ash.Actions.Create.Bulk do
data_layer_can_bulk?, data_layer_can_bulk?,
ref, ref,
changes, changes,
must_return_records_for_changes? must_return_records_for_changes?,
must_be_simple_results
) )
end, end,
opts[:timeout], opts[:timeout],
@ -435,7 +465,8 @@ defmodule Ash.Actions.Create.Bulk do
data_layer_can_bulk?, data_layer_can_bulk?,
ref, ref,
changes, changes,
must_return_records_for_changes? must_return_records_for_changes?,
must_be_simple_results
) )
end end
end end
@ -450,7 +481,8 @@ defmodule Ash.Actions.Create.Bulk do
data_layer_can_bulk?, data_layer_can_bulk?,
ref, ref,
changes, changes,
must_return_records_for_changes? must_return_records_for_changes?,
must_be_simple_results
) do ) do
must_return_records? = must_return_records? =
opts[:notify?] || opts[:notify?] ||
@ -492,6 +524,7 @@ defmodule Ash.Actions.Create.Bulk do
domain, domain,
resource resource
) )
|> Stream.concat(must_be_simple_results)
|> then(fn stream -> |> then(fn stream ->
if opts[:return_stream?] do if opts[:return_stream?] do
stream stream
@ -752,17 +785,6 @@ defmodule Ash.Actions.Create.Bulk do
end) end)
end end
defp reject_and_maybe_store_errors(stream, ref, opts) do
Enum.reject(stream, fn changeset ->
if changeset.valid? do
false
else
store_error(ref, changeset, opts)
true
end
end)
end
defp store_error(_ref, empty, _opts) when empty in [[], nil], do: :ok defp store_error(_ref, empty, _opts) when empty in [[], nil], do: :ok
defp store_error(ref, error, opts) do defp store_error(ref, error, opts) do
@ -778,7 +800,7 @@ defmodule Ash.Actions.Create.Bulk do
changeset changeset
other -> other ->
Ash.Error.to_ash_error(other) Ash.Error.to_error_class(other)
end end
Process.put( Process.put(

View file

@ -804,7 +804,6 @@ defmodule Ash.Actions.Destroy.Bulk do
domain domain
) )
) )
|> reject_and_maybe_store_errors(ref, opts)
|> handle_batch(domain, resource, action, all_changes, opts, ref, base_changeset) |> handle_batch(domain, resource, action, all_changes, opts, ref, base_changeset)
after after
if opts[:notify?] && !opts[:return_notifications?] do if opts[:notify?] && !opts[:return_notifications?] do
@ -1061,8 +1060,39 @@ defmodule Ash.Actions.Destroy.Bulk do
opts[:tenant] opts[:tenant]
) )
batch = {batch, must_be_simple} =
Enum.map(batch, &Ash.Changeset.run_before_transaction_hooks/1) Enum.reduce(batch, {[], []}, fn changeset, {batch, must_be_simple} ->
if changeset.after_transaction in [[], nil] do
changeset = Ash.Changeset.run_before_transaction_hooks(changeset)
{[changeset | batch], must_be_simple}
else
{batch, [%{changeset | __validated_for_action__: action.name} | must_be_simple]}
end
end)
must_be_simple_results =
Enum.flat_map(must_be_simple, fn changeset ->
case Ash.Actions.Destroy.run(
domain,
changeset,
action,
Keyword.put(opts, :return_destroyed?, opts[:return_records?])
) do
:ok ->
[]
{:ok, result} when not is_list(result) ->
[
Ash.Resource.set_metadata(result, %{
bulk_destroy_index: changeset.context.bulk_destroy.index
})
]
{:error, error} ->
store_error(ref, error, opts)
[]
end
end)
if opts[:transaction] == :batch && if opts[:transaction] == :batch &&
Ash.DataLayer.data_layer_can?(resource, :transact) do Ash.DataLayer.data_layer_can?(resource, :transact) do
@ -1094,7 +1124,8 @@ defmodule Ash.Actions.Destroy.Bulk do
ref, ref,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) )
end, end,
opts[:timeout], opts[:timeout],
@ -1137,7 +1168,8 @@ defmodule Ash.Actions.Destroy.Bulk do
ref, ref,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) )
end end
end end
@ -1152,7 +1184,8 @@ defmodule Ash.Actions.Destroy.Bulk do
ref, ref,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) do ) do
must_return_records? = must_return_records? =
opts[:notify?] || opts[:notify?] ||
@ -1195,6 +1228,7 @@ defmodule Ash.Actions.Destroy.Bulk do
resource, resource,
base_changeset base_changeset
) )
|> Stream.concat(must_be_simple_results)
|> then(fn stream -> |> then(fn stream ->
if opts[:return_stream?] do if opts[:return_stream?] do
stream stream
@ -1402,17 +1436,6 @@ defmodule Ash.Actions.Destroy.Bulk do
end) end)
end end
defp reject_and_maybe_store_errors(stream, ref, opts) do
Enum.reject(stream, fn changeset ->
if changeset.valid? do
false
else
store_error(ref, changeset, opts)
true
end
end)
end
defp store_error(ref, errors, opts, count \\ nil) defp store_error(ref, errors, opts, count \\ nil)
defp store_error(ref, empty, _opts, error_count) when empty in [[], nil] do defp store_error(ref, empty, _opts, error_count) when empty in [[], nil] do

View file

@ -8,6 +8,8 @@ defmodule Ash.Actions.Destroy do
@spec run(Ash.Domain.t(), Ash.Changeset.t(), Ash.Resource.Actions.action(), Keyword.t()) :: @spec run(Ash.Domain.t(), Ash.Changeset.t(), Ash.Resource.Actions.action(), Keyword.t()) ::
{:ok, list(Ash.Notifier.Notification.t())} {:ok, list(Ash.Notifier.Notification.t())}
| :ok | :ok
| {:ok, Ash.Resource.record()}
| {:ok, Ash.Resource.record(), list(Ash.Notifier.Notification.t())}
| {:error, Ash.Changeset.t()} | {:error, Ash.Changeset.t()}
| {:error, term} | {:error, term}
def run(domain, changeset, %{soft?: true} = action, opts) do def run(domain, changeset, %{soft?: true} = action, opts) do

View file

@ -998,7 +998,6 @@ defmodule Ash.Actions.Update.Bulk do
context_key context_key
) )
) )
|> reject_and_maybe_store_errors(ref, opts)
|> handle_batch( |> handle_batch(
domain, domain,
resource, resource,
@ -1299,8 +1298,31 @@ defmodule Ash.Actions.Update.Bulk do
context_key context_key
) )
batch = {batch, must_be_simple} =
Enum.map(batch, &Ash.Changeset.run_before_transaction_hooks/1) Enum.reduce(batch, {[], []}, fn changeset, {batch, must_be_simple} ->
if changeset.after_transaction in [[], nil] do
changeset = Ash.Changeset.run_before_transaction_hooks(changeset)
{[changeset | batch], must_be_simple}
else
{batch, [%{changeset | __validated_for_action__: action.name} | must_be_simple]}
end
end)
must_be_simple_results =
Enum.flat_map(must_be_simple, fn changeset ->
case Ash.Actions.Update.run(domain, changeset, action, opts) do
{:ok, result} ->
[
Ash.Resource.set_metadata(result, %{
metadata_key => changeset.context |> Map.get(context_key) |> Map.get(:index)
})
]
{:error, error} ->
store_error(ref, error, opts)
[]
end
end)
if opts[:transaction] == :batch && if opts[:transaction] == :batch &&
Ash.DataLayer.data_layer_can?(resource, :transact) do Ash.DataLayer.data_layer_can?(resource, :transact) do
@ -1338,7 +1360,8 @@ defmodule Ash.Actions.Update.Bulk do
context_key, context_key,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) )
{new_errors, new_error_count} = Process.get({:bulk_update_errors, ref}) || {[], 0} {new_errors, new_error_count} = Process.get({:bulk_update_errors, ref}) || {[], 0}
@ -1396,7 +1419,8 @@ defmodule Ash.Actions.Update.Bulk do
context_key, context_key,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) )
end end
end end
@ -1413,7 +1437,8 @@ defmodule Ash.Actions.Update.Bulk do
context_key, context_key,
base_changeset, base_changeset,
must_return_records_for_changes?, must_return_records_for_changes?,
changes changes,
must_be_simple_results
) do ) do
must_return_records? = must_return_records? =
opts[:notify?] || opts[:notify?] ||
@ -1459,6 +1484,7 @@ defmodule Ash.Actions.Update.Bulk do
domain, domain,
base_changeset base_changeset
) )
|> Stream.concat(must_be_simple_results)
|> then(fn stream -> |> then(fn stream ->
if opts[:return_stream?] do if opts[:return_stream?] do
stream stream
@ -1684,17 +1710,6 @@ defmodule Ash.Actions.Update.Bulk do
end) end)
end end
defp reject_and_maybe_store_errors(stream, ref, opts) do
Enum.reject(stream, fn changeset ->
if changeset.valid? do
false
else
store_error(ref, changeset, opts)
true
end
end)
end
defp store_error(ref, errors, opts, count \\ nil) defp store_error(ref, errors, opts, count \\ nil)
defp store_error(_ref, empty, _opts, 0) when empty in [[], nil], do: :ok defp store_error(_ref, empty, _opts, 0) when empty in [[], nil], do: :ok

View file

@ -11,8 +11,20 @@ defmodule Ash.Actions.Update do
| {:ok, Ash.Resource.record()} | {:ok, Ash.Resource.record()}
| {:error, Ash.Changeset.t()} | {:error, Ash.Changeset.t()}
| {:error, term} | {:error, term}
def run(domain, %{valid?: false} = changeset, _action, _opts) do def run(domain, %{valid?: false, errors: errors} = changeset, action, opts) do
{:error, Ash.Error.to_error_class(changeset.errors, changeset: %{changeset | domain: domain})} changeset = changeset(changeset, domain, action, opts)
errors = Helpers.process_errors(changeset, errors)
case Ash.Changeset.run_after_transactions(
{:error, Ash.Error.to_error_class(errors, changeset: changeset)},
changeset
) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, Ash.Error.to_error_class(error, changeset: %{changeset | domain: domain})}
end
end end
def run(domain, changeset, action, opts) do def run(domain, changeset, action, opts) do
@ -51,6 +63,10 @@ defmodule Ash.Actions.Update do
{{:not_atomic, "cannot atomically run a changeset with an around_transaction hook"}, {{:not_atomic, "cannot atomically run a changeset with an around_transaction hook"},
nil} nil}
!Enum.empty?(changeset.after_transaction) ->
{{:not_atomic, "cannot atomically run a changeset with an after_transaction hook"},
nil}
!primary_read -> !primary_read ->
{{:not_atomic, "cannot atomically update a record without a primary read action"}, {{:not_atomic, "cannot atomically update a record without a primary read action"},
nil} nil}

View file

@ -555,7 +555,8 @@ defmodule Ash.Changeset do
changeset = set_phase(changeset, :atomic) changeset = set_phase(changeset, :atomic)
with :ok <- verify_notifiers_support_atomic(resource, action), with :ok <- verify_notifiers_support_atomic(resource, action),
%Ash.Changeset{} = changeset <- atomic_params(changeset, action, params, opts), %Ash.Changeset{} = changeset <-
atomic_params(changeset, action, params, opts),
%Ash.Changeset{} = changeset <- atomic_changes(changeset, action), %Ash.Changeset{} = changeset <- atomic_changes(changeset, action),
%Ash.Changeset{} = changeset <- atomic_defaults(changeset), %Ash.Changeset{} = changeset <- atomic_defaults(changeset),
%Ash.Changeset{} = changeset <- atomic_update(changeset, opts[:atomic_update] || []), %Ash.Changeset{} = changeset <- atomic_update(changeset, opts[:atomic_update] || []),
@ -1393,7 +1394,12 @@ defmodule Ash.Changeset do
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, value} -> {:atomic, value} ->
value = set_error_field(value, attribute.name) value =
if attribute.primary_key? do
value
else
set_error_field(value, attribute.name)
end
%{changeset | atomics: Keyword.put(changeset.atomics, key, value)} %{changeset | atomics: Keyword.put(changeset.atomics, key, value)}
|> record_atomic_update_for_atomic_upgrade(attribute.name, value) |> record_atomic_update_for_atomic_upgrade(attribute.name, value)
@ -3272,7 +3278,7 @@ defmodule Ash.Changeset do
) )
|> case do |> case do
{:ok, new_result} -> {:ok, new_result} ->
{:ok, %{new_result | after_transaction: []}} {:ok, new_result}
{:error, error} -> {:error, error} ->
{:error, error} {:error, error}

View file

@ -42,6 +42,23 @@ defmodule Ash.Error do
end end
def to_error_class(value, opts) do def to_error_class(value, opts) do
value =
value
|> List.wrap()
|> Enum.map(fn
%Ash.Changeset{} = changeset ->
to_error_class(changeset, opts)
%Ash.Query{} = query ->
to_error_class(query, opts)
%Ash.ActionInput{} = action_input ->
to_error_class(action_input, opts)
other ->
other
end)
class = to_class(value, opts) class = to_class(value, opts)
class = class =

View file

@ -36,7 +36,7 @@
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"}, "sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"},
"spark": {:hex, :spark, "2.1.20", "204db8fd28378783c28a9dcb0bebdaf1d51b14a9ea106e1080457d29510a66ea", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e7a4f8f8ca7a477918af1eb65e20f2015f783a9a23e5f73d1020edf5b2ef69be"}, "spark": {:hex, :spark, "2.1.20", "204db8fd28378783c28a9dcb0bebdaf1d51b14a9ea106e1080457d29510a66ea", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e7a4f8f8ca7a477918af1eb65e20f2015f783a9a23e5f73d1020edf5b2ef69be"},
"splode": {:hex, :splode, "0.2.3", "43a851790699c0993787d92bff017eb36f33ad6544974e47f7643f24ff89ac80", [:mix], [], "hexpm", "c91dc334647b5af4dc65b304635372df3d24c55bc389f9390cbb69d1c5bfd3e0"}, "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},

View file

@ -89,9 +89,10 @@ defmodule Ash.Test.Actions.BulkCreateTest do
create :create_with_change do create :create_with_change do
change fn changeset, _ -> change fn changeset, _ ->
title = Ash.Changeset.get_attribute(changeset, :title) title = Ash.Changeset.get_attribute(changeset, :title)
Ash.Changeset.force_change_attribute(changeset, :title, title <> "_stuff") Ash.Changeset.force_change_attribute(changeset, :title, title <> "_stuff")
end end,
only_when_valid?: true
end end
create :create_with_argument do create :create_with_argument do
@ -201,11 +202,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "runs changes" do test "runs changes" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} = assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}], [%{title: "title1"}, %{title: "title2"}],
Post, Post,
:create_with_change, :create_with_change,
tenant: org.id,
return_records?: true, return_records?: true,
authorize?: false, authorize?: false,
sorted?: true sorted?: true
@ -213,18 +220,29 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "accepts arguments" do test "accepts arguments" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} = assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!( Ash.bulk_create!(
[%{a_title: "title1"}, %{a_title: "title2"}], [%{a_title: "title1"}, %{a_title: "title2"}],
Post, Post,
:create_with_argument, :create_with_argument,
return_records?: true, return_records?: true,
tenant: org.id,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
) )
end end
test "runs after batch hooks" do test "runs after batch hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [%{title: "before_title1_after"}, %{title: "before_title2_after"}] records: [%{title: "before_title1_after"}, %{title: "before_title2_after"}]
} = } =
@ -232,6 +250,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
[%{title: "title1"}, %{title: "title2"}], [%{title: "title1"}, %{title: "title2"}],
Post, Post,
:create_with_after_batch, :create_with_after_batch,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -239,11 +258,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "will return error count" do test "will return error count" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}], error_count: 1, errors: nil} = assert %Ash.BulkResult{records: [%{title: "title1_stuff"}], error_count: 1, errors: nil} =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1"}, %{title: %{foo: :bar}}], [%{title: "title1"}, %{title: %{foo: :bar}}],
Post, Post,
:create_with_change, :create_with_change,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -251,6 +276,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "will return errors on request" do test "will return errors on request" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [%{title: "title1_stuff"}], records: [%{title: "title1_stuff"}],
error_count: 1, error_count: 1,
@ -260,6 +290,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
[%{title: "title1"}, %{title: %{foo: :bar}}], [%{title: "title1"}, %{title: %{foo: :bar}}],
Post, Post,
:create_with_change, :create_with_change,
tenant: org.id,
return_records?: true, return_records?: true,
return_errors?: true, return_errors?: true,
sorted?: true, sorted?: true,
@ -268,6 +299,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "can upsert with list" do test "can upsert with list" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{title: "title1", title2: "changes", title3: "wont"}, %{title: "title1", title2: "changes", title3: "wont"},
@ -281,6 +317,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
], ],
Post, Post,
:create, :create,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -300,6 +337,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
Post, Post,
:create, :create,
return_records?: true, return_records?: true,
tenant: org.id,
upsert?: true, upsert?: true,
upsert_identity: :unique_title, upsert_identity: :unique_title,
upsert_fields: [:title2], upsert_fields: [:title2],
@ -309,6 +347,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "can upsert with :replace" do test "can upsert with :replace" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{title: "title1", title2: "changes", title3: "wont"}, %{title: "title1", title2: "changes", title3: "wont"},
@ -322,6 +365,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
], ],
Post, Post,
:create, :create,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -342,6 +386,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
:create, :create,
return_records?: true, return_records?: true,
upsert?: true, upsert?: true,
tenant: org.id,
upsert_identity: :unique_title, upsert_identity: :unique_title,
upsert_fields: {:replace, [:title2]}, upsert_fields: {:replace, [:title2]},
sorted?: true, sorted?: true,
@ -350,6 +395,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "can upsert with :replace_all" do test "can upsert with :replace_all" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{title: "title1", title2: "changes", title3: "changes"}, %{title: "title1", title2: "changes", title3: "changes"},
@ -364,6 +414,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
Post, Post,
:create, :create,
return_records?: true, return_records?: true,
tenant: org.id,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
) )
@ -382,6 +433,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
Post, Post,
:create, :create,
return_records?: true, return_records?: true,
tenant: org.id,
upsert?: true, upsert?: true,
upsert_identity: :unique_title, upsert_identity: :unique_title,
upsert_fields: :replace_all, upsert_fields: :replace_all,
@ -391,6 +443,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "can upsert with :replace_all_except" do test "can upsert with :replace_all_except" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{title: "title1", title2: "changes", title3: "wont"}, %{title: "title1", title2: "changes", title3: "wont"},
@ -404,6 +461,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
], ],
Post, Post,
:create, :create,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -423,6 +481,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
Post, Post,
:create, :create,
return_records?: true, return_records?: true,
tenant: org.id,
upsert?: true, upsert?: true,
upsert_identity: :unique_title, upsert_identity: :unique_title,
upsert_fields: {:replace_all_except, [:title, :title3]}, upsert_fields: {:replace_all_except, [:title, :title3]},
@ -432,6 +491,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "runs before transaction hooks" do test "runs before transaction hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{title: "before_transaction_title1"}, %{title: "before_transaction_title1"},
@ -442,6 +506,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
[%{title: "title1"}, %{title: "title2"}], [%{title: "title1"}, %{title: "title2"}],
Post, Post,
:create_with_before_transaction, :create_with_before_transaction,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -449,11 +514,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "runs after action hooks" do test "runs after action hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} = assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}], [%{title: "title1"}, %{title: "title2"}],
Post, Post,
:create_with_after_action, :create_with_after_action,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
@ -461,25 +532,40 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "runs after transaction hooks on success" do test "runs after transaction hooks on success" do
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} = org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{
records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]
} =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}], [%{title: "title1"}, %{title: "title2"}],
Post, Post,
:create_with_after_transaction, :create_with_after_transaction,
tenant: org.id,
return_records?: true, return_records?: true,
return_errors?: true,
sorted?: true, sorted?: true,
authorize?: false authorize?: false
) )
end end
test "runs after transaction hooks on failure" do test "runs after transaction hooks on failure" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{error_count: 2} = assert %Ash.BulkResult{error_count: 2} =
Ash.bulk_create( Ash.bulk_create(
[%{title: 1}, %{title: 2}], [%{title: 1}, %{title: 2}],
Post, Post,
:create_with_after_transaction, :create_with_after_transaction,
sorted?: true, sorted?: true,
authorize?: false authorize?: false,
tenant: org.id
) )
assert_receive {:error, _error} assert_receive {:error, _error}
@ -488,18 +574,30 @@ defmodule Ash.Test.Actions.BulkCreateTest do
describe "authorization" do describe "authorization" do
test "policy success results in successes" do test "policy success results in successes" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} = assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}], [%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}],
Post, Post,
:create_with_policy, :create_with_policy,
tenant: org.id,
authorize?: true, authorize?: true,
return_errors?: true,
return_records?: true, return_records?: true,
sorted?: true sorted?: true
) )
end end
test "field authorization is run" do test "field authorization is run" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{ assert %Ash.BulkResult{
records: [ records: [
%{hidden_attribute: %Ash.ForbiddenField{}, hidden_calc: %Ash.ForbiddenField{}}, %{hidden_attribute: %Ash.ForbiddenField{}, hidden_calc: %Ash.ForbiddenField{}},
@ -507,10 +605,14 @@ defmodule Ash.Test.Actions.BulkCreateTest do
] ]
} = } =
Ash.bulk_create!( Ash.bulk_create!(
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}], [
%{title: "title1", authorize?: true},
%{title: "title2", authorize?: true}
],
Post, Post,
:create_with_policy, :create_with_policy,
authorize?: true, authorize?: true,
tenant: org.id,
return_records?: true, return_records?: true,
sorted?: true, sorted?: true,
load: [:hidden_calc] load: [:hidden_calc]
@ -518,9 +620,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "policy failure results in failures" do test "policy failure results in failures" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{errors: [_, _]} = assert %Ash.BulkResult{errors: [_, _]} =
Ash.bulk_create( Ash.bulk_create(
[%{title: "title1", authorize?: false}, %{title: "title2", authorize?: false}], [
%{title: "title1", authorize?: false, org_id: org.id},
%{title: "title2", authorize?: false, org_id: org.id}
],
Post, Post,
:create_with_policy, :create_with_policy,
authorize?: true, authorize?: true,
@ -533,8 +643,16 @@ defmodule Ash.Test.Actions.BulkCreateTest do
describe "streaming" do describe "streaming" do
test "by default nothing is returned in the stream" do test "by default nothing is returned in the stream" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [] = assert [] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}] [
%{title: "title1", authorize?: true, org_id: org.id},
%{title: "title2", authorize?: true, org_id: org.id}
]
|> Ash.bulk_create!( |> Ash.bulk_create!(
Post, Post,
:create_with_policy, :create_with_policy,
@ -545,11 +663,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "batch size is honored while streaming" do test "batch size is honored while streaming" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [_] = assert [_] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}] [%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!( |> Ash.bulk_create!(
Post, Post,
:create_with_policy, :create_with_policy,
tenant: org.id,
authorize?: true, authorize?: true,
batch_size: 1, batch_size: 1,
return_records?: true, return_records?: true,
@ -561,11 +685,17 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "by returning notifications, you get the notifications in the stream" do test "by returning notifications, you get the notifications in the stream" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [{:notification, _}, {:notification, _}] = assert [{:notification, _}, {:notification, _}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}] [%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!( |> Ash.bulk_create!(
Post, Post,
:create_with_policy, :create_with_policy,
tenant: org.id,
authorize?: true, authorize?: true,
return_stream?: true, return_stream?: true,
notify?: true, notify?: true,
@ -575,12 +705,18 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "by returning records, you get the records in the stream" do test "by returning records, you get the records in the stream" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [{:ok, %{title: "title1"}}, {:ok, %{title: "title2"}}] = assert [{:ok, %{title: "title1"}}, {:ok, %{title: "title2"}}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}] [%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!( |> Ash.bulk_create!(
Post, Post,
:create_with_policy, :create_with_policy,
authorize?: true, authorize?: true,
tenant: org.id,
return_stream?: true, return_stream?: true,
return_records?: true return_records?: true
) )
@ -595,6 +731,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "by returning notifications and records, you get them both in the stream" do test "by returning notifications and records, you get them both in the stream" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [ assert [
{:notification, _}, {:notification, _},
{:notification, _}, {:notification, _},
@ -606,6 +747,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
Post, Post,
:create_with_policy, :create_with_policy,
authorize?: true, authorize?: true,
tenant: org.id,
notify?: true, notify?: true,
return_stream?: true, return_stream?: true,
return_notifications?: true, return_notifications?: true,
@ -625,6 +767,11 @@ defmodule Ash.Test.Actions.BulkCreateTest do
end end
test "any errors are also returned in the stream" do test "any errors are also returned in the stream" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [ assert [
{:error, %Ash.Changeset{}}, {:error, %Ash.Changeset{}},
{:notification, _}, {:notification, _},
@ -639,6 +786,7 @@ defmodule Ash.Test.Actions.BulkCreateTest do
:create_with_policy, :create_with_policy,
authorize?: true, authorize?: true,
notify?: true, notify?: true,
tenant: org.id,
return_stream?: true, return_stream?: true,
return_notifications?: true, return_notifications?: true,
return_records?: true, return_records?: true,

View file

@ -149,6 +149,7 @@ defmodule Ash.Test.Actions.BulkUpdateTest do
_changeset, {:error, error}, _context -> _changeset, {:error, error}, _context ->
send(self(), {:error, error}) send(self(), {:error, error})
{:error, error}
end) end)
end end

View file

@ -98,13 +98,13 @@ defmodule Ash.Test.Actions.ValidationTest do
|> Ash.Changeset.for_create(:create, %{foo: true, status: "foo"}) |> Ash.Changeset.for_create(:create, %{foo: true, status: "foo"})
|> Ash.create!() |> Ash.create!()
assert_raise(Ash.Error.Invalid, ~r/status: must not equal foo/, fn -> assert_raise(Ash.Error.Invalid, ~r/status: must not equal \"foo\"/, fn ->
Profile Profile
|> Ash.Changeset.for_create(:create, %{foo: false, status: "foo"}) |> Ash.Changeset.for_create(:create, %{foo: false, status: "foo"})
|> Ash.create!() |> Ash.create!()
end) end)
assert_raise(Ash.Error.Invalid, ~r/status: must equal foo/, fn -> assert_raise(Ash.Error.Invalid, ~r/status: must equal \"foo\"/, fn ->
Profile Profile
|> Ash.Changeset.for_create(:create, %{foo: true, status: "bar"}) |> Ash.Changeset.for_create(:create, %{foo: true, status: "bar"})
|> Ash.create!() |> Ash.create!()
@ -164,7 +164,7 @@ defmodule Ash.Test.Actions.ValidationTest do
end end
test "it fails if the value is not in the list" do test "it fails if the value is not in the list" do
assert_raise(Ash.Error.Invalid, ~r/expected one of foo, bar/, fn -> assert_raise(Ash.Error.Invalid, ~r/expected one of \"foo, bar\"/, fn ->
Profile Profile
|> Ash.Changeset.for_create(:create, %{status: "blart"}) |> Ash.Changeset.for_create(:create, %{status: "blart"})
|> Ash.create!() |> Ash.create!()

View file

@ -322,7 +322,7 @@ defmodule Ash.Test.Changeset.EmbeddedResourceTest do
test "embedded resources run validations on create" do test "embedded resources run validations on create" do
msg = msg =
~r/Invalid value provided for last_name: exactly 2 of first_name,last_name must be present/ ~r/Invalid value provided for last_name: exactly 2 of "first_name,last_name" must be present/
assert_raise Ash.Error.Invalid, assert_raise Ash.Error.Invalid,
msg, msg,

View file

@ -37,7 +37,7 @@ defmodule Ash.Test.ErrorTest do
end end
test "returns exception if it is a map/struct with class: :special wrapped in a list" do test "returns exception if it is a map/struct with class: :special wrapped in a list" do
assert [%{class: :special}] = assert %{class: :special} =
Ash.Error.to_error_class([SpecialError.exception([])], []) Ash.Error.to_error_class([SpecialError.exception([])], [])
end end