Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/delete node inside tree #516

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 161 additions & 19 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,39 @@ defmodule Radiator.Outline do
|> Repo.get!(id)
end

@doc """
Returns the previous node of a given node in the outline tree.
Returns `nil` if prev_id of the node is nil.

## Examples
iex> get_prev_node(%Node{prev_id: nil})
nil

iex> get_prev_node(%Node{prev_id: 42})
%Node{uuid: 42}

"""
def get_prev_node(node) when is_nil(node.prev_id), do: nil

def get_prev_node(node) do
Node
|> where([n], n.uuid == ^node.prev_id)
|> Repo.one()
end

@doc """
Returns all child nodes of a given node.
## Examples
iex> get_all_child_nodes(%Node{})
[%Node{}, %Node{}]

"""
def get_all_child_nodes(node) do
Node
|> where([n], n.parent_id == ^node.uuid)
|> Repo.all()
end

@doc """
Gets a single node.

Expand All @@ -78,6 +111,88 @@ defmodule Radiator.Outline do
|> Repo.get(id)
end

@doc """
Gets all nodes of an episode as a tree.
Uses a Common Table Expression (CTE) to recursively query the database.
Sets the level of each node in the tree. Level 0 are the root nodes (without a parent)
Returns a list with all nodes of the episode sorted by the level.
## Examples

iex> get_node_tree(123)
[%Node{}, %Node{}, ..]

SQL:
WITH RECURSIVE node_tree AS (
SELECT uuid, content, parent_id, prev_id, 0 AS level
FROM outline_nodes
WHERE episode_id = ?::integer and parent_id is NULL
UNION ALL
SELECT outline_nodes.uuid, outline_nodes.content, outline_nodes.parent_id, outline_nodes.prev_id, node_tree.level + 1
FROM outline_nodes
JOIN node_tree ON outline_nodes.parent_id = node_tree.uuid
)
SELECT * FROM node_tree;
"""
def get_node_tree(episode_id) do
node_tree_initial_query =
Node
|> where([n], is_nil(n.parent_id))
|> where([n], n.episode_id == ^episode_id)
|> select([n], %{
uuid: n.uuid,
content: n.content,
parent_id: n.parent_id,
prev_id: n.prev_id,
level: 0
})

node_tree_recursion_query =
from outline_node in "outline_nodes",
join: node_tree in "node_tree",
on: outline_node.parent_id == node_tree.uuid,
select: [
outline_node.uuid,
outline_node.content,
outline_node.parent_id,
outline_node.prev_id,
node_tree.level + 1
]

node_tree_query =
node_tree_initial_query
|> union_all(^node_tree_recursion_query)

tree =
"node_tree"
|> recursive_ctes(true)
|> with_cte("node_tree", as: ^node_tree_query)
|> select([n], %{
uuid: n.uuid,
content: n.content,
parent_id: n.parent_id,
prev_id: n.prev_id,
level: n.level
})
|> Repo.all()
|> Enum.map(fn %{
uuid: uuid,
content: content,
parent_id: parent_id,
prev_id: prev_id,
level: level
} ->
%Node{
uuid: binaray_uuid_to_ecto_uuid(uuid),
content: content,
parent_id: binaray_uuid_to_ecto_uuid(parent_id),
prev_id: binaray_uuid_to_ecto_uuid(prev_id),
level: level
}
end)

{:ok, tree}
end

@doc """
Creates a node.

Expand All @@ -92,26 +207,33 @@ defmodule Radiator.Outline do
"""
def create_node(attrs \\ %{}) do
%Node{}
|> Node.changeset(attrs)
|> Node.insert_changeset(attrs)
|> Repo.insert()
|> broadcast_node_action(:insert)
end

def create_node(attrs, %{id: id}) do
%Node{creator_id: id}
|> Node.insert_changeset(attrs)
|> Repo.insert()
|> broadcast_node_action(:insert)
end

@doc """
Updates a node.
Updates a nodes content.

## Examples

iex> update_node(node, %{field: new_value})
iex> update_node_content(node, %{content: new_value})
{:ok, %Node{}}

iex> update_node(node, %{field: bad_value})
iex> update_node_content(node, %{content: nil})
{:error, %Ecto.Changeset{}}

"""
def update_node(%Node{} = node, attrs) do
def update_node_content(%Node{} = node, attrs) do
node
|> Node.changeset(attrs)
|> Node.update_content_changeset(attrs)
|> Repo.update()
|> broadcast_node_action(:update)
end
Expand All @@ -129,28 +251,48 @@ defmodule Radiator.Outline do

"""
def delete_node(%Node{} = node) do
next_node =
Node
|> where([n], n.prev_id == ^node.uuid)
|> Repo.one()

prev_node = get_prev_node(node)

prev_uuid =
if prev_node do
prev_node.uuid
else
nil
end

if next_node do
next_node
|> Node.move_node_changeset(%{prev_id: prev_uuid})
|> Repo.update()
end

# no tail recursion but we dont have too much levels in a tree
node
|> get_all_child_nodes()
|> Enum.each(fn child_node ->
delete_node(child_node)
end)

node
|> Repo.delete()
|> broadcast_node_action(:delete)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking node changes.

## Examples

iex> change_node(node)
%Ecto.Changeset{data: %Node{}}

"""
def change_node(%Node{} = node, attrs \\ %{}) do
Node.changeset(node, attrs)
end

defp broadcast_node_action({:ok, node}, action) do
PubSub.broadcast(Radiator.PubSub, @topic, {action, node})
{:ok, node}
end

defp broadcast_node_action({:error, error}, _action), do: {:error, error}

defp binaray_uuid_to_ecto_uuid(nil), do: nil

defp binaray_uuid_to_ecto_uuid(uuid) do
Ecto.UUID.load!(uuid)
end
end
42 changes: 26 additions & 16 deletions lib/radiator/outline/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,41 @@ defmodule Radiator.Outline.Node do
field :creator_id, :integer
field :parent_id, Ecto.UUID
field :prev_id, Ecto.UUID
field :level, :integer, virtual: true

belongs_to :episode, Episode

timestamps(type: :utc_datetime)
end

@required_fields [
:episode_id
]

@optional_fields [
:content,
:creator_id,
:parent_id,
:prev_id
]

@all_fields @optional_fields ++ @required_fields
@doc """
A changeset for inserting a new node
Work in progress. Since we currently ignore the tree structure, there is
no concept for a root node.
Also questionable wether a node really needs a content from beginning. So probably a root
doesnt have a content
Another issue might be we need to create the uuid upfront and pass it here
"""
def insert_changeset(node, attributes) do
node
|> cast(attributes, [:content, :episode_id, :creator_id, :parent_id, :prev_id])
|> update_change(:content, &trim/1)
|> validate_required([:content, :episode_id])
end

@doc false
def changeset(node, attrs) do
@doc """
Changeset for updating the content of a node
"""
def update_content_changeset(node, attrs) do
node
|> cast(attrs, @all_fields)
|> cast(attrs, [:content])
|> update_change(:content, &trim/1)
|> validate_required(@required_fields)
|> validate_required([:content])
end

def move_node_changeset(node, attrs) do
node
|> cast(attrs, [:parent_id, :prev_id])
end

defp trim(content) when is_binary(content), do: String.trim(content)
Expand Down
2 changes: 1 addition & 1 deletion lib/radiator_web/live/episode_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ defmodule RadiatorWeb.EpisodeLive.Index do

case Outline.get_node(uuid) do
nil -> nil
node -> Outline.update_node(node, attrs)
node -> Outline.update_node_content(node, attrs)
end

socket
Expand Down
5 changes: 4 additions & 1 deletion priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ alias Radiator.{Accounts, Outline, Podcast}
{:ok, show} =
Podcast.create_show(%{title: "Tech Weekly", network_id: network.id})

{:ok, _episode} =
{:ok, past_episode} =
Podcast.create_episode(%{title: "past episode", show_id: show.id})

{:ok, current_episode} =
Expand Down Expand Up @@ -60,3 +60,6 @@ alias Radiator.{Accounts, Outline, Podcast}
episode_id: current_episode.id,
prev_id: node211.uuid
})

{:ok, past_parent_node} =
Outline.create_node(%{content: "Old Content", episode_id: past_episode.id})
Loading