Skip to content

Commit

Permalink
Merge pull request #521 from podlove/task/split-cleanup-repo
Browse files Browse the repository at this point in the history
Task/split cleanup repo
  • Loading branch information
electronicbites authored Apr 14, 2024
2 parents 988f7ca + 101032c commit 9ab4a77
Show file tree
Hide file tree
Showing 12 changed files with 935 additions and 184 deletions.
294 changes: 198 additions & 96 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,158 +6,260 @@ defmodule Radiator.Outline do
import Ecto.Query, warn: false

alias Radiator.Outline.Node
alias Radiator.Outline.Notify
alias Radiator.Outline.NodeRepository
alias Radiator.Repo

def create(attrs \\ %{}, socket_id \\ nil) do
attrs
|> create_node()
|> Notify.broadcast_node_action(:insert, socket_id)
end

def update(%Node{} = node, attrs, socket_id \\ nil) do
node
|> update_node(attrs)
|> Notify.broadcast_node_action(:update, socket_id)
end

def delete(%Node{} = node, socket_id \\ nil) do
node
|> delete_node()
|> Notify.broadcast_node_action(:delete, socket_id)
end

@doc """
Returns the list of nodes.
Inserts a node.
## Examples
iex> list_nodes()
[%Node{}, ...]
iex> insert_node(%{content: 'foo'})
{:ok, %Node{}}
iex> insert_node(%{content: value})
{:error, :parent_and_prev_not_consistent}
"""
def list_nodes do
Node
|> Repo.all()
# creates a node and inserts it into the outline tree
# if a parent node is given, the new node will be inserted as a child of the parent node
# if a previous node is given, the new node will be inserted after the previous node
# if no parent is given, the new node will be inserted as a root node
# if no previous node is given, the new node will be inserted as the first child of the parent node
def insert_node(attrs) do
Repo.transaction(fn ->
prev_node_id = attrs["prev_node"]
parent_node_id = attrs["parent_node"]
episode_id = attrs["episode_id"]
# find Node which has been previously connected to prev_node
node_to_move =
Node
|> where(episode_id: ^episode_id)
|> where_prev_node_equals(prev_node_id)
|> where_parent_node_equals(parent_node_id)
|> Repo.one()

with parent_node <- NodeRepository.get_node_if(parent_node_id),
prev_node <- NodeRepository.get_node_if(prev_node_id),
true <- parent_and_prev_consistent?(parent_node, prev_node),
{:ok, node} <- NodeRepository.create_node(attrs),
{:ok, _node_to_move} <- move_node_(node_to_move, nil, node.uuid),
{:ok, node} <- move_node_(node, parent_node_id, prev_node_id) do
node
else
false ->
Repo.rollback("Insert node failed. Parent and prev node are not consistent.")

{:error, _} ->
Repo.rollback("Insert node failed. Unkown error")
end
end)
end

@doc """
Returns the list of nodes for an episode.
Updates a nodes content.
## Examples
iex> list_nodes(123)
[%Node{}, ...]
iex> update_node_content(node, %{content: new_value})
{:ok, %Node{}}
"""
iex> update_node_content(node, %{content: nil})
{:error, %Ecto.Changeset{}}
def list_nodes_by_episode(episode_id) do
Node
|> where([p], p.episode_id == ^episode_id)
|> Repo.all()
"""
def update_node_content(%Node{} = node, attrs, _socket_id \\ nil) do
node
|> Node.update_content_changeset(attrs)
|> Repo.update()
end

@doc """
Gets a single node.
Raises `Ecto.NoResultsError` if the Node does not exist.
Removes a node from the tree and deletes it from the repository.
Recursivly deletes all children if there are some.
## Examples
iex> get_node!(123)
%Node{}
iex> remove_node(node)
{:ok, %Node{}}
iex> get_node!(456)
** (Ecto.NoResultsError)
iex> remove_node(node)
{:error, %Ecto.Changeset{}}
"""
def get_node!(id) do
Node
|> Repo.get!(id)
def remove_node(%Node{} = node, _socket_id \\ nil) do
next_node =
Node
|> where([n], n.prev_id == ^node.uuid)
|> Repo.one()

prev_node = get_prev_node(node)

if next_node do
next_node
|> Node.move_node_changeset(%{prev_id: get_node_id(prev_node)})
|> 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 ->
remove_node(child_node)
end)

# finally delete the node itself from the database
NodeRepository.delete_node(node)
end

@doc """
Gets a single node.
Returns `nil` if the Node does not exist.
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_node(123)
%Node{}
iex> get_node(456)
nil
iex> get_prev_node(%Node{prev_id: 42})
%Node{uuid: 42}
"""
def get_node(id) do
def get_prev_node(node) when is_nil(node.prev_id), do: nil

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

@doc """
Creates a node.
Returns all child nodes of a given node.
## Examples
iex> create_node(%{field: value})
{:ok, %Node{}}
iex> create_node(%{field: bad_value})
{:error, %Ecto.Changeset{}}
iex> get_all_child_nodes(%Node{})
[%Node{}, %Node{}]
"""
def create_node(attrs \\ %{}) do
%Node{}
|> Node.changeset(attrs)
|> Repo.insert()
def get_all_child_nodes(node) do
Node
|> where([n], n.parent_id == ^node.uuid)
|> Repo.all()
end

@doc """
Updates a node.
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> update_node(node, %{field: new_value})
{:ok, %Node{}}
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,
episode_id: episode_id
}
end)

{:ok, tree}
end

iex> update_node(node, %{field: bad_value})
{:error, %Ecto.Changeset{}}
defp move_node_(nil, _parent_node_id, _prev_node_id), do: {:ok, nil}

"""
def update_node(%Node{} = node, attrs) do
defp move_node_(node, parent_node_id, prev_node_id) do
node
|> Node.changeset(attrs)
|> Node.move_node_changeset(%{
parent_id: parent_node_id,
prev_id: prev_node_id
})
|> Repo.update()
end

@doc """
Deletes a node.
## Examples
defp parent_and_prev_consistent?(_, nil), do: true
defp parent_and_prev_consistent?(nil, _), do: true

iex> delete_node(node)
{:ok, %Node{}}
defp parent_and_prev_consistent?(parent, prev) do
parent.uuid == prev.parent_id
end

iex> delete_node(node)
{:error, %Ecto.Changeset{}}
defp where_prev_node_equals(node, nil), do: where(node, [n], is_nil(n.prev_id))
defp where_prev_node_equals(node, prev_id), do: where(node, [n], n.prev_id == ^prev_id)

"""
def delete_node(%Node{} = node) do
node
|> Repo.delete()
end
defp where_parent_node_equals(node, nil), do: where(node, [n], is_nil(n.parent_id))
defp where_parent_node_equals(node, parent_id), do: where(node, [n], n.parent_id == ^parent_id)

@doc """
Returns an `%Ecto.Changeset{}` for tracking node changes.
defp get_node_id(nil), do: nil

## Examples
defp get_node_id(%Node{} = node) do
node.uuid
end

iex> change_node(node)
%Ecto.Changeset{data: %Node{}}
defp binaray_uuid_to_ecto_uuid(nil), do: nil

"""
def change_node(%Node{} = node, attrs \\ %{}) do
Node.changeset(node, attrs)
defp binaray_uuid_to_ecto_uuid(uuid) do
Ecto.UUID.load!(uuid)
end
end
2 changes: 1 addition & 1 deletion lib/radiator/outline/event_consumer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ defmodule Radiator.Outline.EventConsumer do

defp process_event(%InsertNodeEvent{payload: payload} = _event) do
payload
|> Outline.create_node()
|> Outline.insert_node()
|> handle_insert_result()

# validate
Expand Down
Loading

0 comments on commit 9ab4a77

Please sign in to comment.