diff --git a/lib/ash/actions/destroy/bulk.ex b/lib/ash/actions/destroy/bulk.ex index ee069ba2f..41a0529d5 100644 --- a/lib/ash/actions/destroy/bulk.ex +++ b/lib/ash/actions/destroy/bulk.ex @@ -184,6 +184,8 @@ defmodule Ash.Actions.Destroy.Bulk do {atomic_changeset, opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, atomic_changeset, opts) + atomic_changeset = Ash.Actions.Helpers.apply_opts_load(atomic_changeset, opts) + atomic_changeset = %{atomic_changeset | domain: domain} atomic_changeset = @@ -503,6 +505,21 @@ defmodule Ash.Actions.Destroy.Bulk do {results, []} end + {results, errors, error_count} = + case load_data( + results, + atomic_changeset.domain, + atomic_changeset.resource, + atomic_changeset, + opts + ) do + {:ok, results} -> + {results, [], 0} + + {:error, error} -> + {[], List.wrap(error), Enum.count(List.wrap(error))} + end + notifications = if opts[:notify?] do notifications ++ @@ -513,12 +530,29 @@ defmodule Ash.Actions.Destroy.Bulk do notifications end + status = + case {error_count, results} do + {0, []} -> + :success + + {0, _results} -> + :success + + {_error_count, []} -> + :error + end + %Ash.BulkResult{ - status: :success, - error_count: 0, + status: status, + error_count: error_count, notifications: notifications, - errors: [], - records: results + errors: errors, + records: + if opts[:return_records?] do + results + else + [] + end } {:error, :no_rollback, @@ -1913,19 +1947,27 @@ defmodule Ash.Actions.Destroy.Bulk do resource |> Ash.Resource.Info.public_attributes() |> Enum.map(& &1.name) end - case List.wrap(changeset.load) ++ select do - [] -> - {:ok, records} - - load -> + case Ash.load(records, select, + reuse_values?: true, + domain: domain, + actor: opts[:actor], + authorize?: opts[:authorize?], + tracer: opts[:tracer] + ) do + {:ok, records} -> Ash.load( records, - load, + List.wrap(changeset.load), + reuse_values?: true, domain: domain, actor: opts[:actor], authorize?: opts[:authorize?], tracer: opts[:tracer] ) + |> Ash.Actions.Helpers.select(changeset) + + other -> + other end end diff --git a/test/actions/bulk/bulk_create_test.exs b/test/actions/bulk/bulk_create_test.exs index f99334cb5..25ce1b78b 100644 --- a/test/actions/bulk/bulk_create_test.exs +++ b/test/actions/bulk/bulk_create_test.exs @@ -66,6 +66,75 @@ defmodule Ash.Test.Actions.BulkCreateTest do end end + defmodule Author do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + actions do + default_accept :* + defaults [:read, :create, :update, :destroy] + + create :create_with_posts do + argument :post_ids, {:array, :uuid} do + allow_nil? false + constraints min_length: 1 + end + + change manage_relationship(:post_ids, :posts, type: :append) + end + end + + attributes do + uuid_primary_key :id + + attribute :name, :string do + public?(true) + end + end + + relationships do + has_many :posts, Ash.Test.Actions.BulkCreateTest.Post, + destination_attribute: :author_id, + public?: true + end + end + + defmodule PostLink do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + attributes do + attribute :type, :string do + public?(true) + end + end + + actions do + default_accept :* + defaults [:read, :destroy, create: :*, update: :*] + end + + relationships do + belongs_to :source_post, Ash.Test.Actions.BulkCreateTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + + belongs_to :destination_post, Ash.Test.Actions.BulkCreateTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + end + end + defmodule Post do @moduledoc false use Ash.Resource, @@ -96,6 +165,15 @@ defmodule Ash.Test.Actions.BulkCreateTest do default_accept :* defaults [:read, :destroy, create: :*, update: :*] + create :create_with_related_posts do + argument :related_post_ids, {:array, :uuid} do + allow_nil? false + constraints min_length: 1 + end + + change manage_relationship(:related_post_ids, :related_posts, type: :append) + end + create :create_with_change do change fn changeset, _ -> title = Ash.Changeset.get_attribute(changeset, :title) @@ -191,6 +269,14 @@ defmodule Ash.Test.Actions.BulkCreateTest do allow_nil? false attribute_public? false end + + belongs_to :author, Author, public?: true + + many_to_many :related_posts, __MODULE__, + through: PostLink, + source_attribute_on_join_resource: :source_post_id, + destination_attribute_on_join_resource: :destination_post_id, + public?: true end end @@ -844,4 +930,174 @@ defmodule Ash.Test.Actions.BulkCreateTest do end) end end + + describe "load" do + test "allows loading has_many relationship" do + org = Ash.create!(Org, %{}) + post1 = Ash.create!(Post, %{title: "Post 1"}, tenant: org.id, authorize?: false) + post2 = Ash.create!(Post, %{title: "Post 2"}, tenant: org.id, authorize?: false) + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_create!( + [%{name: "Author", post_ids: [post2.id, post1.id]}], + Author, + :create_with_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [posts: load_query] + ) + + assert [%Post{title: "Post 1"}, %Post{title: "Post 2"}] = author.posts + end + + test "allows loading paginated has_many relationship" do + org = Ash.create!(Org, %{}) + post1 = Ash.create!(Post, %{title: "Post 1"}, tenant: org.id, authorize?: false) + post2 = Ash.create!(Post, %{title: "Post 2"}, tenant: org.id, authorize?: false) + + offset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_create!( + [%{name: "Author 1", post_ids: [post2.id, post1.id]}], + Author, + :create_with_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [posts: offset_pagination_query] + ) + + assert %Ash.Page.Offset{ + results: [%Post{title: "Post 1", __metadata__: %{keyset: keyset}}], + limit: 1, + offset: 0, + count: 2, + more?: true + } = author.posts + + keyset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1, after: keyset) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_create!( + [%{name: "Author 2", post_ids: [post2.id, post1.id]}], + Author, + :create_with_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [posts: keyset_pagination_query] + ) + + assert %Ash.Page.Keyset{ + results: [%Post{title: "Post 2"}], + limit: 1, + count: 2, + more?: false, + before: nil, + after: ^keyset + } = author.posts + end + + test "allows loading many_to_many relationship" do + org = Ash.create!(Org, %{}) + related_post1 = Ash.create!(Post, %{title: "Related 1"}, tenant: org.id, authorize?: false) + related_post2 = Ash.create!(Post, %{title: "Related 2"}, tenant: org.id, authorize?: false) + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_create!( + [%{title: "Title", related_post_ids: [related_post2.id, related_post1.id]}], + Post, + :create_with_related_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [related_posts: load_query] + ) + + assert [%Post{title: "Related 1"}, %Post{title: "Related 2"}] = post.related_posts + end + + test "allows loading paginated many_to_many relationship" do + org = Ash.create!(Org, %{}) + related_post1 = Ash.create!(Post, %{title: "Related 1"}, tenant: org.id, authorize?: false) + related_post2 = Ash.create!(Post, %{title: "Related 2"}, tenant: org.id, authorize?: false) + + offset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_create!( + [%{title: "Post 1", related_post_ids: [related_post2.id, related_post1.id]}], + Post, + :create_with_related_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [related_posts: offset_pagination_query] + ) + + assert %Ash.Page.Offset{ + results: [%Post{title: "Related 1", __metadata__: %{keyset: keyset}}], + limit: 1, + offset: 0, + count: 2, + more?: true + } = post.related_posts + + keyset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1, after: keyset) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_create!( + [%{title: "Post 2", related_post_ids: [related_post2.id, related_post1.id]}], + Post, + :create_with_related_posts, + return_records?: true, + return_errors?: true, + authorize?: false, + tenant: org.id, + load: [related_posts: keyset_pagination_query] + ) + + assert %Ash.Page.Keyset{ + results: [%Post{title: "Related 2"}], + limit: 1, + count: 2, + more?: false, + before: nil, + after: ^keyset + } = post.related_posts + end + end end diff --git a/test/actions/bulk/bulk_destroy_test.exs b/test/actions/bulk/bulk_destroy_test.exs index 99e2e63c4..94408bbcb 100644 --- a/test/actions/bulk/bulk_destroy_test.exs +++ b/test/actions/bulk/bulk_destroy_test.exs @@ -33,6 +33,66 @@ defmodule Ash.Test.Actions.BulkDestroyTest do end end + defmodule Author do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + actions do + default_accept :* + defaults [:read, :create, :update, :destroy] + end + + attributes do + uuid_primary_key :id + + attribute :name, :string do + public?(true) + end + end + + relationships do + has_many :posts, Ash.Test.Actions.BulkDestroyTest.Post, + destination_attribute: :author_id, + public?: true + end + end + + defmodule PostLink do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + attributes do + attribute :type, :string do + public?(true) + end + end + + actions do + default_accept :* + defaults [:read, :destroy, create: :*, update: :*] + end + + relationships do + belongs_to :source_post, Ash.Test.Actions.BulkDestroyTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + + belongs_to :destination_post, Ash.Test.Actions.BulkDestroyTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + end + end + defmodule Post do @moduledoc false use Ash.Resource, @@ -151,6 +211,16 @@ defmodule Ash.Test.Actions.BulkDestroyTest do timestamps() end + + relationships do + belongs_to :author, Author, public?: true + + many_to_many :related_posts, __MODULE__, + through: PostLink, + source_attribute_on_join_resource: :source_post_id, + destination_attribute_on_join_resource: :destination_post_id, + public?: true + end end test "returns destroyed records" do @@ -581,4 +651,60 @@ defmodule Ash.Test.Actions.BulkDestroyTest do assert [] = Ash.read!(Post) end + + describe "load" do + test "allows loading has_many relationship" do + post1 = Ash.create!(Post, %{title: "Post 1"}) + post2 = Ash.create!(Post, %{title: "Post 2"}) + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [author]} = + Author + |> Ash.Changeset.for_create(:create, %{name: "Author"}) + |> Ash.Changeset.manage_relationship(:posts, [post2, post1], + type: :append_and_remove + ) + |> Ash.create!() + |> List.wrap() + |> Ash.bulk_destroy!(:destroy, %{}, + resource: Author, + return_records?: true, + load: [posts: load_query] + ) + + assert [%Post{title: "Post 1"}, %Post{title: "Post 2"}] = author.posts + end + + test "allows loading many_to_many relationship" do + related_post1 = Ash.create!(Post, %{title: "Related 1"}) + related_post2 = Ash.create!(Post, %{title: "Related 2"}) + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [post]} = + Post + |> Ash.Changeset.for_create(:create, %{title: "Title"}) + |> Ash.Changeset.manage_relationship( + :related_posts, + [related_post2, related_post1], + type: :append_and_remove + ) + |> Ash.create!() + |> List.wrap() + |> Ash.bulk_destroy!(:destroy, %{}, + resource: Post, + return_records?: true, + load: [related_posts: load_query] + ) + + assert [%Post{title: "Related 1"}, %Post{title: "Related 2"}] = post.related_posts + end + end end diff --git a/test/actions/bulk/bulk_update_test.exs b/test/actions/bulk/bulk_update_test.exs index e6b56a27a..1dcf749d8 100644 --- a/test/actions/bulk/bulk_update_test.exs +++ b/test/actions/bulk/bulk_update_test.exs @@ -109,6 +109,66 @@ defmodule Ash.Test.Actions.BulkUpdateTest do end end + defmodule Author do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + actions do + default_accept :* + defaults [:read, :create, :update, :destroy] + end + + attributes do + uuid_primary_key :id + + attribute :name, :string do + public?(true) + end + end + + relationships do + has_many :posts, Ash.Test.Actions.BulkUpdateTest.Post, + destination_attribute: :author_id, + public?: true + end + end + + defmodule PostLink do + @moduledoc false + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + ets do + private?(true) + end + + attributes do + attribute :type, :string do + public?(true) + end + end + + actions do + default_accept :* + defaults [:read, :destroy, create: :*, update: :*] + end + + relationships do + belongs_to :source_post, Ash.Test.Actions.BulkUpdateTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + + belongs_to :destination_post, Ash.Test.Actions.BulkUpdateTest.Post, + primary_key?: true, + allow_nil?: false, + public?: true + end + end + defmodule Post do @moduledoc false use Ash.Resource, @@ -257,6 +317,16 @@ defmodule Ash.Test.Actions.BulkUpdateTest do timestamps() end + + relationships do + belongs_to :author, Author, public?: true + + many_to_many :related_posts, __MODULE__, + through: PostLink, + source_attribute_on_join_resource: :source_post_id, + destination_attribute_on_join_resource: :destination_post_id, + public?: true + end end test "returns updated records" do @@ -785,4 +855,192 @@ defmodule Ash.Test.Actions.BulkUpdateTest do authorize?: false ) end + + describe "load" do + test "allows loading has_many relationship" do + author = + Author + |> Ash.Changeset.for_create(:create, %{name: "Name"}) + |> Ash.create!() + + for n <- [2, 1] do + Post + |> Ash.Changeset.for_create(:create, %{title: "Post #{n}"}) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + end + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_update!([author], :update, %{name: "Updated Name"}, + resource: Author, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [posts: load_query] + ) + + assert [%Post{title: "Post 1"}, %Post{title: "Post 2"}] = author.posts + end + + test "allows loading paginated has_many relationship" do + author = + Author + |> Ash.Changeset.for_create(:create, %{name: "Name"}) + |> Ash.create!() + + for n <- [2, 1] do + Post + |> Ash.Changeset.for_create(:create, %{title: "Post #{n}"}) + |> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove) + |> Ash.create!() + end + + offset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_update!([author], :update, %{name: "Updated Name 1"}, + resource: Author, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [posts: offset_pagination_query] + ) + + assert %Ash.Page.Offset{ + results: [%Post{title: "Post 1", __metadata__: %{keyset: keyset}}], + limit: 1, + offset: 0, + count: 2, + more?: true + } = author.posts + + keyset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1, after: keyset) + + assert %Ash.BulkResult{records: [author]} = + Ash.bulk_update!([author], :update, %{name: "Updated Name 2"}, + resource: Author, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [posts: keyset_pagination_query] + ) + + assert %Ash.Page.Keyset{ + results: [%Post{title: "Post 2"}], + limit: 1, + count: 2, + more?: false, + before: nil, + after: ^keyset + } = author.posts + end + + test "allows loading many_to_many relationship" do + related_post1 = Ash.create!(Post, %{title: "Related 1"}) + related_post2 = Ash.create!(Post, %{title: "Related 2"}) + + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "Title"}) + |> Ash.Changeset.manage_relationship(:related_posts, [related_post2, related_post1], + type: :append_and_remove + ) + |> Ash.create!() + + load_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_update!([post], :update, %{title: "Updated Title"}, + resource: Post, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [related_posts: load_query] + ) + + assert [%Post{title: "Related 1"}, %Post{title: "Related 2"}] = post.related_posts + end + + test "allows loading paginated many_to_many relationship" do + related_post1 = Ash.create!(Post, %{title: "Related 1"}) + related_post2 = Ash.create!(Post, %{title: "Related 2"}) + + post = + Post + |> Ash.Changeset.for_create(:create, %{title: "Title"}) + |> Ash.Changeset.manage_relationship(:related_posts, [related_post2, related_post1], + type: :append_and_remove + ) + |> Ash.create!() + + offset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_update!([post], :update, %{title: "Updated Title 1"}, + resource: Post, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [related_posts: offset_pagination_query] + ) + + assert %Ash.Page.Offset{ + results: [%Post{title: "Related 1", __metadata__: %{keyset: keyset}}], + limit: 1, + offset: 0, + count: 2, + more?: true + } = post.related_posts + + keyset_pagination_query = + Post + |> Ash.Query.sort(title: :asc) + |> Ash.Query.select([:title]) + |> Ash.Query.page(count: true, limit: 1, after: keyset) + + assert %Ash.BulkResult{records: [post]} = + Ash.bulk_update!([post], :update, %{title: "Updated Title 2"}, + resource: Post, + strategy: :atomic_batches, + return_records?: true, + return_errors?: true, + authorize?: false, + load: [related_posts: keyset_pagination_query] + ) + + assert %Ash.Page.Keyset{ + results: [%Post{title: "Related 2"}], + limit: 1, + count: 2, + more?: false, + before: nil, + after: ^keyset + } = post.related_posts + end + end end