ash/test/actions/bulk/bulk_create_test.exs
2024-05-01 22:13:24 -04:00

663 lines
20 KiB
Elixir

defmodule Ash.Test.Actions.BulkCreateTest do
@moduledoc false
use ExUnit.Case, async: true
alias Ash.Test.Domain, as: Domain
defmodule AddAfterToTitle do
use Ash.Resource.Change
def change(changeset, _, %{bulk?: true}) do
changeset
end
def batch_change(changesets, _, _) do
changesets
end
def after_batch(results, _, _) do
Stream.map(results, fn {_changeset, result} ->
{:ok, %{result | title: result.title <> "_after"}}
end)
end
end
defmodule AddBeforeToTitle do
use Ash.Resource.Change
def change(changeset, _, %{bulk?: true}) do
changeset
end
def batch_change(changesets, _, _) do
changesets
end
def before_batch(changesets, _, _) do
changesets
|> Stream.map(fn changeset ->
title = Ash.Changeset.get_attribute(changeset, :title)
Ash.Changeset.force_change_attribute(changeset, :title, "before_" <> title)
end)
end
end
defmodule Org do
@moduledoc false
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
end
actions do
default_accept :*
defaults create: :*
end
end
defmodule Post do
@moduledoc false
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
authorizers: [Ash.Policy.Authorizer]
alias Ash.Test.Actions.BulkCreateTest.Org
ets do
private? true
end
multitenancy do
strategy :attribute
attribute :org_id
global? true
end
calculations do
calculate :hidden_calc, :string, expr("something") do
public?(true)
end
end
actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]
create :create_with_change do
change fn changeset, _ ->
title = Ash.Changeset.get_attribute(changeset, :title)
Ash.Changeset.force_change_attribute(changeset, :title, title <> "_stuff")
end
end
create :create_with_argument do
argument :a_title, :string do
allow_nil? false
end
change set_attribute(:title, arg(:a_title))
end
create :create_with_before_transaction do
change before_transaction(fn changeset, _context ->
title = Ash.Changeset.get_attribute(changeset, :title)
Ash.Changeset.force_change_attribute(
changeset,
:title,
"before_transaction_" <> title
)
end)
end
create :create_with_after_action do
change after_action(fn _changeset, result, _context ->
{:ok, %{result | title: result.title <> "_stuff"}}
end)
end
create :create_with_after_batch do
change AddAfterToTitle
change AddBeforeToTitle
end
create :create_with_after_transaction do
change after_transaction(fn
_changeset, {:ok, result}, _context ->
{:ok, %{result | title: result.title <> "_stuff"}}
_changeset, {:error, error}, _context ->
send(self(), {:error, error})
{:error, error}
end)
end
create :create_with_policy do
argument :authorize?, :boolean, allow_nil?: false
change set_context(%{authorize?: arg(:authorize?)})
end
end
identities do
identity :unique_title, :title do
pre_check_with Ash.Test.Actions.BulkCreateTest.Domain
end
end
field_policies do
field_policy [:hidden_calc, :hidden_attribute] do
forbid_if always()
end
field_policy :* do
authorize_if always()
end
end
policies do
policy action(:create_with_policy) do
authorize_if context_equals(:authorize?, true)
end
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false, public?: true
attribute :title2, :string, public?: true
attribute :title3, :string, public?: true
attribute :hidden_attribute, :string, public?: true
timestamps()
end
relationships do
belongs_to :org, Org do
public?(true)
allow_nil? false
attribute_public? false
end
end
end
test "returns created records" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!([%{title: "title1"}, %{title: "title2"}], Post, :create,
return_records?: true,
return_errors?: true,
authorize?: false,
sorted?: true,
tenant: org.id
)
end
test "runs changes" do
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_change,
return_records?: true,
authorize?: false,
sorted?: true
)
end
test "accepts arguments" do
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!(
[%{a_title: "title1"}, %{a_title: "title2"}],
Post,
:create_with_argument,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "runs after batch hooks" do
assert %Ash.BulkResult{
records: [%{title: "before_title1_after"}, %{title: "before_title2_after"}]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_batch,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "will return error count" do
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}], error_count: 1, errors: nil} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: %{foo: :bar}}],
Post,
:create_with_change,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "will return errors on request" do
assert %Ash.BulkResult{
records: [%{title: "title1_stuff"}],
error_count: 1,
errors: [%Ash.Changeset{}]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: %{foo: :bar}}],
Post,
:create_with_change,
return_records?: true,
return_errors?: true,
sorted?: true,
authorize?: false
)
end
test "can upsert with list" do
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
],
Post,
:create,
return_records?: true,
sorted?: true,
authorize?: false
)
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "did_change", title3: "wont"},
%{title: "title2", title2: "did_change", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "did_change", title3: "oh no"},
%{title: "title2", title2: "did_change", title3: "what happened"}
],
Post,
:create,
return_records?: true,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: [:title2],
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace" do
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
],
Post,
:create,
return_records?: true,
sorted?: true,
authorize?: false
)
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "did_change", title3: "wont"},
%{title: "title2", title2: "did_change", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "did_change", title3: "oh no"},
%{title: "title2", title2: "did_change", title3: "what happened"}
],
Post,
:create,
return_records?: true,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: {:replace, [:title2]},
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace_all" do
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "changes", title3: "changes"},
%{title: "title2", title2: "changes", title3: "changes"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "changes", title3: "changes"},
%{title: "title2", title2: "changes", title3: "changes"}
],
Post,
:create,
return_records?: true,
sorted?: true,
authorize?: false
)
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "did_change", title3: "did_change"},
%{title: "title2", title2: "did_change", title3: "did_change"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "did_change", title3: "did_change"},
%{title: "title2", title2: "did_change", title3: "did_change"}
],
Post,
:create,
return_records?: true,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: :replace_all,
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace_all_except" do
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "changes", title3: "wont"},
%{title: "title2", title2: "changes", title3: "wont"}
],
Post,
:create,
return_records?: true,
sorted?: true,
authorize?: false
)
assert %Ash.BulkResult{
records: [
%{title: "title1", title2: "did_change", title3: "wont"},
%{title: "title2", title2: "did_change", title3: "wont"}
]
} =
Ash.bulk_create!(
[
%{title: "title1", title2: "did_change", title3: "oh no"},
%{title: "title2", title2: "did_change", title3: "what happened"}
],
Post,
:create,
return_records?: true,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: {:replace_all_except, [:title, :title3]},
sorted?: true,
authorize?: false
)
end
test "runs before transaction hooks" do
assert %Ash.BulkResult{
records: [
%{title: "before_transaction_title1"},
%{title: "before_transaction_title2"}
]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_before_transaction,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "runs after action hooks" do
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_action,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "runs after transaction hooks on success" do
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_transaction,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "runs after transaction hooks on failure" do
assert %Ash.BulkResult{error_count: 2} =
Ash.bulk_create(
[%{title: 1}, %{title: 2}],
Post,
:create_with_after_transaction,
sorted?: true,
authorize?: false
)
assert_receive {:error, _error}
assert_receive {:error, _error}
end
describe "authorization" do
test "policy success results in successes" do
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!(
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}],
Post,
:create_with_policy,
authorize?: true,
return_records?: true,
sorted?: true
)
end
test "field authorization is run" do
assert %Ash.BulkResult{
records: [
%{hidden_attribute: %Ash.ForbiddenField{}, hidden_calc: %Ash.ForbiddenField{}},
%{hidden_attribute: %Ash.ForbiddenField{}, hidden_calc: %Ash.ForbiddenField{}}
]
} =
Ash.bulk_create!(
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}],
Post,
:create_with_policy,
authorize?: true,
return_records?: true,
sorted?: true,
load: [:hidden_calc]
)
end
test "policy failure results in failures" do
assert %Ash.BulkResult{errors: [_, _]} =
Ash.bulk_create(
[%{title: "title1", authorize?: false}, %{title: "title2", authorize?: false}],
Post,
:create_with_policy,
authorize?: true,
return_records?: true,
return_errors?: true,
sorted?: true
)
end
end
describe "streaming" do
test "by default nothing is returned in the stream" do
assert [] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
return_stream?: true
)
|> Enum.to_list()
end
test "batch size is honored while streaming" do
assert [_] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
batch_size: 1,
return_records?: true,
return_stream?: true
)
|> Enum.take(1)
assert Ash.count!(Post, authorize?: false) == 1
end
test "by returning notifications, you get the notifications in the stream" do
assert [{:notification, _}, {:notification, _}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
return_stream?: true,
notify?: true,
return_notifications?: true
)
|> Enum.to_list()
end
test "by returning records, you get the records in the stream" do
assert [{:ok, %{title: "title1"}}, {:ok, %{title: "title2"}}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
return_stream?: true,
return_records?: true
)
|> Enum.to_list()
|> Enum.sort_by(fn
{:ok, v} ->
v.title
_ ->
nil
end)
end
test "by returning notifications and records, you get them both in the stream" do
assert [
{:notification, _},
{:notification, _},
{:ok, %{title: "title1"}},
{:ok, %{title: "title2"}}
] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
notify?: true,
return_stream?: true,
return_notifications?: true,
return_records?: true
)
|> Enum.to_list()
|> Enum.sort_by(fn
{:ok, v} ->
v.title
{:notification, _} ->
true
_ ->
nil
end)
end
test "any errors are also returned in the stream" do
assert [
{:error, %Ash.Changeset{}},
{:notification, _},
{:ok, %{title: "title1"}}
] =
[
%{title: "title1", authorize?: true},
%{title: "title2", authorize?: false}
]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
notify?: true,
return_stream?: true,
return_notifications?: true,
return_records?: true,
return_errors?: true
)
|> Enum.to_list()
|> Enum.sort_by(fn
{:ok, v} ->
v.title
{:notification, _} ->
true
{:error, _} ->
false
_ ->
nil
end)
end
end
end