chore: reworked some transaction handling code
This commit is contained in:
@@ -51,7 +51,7 @@ _None._ All modules previously on the queue have been split; refresh the queue i
|
|||||||
|
|
||||||
## 3. Side Effects in Transactions
|
## 3. Side Effects in Transactions
|
||||||
|
|
||||||
**Status:** ✅ done in `BDS.Media` (2026-04-30). Open elsewhere — no audit yet for `BDS.Posts`, `BDS.Publishing`, `BDS.Generation`.
|
**Status:** ✅ done in `BDS.Media` (2026-04-30). Started elsewhere: `BDS.Templates.update_template/2` now keeps only DB writes inside its transaction and runs template-file rewrites, published-post rewrites, and tags JSON flushes after commit. Open elsewhere — no audit yet for `BDS.Posts`, `BDS.Publishing`, `BDS.Generation`.
|
||||||
|
|
||||||
**Plan:** spot-check every `Repo.transaction/1` outside `BDS.Media`. Rule: only DB writes inside; filesystem and `Search.sync_*` after commit.
|
**Plan:** spot-check every `Repo.transaction/1` outside `BDS.Media`. Rule: only DB writes inside; filesystem and `Search.sync_*` after commit.
|
||||||
|
|
||||||
|
|||||||
@@ -111,25 +111,37 @@ defmodule BDS.Templates do
|
|||||||
|> Map.put(:updated_at, now)
|
|> Map.put(:updated_at, now)
|
||||||
|> Map.put(:status, next_status)
|
|> Map.put(:status, next_status)
|
||||||
|
|
||||||
Repo.transaction(fn ->
|
transaction_result =
|
||||||
updated_template =
|
Repo.transaction(fn ->
|
||||||
template
|
updated_template =
|
||||||
|> Template.changeset(updates)
|
template
|
||||||
|> Repo.update!()
|
|> Template.changeset(updates)
|
||||||
|
|> Repo.update!()
|
||||||
|
|
||||||
if slug_changed? do
|
affected_posts =
|
||||||
cascade_template_slug_change(template, updated_template, now)
|
if slug_changed? do
|
||||||
end
|
cascade_template_slug_change(template, updated_template, now)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
if template.file_path not in [nil, ""] and next_file_path != template.file_path do
|
{updated_template, affected_posts}
|
||||||
rewrite_template_file(template, updated_template)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
updated_template
|
case transaction_result do
|
||||||
end)
|
{:ok, {updated_template, affected_posts}} ->
|
||||||
|> case do
|
case sync_template_update_side_effects(
|
||||||
{:ok, updated_template} -> {:ok, updated_template}
|
template,
|
||||||
{:error, reason} -> {:error, reason}
|
updated_template,
|
||||||
|
affected_posts,
|
||||||
|
slug_changed?
|
||||||
|
) do
|
||||||
|
:ok -> {:ok, updated_template}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -215,7 +227,13 @@ defmodule BDS.Templates do
|
|||||||
%Template{file_path: file_path, status: status} = template
|
%Template{file_path: file_path, status: status} = template
|
||||||
when file_path not in [nil, ""] and status == :published ->
|
when file_path not in [nil, ""] and status == :published ->
|
||||||
full_path = full_file_path(template.project_id, template.file_path)
|
full_path = full_file_path(template.project_id, template.file_path)
|
||||||
:ok = Persistence.atomic_write(full_path, serialize_template_file(template, published_template_body(template)))
|
|
||||||
|
:ok =
|
||||||
|
Persistence.atomic_write(
|
||||||
|
full_path,
|
||||||
|
serialize_template_file(template, published_template_body(template))
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, template}
|
{:ok, template}
|
||||||
|
|
||||||
%Template{} ->
|
%Template{} ->
|
||||||
@@ -368,23 +386,43 @@ defmodule BDS.Templates do
|
|||||||
)
|
)
|
||||||
|> Repo.update_all(set: [post_template_slug: updated_template.slug, updated_at: updated_at])
|
|> Repo.update_all(set: [post_template_slug: updated_template.slug, updated_at: updated_at])
|
||||||
|
|
||||||
|
affected_posts
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sync_template_update_side_effects(
|
||||||
|
original_template,
|
||||||
|
updated_template,
|
||||||
|
affected_posts,
|
||||||
|
slug_changed?
|
||||||
|
) do
|
||||||
Enum.each(affected_posts, fn post ->
|
Enum.each(affected_posts, fn post ->
|
||||||
Posts.rewrite_published_post(post.id)
|
Posts.rewrite_published_post(post.id)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Tags.sync_tags_json(original_template.project_id)
|
if slug_changed? do
|
||||||
|
Tags.sync_tags_json(original_template.project_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if original_template.file_path not in [nil, ""] and
|
||||||
|
updated_template.file_path != original_template.file_path do
|
||||||
|
rewrite_template_file(original_template, updated_template)
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp rewrite_template_file(original_template, updated_template) do
|
defp rewrite_template_file(original_template, updated_template) do
|
||||||
body = published_template_body(original_template)
|
body = published_template_body(original_template)
|
||||||
new_full_path = full_file_path(updated_template.project_id, updated_template.file_path)
|
new_full_path = full_file_path(updated_template.project_id, updated_template.file_path)
|
||||||
:ok = Persistence.atomic_write(new_full_path, serialize_template_file(updated_template, body))
|
|
||||||
|
|
||||||
if original_template.file_path != updated_template.file_path do
|
result =
|
||||||
_ = delete_file_if_present(original_template.project_id, original_template.file_path)
|
Persistence.atomic_write(new_full_path, serialize_template_file(updated_template, body))
|
||||||
|
|
||||||
|
if result == :ok and original_template.file_path != updated_template.file_path do
|
||||||
|
delete_file_if_present(original_template.project_id, original_template.file_path)
|
||||||
|
else
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp published_template_body(%Template{content: content}) when is_binary(content), do: content
|
defp published_template_body(%Template{content: content}) when is_binary(content), do: content
|
||||||
@@ -444,7 +482,10 @@ defmodule BDS.Templates do
|
|||||||
template.file_path != "" and
|
template.file_path != "" and
|
||||||
not is_nil(template.file_path)
|
not is_nil(template.file_path)
|
||||||
)
|
)
|
||||||
|> Enum.reject(&(MapSet.member?(tracked_paths, &1.file_path) or File.exists?(full_file_path(project_id, &1.file_path))))
|
|> Enum.reject(
|
||||||
|
&(MapSet.member?(tracked_paths, &1.file_path) or
|
||||||
|
File.exists?(full_file_path(project_id, &1.file_path)))
|
||||||
|
)
|
||||||
|> Enum.each(fn template ->
|
|> Enum.each(fn template ->
|
||||||
clear_template_references(template)
|
clear_template_references(template)
|
||||||
Repo.delete!(template)
|
Repo.delete!(template)
|
||||||
|
|||||||
@@ -218,6 +218,52 @@ defmodule BDS.TemplatesTest do
|
|||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "update_template keeps committed database changes when renaming the published file fails",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
|
assert {:ok, template} =
|
||||||
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Article View",
|
||||||
|
kind: :post,
|
||||||
|
content: "<article>{{ content }}</article>"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published} = BDS.Templates.publish_template(template.id)
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Uses Template",
|
||||||
|
content: "Body",
|
||||||
|
template_slug: published.slug
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published_post} = BDS.Posts.publish_post(post.id)
|
||||||
|
|
||||||
|
assert {:ok, _tag} =
|
||||||
|
BDS.Tags.create_tag(%{
|
||||||
|
project_id: project.id,
|
||||||
|
name: "Feature",
|
||||||
|
post_template_slug: published.slug
|
||||||
|
})
|
||||||
|
|
||||||
|
blocked_path = Path.join([temp_dir, "templates", "feature-view.liquid.tmp"])
|
||||||
|
File.mkdir_p!(blocked_path)
|
||||||
|
|
||||||
|
assert {:error, _reason} =
|
||||||
|
BDS.Templates.update_template(published.id, %{slug: "feature-view"})
|
||||||
|
|
||||||
|
reloaded_template = Repo.get!(BDS.Templates.Template, published.id)
|
||||||
|
assert reloaded_template.slug == "feature-view"
|
||||||
|
assert reloaded_template.file_path == "templates/feature-view.liquid"
|
||||||
|
|
||||||
|
reloaded_post = Repo.get!(Post, published_post.id)
|
||||||
|
assert reloaded_post.template_slug == "feature-view"
|
||||||
|
|
||||||
|
reloaded_tag = Repo.get_by!(Tag, project_id: project.id, name: "Feature")
|
||||||
|
assert reloaded_tag.post_template_slug == "feature-view"
|
||||||
|
end
|
||||||
|
|
||||||
test "rebuild_templates_from_files recreates published templates from disk", %{
|
test "rebuild_templates_from_files recreates published templates from disk", %{
|
||||||
project: project,
|
project: project,
|
||||||
temp_dir: temp_dir
|
temp_dir: temp_dir
|
||||||
@@ -264,9 +310,10 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert template.updated_at == 202
|
assert template.updated_at == 202
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_templates_from_files removes stale published default templates when no local template files exist", %{
|
test "rebuild_templates_from_files removes stale published default templates when no local template files exist",
|
||||||
project: project
|
%{
|
||||||
} do
|
project: project
|
||||||
|
} do
|
||||||
now = BDS.Persistence.now_ms()
|
now = BDS.Persistence.now_ms()
|
||||||
|
|
||||||
stale_template =
|
stale_template =
|
||||||
|
|||||||
Reference in New Issue
Block a user