mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
5967ed3a48
* improvement!: use `%Ash.NotSelected{}` for unselected values * improvement!: default `require_atomic?` to `true` * improvement!: raise errors on unknown generic action arguments * improvement!: default bulk strategy to `:atomic` * improvement!: warnings on `require_atomic?` `true` actions improvement!: revise `Ash.NotSelected` to `Ash.NotLoaded` improvement!: errors on unknown action inputs across the board * doc: clarify wording in notifiers.md closes #889 * improvement!: default `api.authorization.authorize` to `:by_default` * improvement!: require the api when constructing changesets this commit also fixes some work from prior commits around the default value for the `authorize` option * improvement!: code_interface.define_for -> code_interface.api `code_interface.define_for` is now `code_interface.api`. Additionally, it is set automatically if the `api` option is specified on `use Ash.Resource`. * improvement!: remove registries * improvement!: pubsub notifier default to `previous_values?: false` improvement!: requires_original_data? callback defaults to false * improvement!: rename Ash.Calculation -> Ash.Resource.Calculation improvement!: improve `Ash.Query.Calculation.new` signature improvement!: anonymous function calculations now take lists and return lists improvement!: make callback contexts into structs improvement!: pass context to builtin lifecycle hook changes improvement!: calculation arguments are now in the `arguments` key of the context * chore: fix build * improvement!: remove `aggregates` and `calculations` from `Filter.parse` and `Filter.parse_input` * improvement: update spark to 2.0 * improvement!: make picosat_elixir optional with `simple_sat` * improvement!: rename api to domain * docs: add more info to upgrading guide * docs: tweak docs formatting * improvement!: remove `Ash.Changeset.new!` * docs: update docs for `Ash.Changeset.new/1` * improvement!: deprecate `private?: false` in favor of `public?: true` * doc: add upgrade guide for private -> public * improvement: update reactor to 3.0 * improvement!: default `default_accept` is now `[]` * improvement!: `Ash.CiString.new/1` returns `nil` on `nil` input * improvement!(Ash.Reactor): Improve integration with Ash 3.0 changes. * improvement!: clean up and reorganize `Ash` functions this is in preparation of deprecating the functions that are defined on the api improvement!: remove context-based functionality * chore: update docs references from `Ash.Domain` to `Ash` * chore: fix bad merge * chore: fix context access in atomic changes * improvement!: Deprecate calling functions on (domain) api in favor of `Ash` * improvement!: add `attribute_public?` and update `attribute_writable?` behavior * improvement!: update atomic behaviors, default to invalid * chore: update downcase docs * improvement!: changeset.filters -> changeset.filter * improvement!: remove deprecated functions * improvement!: remove and simplify `Ash.Filter.TemplateHelpers` * improvement: import Ash.Expr in modules where it is used improvement: require Ash.QUery in modules where it makes sense * fix!: keyword lists are no longer special cased in ash expressions * improvement: add structs for more context implementations * chore: small tweaks, finish `:all` -> `:*` conversion * chore: update DSL docs for multitenancy.global? * improvement: ensure selects are applied on destroys chore: remove TODOs * chore: some docs changes * improvement!: introduce strict mode to calculations * chore: update tests * improvement: support custom expressions * docs: document custom expressions * chore: fix and test custom expressions and function fragments docs: update relevant docs w/ the changes * improvement!: reverse order of before action & before transaction hooks * improvement!: default read actions are now paginatable * improvement!: require explicit accept lists in default actions * chore: update docs * improvement!: remove Ash.Flow and Ash.Engine * chore: unlock unused deps * chore: don't use unused variable * chore: include ash flow change in upgrade guide * improvement!: standardize various exception keys and names * improvement!: use `Splode` for errors * improvement: update upgrade guide to include Splode * feat: code interface on the domain * improvement: only require primary key if resource has actions or fields improvement: only build schema if resource has actions or fields improvement: verify primary key in its own verifier * improvement: add `resource/1` builtin check * improvement!: move simple_notifiers to an option instead of a DSL builder improvement!: update spark for better autocomplete, configure autocomplete for key functions docs: replace `an domain` with `a domain` * improvement: better code interface documentation * fix: set tenant on query so that root calles to Api.aggreagte work as expected (#929) * chore: fixes from previous improvements * chore: update splode * chore: update splode * improvement!: swap position of sort order and arguments in calculation sorting * improvement!: add `include_nil?` aggregate option, and default it to `false` * improvement: support notifiers within actions * improvement: support specifying multiple filters * improvement: add `sortable?` flags to all fields improvement: support multiple filters on relationships * improvement: support sensitive? on calculations and arguments * improvement: validate resources in inputs to code interface * chore: don't require explicit accept lists when using `default_accept :*` * chore: update spark * chore: update public attribute handling per 3.0 * improvement: update reactor and tests * chore: better error message * chore: fix rebase issue * chore: handle merge issues improvement: don't require domain on relationships if destination has domain * improvement!: errors on unknown inputs for calculations * improvement: always choose to cast atomic * improvement: support casting some embeds atomically * improvement: various 3.0 updates, documented in upgrade.md * chore: Add failing tests for loads with with explicit domains. (#948) Co-authored-by: James Harton <james@harton.nz> * improvement: ensure non-static dynamic domains works * improvement: add Ash.ToTenant protocol * chore: add docs for no ToTenant option * fix: properly construct new query in `build/3` * chore: update simple_sat dependency * chore: don't reselect when missing primary keys * chore: remove IO.inspect * chore: update spark * chore: update spark * improvement: use `Keyword.put_new` in `Ash.Context.to_opts` (#953) * improvement: support bulk and atomic operations in code interfaces --------- Co-authored-by: James Harton <james@harton.nz> Co-authored-by: WIGGLES <55168935+WIGGLES-dev@users.noreply.github.com> Co-authored-by: Dmitry Maganov <vonagam@gmail.com>
921 lines
27 KiB
Elixir
921 lines
27 KiB
Elixir
defmodule Ash.Actions.PaginationTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
require Ash.Query
|
|
|
|
alias Ash.Test.Domain, as: Domain
|
|
|
|
defmodule Post do
|
|
@moduledoc false
|
|
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets
|
|
|
|
ets do
|
|
private?(true)
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
|
|
attribute :user_id, :uuid do
|
|
public?(true)
|
|
end
|
|
|
|
attribute :body, :string do
|
|
public?(true)
|
|
end
|
|
end
|
|
|
|
actions do
|
|
default_accept :*
|
|
defaults [:destroy, create: :*, update: :*]
|
|
|
|
read :read do
|
|
primary? true
|
|
pagination offset?: true, required?: true, default_limit: 25
|
|
end
|
|
end
|
|
|
|
relationships do
|
|
belongs_to :user, Ash.Actions.PaginationTest.User, define_attribute?: false, public?: true
|
|
end
|
|
end
|
|
|
|
defmodule User do
|
|
@moduledoc false
|
|
use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets
|
|
|
|
ets do
|
|
private?(true)
|
|
end
|
|
|
|
actions do
|
|
default_accept :*
|
|
|
|
read :offset do
|
|
pagination offset?: true, countable: true, required?: 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 :keyset_before_action do
|
|
prepare(before_action(&Ash.Query.filter(&1, name in ["0", "1", "2"])))
|
|
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
|
|
|
|
defaults create: :*, update: :*
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
|
|
attribute :name, :string do
|
|
public?(true)
|
|
end
|
|
|
|
attribute :subname, :string do
|
|
public?(true)
|
|
end
|
|
end
|
|
|
|
aggregates do
|
|
count :count_of_posts, :posts do
|
|
public? true
|
|
end
|
|
end
|
|
|
|
calculations do
|
|
calculate :name_with_arg, :string, expr(name) do
|
|
public?(true)
|
|
|
|
argument :does_nothing, :boolean do
|
|
allow_nil? false
|
|
end
|
|
end
|
|
end
|
|
|
|
relationships do
|
|
has_many :posts, Post do
|
|
public?(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
test "pagination is required by default" do
|
|
assert_raise Ash.Error.Invalid, ~r/Pagination is required/, fn ->
|
|
Ash.read!(User, page: false)
|
|
end
|
|
end
|
|
|
|
test "a default limit allows not specifying page parameters" do
|
|
assert_raise Ash.Error.Invalid, ~r/Limit is required/, fn ->
|
|
Ash.read!(User, page: [offset: 1])
|
|
end
|
|
|
|
Ash.read!(User, action: :required_offset_with_default)
|
|
end
|
|
|
|
describe "offset pagination" do
|
|
setup do
|
|
for i <- 0..9 do
|
|
user = Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "#{i}"}))
|
|
|
|
if i != 0 do
|
|
for x <- 1..i do
|
|
Ash.create!(
|
|
Ash.Changeset.for_create(Post, :create, %{body: "#{i}-#{x}", user_id: user.id})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
test "can be limited" do
|
|
assert Enum.count(Ash.read!(User, action: :optional_offset, page: false)) == 10
|
|
|
|
assert Enum.count(Ash.read!(User, action: :optional_offset, page: [limit: 5]).results) ==
|
|
5
|
|
end
|
|
|
|
test "can be offset" do
|
|
assert Enum.count(Ash.read!(User, action: :optional_offset, page: false)) == 10
|
|
|
|
assert Enum.count(
|
|
Ash.read!(User, action: :optional_offset, page: [offset: 5, limit: 5]).results
|
|
) == 5
|
|
end
|
|
|
|
test "can include a full count" do
|
|
assert Ash.read!(User, action: :optional_offset, page: [limit: 1, count: true]).count ==
|
|
10
|
|
end
|
|
|
|
test "can include a full count with an offset" do
|
|
assert Ash.read!(User,
|
|
action: :optional_offset,
|
|
page: [offset: 5, limit: 1, count: true]
|
|
).count ==
|
|
10
|
|
end
|
|
|
|
test "can default to including a count" do
|
|
assert Ash.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(Ash.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(
|
|
Ash.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)
|
|
|> Ash.read!(page: [offset: 5, 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
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.read!(page: [offset: 5, limit: 5])
|
|
|> 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"])
|
|
|> Ash.read!(page: [offset: 1, 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: "3"}]} =
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|> Ash.read!(page: [offset: 1, limit: 1])
|
|
|
|
assert %{results: [%{name: "2"}]} = Ash.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"])
|
|
|> Ash.read!(page: [offset: 1, limit: 1])
|
|
|
|
assert %{results: [%{name: "4"}]} = Ash.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"])
|
|
|> Ash.read!(page: [offset: 2, limit: 1])
|
|
|
|
assert %{results: [%{name: "4"}]} = Ash.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"])
|
|
|> Ash.read!(page: [offset: 1, limit: 1, count: true])
|
|
|
|
assert %{results: [%{name: "0"}]} = Ash.page!(page, :last)
|
|
end
|
|
|
|
test "the same page can be re-fetched" do
|
|
assert %{results: [%{name: "3"}]} =
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|> Ash.read!(page: [offset: 1, limit: 1, count: true])
|
|
|
|
assert %{results: [%{name: "3"}]} = Ash.page!(page, :self)
|
|
end
|
|
end
|
|
|
|
describe "keyset pagination with nil fields" do
|
|
setup do
|
|
users =
|
|
for i <- 0..9 do
|
|
if rem(i, 2) == 0 do
|
|
Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "#{i}", subname: "#{i}"}))
|
|
else
|
|
Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "#{i}"}))
|
|
end
|
|
end
|
|
|
|
[users: users]
|
|
end
|
|
|
|
test "can be paged through when a non-nil value is the keyset" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort([:subname, :name])
|
|
|> Ash.Query.select([:name])
|
|
|> Ash.read!(action: :keyset, page: [limit: 5])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(0 2 4 6 8)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort([:subname, :name])
|
|
|> Ash.Query.select([:name])
|
|
|> Ash.read!(action: :keyset, page: [limit: 5, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(1 3 5 7 9)
|
|
end
|
|
|
|
test "can be paged through when a nil value is the keyset" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort([:subname, :name])
|
|
|> Ash.read!(action: :keyset, page: [limit: 6])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(0 2 4 6 8 1)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort([:subname, :name])
|
|
|> Ash.read!(action: :keyset, page: [limit: 6, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(3 5 7 9)
|
|
end
|
|
|
|
test "can be paged through when a non-nil value is the keyset using asc_nils_first" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :asc_nils_first, name: :asc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(1 3 5 7 9)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :asc_nils_first, name: :asc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(0 2 4 6 8)
|
|
end
|
|
|
|
test "can be paged through when a non-nil value is the keyset using desc_nils_first" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :desc_nils_first, name: :desc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(9 7 5 3 1)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :desc_nils_first, name: :desc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(8 6 4 2 0)
|
|
end
|
|
|
|
test "can be paged through when a nil value is the keyset using asc_nils_first" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :asc_nils_first, name: :asc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 6])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(1 3 5 7 9 0)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :asc_nils_first, name: :asc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(2 4 6 8)
|
|
end
|
|
|
|
test "can be paged through when a nil value is the keyset using desc_nils_first" do
|
|
%{results: first_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :desc_nils_first, name: :desc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 6])
|
|
|
|
assert Enum.map(first_results, & &1.name) == ~w(9 7 5 3 1 8)
|
|
|
|
keyset =
|
|
first_results |> List.last(first_results) |> Map.get(:__metadata__) |> Map.get(:keyset)
|
|
|
|
%{results: second_results} =
|
|
User
|
|
|> Ash.Query.sort(subname: :desc_nils_first, name: :desc_nils_first)
|
|
|> Ash.read!(action: :keyset, page: [limit: 5, after: keyset])
|
|
|
|
assert Enum.map(second_results, & &1.name) == ~w(6 4 2 0)
|
|
end
|
|
end
|
|
|
|
describe "keyset pagination" do
|
|
setup do
|
|
users =
|
|
for i <- 0..9 do
|
|
user = Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "#{i}"}))
|
|
|
|
if i != 0 do
|
|
for x <- 1..i do
|
|
Ash.create!(
|
|
Ash.Changeset.for_create(Post, :create, %{body: "#{i}-#{x}", user_id: user.id})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
[users: users]
|
|
end
|
|
|
|
test "can be limited" do
|
|
assert Enum.count(Ash.read!(User, action: :optional_keyset, page: false)) == 10
|
|
|
|
assert Enum.count(Ash.read!(User, action: :optional_keyset, page: [limit: 5]).results) ==
|
|
5
|
|
end
|
|
|
|
test "can include a full count" do
|
|
assert Ash.read!(User, action: :optional_keyset, page: [limit: 1, count: true]).count ==
|
|
10
|
|
end
|
|
|
|
test "can include a full count with a sort and limit" do
|
|
assert 10 =
|
|
User
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(action: :optional_keyset, page: [limit: 1, count: true])
|
|
|> Map.get(:count)
|
|
end
|
|
|
|
test "can default to including a count" do
|
|
assert Ash.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(Ash.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(
|
|
Ash.read!(User,
|
|
action: :keyset_countable_by_default,
|
|
page: [limit: 1, count: false]
|
|
).count
|
|
)
|
|
end
|
|
|
|
test "can ask for records after a specific keyset" do
|
|
%{results: [%{id: id, __metadata__: %{keyset: keyset}}]} =
|
|
Ash.read!(User, action: :keyset, page: [limit: 1])
|
|
|
|
%{results: [%{id: next_id}]} =
|
|
Ash.read!(User, action: :keyset, page: [limit: 1, after: keyset])
|
|
|
|
refute id == next_id
|
|
end
|
|
|
|
test "can get the full count when asking for records after a specific keyset" do
|
|
%{results: [%{__metadata__: %{keyset: keyset}}], count: 10} =
|
|
Ash.read!(User, action: :keyset, page: [count: true, limit: 1])
|
|
|
|
assert %{count: 10} =
|
|
Ash.read!(User, action: :keyset, page: [count: true, limit: 1, after: keyset])
|
|
end
|
|
|
|
test "can get the full count when asking for records after a specific keyset use the query after applying `before_action` hooks" do
|
|
%{results: [%{__metadata__: %{keyset: keyset}}], count: 3} =
|
|
Ash.read!(User, action: :keyset_before_action, page: [count: true, limit: 1])
|
|
|
|
assert %{count: 3} =
|
|
Ash.read!(User,
|
|
action: :keyset_before_action,
|
|
page: [count: true, limit: 1, after: keyset]
|
|
)
|
|
end
|
|
|
|
test "an invalid keyset returns an appropriate error" do
|
|
assert_raise(Ash.Error.Invalid, ~r/Invalid value provided as a keyset/, fn ->
|
|
Ash.read!(User, action: :keyset, page: [limit: 1, after: "~"])
|
|
end)
|
|
end
|
|
|
|
test "can ask for records before a specific keyset" do
|
|
%{results: [%{id: id, __metadata__: %{keyset: keyset}}]} =
|
|
Ash.read!(User, action: :keyset, page: [limit: 1])
|
|
|
|
%{results: [%{id: next_id, __metadata__: %{keyset: keyset2}}]} =
|
|
Ash.read!(User, action: :keyset, page: [limit: 1, after: keyset])
|
|
|
|
refute id == next_id
|
|
|
|
%{results: [%{id: before_id}]} =
|
|
Ash.read!(User, action: :keyset, page: [limit: 1, before: keyset2])
|
|
|
|
assert id == before_id
|
|
end
|
|
|
|
test "can ask for records before a specific keyset, with the sort order honored" do
|
|
%{results: users} =
|
|
User |> Ash.Query.sort(:name) |> Ash.read!(action: :keyset, page: [limit: 100])
|
|
|
|
users = Enum.sort_by(users, & &1.name)
|
|
last_user = List.last(users)
|
|
|
|
%{results: results} =
|
|
User
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(action: :keyset, page: [limit: 2, before: last_user.__metadata__.keyset])
|
|
|
|
assert Enum.map(results, & &1.name) == [
|
|
"7",
|
|
"8"
|
|
]
|
|
end
|
|
|
|
test "can ask for records before a specific keyset, with the full count shown" do
|
|
%{results: users} =
|
|
User |> Ash.Query.sort(:name) |> Ash.read!(action: :keyset, page: [limit: 100])
|
|
|
|
users = Enum.sort_by(users, & &1.name)
|
|
last_user = List.last(users)
|
|
assert Enum.count(users) == 10
|
|
|
|
assert %{count: 10} =
|
|
User
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(
|
|
action: :keyset,
|
|
page: [count: true, limit: 2, before: last_user.__metadata__.keyset]
|
|
)
|
|
end
|
|
|
|
test "pagination works with a sort applied" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(name == "4")
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.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 :desc sort applied" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(name == "4")
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.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 sort applied that uses an aggregate" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(count_of_posts == 4)
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(:count_of_posts)
|
|
|> Ash.read!(page: [after: keyset, limit: 4])
|
|
|
|
names =
|
|
page
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["5", "6", "7", "8"]
|
|
assert page.more?
|
|
end
|
|
|
|
test "pagination more? is false when there are no more records" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(count_of_posts == 5)
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(:count_of_posts)
|
|
|> Ash.read!(page: [after: keyset, limit: 4])
|
|
|
|
names =
|
|
page
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["6", "7", "8", "9"]
|
|
refute page.more?
|
|
end
|
|
|
|
test "pagination works with a sort applied that uses an aggregate using `before`" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(count_of_posts == 4)
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(:count_of_posts)
|
|
|> Ash.read!(page: [before: keyset, limit: 3])
|
|
|
|
names =
|
|
page
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["1", "2", "3"]
|
|
assert page.more?
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(:count_of_posts)
|
|
|> Ash.read!(page: [after: keyset, limit: 4])
|
|
|
|
names =
|
|
page
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["5", "6", "7", "8"]
|
|
assert page.more?
|
|
end
|
|
|
|
test "pagination more? is false when there are no more records using `before`" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(count_of_posts == 4)
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(:count_of_posts)
|
|
|> Ash.read!(page: [before: keyset, limit: 4])
|
|
|
|
names =
|
|
page
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["0", "1", "2", "3"]
|
|
refute page.more?
|
|
end
|
|
|
|
test "pagination works with a sort applied that uses an aggregate desc" do
|
|
User
|
|
|> Ash.Query.load(:count_of_posts)
|
|
|> Ash.read!(page: [limit: 10])
|
|
|> Map.get(:results)
|
|
|> Enum.map(&{&1.name, &1.count_of_posts})
|
|
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(count_of_posts == 4)
|
|
|> Ash.Query.sort(:name)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(count_of_posts: :desc)
|
|
|> Ash.read!(page: [after: keyset, limit: 5])
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["3", "2", "1", "0"]
|
|
end
|
|
|
|
test "pagination works with a sort applied that uses a calculation with arguments" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(name_with_arg(does_nothing: true) == "4")
|
|
|> Ash.Query.sort(name_with_arg: %{does_nothing: true})
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(name_with_arg: %{does_nothing: true})
|
|
|> Ash.read!(page: [after: keyset, limit: 5])
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["5", "6", "7", "8", "9"]
|
|
end
|
|
|
|
test "pagination utilities work with a sort applied that uses a calculation with arguments" do
|
|
assert %{results: [%{name_with_arg: "5"}]} =
|
|
User
|
|
|> Ash.Query.sort(name_with_arg: %{does_nothing: true})
|
|
|> Ash.Query.load(name_with_arg: %{does_nothing: true})
|
|
|> Ash.read!(page: [limit: 1, offset: 4])
|
|
|> Ash.page!(:next)
|
|
end
|
|
|
|
test "pagination works with a sort applied that uses a calculation desc" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(name_with_arg(does_nothing: true) == "4")
|
|
|> Ash.Query.sort(name_with_arg: {%{does_nothing: true}, :desc})
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(name_with_arg: {%{does_nothing: true}, :desc})
|
|
|> Ash.read!(page: [after: keyset, limit: 5])
|
|
|> Map.get(:results)
|
|
|> Enum.map(& &1.name)
|
|
|
|
assert names == ["3", "2", "1", "0"]
|
|
end
|
|
|
|
test "pagination works with a reversed sort applied" do
|
|
page =
|
|
User
|
|
|> Ash.Query.filter(name == "5")
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.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)
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
keyset = Enum.at(page.results, 0).__metadata__.keyset
|
|
|
|
names =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.Query.filter(name != "4")
|
|
|> Ash.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"])
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
assert %{results: [%{name: "3"}]} = Ash.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"])
|
|
|> Ash.read!(page: [limit: 1], action: :optional_keyset)
|
|
|
|
assert %{results: [%{name: "3"}]} = page = Ash.page!(page, :next)
|
|
assert %{results: [%{name: "4"}]} = Ash.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"])
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
assert %{results: [%{name: "3"}]} = page = Ash.page!(page, :next)
|
|
assert %{results: [%{name: "4"}]} = Ash.page!(page, :first)
|
|
end
|
|
|
|
test "the same page can be re-fetched" do
|
|
assert %{results: [%{name: "4"}]} =
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|> Ash.read!(page: [limit: 1])
|
|
|
|
assert %{results: [%{name: "3"}]} = page = Ash.page!(page, :next)
|
|
assert %{results: [%{name: "3"}]} = Ash.page!(page, :self)
|
|
end
|
|
|
|
test "the prev request right after the initial query remains the same as the initial result (like offset pagination)" do
|
|
assert %{results: [%{name: "4"}]} =
|
|
page =
|
|
User
|
|
|> Ash.Query.sort(name: :desc)
|
|
|> Ash.Query.filter(name in ["4", "3", "2", "1", "0"])
|
|
|> Ash.read!(action: :keyset, page: [limit: 1])
|
|
|
|
assert %{results: [%{name: "4"}]} = Ash.page!(page, :prev)
|
|
end
|
|
end
|
|
|
|
describe "when both are supported" do
|
|
setup do
|
|
for i <- 0..9 do
|
|
Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "#{i}"}))
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
test "it defaults to offset pagination" do
|
|
assert %Ash.Page.Offset{} = Ash.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 <- Ash.read!(User, action: :both_optional, page: [limit: 10]).results do
|
|
refute is_nil(result.__metadata__.keyset)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "loading with pagination" do
|
|
test "it does not paginate loads" do
|
|
user = Ash.create!(Ash.Changeset.for_create(User, :create, %{name: "user"}))
|
|
Ash.create!(Ash.Changeset.for_create(Post, :create, %{user_id: user.id}))
|
|
|
|
assert [_ | _] =
|
|
user
|
|
|> Ash.load!([posts: :user], tenant: nil, actor: nil)
|
|
|> Map.get(:posts)
|
|
end
|
|
end
|
|
end
|