From ea9725289b5c669239132afa3b5cc7f2ca1db584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Sat, 5 Oct 2024 13:08:30 +0200 Subject: [PATCH] delete all urls of a node before creating new ones --- lib/radiator/outline/node.ex | 2 + lib/radiator/outline/node_change_listener.ex | 1 - lib/radiator/resources.ex | 44 +++++++++- lib/radiator/resources/url_worker.ex | 12 +-- test/radiator/resources/url_worker_test.exs | 36 ++++++++ test/radiator/resources_test.exs | 86 +++++++++++++++----- test/support/fixtures/resources_fixtures.ex | 4 +- 7 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 test/radiator/resources/url_worker_test.exs diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex index afa6b46b..be544ecf 100644 --- a/lib/radiator/outline/node.ex +++ b/lib/radiator/outline/node.ex @@ -5,6 +5,7 @@ defmodule Radiator.Outline.Node do use Ecto.Schema import Ecto.Changeset alias Radiator.Podcast.Episode + alias Radiator.Resources.Url @derive {Jason.Encoder, only: [:uuid, :content, :creator_id, :parent_id, :prev_id]} @@ -17,6 +18,7 @@ defmodule Radiator.Outline.Node do field :level, :integer, virtual: true belongs_to :episode, Episode + has_many :urls, Url, foreign_key: :node_id timestamps(type: :utc_datetime) end diff --git a/lib/radiator/outline/node_change_listener.ex b/lib/radiator/outline/node_change_listener.ex index 15c2866c..bcc2fa37 100644 --- a/lib/radiator/outline/node_change_listener.ex +++ b/lib/radiator/outline/node_change_listener.ex @@ -13,7 +13,6 @@ defmodule Radiator.Outline.NodeChangeListener do } alias Radiator.Outline.Dispatch - # alias Radiator.Resources alias Radiator.Resources.UrlWorker require Logger diff --git a/lib/radiator/resources.ex b/lib/radiator/resources.ex index b209b298..f760c794 100644 --- a/lib/radiator/resources.ex +++ b/lib/radiator/resources.ex @@ -6,6 +6,7 @@ defmodule Radiator.Resources do import Ecto.Query, warn: false + alias Radiator.Outline.Node alias Radiator.Repo alias Radiator.Resources.Url @@ -16,11 +17,11 @@ defmodule Radiator.Resources do ## Examples - iex> list_urls(episode_id) + iex> list_urls_by_episode(episode_id) [%Url{}, ...] """ - def list_urls(episode_id) do + def list_urls_by_episode(episode_id) do query = from u in Url, join: n in assoc(u, :node), @@ -45,6 +46,28 @@ defmodule Radiator.Resources do """ def get_url!(id), do: Repo.get!(Url, id) + @doc """ + creates all URL entities for a node. Before all existing URLs for this + node get deleted. + """ + def rebuild_node_urls(node_id, url_attributes) do + {:ok, created_urls} = + Repo.transaction(fn -> + _number_of_nodes = delete_urls_for_node(node_id) + + Enum.map(url_attributes, fn attributes -> + {:ok, url} = + attributes + |> Map.put(:node_id, node_id) + |> create_url() + + url + end) + end) + + created_urls + end + @doc """ Creates a url. @@ -97,6 +120,23 @@ defmodule Radiator.Resources do Repo.delete(url) end + @doc """ + Deletes all urls for a given node. + Returns the number of deleted urls. + ## Examples + + iex> delete_urls_for_node(node_id) + 42 + + """ + def delete_urls_for_node(%Node{uuid: node_id}), do: delete_urls_for_node(node_id) + + def delete_urls_for_node(node_id) when is_binary(node_id) do + query = from u in Url, where: u.node_id == ^node_id + {number_of_nodes, nil} = Repo.delete_all(query) + number_of_nodes + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking url changes. diff --git a/lib/radiator/resources/url_worker.ex b/lib/radiator/resources/url_worker.ex index 99783328..1477e2a6 100644 --- a/lib/radiator/resources/url_worker.ex +++ b/lib/radiator/resources/url_worker.ex @@ -15,15 +15,15 @@ defmodule Radiator.Resources.UrlWorker do def perform(node_id, content) do result = UrlExtractor.extract_urls(content) - # TODO - handle old existing urls for this node, error handling! - Enum.each(result, fn info -> - {:ok, _url} = + + url_attributes = + Enum.map(result, fn info -> info - |> Map.put(:node_id, node_id) |> Map.put(:url, info.parsed_url) - |> Resources.create_url() - end) + |> Map.delete(:parsed_url) + end) + _created_urls = Resources.rebuild_node_urls(node_id, url_attributes) :ok end end diff --git a/test/radiator/resources/url_worker_test.exs b/test/radiator/resources/url_worker_test.exs new file mode 100644 index 00000000..a8948d6e --- /dev/null +++ b/test/radiator/resources/url_worker_test.exs @@ -0,0 +1,36 @@ +defmodule Radiator.Resources.UrlWorkerTest do + use Radiator.DataCase + + alias Radiator.Outline.Node + alias Radiator.OutlineFixtures + alias Radiator.Resources.UrlWorker + + describe "extract_url_positions/1" do + test "extracts urls in text" do + node_id = OutlineFixtures.node_fixture().uuid + + content = """ + bad: https://www.theatlantic.com/politics/archive/2024/10/donald-trump-elon-musk-butler/680174/ + racism: https://www.newyorker.com/magazine/2024/10/14/trumps-dangerous-immigration-obsession + what else? + """ + + UrlWorker.perform(node_id, content) + + updated_node = + Node + |> Repo.get(node_id) + |> Repo.preload(:urls) + + first_url = Enum.find(updated_node.urls, fn url -> url.start_bytes == 7 end) + second_url = Enum.find(updated_node.urls, fn url -> url.start_bytes == 108 end) + assert 2 == Enum.count(updated_node.urls) + + assert first_url.url == + "https://www.theatlantic.com/politics/archive/2024/10/donald-trump-elon-musk-butler/680174/" + + assert second_url.url == + "https://www.newyorker.com/magazine/2024/10/14/trumps-dangerous-immigration-obsession" + end + end +end diff --git a/test/radiator/resources_test.exs b/test/radiator/resources_test.exs index 140c999e..75397f9d 100644 --- a/test/radiator/resources_test.exs +++ b/test/radiator/resources_test.exs @@ -8,33 +8,30 @@ defmodule Radiator.ResourcesbTest do alias Radiator.Resources alias Radiator.Resources.Url - describe "urls" do - setup do - node = - OutlineFixtures.node_fixture() - |> Repo.preload([:episode]) - - episode = node.episode - - %{ - episode: episode, - node: node - } - end + @invalid_attrs %{url: nil, start_bytes: nil, size_bytes: nil} - @invalid_attrs %{url: nil, start_bytes: nil, size_bytes: nil} + describe "list_urls_by_episode/0" do + setup :set_up_single_url - test "list_urls/0 returns all urls", %{episode: episode, node: node} do + test "returns all urls of an episode", %{episode: episode, node: node} do url = url_fixture(node_id: node.uuid) - assert Resources.list_urls(episode.id) == [url] + assert Resources.list_urls_by_episode(episode.id) == [url] end + end + + describe "get_url!/1" do + setup :set_up_single_url test "get_url!/1 returns the url with given id" do url = url_fixture() assert Resources.get_url!(url.id) == url end + end - test "create_url/1 with valid data creates a url", %{node: node} do + describe "create_url!/1" do + setup :set_up_single_url + + test "creates a url with valid data", %{node: node} do valid_attrs = %{url: "some url", start_bytes: 42, size_bytes: 42, node_id: node.uuid} assert {:ok, %Url{} = url} = Resources.create_url(valid_attrs) @@ -43,11 +40,32 @@ defmodule Radiator.ResourcesbTest do assert url.size_bytes == 42 end - test "create_url/1 with invalid data returns error changeset" do + test "with invalid data returns error changeset" do assert {:error, %Ecto.Changeset{}} = Resources.create_url(@invalid_attrs) end + end + + describe "rebuild_node_urls/2" do + setup :set_up_single_url + + test "rebuilds all urls for a node" do + url_text = "https://hexdocs.pm" + node = OutlineFixtures.node_fixture() + old_url = url_fixture(node_id: node.uuid) + + assert [%Url{url: ^url_text, start_bytes: 42, size_bytes: 42}] = + Resources.rebuild_node_urls(node.uuid, [ + %{url: url_text, start_bytes: 42, size_bytes: 42} + ]) + + assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(old_url.id) end + end + end + + describe "update_url/2" do + setup :set_up_single_url - test "update_url/2 with valid data updates the url" do + test "with valid data updates the url" do url = url_fixture() update_attrs = %{url: "some updated url", start_bytes: 43, size_bytes: 43} @@ -57,21 +75,45 @@ defmodule Radiator.ResourcesbTest do assert url.size_bytes == 43 end - test "update_url/2 with invalid data returns error changeset" do + test "with invalid data returns error changeset" do url = url_fixture() assert {:error, %Ecto.Changeset{}} = Resources.update_url(url, @invalid_attrs) assert url == Resources.get_url!(url.id) end + end - test "delete_url/1 deletes the url" do + describe "delete_url/1" do + test " deletes the url" do url = url_fixture() assert {:ok, %Url{}} = Resources.delete_url(url) assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(url.id) end end + end + + describe "delete_urls_for_node/1" do + test "deletes all urls from a node" do + node = OutlineFixtures.node_fixture() + url = url_fixture(node_id: node.uuid) + + assert 1 = Resources.delete_urls_for_node(node) + assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(url.id) end + end + end - test "change_url/1 returns a url changeset" do + describe "change_url/1" do + test "returns a url changeset" do url = url_fixture() assert %Ecto.Changeset{} = Resources.change_url(url) end end + + def set_up_single_url(_) do + node = + OutlineFixtures.node_fixture() + |> Repo.preload([:episode]) + + episode = node.episode + + {:ok, episode: episode, node: node} + end end diff --git a/test/support/fixtures/resources_fixtures.ex b/test/support/fixtures/resources_fixtures.ex index 46321fb1..a5d3c068 100644 --- a/test/support/fixtures/resources_fixtures.ex +++ b/test/support/fixtures/resources_fixtures.ex @@ -16,8 +16,8 @@ defmodule Radiator.ResourcesFixtures do attrs |> Enum.into(%{ size_bytes: 42, - start_bytes: 42, - url: "some url", + start_bytes: 23, + url: "https://elixirschool.com", node_id: node_id }) |> Resources.create_url()