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

Task/split cleanup repo #521

Merged
merged 6 commits into from
Apr 14, 2024
Merged
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
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
Loading