Skip to content

Commit aff1cae

Browse files
rapidfsubjechol
andauthored
Add has_many_sort option (#1)
Co-authored-by: Jechol Lee <jechol@devall.org>
1 parent 404d8d6 commit aff1cae

File tree

10 files changed

+175
-7
lines changed

10 files changed

+175
-7
lines changed

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
spark_locals_without_parens = [except: 1, include_primary_read?: 1, sort: 1]
1+
spark_locals_without_parens = [except: 1, has_many_sort: 1, include_primary_read?: 1, sort: 1]
22

33
[
44
import_deps: [:spark, :reactor, :ash],

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Add `ash_default_sort` to your list of dependencies in `mix.exs`:
99
```elixir
1010
def deps do
1111
[
12-
{:ash_default_sort, "~> 0.1.0"}
12+
{:ash_default_sort, "~> 0.2.0"}
1313
]
1414
end
1515
```

lib/ash_default_sort.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,21 @@ defmodule AshDefaultSort do
3131
required: false,
3232
default: [],
3333
doc: "List of read actions to exclude"
34+
],
35+
has_many_sort: [
36+
type: :keyword_list,
37+
required: false,
38+
default: [],
39+
doc: "The sort to apply when a has_many relationship has no sort"
3440
]
3541
],
3642
entities: []
3743
}
3844

3945
use Spark.Dsl.Extension,
4046
sections: [@default_sort],
41-
transformers: [AshDefaultSort.Transformer]
47+
transformers: [
48+
AshDefaultSort.Sort.Transformer,
49+
AshDefaultSort.HasManySort.Transformer
50+
]
4251
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule AshDefaultSort.HasManySort.Transformer do
2+
use Spark.Dsl.Transformer
3+
alias Spark.Dsl.Transformer
4+
5+
@impl Spark.Dsl.Transformer
6+
def transform(dsl_state) do
7+
except = Transformer.get_option(dsl_state, [:default_sort], :except)
8+
9+
case Transformer.get_option(dsl_state, [:default_sort], :has_many_sort) do
10+
[] ->
11+
dsl_state
12+
13+
has_many_sort ->
14+
dsl_state
15+
|> Transformer.get_entities([:relationships])
16+
|> Enum.filter(&(&1.type == :has_many))
17+
|> Enum.reject(&(&1.name in except))
18+
|> Enum.reject(& &1.default_sort)
19+
|> Enum.map(&%{&1 | default_sort: has_many_sort})
20+
|> Enum.reduce(dsl_state, fn has_many, dsl_state ->
21+
Transformer.replace_entity(
22+
dsl_state,
23+
[:relationships],
24+
has_many,
25+
&(&1.name == has_many.name)
26+
)
27+
end)
28+
end
29+
|> then(fn dsl_state -> {:ok, dsl_state} end)
30+
end
31+
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule AshDefaultSort.Preparation do
1+
defmodule AshDefaultSort.Sort.Preparation do
22
use Ash.Resource.Preparation
33

44
def prepare(query, [sort: sort], _context) do
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule AshDefaultSort.Transformer do
1+
defmodule AshDefaultSort.Sort.Transformer do
22
use Spark.Dsl.Transformer
33

44
alias Spark.Dsl.Transformer
@@ -20,7 +20,9 @@ defmodule AshDefaultSort.Transformer do
2020
|> Enum.reject(&(&1.name in except))
2121
|> Enum.reject(&(&1.primary? && !include_primary_read?))
2222
|> Enum.map(fn %Ash.Resource.Actions.Read{preparations: preparations} = read ->
23-
{:ok, preparation} = Builder.build_preparation({AshDefaultSort.Preparation, sort: sort})
23+
{:ok, preparation} =
24+
Builder.build_preparation({AshDefaultSort.Sort.Preparation, sort: sort})
25+
2426
%Ash.Resource.Actions.Read{read | preparations: preparations ++ [preparation]}
2527
end)
2628
|> Enum.reduce(dsl_state, fn action, dsl_state ->

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule AshDefaultSort.MixProject do
44
def project do
55
[
66
app: :ash_default_sort,
7-
version: "0.1.0",
7+
version: "0.2.0",
88
elixir: "~> 1.17",
99
consolidate_protocols: Mix.env() not in [:dev, :test],
1010
start_permanent: Mix.env() == :prod,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
defmodule AshDefaultSort.HasManySortTest do
2+
use ExUnit.Case, async: true
3+
4+
alias __MODULE__.{TodoList, Task, Domain}
5+
6+
defmodule TodoList do
7+
use Ash.Resource,
8+
data_layer: Ash.DataLayer.Ets,
9+
domain: Domain,
10+
extensions: [AshDefaultSort]
11+
12+
attributes do
13+
uuid_v7_primary_key :id
14+
attribute :title, :string, allow_nil?: false, public?: true
15+
end
16+
17+
relationships do
18+
has_many :tasks, Task
19+
end
20+
21+
actions do
22+
defaults [:read, create: :*]
23+
24+
update :add_task do
25+
require_atomic? false
26+
argument :task, :map, allow_nil?: false
27+
change manage_relationship(:task, :tasks, type: :create)
28+
end
29+
end
30+
31+
default_sort do
32+
has_many_sort priority: :asc
33+
end
34+
end
35+
36+
defmodule Task do
37+
use Ash.Resource,
38+
data_layer: Ash.DataLayer.Ets,
39+
domain: Domain,
40+
extensions: [AshDefaultSort]
41+
42+
attributes do
43+
uuid_v7_primary_key :id
44+
attribute :priority, :integer, allow_nil?: false, public?: true
45+
end
46+
47+
relationships do
48+
belongs_to :todo_list, TodoList
49+
end
50+
51+
actions do
52+
defaults [:read, create: :*]
53+
end
54+
55+
default_sort do
56+
end
57+
end
58+
59+
defmodule Domain do
60+
use Ash.Domain
61+
62+
resources do
63+
resource TodoList
64+
resource Task
65+
end
66+
end
67+
68+
test "pass" do
69+
params = %{title: "Todo List 1"}
70+
list = Ash.Changeset.for_create(TodoList, :create, params) |> Ash.create!()
71+
72+
for priority <- Enum.concat(1..5//2, 2..5//2) do
73+
params = %{task: %{priority: priority}}
74+
Ash.Changeset.for_update(list, :add_task, params) |> Ash.update!()
75+
end
76+
77+
list = Ash.load!(list, [:tasks])
78+
assert [1, 2, 3, 4, 5] = Enum.map(list.tasks, & &1.priority)
79+
end
80+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
defmodule AshDefaultSort.SortTest do
2+
use ExUnit.Case, async: true
3+
4+
alias __MODULE__.{Obj, Domain}
5+
6+
defmodule Obj do
7+
use Ash.Resource,
8+
data_layer: Ash.DataLayer.Ets,
9+
domain: Domain,
10+
extensions: [AshDefaultSort]
11+
12+
attributes do
13+
uuid_v7_primary_key :id
14+
attribute :age, :integer, allow_nil?: false, public?: true
15+
end
16+
17+
actions do
18+
defaults [:read, create: :*]
19+
read :list
20+
end
21+
22+
default_sort do
23+
sort age: :desc
24+
end
25+
end
26+
27+
defmodule Domain do
28+
use Ash.Domain
29+
30+
resources do
31+
resource Obj
32+
end
33+
end
34+
35+
test "pass" do
36+
for age <- Enum.concat(1..5//2, 2..5//2) do
37+
Ash.Changeset.for_create(Obj, :create, %{age: age}) |> Ash.create!()
38+
end
39+
40+
assert [1, 3, 5, 2, 4] =
41+
Ash.Query.for_read(Obj, :read) |> Ash.read!() |> Enum.map(& &1.age)
42+
43+
assert [5, 4, 3, 2, 1] =
44+
Ash.Query.for_read(Obj, :list) |> Ash.read!() |> Enum.map(& &1.age)
45+
end
46+
end

0 commit comments

Comments
 (0)