diff --git a/lib/bds/tags.ex b/lib/bds/tags.ex index 9153c77..d1b6ad3 100644 --- a/lib/bds/tags.ex +++ b/lib/bds/tags.ex @@ -47,6 +47,52 @@ defmodule BDS.Tags do :ok end + def sync_tags_from_posts(project_id) do + Repo.transaction(fn -> + existing_names = + project_id + |> list_tags() + |> Enum.map(&String.downcase(&1.name)) + |> MapSet.new() + + missing_names = + project_id + |> post_tag_names() + |> Enum.reduce({existing_names, []}, fn tag_name, {seen_names, acc} -> + normalized = String.downcase(tag_name) + + if MapSet.member?(seen_names, normalized) do + {seen_names, acc} + else + {MapSet.put(seen_names, normalized), [tag_name | acc]} + end + end) + |> elem(1) + |> Enum.reverse() + + now = System.system_time(:second) + + Enum.each(missing_names, fn name -> + %Tag{} + |> Tag.changeset(%{ + id: Ecto.UUID.generate(), + project_id: project_id, + name: name, + created_at: now, + updated_at: now + }) + |> Repo.insert!() + end) + + write_tags_json(project_id) + list_tags(project_id) + end) + |> case do + {:ok, tags} -> {:ok, tags} + {:error, reason} -> {:error, reason} + end + end + def update_tag(tag_id, attrs) do case Repo.get(Tag, tag_id) do nil -> @@ -212,6 +258,16 @@ defmodule BDS.Tags do |> Enum.filter(fn post -> Enum.any?(post.tags || [], &(&1 in tag_names)) end) end + defp post_tag_names(project_id) do + Repo.all(from post in Post, where: post.project_id == ^project_id) + |> Enum.flat_map(fn post -> + post.tags + |> Kernel.||([]) + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + end) + end + defp replace_tag(tags, old_name, new_name) do tags |> Enum.map(fn tag -> if tag == old_name, do: new_name, else: tag end) diff --git a/test/bds/tags_test.exs b/test/bds/tags_test.exs index 11ac45b..0e9e9cf 100644 --- a/test/bds/tags_test.exs +++ b/test/bds/tags_test.exs @@ -145,6 +145,50 @@ defmodule BDS.TagsTest do assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path)) end + test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json", %{project: project, temp_dir: temp_dir} do + assert {:ok, existing} = + BDS.Tags.create_tag(%{ + project_id: project.id, + name: "Existing", + color: "#112233", + post_template_slug: "feature-view" + }) + + assert {:ok, _post_a} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "First", + content: "Body", + tags: ["Existing", "Missing", "Missing"] + }) + + assert {:ok, _post_b} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "Second", + content: "Body", + tags: ["Another", "Missing"] + }) + + assert {:ok, synced_tags} = BDS.Tags.sync_tags_from_posts(project.id) + + assert Enum.map(synced_tags, & &1.name) == ["Another", "Existing", "Missing"] + + reloaded_existing = Repo.get!(BDS.Tags.Tag, existing.id) + assert reloaded_existing.color == "#112233" + assert reloaded_existing.post_template_slug == "feature-view" + + tags_path = Path.join([temp_dir, "meta", "tags.json"]) + + assert %{ + "tags" => [ + %{"name" => "Another"}, + %{"name" => "Existing", "color" => "#112233", "post_template_slug" => "feature-view"}, + %{"name" => "Missing"} + ] + } = Jason.decode!(File.read!(tags_path)) + end + defp errors_on(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> Regex.replace(~r"%{(\w+)}", message, fn _, key ->