2019-12-16 13:20:44 +13:00
|
|
|
defmodule Ash.Test.Filter.FilterTest do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2019-12-16 13:20:44 +13:00
|
|
|
use ExUnit.Case, async: true
|
|
|
|
|
2020-06-19 14:59:30 +12:00
|
|
|
alias Ash.Filter
|
|
|
|
|
2019-12-20 18:30:27 +13:00
|
|
|
defmodule Profile do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2020-06-14 18:39:11 +12:00
|
|
|
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private?(true)
|
|
|
|
end
|
2019-12-20 18:30:27 +13:00
|
|
|
|
|
|
|
actions do
|
|
|
|
read :default
|
|
|
|
create :default
|
|
|
|
update :default
|
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
2020-06-01 17:14:23 +12:00
|
|
|
attribute :id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0
|
2019-12-20 18:30:27 +13:00
|
|
|
attribute :bio, :string
|
|
|
|
end
|
|
|
|
|
|
|
|
relationships do
|
2019-12-24 17:22:31 +13:00
|
|
|
belongs_to :user, Ash.Test.Filter.FilterTest.User
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-16 13:20:44 +13:00
|
|
|
defmodule User do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2020-06-14 18:39:11 +12:00
|
|
|
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private?(true)
|
|
|
|
end
|
2019-12-16 13:20:44 +13:00
|
|
|
|
|
|
|
actions do
|
|
|
|
read :default
|
|
|
|
create :default
|
2019-12-20 18:30:27 +13:00
|
|
|
update :default
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
2020-06-01 17:14:23 +12:00
|
|
|
attribute :id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0
|
2019-12-16 13:20:44 +13:00
|
|
|
attribute :name, :string
|
|
|
|
attribute :allow_second_author, :boolean
|
|
|
|
end
|
|
|
|
|
|
|
|
relationships do
|
2020-06-22 16:34:44 +12:00
|
|
|
has_many :posts, Ash.Test.Filter.FilterTest.Post, destination_field: :author1_id
|
2019-12-20 18:30:27 +13:00
|
|
|
|
2020-06-22 16:34:44 +12:00
|
|
|
has_many :second_posts, Ash.Test.Filter.FilterTest.Post, destination_field: :author1_id
|
2019-12-24 17:22:31 +13:00
|
|
|
|
2020-06-05 15:34:44 +12:00
|
|
|
has_one :profile, Profile, destination_field: :user_id
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defmodule PostLink do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2020-06-14 18:39:11 +12:00
|
|
|
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private?(true)
|
|
|
|
end
|
2019-12-20 18:30:27 +13:00
|
|
|
|
|
|
|
actions do
|
|
|
|
read :default
|
|
|
|
|
|
|
|
create :default
|
|
|
|
update :default
|
|
|
|
end
|
|
|
|
|
|
|
|
relationships do
|
|
|
|
belongs_to :source_post, Ash.Test.Filter.FilterTest.Post, primary_key?: true
|
|
|
|
belongs_to :destination_post, Ash.Test.Filter.FilterTest.Post, primary_key?: true
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defmodule Post do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2020-06-14 18:39:11 +12:00
|
|
|
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private?(true)
|
|
|
|
end
|
2019-12-16 13:20:44 +13:00
|
|
|
|
|
|
|
actions do
|
|
|
|
read :default
|
|
|
|
|
|
|
|
create :default
|
2019-12-20 18:30:27 +13:00
|
|
|
|
|
|
|
update :default
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
2020-06-01 17:14:23 +12:00
|
|
|
attribute :id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0
|
2019-12-16 13:20:44 +13:00
|
|
|
attribute :title, :string
|
|
|
|
attribute :contents, :string
|
2019-12-20 18:30:27 +13:00
|
|
|
attribute :points, :integer
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
relationships do
|
|
|
|
belongs_to :author1, User,
|
|
|
|
destination_field: :id,
|
|
|
|
source_field: :author1_id
|
|
|
|
|
|
|
|
belongs_to :author2, User,
|
|
|
|
destination_field: :id,
|
|
|
|
source_field: :author2_id
|
2019-12-20 18:30:27 +13:00
|
|
|
|
|
|
|
many_to_many :related_posts, __MODULE__,
|
|
|
|
through: PostLink,
|
|
|
|
source_field_on_join_table: :source_post_id,
|
2020-06-22 16:34:44 +12:00
|
|
|
destination_field_on_join_table: :destination_post_id
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defmodule Api do
|
2020-06-02 17:47:25 +12:00
|
|
|
@moduledoc false
|
2019-12-16 13:20:44 +13:00
|
|
|
use Ash.Api
|
|
|
|
|
2020-06-14 18:39:11 +12:00
|
|
|
resources do
|
|
|
|
resource(Post)
|
|
|
|
resource(User)
|
|
|
|
resource(Profile)
|
|
|
|
resource(PostLink)
|
|
|
|
end
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
|
|
|
|
2019-12-20 18:30:27 +13:00
|
|
|
describe "simple attribute filters" do
|
|
|
|
setup do
|
|
|
|
post1 = Api.create!(Post, attributes: %{title: "title1", contents: "contents1", points: 1})
|
|
|
|
post2 = Api.create!(Post, attributes: %{title: "title2", contents: "contents2", points: 2})
|
|
|
|
|
|
|
|
%{post1: post1, post2: post2}
|
|
|
|
end
|
|
|
|
|
|
|
|
test "single filter field", %{post1: post1} do
|
2020-05-10 14:23:23 +12:00
|
|
|
assert [^post1] =
|
2020-05-14 03:54:44 +12:00
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(title: post1.title)
|
|
|
|
|> Api.read!()
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "multiple filter field matches", %{post1: post1} do
|
2020-05-10 14:23:23 +12:00
|
|
|
assert [^post1] =
|
2020-05-14 03:54:44 +12:00
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(title: post1.title, contents: post1.contents)
|
|
|
|
|> Api.read!()
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "no field matches" do
|
2020-05-10 14:23:23 +12:00
|
|
|
assert [] =
|
2020-05-14 03:54:44 +12:00
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(title: "no match")
|
|
|
|
|> Api.read!()
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "no field matches single record, but each matches one record", %{
|
|
|
|
post1: post1,
|
|
|
|
post2: post2
|
|
|
|
} do
|
2020-05-10 14:23:23 +12:00
|
|
|
assert [] =
|
2020-05-14 03:54:44 +12:00
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(title: post1.title, contents: post2.contents)
|
|
|
|
|> Api.read!()
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
2020-06-29 15:36:38 +12:00
|
|
|
|
|
|
|
test "less than works", %{
|
|
|
|
post1: post1,
|
|
|
|
post2: post2
|
|
|
|
} do
|
|
|
|
assert [^post1] =
|
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(points: [lt: 2])
|
|
|
|
|> Api.read!()
|
|
|
|
|
|
|
|
assert [^post1, ^post2] =
|
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(points: [lt: 3])
|
|
|
|
|> Ash.Query.sort(points: :asc)
|
|
|
|
|> Api.read!()
|
|
|
|
end
|
|
|
|
|
|
|
|
test "greater than works", %{
|
|
|
|
post1: post1,
|
|
|
|
post2: post2
|
|
|
|
} do
|
|
|
|
assert [^post2] =
|
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(points: [gt: 1])
|
|
|
|
|> Api.read!()
|
|
|
|
|
|
|
|
assert [^post1, ^post2] =
|
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(points: [gt: 0])
|
|
|
|
|> Ash.Query.sort(points: :asc)
|
|
|
|
|> Api.read!()
|
|
|
|
end
|
2019-12-20 18:30:27 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "relationship filters" do
|
|
|
|
setup do
|
|
|
|
post1 = Api.create!(Post, attributes: %{title: "title1", contents: "contents1", points: 1})
|
|
|
|
post2 = Api.create!(Post, attributes: %{title: "title2", contents: "contents2", points: 2})
|
|
|
|
|
2019-12-22 21:17:29 +13:00
|
|
|
post3 =
|
|
|
|
Api.create!(Post,
|
|
|
|
attributes: %{title: "title3", contents: "contents3", points: 3},
|
|
|
|
relationships: %{related_posts: [post1, post2]}
|
|
|
|
)
|
2019-12-20 18:30:27 +13:00
|
|
|
|
2019-12-24 17:22:31 +13:00
|
|
|
post4 =
|
|
|
|
Api.create!(Post,
|
|
|
|
attributes: %{title: "title4", contents: "contents3", points: 4},
|
|
|
|
relationships: %{related_posts: [post3]}
|
|
|
|
)
|
|
|
|
|
2019-12-20 18:30:27 +13:00
|
|
|
profile1 = Api.create!(Profile, attributes: %{bio: "dope"})
|
|
|
|
|
|
|
|
user1 =
|
|
|
|
Api.create!(User,
|
|
|
|
attributes: %{name: "broseph"},
|
|
|
|
relationships: %{posts: [post1, post2], profile: profile1}
|
|
|
|
)
|
|
|
|
|
|
|
|
user2 = Api.create!(User, attributes: %{name: "broseph"}, relationships: %{posts: [post2]})
|
|
|
|
|
|
|
|
profile2 = Api.create!(Profile, attributes: %{bio: "dope2"}, relationships: %{user: user2})
|
|
|
|
|
2019-12-22 22:06:33 +13:00
|
|
|
%{
|
2019-12-24 17:22:31 +13:00
|
|
|
post1: Api.reload!(post1),
|
|
|
|
post2: Api.reload!(post2),
|
|
|
|
post3: Api.reload!(post3),
|
|
|
|
post4: Api.reload!(post4),
|
|
|
|
profile1: Api.reload!(profile1),
|
|
|
|
user1: Api.reload!(user1),
|
|
|
|
user2: Api.reload!(user2),
|
|
|
|
profile2: Api.reload!(profile2)
|
2019-12-22 22:06:33 +13:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2019-12-24 17:22:31 +13:00
|
|
|
test "filtering on a has_one relationship", %{profile2: profile2, user2: user2} do
|
2020-05-14 03:54:44 +12:00
|
|
|
assert [^user2] =
|
|
|
|
User
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(profile: profile2.id)
|
|
|
|
|> Api.read!()
|
2019-12-24 17:22:31 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "filtering on a belongs_to relationship", %{profile1: profile1, user1: user1} do
|
2020-05-14 03:54:44 +12:00
|
|
|
assert [^profile1] =
|
|
|
|
Profile
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(user: user1.id)
|
|
|
|
|> Api.read!()
|
2019-12-24 17:22:31 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "filtering on a has_many relationship", %{user2: user2, post2: post2} do
|
2020-05-14 03:54:44 +12:00
|
|
|
assert [^user2] =
|
|
|
|
User
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(posts: post2.id)
|
|
|
|
|> Api.read!()
|
2019-12-24 17:22:31 +13:00
|
|
|
end
|
|
|
|
|
|
|
|
test "filtering on a many_to_many relationship", %{post4: post4, post3: post3} do
|
2020-05-14 03:54:44 +12:00
|
|
|
assert [^post4] =
|
|
|
|
Post
|
|
|
|
|> Api.query()
|
|
|
|
|> Ash.Query.filter(related_posts: post3.id)
|
|
|
|
|> Api.read!()
|
2019-12-24 17:22:31 +13:00
|
|
|
end
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|
2020-06-19 14:59:30 +12:00
|
|
|
|
|
|
|
describe "filter subset logic" do
|
|
|
|
test "can detect a filter is a subset of itself" do
|
|
|
|
filter = Filter.parse!(Api, Post, %{points: 1})
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, filter)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can detect a filter is a subset of itself *and* something else" do
|
|
|
|
filter = Filter.parse!(Api, Post, points: 1)
|
|
|
|
|
|
|
|
candidate = Filter.add_to_filter!(filter, title: "Title")
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can detect a filter is not a subset of itself *or* something else" do
|
|
|
|
filter = Filter.parse!(Api, Post, points: 1)
|
|
|
|
|
|
|
|
candidate = Filter.add_to_filter!(filter, :or, title: "Title")
|
|
|
|
|
|
|
|
refute Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can detect a filter is a subset based on a simplification" do
|
|
|
|
filter = Filter.parse!(Api, Post, points: [in: [1, 2]])
|
|
|
|
|
|
|
|
candidate = Filter.parse!(Api, Post, points: 1)
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can detect a filter is not a subset based on a simplification" do
|
|
|
|
filter = Filter.parse!(Api, Post, points: [in: [1, 2]])
|
|
|
|
|
|
|
|
candidate = Filter.parse!(Api, Post, points: 3)
|
|
|
|
|
|
|
|
refute Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "can detect a more complicated scenario" do
|
|
|
|
filter = Filter.parse!(Api, Post, or: [points: [in: [1, 2, 3]], points: 4, points: 5])
|
|
|
|
|
|
|
|
candidate = Filter.parse!(Api, Post, or: [points: 1, points: 3, points: 5])
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "understands unrelated negations" do
|
|
|
|
filter = Filter.parse!(Api, Post, or: [points: [in: [1, 2, 3]], points: 4, points: 5])
|
|
|
|
|
|
|
|
candidate =
|
|
|
|
Filter.parse!(Api, Post, or: [points: 1, points: 3, points: 5], not: [points: 7])
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
2020-06-22 15:26:47 +12:00
|
|
|
|
|
|
|
test "understands relationship filter subsets" do
|
|
|
|
id1 = Ecto.UUID.generate()
|
|
|
|
id2 = Ecto.UUID.generate()
|
|
|
|
filter = Filter.parse!(Api, Post, author1: [id: [in: [id1, id2]]])
|
|
|
|
|
|
|
|
candidate = Filter.parse!(Api, Post, author1: id1)
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
|
|
|
|
|
|
|
test "understands relationship filter subsets when a value coincides with the join field" do
|
|
|
|
id1 = Ecto.UUID.generate()
|
|
|
|
id2 = Ecto.UUID.generate()
|
|
|
|
filter = Filter.parse!(Api, Post, author1: [id: [in: [id1, id2]]])
|
|
|
|
|
|
|
|
candidate = Filter.parse!(Api, Post, author1_id: id1)
|
|
|
|
|
|
|
|
assert Filter.strict_subset_of?(filter, candidate)
|
|
|
|
end
|
2020-06-19 14:59:30 +12:00
|
|
|
end
|
2019-12-16 13:20:44 +13:00
|
|
|
end
|