ash/test/actions/bulk/bulk_create_test.exs
Davide Briani cd06f919c0
fix: load relationships on bulk operations (#1234)
This change validates that the `load` statement of bulk operations is
respected when specified, and correctly loads relationships.

Loading relationships with pagination on results for bulk destroys is
still not supported. Indeed, relationships are currently queried using a
lateral join but after the resource deletion has happened, so it looks
like nothing is related.

Signed-off-by: Davide Briani <davide@briani.dev>
2024-06-10 20:14:37 -04:00

1103 lines
32 KiB
Elixir

defmodule Ash.Test.Actions.BulkCreateTest do
@moduledoc false
use ExUnit.Case, async: true
alias Ash.Test.Domain, as: Domain
defmodule Notifier do
use Ash.Notifier
def notify(notification) do
send(self(), {:notification, notification})
end
end
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 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,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
notifiers: [Notifier],
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_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)
Ash.Changeset.force_change_attribute(changeset, :title, title <> "_stuff")
end,
only_when_valid?: true
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
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 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
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_change,
tenant: org.id,
return_records?: true,
authorize?: false,
sorted?: true
)
end
test "accepts arguments" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{records: [%{title: "title1"}, %{title: "title2"}]} =
Ash.bulk_create!(
[%{a_title: "title1"}, %{a_title: "title2"}],
Post,
:create_with_argument,
return_records?: true,
tenant: org.id,
sorted?: true,
authorize?: false
)
end
test "runs after batch hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{
records: [%{title: "before_title1_after"}, %{title: "before_title2_after"}]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_batch,
tenant: org.id,
return_records?: true,
sorted?: true,
authorize?: false
)
end
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} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: %{foo: :bar}}],
Post,
:create_with_change,
tenant: org.id,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "will return errors on request" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{
records: [%{title: "title1_stuff"}],
error_count: 1,
errors: [%Ash.Error.Invalid{}]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: %{foo: :bar}}],
Post,
:create_with_change,
tenant: org.id,
return_records?: true,
return_errors?: true,
sorted?: true,
authorize?: false
)
end
test "can upsert with list" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
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,
tenant: org.id,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: [:title2],
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
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,
tenant: org.id,
upsert_identity: :unique_title,
upsert_fields: {:replace, [:title2]},
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace_all" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
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,
tenant: org.id,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: :replace_all,
sorted?: true,
authorize?: false
)
end
test "can upsert with :replace_all_except" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
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,
tenant: org.id,
upsert?: true,
upsert_identity: :unique_title,
upsert_fields: {:replace_all_except, [:title, :title3]},
sorted?: true,
authorize?: false
)
end
test "runs before transaction hooks" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{
records: [
%{title: "before_transaction_title1"},
%{title: "before_transaction_title2"}
]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_before_transaction,
tenant: org.id,
return_records?: true,
sorted?: true,
authorize?: false
)
end
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"}]} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_action,
tenant: org.id,
return_records?: true,
sorted?: true,
authorize?: false
)
end
test "runs after transaction hooks on success" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{
records: [%{title: "title1_stuff"}, %{title: "title2_stuff"}]
} =
Ash.bulk_create!(
[%{title: "title1"}, %{title: "title2"}],
Post,
:create_with_after_transaction,
tenant: org.id,
return_records?: true,
return_errors?: true,
sorted?: true,
authorize?: false
)
end
test "runs after transaction hooks on failure" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{error_count: 2} =
Ash.bulk_create(
[%{title: 1}, %{title: 2}],
Post,
:create_with_after_transaction,
sorted?: true,
authorize?: false,
tenant: org.id
)
assert_receive {:error, _error}
assert_receive {:error, _error}
end
describe "authorization" 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"}]} =
Ash.bulk_create!(
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}],
Post,
:create_with_policy,
tenant: org.id,
authorize?: true,
return_errors?: true,
return_records?: true,
sorted?: true
)
end
test "field authorization is run" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
return_records?: true,
sorted?: true,
load: [:hidden_calc]
)
end
test "policy failure results in failures" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert %Ash.BulkResult{errors: [_, _]} =
Ash.bulk_create(
[
%{title: "title1", authorize?: false, org_id: org.id},
%{title: "title2", authorize?: false, org_id: org.id}
],
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
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [] =
[
%{title: "title1", authorize?: true, org_id: org.id},
%{title: "title2", authorize?: true, org_id: org.id}
]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
return_stream?: true
)
|> Enum.to_list()
end
test "batch size is honored while streaming" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [_] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
tenant: org.id,
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
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [{:notification, _}, {:notification, _}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
tenant: org.id,
authorize?: true,
return_stream?: true,
return_notifications?: true
)
|> Enum.to_list()
end
test "notifications are sent with notify?: true" do
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [{:ok, %{title: "title1"}}, {:ok, %{title: "title2"}}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
tenant: org.id,
notify?: true,
return_stream?: true,
return_records?: true
)
|> Enum.to_list()
|> Enum.sort_by(fn
{:ok, v} ->
v.title
_ ->
nil
end)
assert_received {:notification, %{data: %{title: "title1"}}}
assert_received {:notification, %{data: %{title: "title2"}}}
end
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"}}] =
[%{title: "title1", authorize?: true}, %{title: "title2", authorize?: true}]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
tenant: org.id,
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
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
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,
tenant: org.id,
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
org =
Org
|> Ash.Changeset.for_create(:create, %{})
|> Ash.create!()
assert [
{:error, %Ash.Error.Forbidden{}},
{:notification, _},
{:ok, %{title: "title1"}}
] =
[
%{title: "title1", authorize?: true},
%{title: "title2", authorize?: false}
]
|> Ash.bulk_create!(
Post,
:create_with_policy,
authorize?: true,
tenant: org.id,
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
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