Amplified.Nested
Utilities for working with hierarchical (tree-structured) data in Elixir.
tags = Repo.all(Tag)
tree = Tag.nest(tags)
Many applications have self-referential schemas — categories with
subcategories, comments with replies, tag taxonomies. These are typically
stored as flat rows with a :parent_id foreign key and a :children
association. Amplified.Nested provides a small, composable toolkit for
the operations you almost always need: nesting a flat list into a tree,
flattening a tree back into a list, walking ancestors and descendants, and
filtering while preserving the ancestor chain.
All functions are accessed through the schema module (e.g. Tag.nest/1,
Tag.ancestors/2) — not by calling Amplified.Nested directly.
Installation
Add amplified_nested to your list of dependencies in mix.exs:
def deps do
[
{:amplified_nested, "~> 0.1.0"}
]
endConfiguration
Configure the Ecto repo if you use descendant_ancestry/3 on structs whose
:children association has not been loaded:
# config/config.exs
config :amplified_nested, repo: MyApp.Repo
If you never use descendant_ancestry/3 with unloaded associations, no
configuration is needed.
Setup
Schema modules opt in with use Amplified.Nested:
defmodule MyApp.Tags.Tag do
use Ecto.Schema
use Amplified.Nested
schema "tags" do
field :name, :string
belongs_to :parent, __MODULE__
has_many :children, __MODULE__, foreign_key: :parent_id
end
end
This generates delegating functions on the schema module so you can call
Tag.nest(tags) or Tag.flatten(tag) directly, with field names baked in.
Custom field names
If your schema uses non-standard field names, pass them as options:
use Amplified.Nested, parent: :parent_tag_id, child: :sub_tags, id: :tag_idUsage
Nesting a flat list into a tree
tree =
Tag
|> Repo.all()
|> Tag.nest()
Works with {:ok, list} tuples from Repo operations:
Tag.nest({:ok, tags})Flattening a tree back into a list
Tag.flatten(tree)
# => [%Tag{id: 1, ...}, %Tag{id: 2, ...}, ...]Finding ancestors
Returns the full ancestry chain from root to entity:
all_tags = Repo.all(Tag)
liveview_tag = Enum.find(all_tags, &(&1.name == "LiveView"))
Tag.ancestors(all_tags, liveview_tag)
# => [%Tag{name: "Elixir"}, %Tag{name: "Phoenix"}, %Tag{name: "LiveView"}]Finding descendants
Returns all descendants plus the entity itself:
Tag.descendants(all_tags, elixir_tag)
# => [%Tag{name: "Elixir"}, %Tag{name: "Phoenix"}, %Tag{name: "LiveView"}, %Tag{name: "Ecto"}]Filtering with ancestor preservation
Filter a nested tree, keeping matching nodes and their ancestors:
Tag.filter(tree, &(&1.name == "LiveView"))
# => [%Tag{name: "Elixir"}, %Tag{name: "Phoenix"}, %Tag{name: "LiveView"}]Documentation
Full documentation is available on HexDocs.
Licence
MIT — see LICENCE.md.