2020-10-12 16:55:47 +13:00
|
|
|
defmodule Ash.Actions.PaginationTest do
|
|
|
|
use ExUnit.Case, async: true
|
|
|
|
|
|
|
|
require Ash.Query
|
|
|
|
|
|
|
|
defmodule User do
|
|
|
|
@moduledoc false
|
|
|
|
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private?(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
actions do
|
|
|
|
read :offset do
|
|
|
|
pagination offset?: true, countable: true
|
|
|
|
end
|
|
|
|
|
|
|
|
read :optional_offset do
|
|
|
|
pagination offset?: true, countable: true, required?: false
|
|
|
|
end
|
|
|
|
|
|
|
|
read :offset_countable_by_default do
|
|
|
|
pagination offset?: true, countable: :by_default, required?: false
|
|
|
|
end
|
|
|
|
|
|
|
|
read :required_offset_with_default do
|
|
|
|
pagination offset?: true, countable: true, required?: false, default_limit: 25
|
|
|
|
end
|
|
|
|
|
|
|
|
read :keyset do
|
|
|
|
pagination keyset?: true, countable: true
|
|
|
|
end
|
|
|
|
|
|
|
|
read :optional_keyset do
|
|
|
|
pagination keyset?: true, countable: true, required?: false
|
|
|
|
end
|
|
|
|
|
|
|
|
read :keyset_countable_by_default do
|
|
|
|
pagination keyset?: true, countable: :by_default, required?: false
|
|
|
|
end
|
|
|
|
|
|
|
|
read :required_keyset_with_default do
|
|
|
|
pagination keyset?: true, countable: true, required?: false, default_limit: 25
|
|
|
|
end
|
|
|
|
|
|
|
|
read :both_required do
|
|
|
|
primary? true
|
|
|
|
pagination keyset?: true, offset?: true, countable: true
|
|
|
|
end
|
|
|
|
|
|
|
|
read :both_optional do
|
|
|
|
pagination keyset?: true, offset?: true, countable: true, default_limit: 25
|
|
|
|
end
|
|
|
|
|
2021-03-08 18:59:32 +13:00
|
|
|
create :create
|
|
|
|
update :update
|
2020-10-12 16:55:47 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
2021-01-13 09:40:55 +13:00
|
|
|
uuid_primary_key :id
|
2020-10-12 16:55:47 +13:00
|
|
|
attribute :name, :string
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-10-07 19:41:02 +13:00
|
|
|
defmodule Registry do
|
|
|
|
@moduledoc false
|
|
|
|
use Ash.Registry
|
|
|
|
|
|
|
|
entries do
|
|
|
|
entry(User)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-12 16:55:47 +13:00
|
|
|
defmodule Api do
|
|
|
|
use Ash.Api
|
|
|
|
|
|
|
|
resources do
|
2021-10-07 19:41:02 +13:00
|
|
|
registry Registry
|
2020-10-12 16:55:47 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination is required by default" do
|
2021-01-22 09:21:58 +13:00
|
|
|
assert_raise Ash.Error.Invalid, ~r/Pagination is required/, fn ->
|
2020-10-12 16:55:47 +13:00
|
|
|
Api.read!(User, page: false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "a default limit allows not specifying page parameters" do
|
2021-01-22 09:21:58 +13:00
|
|
|
assert_raise Ash.Error.Invalid, ~r/Limit is required/, fn ->
|
2020-10-21 06:11:21 +13:00
|
|
|
Api.read!(User, page: [offset: 1])
|
2020-10-12 16:55:47 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
Api.read!(User, action: :required_offset_with_default)
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "offset pagination" do
|
|
|
|
setup do
|
|
|
|
for i <- 0..9 do
|
|
|
|
Api.create!(Ash.Changeset.new(User, %{name: "#{i}"}))
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can be limited" do
|
|
|
|
assert Enum.count(Api.read!(User, action: :optional_offset, page: false)) == 10
|
|
|
|
assert Enum.count(Api.read!(User, action: :optional_offset, page: [limit: 5]).results) == 5
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can be offset" do
|
|
|
|
assert Enum.count(Api.read!(User, action: :optional_offset, page: false)) == 10
|
2020-10-21 06:11:21 +13:00
|
|
|
|
|
|
|
assert Enum.count(
|
|
|
|
Api.read!(User, action: :optional_offset, page: [offset: 5, limit: 5]).results
|
|
|
|
) == 5
|
2020-10-12 16:55:47 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "can include a full count" do
|
|
|
|
assert Api.read!(User, action: :optional_offset, page: [limit: 1, count: true]).count == 10
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can default to including a count" do
|
|
|
|
assert Api.read!(User, action: :offset_countable_by_default, page: [limit: 1]).count == 10
|
|
|
|
end
|
|
|
|
|
|
|
|
test "count is not included by default otherwise" do
|
|
|
|
assert is_nil(Api.read!(User, action: :optional_offset, page: [limit: 1]).count)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "`count: false` prevents the count from occurring even if it is on `by_default`" do
|
|
|
|
assert is_nil(
|
|
|
|
Api.read!(User,
|
|
|
|
action: :offset_countable_by_default,
|
|
|
|
page: [limit: 1, count: false]
|
|
|
|
).count
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a sort applied" do
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(:name)
|
2020-10-21 06:11:21 +13:00
|
|
|
|> Api.read!(page: [offset: 5, limit: 5])
|
2020-10-12 16:55:47 +13:00
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["5", "6", "7", "8", "9"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a reversed sort applied" do
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
2020-10-21 06:11:21 +13:00
|
|
|
|> Api.read!(page: [offset: 5, limit: 5])
|
2020-10-12 16:55:47 +13:00
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["4", "3", "2", "1", "0"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a filter" do
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
2020-10-21 06:11:21 +13:00
|
|
|
|> Api.read!(page: [offset: 1, limit: 5])
|
2020-10-12 16:55:47 +13:00
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["3", "2", "1", "0"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the next page can be fetched" do
|
|
|
|
assert %{results: [%{name: "3"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [offset: 1, limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "2"}]} = Api.page!(page, :next)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the previous page can be fetched" do
|
|
|
|
assert %{results: [%{name: "3"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [offset: 1, limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "4"}]} = Api.page!(page, :prev)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the first page can be fetched" do
|
|
|
|
assert %{results: [%{name: "2"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [offset: 2, limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "4"}]} = Api.page!(page, :first)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the last page can be fetched if the count was requested" do
|
|
|
|
assert %{results: [%{name: "3"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [offset: 1, limit: 1, count: true])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "0"}]} = Api.page!(page, :last)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "keyset pagination" do
|
|
|
|
setup do
|
|
|
|
for i <- 0..9 do
|
|
|
|
Api.create!(Ash.Changeset.new(User, %{name: "#{i}"}))
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can be limited" do
|
|
|
|
assert Enum.count(Api.read!(User, action: :optional_keyset, page: false)) == 10
|
|
|
|
assert Enum.count(Api.read!(User, action: :optional_keyset, page: [limit: 5]).results) == 5
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can include a full count" do
|
|
|
|
assert Api.read!(User, action: :optional_keyset, page: [limit: 1, count: true]).count == 10
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can default to including a count" do
|
|
|
|
assert Api.read!(User, action: :keyset_countable_by_default, page: [limit: 1]).count == 10
|
|
|
|
end
|
|
|
|
|
|
|
|
test "count is not included by default otherwise" do
|
|
|
|
assert is_nil(Api.read!(User, action: :optional_keyset, page: [limit: 1]).count)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "`count: false` prevents the count from occurring even if it is on `by_default`" do
|
|
|
|
assert is_nil(
|
|
|
|
Api.read!(User,
|
|
|
|
action: :keyset_countable_by_default,
|
|
|
|
page: [limit: 1, count: false]
|
|
|
|
).count
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can ask for records after a specific keyset" do
|
2020-10-28 18:14:17 +13:00
|
|
|
%{results: [%{id: id, __metadata__: %{keyset: keyset}}]} =
|
2020-10-12 16:55:47 +13:00
|
|
|
Api.read!(User, action: :keyset, page: [limit: 1])
|
|
|
|
|
|
|
|
%{results: [%{id: next_id}]} =
|
|
|
|
Api.read!(User, action: :keyset, page: [limit: 1, after: keyset])
|
|
|
|
|
|
|
|
refute id == next_id
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can ask for records before a specific keyset" do
|
2020-10-28 18:14:17 +13:00
|
|
|
%{results: [%{id: id, __metadata__: %{keyset: keyset}}]} =
|
2020-10-12 16:55:47 +13:00
|
|
|
Api.read!(User, action: :keyset, page: [limit: 1])
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
%{results: [%{id: next_id, __metadata__: %{keyset: keyset2}}]} =
|
2020-10-12 16:55:47 +13:00
|
|
|
Api.read!(User, action: :keyset, page: [limit: 1, after: keyset])
|
|
|
|
|
|
|
|
refute id == next_id
|
|
|
|
|
|
|
|
%{results: [%{id: before_id}]} =
|
|
|
|
Api.read!(User, action: :keyset, page: [limit: 1, before: keyset2])
|
|
|
|
|
|
|
|
assert id == before_id
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a sort applied" do
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.filter(name == "4")
|
|
|
|
|> Ash.Query.sort(:name)
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
2020-10-12 16:55:47 +13:00
|
|
|
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(:name)
|
|
|
|
|> Api.read!(page: [after: keyset, limit: 5])
|
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["5", "6", "7", "8", "9"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a reversed sort applied" do
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.filter(name == "5")
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
2020-10-12 16:55:47 +13:00
|
|
|
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Api.read!(page: [after: keyset, limit: 5])
|
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["4", "3", "2", "1", "0"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "pagination works with a filter" do
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.filter(name == "5")
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
2020-10-28 18:14:17 +13:00
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
2020-10-12 16:55:47 +13:00
|
|
|
|
|
|
|
names =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name != "4")
|
|
|
|
|> Api.read!(page: [after: keyset, limit: 5])
|
|
|
|
|> Map.get(:results)
|
|
|
|
|> Enum.map(& &1.name)
|
|
|
|
|
|
|
|
assert names == ["3", "2", "1", "0"]
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the next page can be fetched" do
|
|
|
|
assert %{results: [%{name: "4"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "3"}]} = Api.page!(page, :next)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the previous page can be fetched" do
|
|
|
|
assert %{results: [%{name: "4"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "3"}]} = page = Api.page!(page, :next)
|
|
|
|
assert %{results: [%{name: "4"}]} = Api.page!(page, :prev)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "the first page can be fetched" do
|
|
|
|
assert %{results: [%{name: "4"}]} =
|
|
|
|
page =
|
|
|
|
User
|
|
|
|
|> Ash.Query.sort(name: :desc)
|
|
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|
|
|> Api.read!(page: [limit: 1])
|
|
|
|
|
|
|
|
assert %{results: [%{name: "3"}]} = page = Api.page!(page, :next)
|
|
|
|
assert %{results: [%{name: "4"}]} = Api.page!(page, :first)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "when both are supported" do
|
|
|
|
setup do
|
|
|
|
for i <- 0..9 do
|
|
|
|
Api.create!(Ash.Changeset.new(User, %{name: "#{i}"}))
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it defaults to offset pagination" do
|
|
|
|
assert %Ash.Page.Offset{} = Api.read!(User, action: :both_optional, page: [limit: 10])
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it adds a keyset to the records, even though it returns an offset page" do
|
|
|
|
for result <- Api.read!(User, action: :both_optional, page: [limit: 10]).results do
|
2020-10-28 18:14:17 +13:00
|
|
|
refute is_nil(result.__metadata__.keyset)
|
2020-10-12 16:55:47 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|