diff --git a/SPECGAPS.md b/SPECGAPS.md index be5f6ac..ab08bdf 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -139,7 +139,7 @@ All reconciled to follow code. Specs must be self-consistent and match code. | 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-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-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 | diff --git a/lib/bds/templates.ex b/lib/bds/templates.ex index 9a67b01..33e7982 100644 --- a/lib/bds/templates.ex +++ b/lib/bds/templates.ex @@ -109,6 +109,54 @@ defmodule BDS.Templates do 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} def update_template(template_id, attrs) do with %Template{} = template <- Repo.get(Template, template_id) do diff --git a/test/bds/templates_test.exs b/test/bds/templates_test.exs index 0e97dde..8c877e9 100644 --- a/test/bds/templates_test.exs +++ b/test/bds/templates_test.exs @@ -488,6 +488,72 @@ defmodule BDS.TemplatesTest do assert template.updated_at == 202 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: "
{{ content }}
" + }) + + 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
{{ content }}
\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: "
v1
" + }) + + assert {:ok, second} = + BDS.Templates.create_and_publish_template(%{ + project_id: project.id, + title: "Same Title", + kind: :list, + content: "
v2
" + }) + + 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", %{ project: project