Skip to content

Commit

Permalink
add subtree and fix minor issues
Browse files Browse the repository at this point in the history
  • Loading branch information
StephaneRob committed Jul 8, 2020
1 parent d569668 commit 1d3be86
Show file tree
Hide file tree
Showing 17 changed files with 418 additions and 39 deletions.
65 changes: 51 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,57 @@ end

#### Available functions

| method | return value | usage example |
| ---------------- | -------------------------------------------------------------------------- | ----------------------------------- |
| `roots` | all root node | `MyApp.Page.roots` |
| `is_root?` | true if the record is a root node, false otherwise | `MyApp.Page.is_root?(record)` |
| `parent` | parent of the record, nil for a root node | `MyApp.Page.parent(record)` |
| `parent_id` | parent id of the record, nil for a root node | `MyApp.Page.parent_id(record)` |
| `ancestors` | ancestors of the record, starting with the root and ending with the parent | `MyApp.Page.ancestors(record)` |
| `ancestor_ids` | ancestor ids of the record | `MyApp.Page.ancestor_ids(record)` |
| `children` | direct children of the record | `MyApp.Page.children(record)` |
| `child_ids` | direct children's ids | `MyApp.Page.child_ids(record)` |
| `siblings` | siblings of the record, the record itself is included\* | `MyApp.Page.siblings(record)` |
| `sibling_ids` | sibling ids | `MyApp.Page.sibling_ids(record)` |
| `descendants` | direct and indirect children of the record | `MyApp.Page.descendants(record)` |
| `descendant_ids` | direct and indirect children's ids of the record | `MyApp.Page.descendant_ids(record)` |
| method | return value | usage example |
| ------------------ | -------------------------------------------------------------------------- | ------------------------------------------- |
| `roots/0` | all root node | `MyApp.Page.roots` |
| `is_root?/1` | true if the record is a root node, false otherwise | `MyApp.Page.is_root?(record)` |
| `parent/1` | parent of the record, nil for a root node | `MyApp.Page.parent(record)` |
| `parent_id/1` | parent id of the record, nil for a root node | `MyApp.Page.parent_id(record)` |
| `has_parent?/1` | true if the record has a parent, false otherwise | `MyApp.Page.has_parent?(record)` |
| `ancestors/1` | ancestors of the record, starting with the root and ending with the parent | `MyApp.Page.ancestors(record)` |
| `ancestor_ids/1` | ancestor ids of the record | `MyApp.Page.ancestor_ids(record)` |
| `children/1` | direct children of the record | `MyApp.Page.children(record)` |
| `child_ids/1` | direct children's ids | `MyApp.Page.child_ids(record)` |
| `siblings/1` | siblings of the record, the record itself is included\* | `MyApp.Page.siblings(record)` |
| `sibling_ids/1` | sibling ids | `MyApp.Page.sibling_ids(record)` |
| `has_siblings?/1` | true if the record's parent has more than one child | `MyApp.Page.has_siblings?(record)` |
| `descendants/1` | direct and indirect children of the record | `MyApp.Page.descendants(record)` |
| `descendant_ids/1` | direct and indirect children's ids of the record | `MyApp.Page.descendant_ids(record)` |
| `subtree/1` | the model on descendants and itself | `MyApp.Page.subtree(record)` |
| `subtree/2` | the arranged model on descendants and itself | `MyApp.Page.subtree(record, arrange: true)` |
| `subtree_ids/1` | a list of all ids in the record's subtree | `MyApp.Page.subtree_ids(record)` |

##### Subtree w/ arrangement

```elixir
%{
%AncestryEcto.Page{
ancestry: nil,
id: 319,
} => %{
%AncestryEcto.Page{
ancestry: "319",
id: 320,
} => %{
%AncestryEcto.Page{
ancestry: "319/320",
"a9b305f0-34e5-4940-9e7d-5b9fc755bae7/9566563f-1281-48d2-8b1a-cdb98ba1f25d",
id: 321,
} => %{
%AncestryEcto.Page{
ancestry: "319/320/321",
id: 322,
} => %{}
}
},
%AncestryEcto.Page{
ancestry: "319",
id: 324,
} => %{}
}
}

```

### Changeset usage

Expand Down
5 changes: 5 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"skip_files": [
"test/support"
]
}
24 changes: 23 additions & 1 deletion lib/ancestry_ecto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ defmodule AncestryEcto do
"""
@callback siblings_ids(record :: Ecto.Schema.t()) :: [String.t() | Integer.t()]

@doc """
true if the record's parent has more than one child
"""
@callback has_siblings?(record :: Ecto.Schema.t()) :: boolean()

@doc """
Delete record and apply strategy to children
"""
Expand Down Expand Up @@ -95,6 +100,7 @@ defmodule AncestryEcto do
Parent,
Repo,
Root,
Subtree,
Sibling
}

Expand Down Expand Up @@ -122,6 +128,10 @@ defmodule AncestryEcto do
Parent.get(model, @opts)
end

def has_parent?(model) do
Parent.any?(model, @opts)
end

def children(model) do
Children.list(model, @opts)
end
Expand All @@ -131,7 +141,7 @@ defmodule AncestryEcto do
end

def children?(model) do
Children.children?(model, @opts)
Children.any?(model, @opts)
end

def descendants(model) do
Expand All @@ -150,6 +160,18 @@ defmodule AncestryEcto do
Sibling.ids(model, @opts)
end

def has_siblings?(model) do
Sibling.any?(model, @opts)
end

def subtree(model, options \\ []) do
Subtree.list(model, options, @opts)
end

def subtree_ids(model) do
Subtree.ids(model, @opts)
end

def delete(model) do
Repo.delete(model, @opts)
end
Expand Down
5 changes: 3 additions & 2 deletions lib/ancestry_ecto/children.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ defmodule AncestryEcto.Children do
end
end

def children?(model, opts) do
list(model, opts) |> Enum.any?()
def any?(model, opts) do
query(model, opts)
|> repo(opts).exists?()
end

def query(model, opts) do
Expand Down
4 changes: 0 additions & 4 deletions lib/ancestry_ecto/descendant.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ defmodule AncestryEcto.Descendant do
for child <- list(model, opts), do: Map.get(child, attribute_column(opts))
end

def children?(model, opts) do
list(model, opts) |> Enum.any?()
end

def query(model, opts) do
from(u in module(opts),
where:
Expand Down
7 changes: 7 additions & 0 deletions lib/ancestry_ecto/parent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@ defmodule AncestryEcto.Parent do
repo(opts).get_by!(module(opts), [{attribute_column(opts), id}])
end
end

def any?(model, opts) do
case id(model, opts) do
nil -> false
_ -> true
end
end
end
2 changes: 1 addition & 1 deletion lib/ancestry_ecto/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule AncestryEcto.Repo do
end

def apply_orphan_strategy(_repo, %{delete: model}, :restrict, opts) do
case Children.children?(model, opts) do
case Children.any?(model, opts) do
true ->
raise(AncestryEcto.RestrictError)

Expand Down
7 changes: 5 additions & 2 deletions lib/ancestry_ecto/sibling.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ defmodule AncestryEcto.Sibling do
for sibling <- list(model, opts), do: Map.get(sibling, attribute_column(opts))
end

def siblings?(model, opts) do
list(model, opts) |> Enum.any?()
def any?(model, opts) do
from(s in query(model, opts),
where: field(s, ^attribute_column(opts)) != ^Map.get(model, attribute_column(opts))
)
|> repo(opts).exists?()
end

defp query(model, opts) do
Expand Down
49 changes: 49 additions & 0 deletions lib/ancestry_ecto/subtree.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule AncestryEcto.Subtree do
@moduledoc false

import AncestryEcto.Utils

alias AncestryEcto.{Children, Descendant}

def list(model, options, opts) do
case Keyword.get(options, :arrange) do
nil ->
flat_list(model, opts)

_ ->
arranged_list(model, opts)
end
end

defp arranged_list(model, opts) do
case Children.any?(model, opts) do
true ->
%{model => build_children(model, opts)}

false ->
%{model => %{}}
end
end

defp flat_list(model, opts) do
[model | Descendant.list(model, opts)]
end

def ids(model, opts) do
for child <- flat_list(model, opts), do: Map.get(child, attribute_column(opts))
end

defp build_children(model, opts) do
model
|> Children.list(opts)
|> Enum.into(%{}, fn child ->
case Children.any?(child, opts) do
true ->
{child, build_children(child, opts)}

false ->
{child, %{}}
end
end)
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ defmodule AncestryEcto.Mixfile do
{:postgrex, ">= 0.0.0", only: [:dev, :test]},
{:ex_machina, "~> 2.4", only: :test},
{:excoveralls, "~> 0.10", only: :test},
{:credo, "~> 1.3.0", only: [:dev, :test], runtime: false}
{:credo, "~> 1.3.0", only: [:dev, :test], runtime: false},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}
]
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
"ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"},
"excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
"makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm", "8f7168911120e13419e086e78d20e4d1a6776f1eee2411ac9f790af10813389f"},
Expand Down
24 changes: 13 additions & 11 deletions test/ancestry_ecto/children_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule AncestryEcto.ChildrenTest do
options: options,
pages: %{page1: page1, page6: page6, page2: page2}
} do
assert Children.ids(page1, options) == [page6.id, page2.id]
assert Children.ids(page1, options) |> Enum.sort() == [page2.id, page6.id]
end

test "list children", %{
Expand All @@ -17,15 +17,17 @@ defmodule AncestryEcto.ChildrenTest do
} do
page6_id = page6.id
page2_id = page2.id
assert [%Page{id: ^page6_id}, %Page{id: ^page2_id}] = Children.list(page1, options)

assert [%Page{id: ^page2_id}, %Page{id: ^page6_id}] =
Children.list(page1, options) |> Enum.sort(&(&1.id < &2.id))
end

test "children?/2", %{
test "any?/2", %{
options: options,
pages: %{page1: page1, page6: page6}
} do
assert Children.children?(page1, options)
refute Children.children?(page6, options)
assert Children.any?(page1, options)
refute Children.any?(page6, options)
end
end

Expand All @@ -49,12 +51,12 @@ defmodule AncestryEcto.ChildrenTest do
Children.list(page1, options) |> Enum.sort(&(&1.id < &2.id))
end

test "children?/2", %{
test "any?/2", %{
options: options,
pages: %{page1: page1, page6: page6}
} do
assert Children.children?(page1, options)
refute Children.children?(page6, options)
assert Children.any?(page1, options)
refute Children.any?(page6, options)
end
end

Expand Down Expand Up @@ -83,12 +85,12 @@ defmodule AncestryEcto.ChildrenTest do
Children.list(page1, options) |> Enum.sort(&(&1.id < &2.id))
end

test "children?/2", %{
test "any?/2", %{
options: options,
pages: %{page1: page1, page6: page6}
} do
assert Children.children?(page1, options)
refute Children.children?(page6, options)
assert Children.any?(page1, options)
refute Children.any?(page6, options)
end
end
end
8 changes: 8 additions & 0 deletions test/ancestry_ecto/parent_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ defmodule AncestryEcto.ParentTest do
assert Parent.get(page6, options) == page1
assert Parent.get(page4, options) == page3
end

test "any?/2", %{
options: options,
pages: %{page1: page1, page6: page6}
} do
refute Parent.any?(page1, options)
assert Parent.any?(page6, options)
end
end

describe "w/ custom ancestry column" do
Expand Down
8 changes: 5 additions & 3 deletions test/ancestry_ecto/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ defmodule AncestryEcto.RepoTest do
options: options,
pages: %{page2: page2}
} do
assert_raise AncestryEcto.RestrictError, fn ->
assert Repo.delete(page2, options)
end
assert_raise AncestryEcto.RestrictError,
"Cannot delete record because it has descendants.",
fn ->
assert Repo.delete(page2, options)
end
end
end
end
8 changes: 8 additions & 0 deletions test/ancestry_ecto/siblings_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ defmodule AncestryEcto.SiblingTest do
%Page{id: ^page1_id}
] = Sibling.list(page1, options) |> Enum.sort(&(&1.id > &2.id))
end

test "any?/2", %{
options: options,
pages: %{page2: page2, page4: page4}
} do
refute Sibling.any?(page4, options)
assert Sibling.any?(page2, options)
end
end

describe "w/ custom ancestry column" do
Expand Down
Loading

0 comments on commit 1d3be86

Please sign in to comment.