Add create_and_publish_template/1 (D2-2 spec gap)

This commit is contained in:
2026-05-30 14:51:44 +02:00
parent cb658aba1a
commit cf553e2f78
3 changed files with 115 additions and 1 deletions

View File

@@ -139,7 +139,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
| ID | Claim | Spec | Path | | ID | Claim | Spec | Path |
|---|---|---|---| |---|---|---|---|
| ~~D2-1~~ | ~~RemoveCategory rule~~ | ~~metadata.allium:100~~ | **Resolved:** 2 tests added in metadata_test.exs — remove_category removes category+settings from state/files/DB (6 assertions); remove_category is a no-op for non-existent category | | ~~D2-1~~ | ~~RemoveCategory rule~~ | ~~metadata.allium:100~~ | **Resolved:** 2 tests added in metadata_test.exs — remove_category removes category+settings from state/files/DB (6 assertions); remove_category is a no-op for non-existent category |
| D2-2 | CreateAndPublishTemplate rule | template.allium:105 | Write test: create+publish in one step | | ~~D2-2~~ | ~~CreateAndPublishTemplate rule~~ | template.allium:105 | **Resolved:** added `create_and_publish_template/1` (validates Liquid, creates published template + writs file), 3 tests added (happy path file assertions, invalid liquid rejection, slug dedup on title conflict) |
| D2-3 | CreateAndPublishScript rule | script.allium:160 | Write test: create+publish in one step | | D2-3 | CreateAndPublishScript rule | script.allium:160 | Write test: create+publish in one step |
| D2-4 | UniqueScriptSlug dedup | script.allium:115 | Write test: two scripts same title → dedup slug | | D2-4 | UniqueScriptSlug dedup | script.allium:115 | Write test: two scripts same title → dedup slug |
| D2-5 | FrontmatterRoundtrip invariant | post.allium:223 | Write test: write file, read back, assert all DB fields match | | D2-5 | FrontmatterRoundtrip invariant | post.allium:223 | Write test: write file, read back, assert all DB fields match |

View File

@@ -109,6 +109,54 @@ defmodule BDS.Templates do
end end
end end
@spec create_and_publish_template(attrs()) :: template_result()
def create_and_publish_template(attrs) do
project_id = attr(attrs, :project_id)
title = attr(attrs, :title) || ""
kind = attr(attrs, :kind)
content = attr(attrs, :content) || ""
case validate_liquid(content) do
:ok ->
slug = unique_slug(project_id, Slug.slugify(title), "template")
file_path = template_file_path(slug)
now = Persistence.now_ms()
changeset =
%Template{}
|> Template.changeset(%{
id: Ecto.UUID.generate(),
project_id: project_id,
slug: slug,
title: title,
kind: kind,
enabled: true,
version: 1,
file_path: file_path,
status: :published,
content: nil,
created_at: now,
updated_at: now
})
with {:ok, template} <- Repo.insert(changeset) do
full_path = full_file_path(template.project_id, file_path)
File.mkdir_p!(Path.dirname(full_path))
:ok =
Persistence.atomic_write(
full_path,
serialize_template_file(template, content)
)
{:ok, template}
end
{:error, reason} ->
{:error, {:invalid_liquid, reason}}
end
end
@spec update_template(String.t(), attrs()) :: template_result() | {:error, :not_found} @spec update_template(String.t(), attrs()) :: template_result() | {:error, :not_found}
def update_template(template_id, attrs) do def update_template(template_id, attrs) do
with %Template{} = template <- Repo.get(Template, template_id) do with %Template{} = template <- Repo.get(Template, template_id) do

View File

@@ -488,6 +488,72 @@ defmodule BDS.TemplatesTest do
assert template.updated_at == 202 assert template.updated_at == 202
end end
test "create_and_publish_template creates a published template with file in one step", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, template} =
BDS.Templates.create_and_publish_template(%{
project_id: project.id,
title: "Published Layout",
kind: :list,
content: "<section>{{ content }}</section>"
})
assert template.status == :published
assert template.content == nil
assert template.slug == "published-layout"
assert template.enabled == true
assert template.version == 1
assert template.file_path == "templates/published-layout.liquid"
full_path = Path.join(temp_dir, template.file_path)
assert File.exists?(full_path)
contents = File.read!(full_path)
assert contents =~ "---\nid: #{template.id}\n"
assert contents =~ "projectId: #{project.id}\n"
assert contents =~ "slug: published-layout\n"
assert contents =~ "title: Published Layout\n"
assert contents =~ "kind: list\n"
assert contents =~ "enabled: true\n"
assert contents =~ "version: 1\n"
assert contents =~ ~r/createdAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ ~r/updatedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/
assert contents =~ "\n---\n<section>{{ content }}</section>\n"
end
test "create_and_publish_template rejects invalid Liquid syntax", %{project: project} do
assert {:error, {:invalid_liquid, _reason}} =
BDS.Templates.create_and_publish_template(%{
project_id: project.id,
title: "Bad Layout",
kind: :list,
content: "{% for item in items %}unclosed"
})
end
test "create_and_publish_template deduplicates slug on title conflict", %{project: project} do
assert {:ok, _first} =
BDS.Templates.create_and_publish_template(%{
project_id: project.id,
title: "Same Title",
kind: :list,
content: "<section>v1</section>"
})
assert {:ok, second} =
BDS.Templates.create_and_publish_template(%{
project_id: project.id,
title: "Same Title",
kind: :list,
content: "<section>v2</section>"
})
assert second.slug == "same-title-2"
assert second.status == :published
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 project: project