From e2e4332297e4c05da9d9d2840cce1fd3526d3593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Mon, 29 Jan 2024 22:11:11 +0100 Subject: [PATCH 1/8] split nodes changeset in different use cases --- lib/radiator/outline.ex | 27 ++++--------- lib/radiator/outline/node.ex | 44 ++++++++++++++------- lib/radiator_web/live/episode_live/index.ex | 2 +- test/radiator/outline_test.exs | 13 ++---- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 38d2a6e7..7ef303b4 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -76,33 +76,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.changeset(attrs) + |> 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 @@ -125,19 +125,6 @@ defmodule Radiator.Outline do |> 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} diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex index a078b009..0ce640c6 100644 --- a/lib/radiator/outline/node.ex +++ b/lib/radiator/outline/node.ex @@ -10,6 +10,7 @@ defmodule Radiator.Outline.Node do @derive {Jason.Encoder, only: [:uuid, :content, :creator_id, :parent_id, :prev_id]} @primary_key {:uuid, :binary_id, autogenerate: true} + schema "outline_nodes" do field :content, :string field :creator_id, :integer @@ -21,25 +22,38 @@ defmodule Radiator.Outline.Node do timestamps(type: :utc_datetime) end - @required_fields [ - :content, - :episode_id - ] - - @optional_fields [ - :creator_id, - :parent_id, - :prev_id - ] + @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 - @all_fields @optional_fields ++ @required_fields + @doc """ + Changeset for moving a node + Only the parent_id is allowed and expected to be changed + """ + def move_changeset(node, attrs) do + node + |> cast(attrs, [:parent_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 defp trim(content) when is_binary(content), do: String.trim(content) diff --git a/lib/radiator_web/live/episode_live/index.ex b/lib/radiator_web/live/episode_live/index.ex index 23622ba5..92d9ba74 100644 --- a/lib/radiator_web/live/episode_live/index.ex +++ b/lib/radiator_web/live/episode_live/index.ex @@ -64,7 +64,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 diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index 505364ef..b6a1baa3 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -51,17 +51,17 @@ defmodule Radiator.OutlineTest do assert {:error, %Ecto.Changeset{}} = Outline.create_node(@invalid_attrs) end - test "update_node/2 with valid data updates the node" do + test "update_node_content/2 with valid data updates the node" do node = node_fixture() update_attrs = %{content: "some updated content"} - assert {:ok, %Node{} = node} = Outline.update_node(node, update_attrs) + assert {:ok, %Node{} = node} = Outline.update_node_content(node, update_attrs) assert node.content == "some updated content" end - test "update_node/2 with invalid data returns error changeset" do + test "update_node_content/2 with invalid data returns error changeset" do node = node_fixture() - assert {:error, %Ecto.Changeset{}} = Outline.update_node(node, @invalid_attrs) + assert {:error, %Ecto.Changeset{}} = Outline.update_node_content(node, @invalid_attrs) assert node == Outline.get_node!(node.uuid) end @@ -70,10 +70,5 @@ defmodule Radiator.OutlineTest do assert {:ok, %Node{}} = Outline.delete_node(node) assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end end - - test "change_node/1 returns a node changeset" do - node = node_fixture() - assert %Ecto.Changeset{} = Outline.change_node(node) - end end end From a44abafc77e3d03295fe5d66a74b8cbb0ff0dc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Mon, 29 Jan 2024 22:55:38 +0100 Subject: [PATCH 2/8] add move node function --- lib/radiator/outline.ex | 19 ++++++++++++ lib/radiator/outline/node.ex | 23 ++++++++++++-- test/radiator/outline_test.exs | 55 +++++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 7ef303b4..d624b44a 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -107,6 +107,25 @@ defmodule Radiator.Outline do |> broadcast_node_action(:update) end + @doc """ + Moves a nodes to another parent. + + ## Examples + + iex> move_node(node, %Node{uuid: new_parent_id}) + {:ok, %Node{}} + + iex> move_node(node, nil) + {:error, %Ecto.Changeset{}} + + """ + def move_node(%Node{} = node, %Node{} = parent_node) do + node + |> Node.move_changeset(parent_node) + |> Repo.update() + |> broadcast_node_action(:update) + end + @doc """ Deletes a node. diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex index 0ce640c6..bc38ca67 100644 --- a/lib/radiator/outline/node.ex +++ b/lib/radiator/outline/node.ex @@ -41,9 +41,10 @@ defmodule Radiator.Outline.Node do Changeset for moving a node Only the parent_id is allowed and expected to be changed """ - def move_changeset(node, attrs) do + def move_changeset(node, new_parent_node) do node - |> cast(attrs, [:parent_id]) + |> cast(%{parent_id: new_parent_node.uuid}, [:parent_id]) + |> validate_parent(new_parent_node) end @doc """ @@ -58,4 +59,22 @@ defmodule Radiator.Outline.Node do defp trim(content) when is_binary(content), do: String.trim(content) defp trim(content), do: content + + defp validate_parent(changeset, nil), do: add_error(changeset, :parent_id, "must not be nil") + + defp validate_parent(changeset, parent_node) do + cond do + parent_node.uuid == changeset.data.uuid -> + add_error(changeset, :parent_id, "must not be the same as the node itself") + + parent_node.parent_id == changeset.data.uuid -> + add_error(changeset, :parent_id, "node is already parent of the parent node") + + parent_node.episode_id != changeset.data.episode_id -> + add_error(changeset, :parent_id, "nodes must be in the same episode") + + true -> + changeset + end + end end diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index b6a1baa3..e88123f2 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -3,25 +3,29 @@ defmodule Radiator.OutlineTest do alias Radiator.Outline - describe "outline_nodes" do - alias Radiator.Outline.Node + alias Radiator.Outline.Node - import Radiator.OutlineFixtures - alias Radiator.PodcastFixtures + import Radiator.OutlineFixtures + alias Radiator.PodcastFixtures - @invalid_attrs %{content: nil} + @invalid_attrs %{content: nil} - test "list_nodes/0 returns all nodes" do + describe "list_nodes/0" do + test "returns all nodes" do node = node_fixture() assert Outline.list_nodes() == [node] end + end - test "get_node!/1 returns the node with given id" do + describe "get_node!/1" do + test "returns the node with given id" do node = node_fixture() assert Outline.get_node!(node.uuid) == node end + end - test "create_node/1 with valid data creates a node" do + describe "create_node/1" do + test "with valid data creates a node" do episode = PodcastFixtures.episode_fixture() valid_attrs = %{content: "some content", episode_id: episode.id} @@ -29,7 +33,7 @@ defmodule Radiator.OutlineTest do assert node.content == "some content" end - test "create_node/1 trims whitespace from content" do + test "trims whitespace from content" do episode = PodcastFixtures.episode_fixture() valid_attrs = %{content: " some content ", episode_id: episode.id} @@ -37,7 +41,7 @@ defmodule Radiator.OutlineTest do assert node.content == "some content" end - test "create_node/1 can have a creator" do + test "can have a creator" do episode = PodcastFixtures.episode_fixture() user = %{id: 2} valid_attrs = %{content: "some content", episode_id: episode.id} @@ -47,11 +51,13 @@ defmodule Radiator.OutlineTest do assert node.creator_id == user.id end - test "create_node/1 with invalid data returns error changeset" do + test "with invalid data returns error changeset" do assert {:error, %Ecto.Changeset{}} = Outline.create_node(@invalid_attrs) end + end - test "update_node_content/2 with valid data updates the node" do + describe "update_node_content/2" do + test "with valid data updates the node" do node = node_fixture() update_attrs = %{content: "some updated content"} @@ -59,13 +65,34 @@ defmodule Radiator.OutlineTest do assert node.content == "some updated content" end - test "update_node_content/2 with invalid data returns error changeset" do + test "with invalid data returns error changeset" do node = node_fixture() assert {:error, %Ecto.Changeset{}} = Outline.update_node_content(node, @invalid_attrs) assert node == Outline.get_node!(node.uuid) end + end + + describe "move_node/2" do + test "moves node to another parent" do + node = node_fixture() + new_parent = node_fixture(episode_id: node.episode_id) + + assert {:ok, %Node{} = node} = Outline.move_node(node, new_parent) + assert node.parent_id == new_parent.uuid + end + + test "update_node_content/2 with parent from another episode returns error changeset" do + node = node_fixture() + new_bad_parent = node_fixture() + assert node.episode_id != new_bad_parent.episode_id + + assert {:error, %Ecto.Changeset{}} = Outline.move_node(node, new_bad_parent) + assert node == Outline.get_node!(node.uuid) + end + end - test "delete_node/1 deletes the node" do + describe "delete_node/1" do + test "deletes the node" do node = node_fixture() assert {:ok, %Node{}} = Outline.delete_node(node) assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end From e837a88a85972d8a6c6791d2090bc1a99a5fb222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Wed, 14 Feb 2024 21:38:06 +0100 Subject: [PATCH 3/8] WIP: tree node function --- lib/radiator/outline.ex | 33 +++++++++++++++++++++++++ test/radiator/outline_test.exs | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index d624b44a..6007bcbe 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -62,6 +62,39 @@ defmodule Radiator.Outline do |> Repo.get(id) end + @doc """ + Gets all nodes of an episode as a tree. + + ## Examples + + iex> get_node_tree(123) + [%Node{}, %Node{}, ..] + """ + # episode_id = 2 + # Radiator.Outline.get_node_tree(episode_id) + def get_node_tree(episode_id) do + + node_tree_initial_query = + Node + |> where([n], is_nil(n.parent_id)) + + node_tree_recursion_query = + Node + |> join(:inner, [n], nd in "node_tree", on: n.parent_id == nd.uuid) + + node_tree_query = + node_tree_initial_query + |> union_all(^node_tree_recursion_query) + + tree = + Node + |> recursive_ctes(true) + |> with_cte("node_tree", as: ^node_tree_query) + |> Repo.all() + + {:ok, tree} + end + @doc """ Creates a node. diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index e88123f2..39ad27a1 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -98,4 +98,49 @@ defmodule Radiator.OutlineTest do assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end end end + + describe "get_node_tree/1" do + test "returns all nodes from a episode", %{ + node_1: node_1, + node_2: node_2, + node_3: node_3, + node_4: node_4, + node_5: node_5, + node_6: node_6, + nested_node_1: nested_node_1, + nested_node_2: nested_node_2, + parent: parent + } do + assert {:ok, tree}} = Outline.get_node_tree(parent.episode_id) + assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end + assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(child.uuid) end + assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(grandchild.uuid) end + end + end + + defp complex_node_fixture(_) do + episode = PodcastFixtures.episode_fixture() + parent = node_fixture(episode_id: episode.id) + node_1 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: nil) + node_2 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_1.uuid) + node_3 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_2.uuid) + node_4 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_3.uuid) + node_5 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_4.uuid) + node_6 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_5.uuid) + + nested_node_1 = node_fixture(episode_id: episode.id, parent_id: node_3.uuid, prev_id: nil) + nested_node_2 = node_fixture(episode_id: episode.id, parent_id: node_3.uuid, prev_id: nested_node_1.uuid) + + %{ + node_1: node_1, + node_2: node_2, + node_3: node_3, + node_4: node_4, + node_5: node_5, + node_6: node_6, + nested_node_1: nested_node_1, + nested_node_2: nested_node_2, + parent: parent + } + end end From 7838821671c1d0c0e4b50a0b1e3b5ba2c4ee6225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Wed, 14 Feb 2024 21:47:39 +0100 Subject: [PATCH 4/8] WIP: tree node function fixup --- test/radiator/outline_test.exs | 72 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index 39ad27a1..75c850cd 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -2,11 +2,12 @@ defmodule Radiator.OutlineTest do use Radiator.DataCase alias Radiator.Outline - alias Radiator.Outline.Node + alias Radiator.PodcastFixtures + alias Radiator.Repo import Radiator.OutlineFixtures - alias Radiator.PodcastFixtures + import Ecto.Query, warn: false @invalid_attrs %{content: nil} @@ -72,24 +73,41 @@ defmodule Radiator.OutlineTest do end end - describe "move_node/2" do - test "moves node to another parent" do - node = node_fixture() - new_parent = node_fixture(episode_id: node.episode_id) - - assert {:ok, %Node{} = node} = Outline.move_node(node, new_parent) - assert node.parent_id == new_parent.uuid - end - - test "update_node_content/2 with parent from another episode returns error changeset" do - node = node_fixture() - new_bad_parent = node_fixture() - assert node.episode_id != new_bad_parent.episode_id - - assert {:error, %Ecto.Changeset{}} = Outline.move_node(node, new_bad_parent) - assert node == Outline.get_node!(node.uuid) - end - end + # describe "move_node/2" do + # test "moves node to another parent" do + # node = node_fixture() + # new_parent = node_fixture(episode_id: node.episode_id) + + # assert {:ok, %Node{} = node} = Outline.move_node(node, new_parent) + # assert node.parent_id == new_parent.uuid + # end + + # test "update_node_content/2 with parent from another episode returns error changeset" do + # node = node_fixture() + # new_bad_parent = node_fixture() + # assert node.episode_id != new_bad_parent.episode_id + + # assert {:error, %Ecto.Changeset{}} = Outline.move_node(node, new_bad_parent) + # assert node == Outline.get_node!(node.uuid) + # end + # end + + # describe "sort_node/2" do + # setup :complex_node_fixture + + # test "moves node 6 to top", %{ + # node_1: node_1, + # node_2: node_2, + # node_3: node_3, + # node_4: node_4, + # node_5: node_5, + # node_6: node_6, + # parent: parent + # } do + # assert {:ok, %Node{} = node} = Outline.sort_node(node_6, nil) + # assert node.parent_id == new_parent.uuid + # end + # end describe "delete_node/1" do test "deletes the node" do @@ -100,6 +118,8 @@ defmodule Radiator.OutlineTest do end describe "get_node_tree/1" do + setup :complex_node_fixture + test "returns all nodes from a episode", %{ node_1: node_1, node_2: node_2, @@ -111,10 +131,14 @@ defmodule Radiator.OutlineTest do nested_node_2: nested_node_2, parent: parent } do - assert {:ok, tree}} = Outline.get_node_tree(parent.episode_id) - assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end - assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(child.uuid) end - assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(grandchild.uuid) end + episode_id = parent.episode_id + assert {:ok, tree} = Outline.get_node_tree(episode_id) + + all_nodes = + Node + |> where([n], n.episode_id == ^episode_id) + |> Repo.all() + assert Enum.count(tree) == Enum.count(all_nodes) end end From 928e6794c62730fec277f5fa4cb473e8b2e79c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Wed, 14 Feb 2024 21:59:56 +0100 Subject: [PATCH 5/8] WIP: tree node function fixup 2 --- lib/radiator/outline.ex | 1 + test/radiator/outline_test.exs | 32 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 6007bcbe..457f2a96 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -77,6 +77,7 @@ defmodule Radiator.Outline do node_tree_initial_query = Node |> where([n], is_nil(n.parent_id)) + |> where([n], n.episode_id == ^episode_id) node_tree_recursion_query = Node diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index 75c850cd..d625ed69 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -121,14 +121,14 @@ defmodule Radiator.OutlineTest do setup :complex_node_fixture test "returns all nodes from a episode", %{ - node_1: node_1, - node_2: node_2, - node_3: node_3, - node_4: node_4, - node_5: node_5, - node_6: node_6, - nested_node_1: nested_node_1, - nested_node_2: nested_node_2, + # node_1: node_1, + # node_2: node_2, + # node_3: node_3, + # node_4: node_4, + # node_5: node_5, + # node_6: node_6, + # nested_node_1: nested_node_1, + # nested_node_2: nested_node_2, parent: parent } do episode_id = parent.episode_id @@ -139,12 +139,26 @@ defmodule Radiator.OutlineTest do |> where([n], n.episode_id == ^episode_id) |> Repo.all() assert Enum.count(tree) == Enum.count(all_nodes) + Enum.each(tree, fn node -> + assert node == List.first(Enum.filter(all_nodes, fn n -> n.uuid == node.uuid end)) + end) + end + + test "does not return a node not in this episode", %{ + parent: parent + } do + episode_id = parent.episode_id + other_node = node_fixture(parent_id: nil, prev_id: nil, content: "other content") + assert other_node.episode_id != episode_id + {:ok, tree} = Outline.get_node_tree(episode_id) + assert Enum.filter(tree, fn n -> n.uuid == other_node.uuid end) == nil end + end defp complex_node_fixture(_) do episode = PodcastFixtures.episode_fixture() - parent = node_fixture(episode_id: episode.id) + parent = node_fixture(episode_id: episode.id, parent_id: nil, prev_id: nil) node_1 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: nil) node_2 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_1.uuid) node_3 = node_fixture(episode_id: episode.id, parent_id: parent.uuid, prev_id: node_2.uuid) From fce68ceb01891828fc2cc8b180224abea0932b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Wed, 14 Feb 2024 22:41:31 +0100 Subject: [PATCH 6/8] WIP: tree node function fixup 3 --- lib/radiator/outline.ex | 42 +++++++++++++++++++++++++++++++++--- lib/radiator/outline/node.ex | 1 + priv/repo/seeds.exs | 6 +++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 457f2a96..36aee574 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -78,10 +78,18 @@ defmodule Radiator.Outline do 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 = - Node - |> join(:inner, [n], nd in "node_tree", on: n.parent_id == nd.uuid) + # node_tree_recursion_query = + # Node + # |> join(:inner, [n], nd in "node_tree", on: n.parent_id == nd.uuid) + # |> fragmet("JOIN node_tree ON outline_nodes.parent_id = node_tree.uuid") + # |> select([n], %{uuid: n.uuid, content: n.content, parent_id: n.parent_id, prev_id: n.prev_id, level: fragment("'node_tree.level + 1'")}) + + + 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 @@ -96,6 +104,34 @@ defmodule Radiator.Outline do {:ok, tree} end + + + # WITH RECURSIVE cte AS ( + # SELECT uuid, content, parent_id, prev_id, 0 AS level + # FROM outline_nodes + # WHERE episode_id = 2 and parent_id is NULL + # UNION ALL + # SELECT outline_nodes.uuid, outline_nodes.content, outline_nodes.parent_id, outline_nodes.prev_id, cte.level + 1 + # FROM outline_nodes + # JOIN cte ON outline_nodes.parent_id = cte.uuid + # ) + # SELECT * FROM cte; + # + + # + # WITH RECURSIVE "node_tree" AS ( + # SELECT so0."uuid" AS "uuid", so0."content" AS "content", so0."parent_id" AS "parent_id", so0."prev_id" AS "prev_id", 0 AS "level" + # FROM "outline_nodes" AS so0 + # WHERE (so0."parent_id" IS NULL) AND (so0."episode_id" = $1) + # UNION ALL ( + # SELECT so0."uuid", so0."content", so0."parent_id", so0."prev_id", 'node_tree.level + 1' + # FROM "outline_nodes" AS so0 + # INNER JOIN "node_tree" AS sn1 ON so0."parent_id" = sn1."uuid")) SELECT o0."uuid", o0."content", o0."creator_id", o0."parent_id", o0."prev_id", o0."episode_id", o0."inserted_at", o0."updated_at" FROM "outline_nodes" AS o0 + + + + + @doc """ Creates a node. diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex index bc38ca67..42fc7265 100644 --- a/lib/radiator/outline/node.ex +++ b/lib/radiator/outline/node.ex @@ -16,6 +16,7 @@ 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 diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 03a5e3b0..d5e39037 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -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} = @@ -60,3 +60,7 @@ 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}) From 7b07072e2c0994f68b56b141a3a1bb2d8bfc2c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Fri, 16 Feb 2024 20:28:37 +0100 Subject: [PATCH 7/8] WIP: tree node function fixup 4 --- lib/radiator/outline.ex | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 36aee574..9c6de947 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -62,6 +62,15 @@ defmodule Radiator.Outline do |> Repo.get(id) end + @raw_sql_node_tree """ + SELECT uuid, content, parent_id, prev_id, 0 AS level + FROM outline_nodes + WHERE episode_id = 2 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 + """ @doc """ Gets all nodes of an episode as a tree. @@ -96,12 +105,19 @@ defmodule Radiator.Outline do |> union_all(^node_tree_recursion_query) tree = - Node + "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: 0}) + |> Repo.all() + + + tree2 = Node + |> recursive_ctes(true) + |> with_cte("node_tree", as: fragment(@raw_sql_node_tree)) |> Repo.all() - {:ok, tree} + {:ok, tree, tree2} end From aad5c504291bb3724b2d63f0ce72a8c57f5c6326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Fri, 16 Feb 2024 20:45:19 +0100 Subject: [PATCH 8/8] WIP: tree node function fixup 5 --- lib/radiator/outline.ex | 59 ++++++---------------------------- test/radiator/outline_test.exs | 5 ++- 2 files changed, 11 insertions(+), 53 deletions(-) diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 9c6de947..995a5e51 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -62,15 +62,6 @@ defmodule Radiator.Outline do |> Repo.get(id) end - @raw_sql_node_tree """ - SELECT uuid, content, parent_id, prev_id, 0 AS level - FROM outline_nodes - WHERE episode_id = 2 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 - """ @doc """ Gets all nodes of an episode as a tree. @@ -89,13 +80,6 @@ defmodule Radiator.Outline do |> 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 = - # Node - # |> join(:inner, [n], nd in "node_tree", on: n.parent_id == nd.uuid) - # |> fragmet("JOIN node_tree ON outline_nodes.parent_id = node_tree.uuid") - # |> select([n], %{uuid: n.uuid, content: n.content, parent_id: n.parent_id, prev_id: n.prev_id, level: fragment("'node_tree.level + 1'")}) - - 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] @@ -108,43 +92,18 @@ defmodule Radiator.Outline do "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: 0}) - |> Repo.all() - - - tree2 = Node - |> recursive_ctes(true) - |> with_cte("node_tree", as: fragment(@raw_sql_node_tree)) + |> select([n], %{uuid: n.uuid, content: n.content, parent_id: n.parent_id, prev_id: n.prev_id, level: n.level}) |> Repo.all() - - {:ok, tree, tree2} + |> 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 - - - # WITH RECURSIVE cte AS ( - # SELECT uuid, content, parent_id, prev_id, 0 AS level - # FROM outline_nodes - # WHERE episode_id = 2 and parent_id is NULL - # UNION ALL - # SELECT outline_nodes.uuid, outline_nodes.content, outline_nodes.parent_id, outline_nodes.prev_id, cte.level + 1 - # FROM outline_nodes - # JOIN cte ON outline_nodes.parent_id = cte.uuid - # ) - # SELECT * FROM cte; - # - - # - # WITH RECURSIVE "node_tree" AS ( - # SELECT so0."uuid" AS "uuid", so0."content" AS "content", so0."parent_id" AS "parent_id", so0."prev_id" AS "prev_id", 0 AS "level" - # FROM "outline_nodes" AS so0 - # WHERE (so0."parent_id" IS NULL) AND (so0."episode_id" = $1) - # UNION ALL ( - # SELECT so0."uuid", so0."content", so0."parent_id", so0."prev_id", 'node_tree.level + 1' - # FROM "outline_nodes" AS so0 - # INNER JOIN "node_tree" AS sn1 ON so0."parent_id" = sn1."uuid")) SELECT o0."uuid", o0."content", o0."creator_id", o0."parent_id", o0."prev_id", o0."episode_id", o0."inserted_at", o0."updated_at" FROM "outline_nodes" AS o0 - - + defp binaray_uuid_to_ecto_uuid(nil), do: nil + defp binaray_uuid_to_ecto_uuid(uuid) do + Ecto.UUID.load!(uuid) + end diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index d625ed69..29220a46 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -140,7 +140,7 @@ defmodule Radiator.OutlineTest do |> Repo.all() assert Enum.count(tree) == Enum.count(all_nodes) Enum.each(tree, fn node -> - assert node == List.first(Enum.filter(all_nodes, fn n -> n.uuid == node.uuid end)) + assert node.uuid == List.first(Enum.filter(all_nodes, fn n -> n.uuid == node.uuid end)).uuid end) end @@ -151,9 +151,8 @@ defmodule Radiator.OutlineTest do other_node = node_fixture(parent_id: nil, prev_id: nil, content: "other content") assert other_node.episode_id != episode_id {:ok, tree} = Outline.get_node_tree(episode_id) - assert Enum.filter(tree, fn n -> n.uuid == other_node.uuid end) == nil + assert Enum.filter(tree, fn n -> n.uuid == other_node.uuid end) == [] end - end defp complex_node_fixture(_) do