From b90a4569da184cc5512a83daedcd5449a4f38ac7 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 25 Apr 2026 11:35:36 +0200 Subject: [PATCH] fix: more fixes to file formats --- lib/bds/frontmatter.ex | 5 +- lib/bds/media.ex | 65 +++++++++----- lib/bds/menu.ex | 50 ++++++++--- lib/bds/metadata.ex | 127 ++++++++++++++++++++++++++-- lib/bds/posts.ex | 55 ++++++------ lib/bds/scripting/capabilities.ex | 8 +- lib/bds/scripts.ex | 34 ++++++-- lib/bds/sidecar.ex | 5 +- lib/bds/tags.ex | 20 ++--- lib/bds/templates.ex | 32 +++++-- test/bds/media_test.exs | 23 ++--- test/bds/menu_test.exs | 22 +++-- test/bds/metadata_test.exs | 119 +++++++++++++++++++++----- test/bds/post_translations_test.exs | 9 +- test/bds/posts_test.exs | 22 ++--- test/bds/scripts_test.exs | 10 ++- test/bds/tags_test.exs | 36 ++++---- test/bds/templates_test.exs | 16 ++-- 18 files changed, 477 insertions(+), 181 deletions(-) diff --git a/lib/bds/frontmatter.ex b/lib/bds/frontmatter.ex index fae0d63..24ce7af 100644 --- a/lib/bds/frontmatter.ex +++ b/lib/bds/frontmatter.ex @@ -175,5 +175,8 @@ defmodule BDS.Frontmatter do end end - defp timestamp_key?(key), do: String.ends_with?(to_string(key), "_at") + defp timestamp_key?(key) do + rendered = to_string(key) + String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At") + end end diff --git a/lib/bds/media.ex b/lib/bds/media.ex index d393fe9..feb3d93 100644 --- a/lib/bds/media.ex +++ b/lib/bds/media.ex @@ -341,6 +341,7 @@ defmodule BDS.Media do defp upsert_media_from_sidecar(project, sidecar_path) do {:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document() + fields = normalize_media_sidecar_fields(fields) relative_sidecar_path = Path.relative_to(sidecar_path, Projects.project_data_dir(project)) relative_file_path = String.trim_trailing(relative_sidecar_path, ".meta") filename = Path.basename(relative_file_path) @@ -386,20 +387,21 @@ defmodule BDS.Media do atomic_write( path, Sidecar.serialize_document([ - {:id, media.id}, - {:original_name, media.original_name}, - {:mime_type, media.mime_type}, - {:size, media.size}, - {:width, media.width}, - {:height, media.height}, - {:title, media.title}, - {:alt, media.alt}, - {:caption, media.caption}, - {:author, media.author}, - {:language, media.language}, - {:created_at, media.created_at}, - {:updated_at, media.updated_at}, - {:tags, media.tags || []} + {"id", media.id}, + {"originalName", media.original_name}, + {"mimeType", media.mime_type}, + {"size", media.size}, + {"width", media.width}, + {"height", media.height}, + {"title", media.title}, + {"alt", media.alt}, + {"caption", media.caption}, + {"author", media.author}, + {"language", media.language}, + {"createdAt", media.created_at}, + {"updatedAt", media.updated_at}, + {"linkedPostIds", linked_post_ids(media.id)}, + {"tags", media.tags || []} ]) ) end @@ -416,11 +418,11 @@ defmodule BDS.Media do atomic_write( path, Sidecar.serialize_document([ - {:translation_for, media.id}, - {:language, translation.language}, - {:title, translation.title}, - {:alt, translation.alt}, - {:caption, translation.caption} + {"translationFor", media.id}, + {"language", translation.language}, + {"title", translation.title}, + {"alt", translation.alt}, + {"caption", translation.caption} ]) ) end @@ -434,6 +436,7 @@ defmodule BDS.Media do media -> {:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document() + fields = normalize_media_sidecar_fields(fields) now = Persistence.now_ms() language = Map.fetch!(fields, "language") @@ -607,6 +610,30 @@ defmodule BDS.Media do Persistence.atomic_write(path, contents) end + defp normalize_media_sidecar_fields(fields) when is_map(fields) do + [ + {"originalName", "original_name"}, + {"mimeType", "mime_type"}, + {"createdAt", "created_at"}, + {"updatedAt", "updated_at"}, + {"translationFor", "translation_for"}, + {"linkedPostIds", "linked_post_ids"} + ] + |> Enum.reduce(fields, fn {file_key, current_key}, acc -> + case Map.fetch(acc, file_key) do + {:ok, value} -> Map.put_new(acc, current_key, value) + :error -> acc + end + end) + end + + defp linked_post_ids(media_id) do + case Repo.query("SELECT post_id FROM post_media WHERE media_id = ? ORDER BY sort_order ASC, post_id ASC", [media_id]) do + {:ok, %{rows: rows}} -> Enum.map(rows, fn [post_id] -> post_id end) + {:error, _reason} -> [] + end + end + defp blank_to_nil(nil), do: nil defp blank_to_nil(""), do: nil defp blank_to_nil(value), do: value diff --git a/lib/bds/menu.ex b/lib/bds/menu.ex index 902e515..c1485a2 100644 --- a/lib/bds/menu.ex +++ b/lib/bds/menu.ex @@ -128,11 +128,8 @@ defmodule BDS.Menu do end defp render_attributes(item) do - [ - {"kind", item.kind}, - {"text", item.label}, - {"slug", item.slug} - ] + item + |> render_outline_attributes() |> Enum.reject(fn {_key, value} -> value in [nil, ""] end) |> Enum.map_join("", fn {key, value} -> ~s( #{key}="#{xml_escape(to_string(value))}") end) end @@ -145,12 +142,12 @@ defmodule BDS.Menu do end defp parse_outline(element) do - kind = element |> xml_attr(:kind) |> normalize_kind() + kind = element |> outline_kind() |> normalize_kind() base = %{ kind: kind, label: xml_attr(element, :text) || "", - slug: normalize_optional_string(xml_attr(element, :slug)) + slug: element |> outline_slug(kind) |> normalize_optional_string() } children = @@ -164,6 +161,33 @@ defmodule BDS.Menu do end end + defp render_outline_attributes(item) do + kind = Map.get(item, :kind) + + [ + {"text", item.label}, + {"type", render_outline_kind(kind)}, + {"pageSlug", render_page_slug(kind, item.slug)}, + {"categoryName", render_category_name(kind, item.slug)} + ] + end + + defp outline_kind(element), do: xml_attr(element, :type) || xml_attr(element, :kind) + + defp outline_slug(element, :category_archive), do: xml_attr(element, :categoryName) || xml_attr(element, :slug) + defp outline_slug(element, :home), do: xml_attr(element, :pageSlug) || xml_attr(element, :slug) + defp outline_slug(element, _kind), do: xml_attr(element, :pageSlug) || xml_attr(element, :slug) + + defp render_outline_kind(:category_archive), do: "category-archive" + defp render_outline_kind(kind), do: to_string(kind) + + defp render_page_slug(:home, _slug), do: "home" + defp render_page_slug(kind, slug) when kind in [:page], do: slug + defp render_page_slug(_kind, _slug), do: nil + + defp render_category_name(:category_archive, slug), do: slug + defp render_category_name(_kind, _slug), do: nil + defp xml_attr(element, name) do element |> xmlElement(:attributes) @@ -176,10 +200,16 @@ defmodule BDS.Menu do defp normalize_kind(kind) when is_atom(kind) and kind in @valid_kinds, do: kind + defp normalize_kind(nil), do: :page + defp normalize_kind(kind) when is_binary(kind) do - kind - |> String.to_existing_atom() - |> normalize_kind() + case kind do + "category-archive" -> :category_archive + other -> + other + |> String.to_existing_atom() + |> normalize_kind() + end rescue _error -> :page end diff --git a/lib/bds/metadata.ex b/lib/bds/metadata.ex index f463a7f..e2128bb 100644 --- a/lib/bds/metadata.ex +++ b/lib/bds/metadata.ex @@ -157,8 +157,8 @@ defmodule BDS.Metadata do persist_setting(project_id, "category_meta", category_meta_from_files, now) persist_setting(project_id, "publishing", publishing_from_files, now) write_project_json(updated_project, project_metadata_from_files) - write_categories_json(updated_project, categories_from_files["categories"] || @default_categories) - write_category_meta_json(updated_project, category_meta_from_files["categories"] || %{}) + write_categories_json(updated_project, normalized_categories(categories_from_files)) + write_category_meta_json(updated_project, normalized_category_settings(category_meta_from_files)) write_publishing_json(updated_project, publishing_from_files) load_state(updated_project) end) @@ -248,7 +248,8 @@ defmodule BDS.Metadata do "post_template_slug" => Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")), "list_template_slug" => - Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug")) + Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug")), + "title" => Map.get(settings, :title, Map.get(settings, "title")) } end @@ -284,18 +285,18 @@ defmodule BDS.Metadata do end defp write_project_json(project, project_json), - do: write_json(project, "project.json", project_json) + do: write_json(project, "project.json", render_project_metadata_json(project_json)) defp write_categories_json(project, categories) do - write_json(project, "categories.json", %{"categories" => Enum.sort(categories)}) + write_json(project, "categories.json", Enum.sort(categories)) end defp write_category_meta_json(project, category_settings) do - write_json(project, "category-meta.json", %{"categories" => category_settings}) + write_json(project, "category-meta.json", render_category_meta_json(category_settings)) end defp write_publishing_json(project, publishing_preferences) do - write_json(project, "publishing.json", publishing_preferences) + write_json(project, "publishing.json", render_publishing_json(publishing_preferences)) end defp write_json(project, file_name, payload) do @@ -308,11 +309,121 @@ defmodule BDS.Metadata do path = Path.join([Projects.project_data_dir(project), "meta", file_name]) case File.read(path) do - {:ok, contents} -> Jason.decode!(contents) + {:ok, contents} -> contents |> Jason.decode!() |> normalize_json(file_name) {:error, :enoent} -> nil end end + defp normalize_json(payload, "project.json"), do: parse_project_metadata_json(payload) + defp normalize_json(payload, "categories.json"), do: parse_categories_json(payload) + defp normalize_json(payload, "category-meta.json"), do: parse_category_meta_json(payload) + defp normalize_json(payload, "publishing.json"), do: parse_publishing_json(payload) + defp normalize_json(payload, _file_name), do: payload + + defp parse_project_metadata_json(payload) when is_map(payload) do + %{ + "name" => Map.get(payload, "name"), + "description" => Map.get(payload, "description"), + "public_url" => Map.get(payload, "publicUrl"), + "main_language" => Map.get(payload, "mainLanguage"), + "default_author" => Map.get(payload, "defaultAuthor"), + "max_posts_per_page" => Map.get(payload, "maxPostsPerPage", @default_max_posts_per_page), + "blogmark_category" => Map.get(payload, "blogmarkCategory"), + "pico_theme" => Map.get(payload, "picoTheme"), + "semantic_similarity_enabled" => Map.get(payload, "semanticSimilarityEnabled", false), + "blog_languages" => Map.get(payload, "blogLanguages", []) + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() + end + + defp parse_categories_json(payload) when is_list(payload), do: %{"categories" => payload} + defp parse_categories_json(_payload), do: %{"categories" => @default_categories} + + defp parse_category_meta_json(payload) when is_map(payload) do + %{"categories" => normalized_category_settings(payload)} + end + + defp parse_publishing_json(payload) when is_map(payload) do + %{ + "ssh_host" => Map.get(payload, "sshHost"), + "ssh_user" => Map.get(payload, "sshUser"), + "ssh_remote_path" => Map.get(payload, "sshRemotePath"), + "ssh_mode" => Map.get(payload, "sshMode", "scp") + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() + end + + defp normalized_categories(%{"categories" => categories}) when is_list(categories), do: categories + defp normalized_categories(categories) when is_list(categories), do: categories + defp normalized_categories(_payload), do: @default_categories + + defp normalized_category_settings(%{"categories" => settings}) when is_map(settings), + do: normalized_category_settings(settings) + + defp normalized_category_settings(settings) when is_map(settings) do + Map.new(settings, fn {category, category_settings} -> + {category, + %{ + "render_in_lists" => + Map.get(category_settings, "render_in_lists", Map.get(category_settings, "renderInLists", true)), + "show_title" => + Map.get(category_settings, "show_title", Map.get(category_settings, "showTitle", true)), + "post_template_slug" => + Map.get(category_settings, "post_template_slug", Map.get(category_settings, "postTemplateSlug")), + "list_template_slug" => + Map.get(category_settings, "list_template_slug", Map.get(category_settings, "listTemplateSlug")), + "title" => Map.get(category_settings, "title") + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new()} + end) + end + + defp render_project_metadata_json(project_metadata) when is_map(project_metadata) do + %{ + "name" => Map.get(project_metadata, "name"), + "description" => Map.get(project_metadata, "description"), + "publicUrl" => Map.get(project_metadata, "public_url"), + "mainLanguage" => Map.get(project_metadata, "main_language"), + "defaultAuthor" => Map.get(project_metadata, "default_author"), + "maxPostsPerPage" => Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page), + "blogmarkCategory" => Map.get(project_metadata, "blogmark_category"), + "picoTheme" => Map.get(project_metadata, "pico_theme"), + "semanticSimilarityEnabled" => Map.get(project_metadata, "semantic_similarity_enabled", false), + "blogLanguages" => Map.get(project_metadata, "blog_languages", []) + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() + end + + defp render_category_meta_json(category_settings) when is_map(category_settings) do + Map.new(category_settings, fn {category, settings} -> + {category, + %{ + "renderInLists" => Map.get(settings, "render_in_lists", true), + "showTitle" => Map.get(settings, "show_title", true), + "postTemplateSlug" => Map.get(settings, "post_template_slug"), + "listTemplateSlug" => Map.get(settings, "list_template_slug"), + "title" => Map.get(settings, "title") + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new()} + end) + end + + defp render_publishing_json(publishing_preferences) when is_map(publishing_preferences) do + %{ + "sshHost" => Map.get(publishing_preferences, "ssh_host"), + "sshUser" => Map.get(publishing_preferences, "ssh_user"), + "sshRemotePath" => Map.get(publishing_preferences, "ssh_remote_path"), + "sshMode" => Map.get(publishing_preferences, "ssh_mode", "scp") + } + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() + end + defp load_setting(project_id, suffix) do case Repo.get(Setting, setting_key(project_id, suffix)) do nil -> nil diff --git a/lib/bds/posts.ex b/lib/bds/posts.ex index f6963bb..7e34fea 100644 --- a/lib/bds/posts.ex +++ b/lib/bds/posts.ex @@ -605,20 +605,20 @@ defmodule BDS.Posts do defp serialize_post_file(post, published_at) do Frontmatter.serialize_document( [ - {:id, post.id}, - {:title, post.title}, - {:slug, post.slug}, - {:excerpt, post.excerpt}, - {:status, :published}, - {:author, post.author}, - {:language, post.language}, - {:do_not_translate, post.do_not_translate}, - {:template_slug, post.template_slug}, - {:created_at, post.created_at}, - {:updated_at, post.updated_at}, - {:published_at, published_at}, - {:tags, post.tags || []}, - {:categories, post.categories || []} + {"id", post.id}, + {"title", post.title}, + {"slug", post.slug}, + {"excerpt", post.excerpt}, + {"status", :published}, + {"author", post.author}, + {"language", post.language}, + {"doNotTranslate", post.do_not_translate}, + {"templateSlug", post.template_slug}, + {"createdAt", post.created_at}, + {"updatedAt", post.updated_at}, + {"publishedAt", published_at}, + {"tags", post.tags || []}, + {"categories", post.categories || []} ], post.content ) @@ -641,9 +641,8 @@ defmodule BDS.Posts do end defp upsert_post_from_file(project_id, project, path) do - project - |> parse_rebuild_file(path) - |> upsert_post_from_rebuild_file(project_id) + rebuild_file = parse_rebuild_file(project, path) + upsert_post_from_rebuild_file(project_id, rebuild_file) end defp upsert_post_from_rebuild_file(project_id, rebuild_file) do @@ -749,8 +748,8 @@ defmodule BDS.Posts do {"updatedAt", "updated_at"}, {"publishedAt", "published_at"} ] - |> Enum.reduce(fields, fn {legacy_key, current_key}, acc -> - case Map.fetch(acc, legacy_key) do + |> Enum.reduce(fields, fn {file_key, current_key}, acc -> + case Map.fetch(acc, file_key) do {:ok, value} -> Map.put_new(acc, current_key, normalize_rebuild_field_value(current_key, value)) :error -> acc end @@ -875,15 +874,15 @@ defmodule BDS.Posts do defp serialize_translation_file(translation, published_at) do Frontmatter.serialize_document( [ - {:id, translation.id}, - {:translation_for, translation.translation_for}, - {:language, translation.language}, - {:title, translation.title}, - {:excerpt, translation.excerpt}, - {:status, :published}, - {:created_at, translation.created_at}, - {:updated_at, translation.updated_at}, - {:published_at, published_at} + {"id", translation.id}, + {"translationFor", translation.translation_for}, + {"language", translation.language}, + {"title", translation.title}, + {"excerpt", translation.excerpt}, + {"status", :published}, + {"createdAt", translation.created_at}, + {"updatedAt", translation.updated_at}, + {"publishedAt", published_at} ], translation.content ) diff --git a/lib/bds/scripting/capabilities.ex b/lib/bds/scripting/capabilities.ex index 666782a..c7ad4d6 100644 --- a/lib/bds/scripting/capabilities.ex +++ b/lib/bds/scripting/capabilities.ex @@ -3,6 +3,8 @@ defmodule BDS.Scripting.Capabilities do import Ecto.Query + @compiled_env Application.compile_env(:bds, :current_env, Mix.env()) + alias BDS.AI alias BDS.Desktop.FolderPicker alias BDS.Desktop.MenuBar @@ -1704,6 +1706,10 @@ defmodule BDS.Scripting.Capabilities do end defp test_mode? do - Application.get_env(:bds, :test_mode, false) + Application.get_env(:bds, :test_mode, false) or current_env() == :test + end + + defp current_env do + Application.get_env(:bds, :current_env_override) || @compiled_env end end diff --git a/lib/bds/scripts.ex b/lib/bds/scripts.ex index c42a061..1a7bd37 100644 --- a/lib/bds/scripts.ex +++ b/lib/bds/scripts.ex @@ -174,15 +174,16 @@ defmodule BDS.Scripts do defp serialize_script_file(script, content) do Frontmatter.serialize_document( [ - {:id, script.id}, - {:slug, script.slug}, - {:title, script.title}, - {:kind, script.kind}, - {:entrypoint, script.entrypoint}, - {:enabled, script.enabled}, - {:version, script.version}, - {:created_at, script.created_at}, - {:updated_at, script.updated_at} + {"id", script.id}, + {"projectId", script.project_id}, + {"slug", script.slug}, + {"title", script.title}, + {"kind", script.kind}, + {"entrypoint", script.entrypoint}, + {"enabled", script.enabled}, + {"version", script.version}, + {"createdAt", script.created_at}, + {"updatedAt", script.updated_at} ], content ) @@ -203,6 +204,7 @@ defmodule BDS.Scripts do defp upsert_script_from_file(project_id, project, path) do contents = File.read!(path) {:ok, %{fields: fields}} = Frontmatter.parse_document(contents) + fields = normalize_script_fields(fields) relative_path = Path.relative_to(path, Projects.project_data_dir(project)) now = Persistence.now_ms() @@ -234,6 +236,20 @@ defmodule BDS.Scripts do defp parse_script_kind("utility"), do: :utility defp parse_script_kind("transform"), do: :transform + defp normalize_script_fields(fields) when is_map(fields) do + [ + {"createdAt", "created_at"}, + {"updatedAt", "updated_at"}, + {"projectId", "project_id"} + ] + |> Enum.reduce(fields, fn {file_key, current_key}, acc -> + case Map.fetch(acc, file_key) do + {:ok, value} -> Map.put_new(acc, current_key, value) + :error -> acc + end + end) + end + defp list_matching_files(dir, pattern) do if File.dir?(dir) do Path.join(dir, pattern) diff --git a/lib/bds/sidecar.ex b/lib/bds/sidecar.ex index 6429180..b52400b 100644 --- a/lib/bds/sidecar.ex +++ b/lib/bds/sidecar.ex @@ -145,5 +145,8 @@ defmodule BDS.Sidecar do end end - defp timestamp_key?(key), do: String.ends_with?(to_string(key), "_at") + defp timestamp_key?(key) do + rendered = to_string(key) + String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At") + end end diff --git a/lib/bds/tags.ex b/lib/bds/tags.ex index 2266a6c..2ba07bd 100644 --- a/lib/bds/tags.ex +++ b/lib/bds/tags.ex @@ -215,17 +215,15 @@ defmodule BDS.Tags do path = Path.join([Projects.project_data_dir(project), "meta", "tags.json"]) :ok = File.mkdir_p(Path.dirname(path)) - payload = %{ - "tags" => - project_id - |> list_tags() - |> Enum.sort_by(&String.downcase(&1.name)) - |> Enum.map(fn tag -> - %{"name" => tag.name} - |> maybe_put("color", tag.color) - |> maybe_put("post_template_slug", tag.post_template_slug) - end) - } + payload = + project_id + |> list_tags() + |> Enum.sort_by(&String.downcase(&1.name)) + |> Enum.map(fn tag -> + %{"name" => tag.name} + |> maybe_put("color", tag.color) + |> maybe_put("postTemplateSlug", tag.post_template_slug) + end) :ok = Persistence.atomic_write(path, Jason.encode!(payload)) end diff --git a/lib/bds/templates.ex b/lib/bds/templates.ex index afa2cda..4f88bc2 100644 --- a/lib/bds/templates.ex +++ b/lib/bds/templates.ex @@ -219,14 +219,15 @@ defmodule BDS.Templates do defp serialize_template_file(template, content) do Frontmatter.serialize_document( [ - {:id, template.id}, - {:slug, template.slug}, - {:title, template.title}, - {:kind, template.kind}, - {:enabled, template.enabled}, - {:version, template.version}, - {:created_at, template.created_at}, - {:updated_at, template.updated_at} + {"id", template.id}, + {"projectId", template.project_id}, + {"slug", template.slug}, + {"title", template.title}, + {"kind", template.kind}, + {"enabled", template.enabled}, + {"version", template.version}, + {"createdAt", template.created_at}, + {"updatedAt", template.updated_at} ], content ) @@ -342,6 +343,7 @@ defmodule BDS.Templates do defp upsert_template_from_file(project_id, project, path) do contents = File.read!(path) {:ok, %{fields: fields}} = Frontmatter.parse_document(contents) + fields = normalize_template_fields(fields) relative_path = Path.relative_to(path, Projects.project_data_dir(project)) now = Persistence.now_ms() @@ -373,6 +375,20 @@ defmodule BDS.Templates do defp parse_template_kind("not_found"), do: :not_found defp parse_template_kind("partial"), do: :partial + defp normalize_template_fields(fields) when is_map(fields) do + [ + {"createdAt", "created_at"}, + {"updatedAt", "updated_at"}, + {"projectId", "project_id"} + ] + |> Enum.reduce(fields, fn {file_key, current_key}, acc -> + case Map.fetch(acc, file_key) do + {:ok, value} -> Map.put_new(acc, current_key, value) + :error -> acc + end + end) + end + defp list_matching_files(dir, pattern) do if File.dir?(dir) do Path.join(dir, pattern) diff --git a/test/bds/media_test.exs b/test/bds/media_test.exs index a9edd8f..3037452 100644 --- a/test/bds/media_test.exs +++ b/test/bds/media_test.exs @@ -43,16 +43,17 @@ defmodule BDS.MediaTest do sidecar = File.read!(Path.join(temp_dir, media.sidecar_path)) assert sidecar =~ "id: #{media.id}\n" - assert sidecar =~ "original_name: sample.txt\n" - assert sidecar =~ "mime_type: text/plain\n" + assert sidecar =~ "originalName: sample.txt\n" + assert sidecar =~ "mimeType: text/plain\n" assert sidecar =~ "title: Sample\n" assert sidecar =~ "alt: Alt text\n" assert sidecar =~ "caption: Caption\n" assert sidecar =~ "author: Writer\n" assert sidecar =~ "language: en\n" assert sidecar =~ "tags:\n - alpha\n" - assert sidecar =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ - assert sidecar =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ + assert sidecar =~ "linkedPostIds:\n" + assert sidecar =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ + assert sidecar =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ refute File.exists?(Path.join(temp_dir, media.sidecar_path <> ".tmp")) end @@ -130,8 +131,8 @@ defmodule BDS.MediaTest do sidecar_path, [ "id: media-from-file", - "original_name: original.jpg", - "mime_type: image/jpeg", + "originalName: original.jpg", + "mimeType: image/jpeg", "size: #{byte_size(tiny_jpeg_binary())}", "width: 3", "height: 2", @@ -140,8 +141,10 @@ defmodule BDS.MediaTest do "caption: Recovered caption", "author: Writer", "language: en", - "created_at: 2024-03-30T21:20:00.000Z", - "updated_at: 2024-03-31T21:20:00.000Z", + "createdAt: 2024-03-30T21:20:00.000Z", + "updatedAt: 2024-03-31T21:20:00.000Z", + "linkedPostIds:", + " - post-a", "tags:", " - alpha", "" @@ -152,7 +155,7 @@ defmodule BDS.MediaTest do File.write!( binary_path <> ".de.meta", [ - "translation_for: media-from-file", + "translationFor: media-from-file", "language: de", "title: Titel", "alt: Alt text", @@ -371,7 +374,7 @@ defmodule BDS.MediaTest do translated_sidecar_path = Path.join(temp_dir, media.file_path <> ".de.meta") contents = File.read!(translated_sidecar_path) - assert contents =~ "translation_for: #{media.id}\n" + assert contents =~ "translationFor: #{media.id}\n" assert contents =~ "language: de\n" assert contents =~ "title: Titel\n" assert contents =~ "alt: Alt text\n" diff --git a/test/bds/menu_test.exs b/test/bds/menu_test.exs index fbe19f7..32bd31e 100644 --- a/test/bds/menu_test.exs +++ b/test/bds/menu_test.exs @@ -44,16 +44,16 @@ defmodule BDS.MenuTest do assert File.exists?(opml_path) contents = File.read!(opml_path) - assert contents =~ ~s(), ~s(), + ~s( ), + ~s( Blog Menu), + ~s( ), ~s( ), - ~s( ), - ~s( ), - ~s( ), + ~s( ), + ~s( ), + ~s( ), + ~s( ), ~s( ), ~s( ), ~s() @@ -80,12 +84,12 @@ defmodule BDS.MenuTest do assert menu.items == [ %{kind: :home, label: "Home", slug: nil}, - %{kind: :page, label: "Blog", slug: "blog"}, %{ kind: :submenu, label: "Topics", slug: nil, children: [ + %{kind: :page, label: "Blog", slug: "blog"}, %{kind: :category_archive, label: "Elixir", slug: "elixir"} ] } diff --git a/test/bds/metadata_test.exs b/test/bds/metadata_test.exs index 9c0dea6..45a1519 100644 --- a/test/bds/metadata_test.exs +++ b/test/bds/metadata_test.exs @@ -38,14 +38,14 @@ defmodule BDS.MetadataTest do assert %{ "name" => "Renamed Blog", "description" => "Description", - "public_url" => "https://example.com", - "main_language" => "en", - "default_author" => "Writer", - "max_posts_per_page" => 25, - "blogmark_category" => "links", - "pico_theme" => "blue", - "semantic_similarity_enabled" => true, - "blog_languages" => ["de", "fr"] + "publicUrl" => "https://example.com", + "mainLanguage" => "en", + "defaultAuthor" => "Writer", + "maxPostsPerPage" => 25, + "blogmarkCategory" => "links", + "picoTheme" => "blue", + "semanticSimilarityEnabled" => true, + "blogLanguages" => ["de", "fr"] } = Jason.decode!(File.read!(project_json_path)) assert {:ok, loaded} = BDS.Metadata.get_project_metadata(project.id) @@ -78,25 +78,23 @@ defmodule BDS.MetadataTest do category_meta_path = Path.join([temp_dir, "meta", "category-meta.json"]) publishing_path = Path.join([temp_dir, "meta", "publishing.json"]) - assert %{"categories" => ["article", "aside", "news", "page", "picture"]} = + assert ["article", "aside", "news", "page", "picture"] = Jason.decode!(File.read!(categories_path)) assert %{ - "categories" => %{ - "news" => %{ - "render_in_lists" => false, - "show_title" => true, - "post_template_slug" => "article", - "list_template_slug" => "listing" - } + "news" => %{ + "renderInLists" => false, + "showTitle" => true, + "postTemplateSlug" => "article", + "listTemplateSlug" => "listing" } } = Jason.decode!(File.read!(category_meta_path)) assert %{ - "ssh_host" => "example.com", - "ssh_user" => "deploy", - "ssh_remote_path" => "/srv/site", - "ssh_mode" => "rsync" + "sshHost" => "example.com", + "sshUser" => "deploy", + "sshRemotePath" => "/srv/site", + "sshMode" => "rsync" } = Jason.decode!(File.read!(publishing_path)) assert {:ok, synced} = BDS.Metadata.sync_project_metadata_from_filesystem(project.id) @@ -117,6 +115,87 @@ defmodule BDS.MetadataTest do } end + test "sync_project_metadata_from_filesystem reads canonical bDS metadata file shapes", %{ + project: project, + temp_dir: temp_dir + } do + meta_dir = Path.join(temp_dir, "meta") + File.mkdir_p!(meta_dir) + + File.write!( + Path.join(meta_dir, "project.json"), + Jason.encode!(%{ + "name" => "Legacy Blog", + "description" => "Imported", + "publicUrl" => "https://legacy.example", + "mainLanguage" => "de", + "defaultAuthor" => "Legacy Writer", + "maxPostsPerPage" => 17, + "blogmarkCategory" => "aside", + "picoTheme" => "slate", + "semanticSimilarityEnabled" => true, + "blogLanguages" => ["en"] + }) + ) + + File.write!( + Path.join(meta_dir, "categories.json"), + Jason.encode!(["article", "aside", "legacy", "page", "picture"]) + ) + + File.write!( + Path.join(meta_dir, "category-meta.json"), + Jason.encode!(%{ + "legacy" => %{ + "renderInLists" => false, + "showTitle" => true, + "postTemplateSlug" => "feature-view", + "listTemplateSlug" => "feature-list", + "title" => "Legacy" + } + }) + ) + + File.write!( + Path.join(meta_dir, "publishing.json"), + Jason.encode!(%{ + "sshHost" => "legacy.example", + "sshUser" => "deploy", + "sshRemotePath" => "/srv/legacy", + "sshMode" => "rsync" + }) + ) + + assert {:ok, synced} = BDS.Metadata.sync_project_metadata_from_filesystem(project.id) + + assert synced.name == "Legacy Blog" + assert synced.description == "Imported" + assert synced.public_url == "https://legacy.example" + assert synced.main_language == "de" + assert synced.default_author == "Legacy Writer" + assert synced.max_posts_per_page == 17 + assert synced.blogmark_category == "aside" + assert synced.pico_theme == "slate" + assert synced.semantic_similarity_enabled == true + assert synced.blog_languages == ["en"] + assert synced.categories == ["article", "aside", "legacy", "page", "picture"] + + assert synced.category_settings["legacy"] == %{ + "render_in_lists" => false, + "show_title" => true, + "post_template_slug" => "feature-view", + "list_template_slug" => "feature-list", + "title" => "Legacy" + } + + assert synced.publishing_preferences == %{ + "ssh_host" => "legacy.example", + "ssh_user" => "deploy", + "ssh_remote_path" => "/srv/legacy", + "ssh_mode" => "rsync" + } + end + test "enabling semantic similarity backfills embeddings for existing published posts", %{ project: project } do diff --git a/test/bds/post_translations_test.exs b/test/bds/post_translations_test.exs index 10e9756..4ef4ff9 100644 --- a/test/bds/post_translations_test.exs +++ b/test/bds/post_translations_test.exs @@ -56,6 +56,7 @@ defmodule BDS.PostTranslationsTest do assert File.exists?(translation_path) translation_contents = File.read!(translation_path) + assert translation_contents =~ "translationFor: #{post.id}\n" assert translation_contents =~ "title: Kanonischer Beitrag\n" assert translation_contents =~ "language: de\n" assert translation_contents =~ "status: published\n" @@ -124,13 +125,13 @@ defmodule BDS.PostTranslationsTest do [ "---", "id: orphan-translation", - "translation_for: missing-post", + "translationFor: missing-post", "language: fr", "title: Orpheline", "status: published", - "created_at: 1711843200", - "updated_at: 1711929600", - "published_at: 1712016000", + "createdAt: 1711843200", + "updatedAt: 1711929600", + "publishedAt: 1712016000", "---", "Texte orphelin", "" diff --git a/test/bds/posts_test.exs b/test/bds/posts_test.exs index 4ad7a36..d43d86b 100644 --- a/test/bds/posts_test.exs +++ b/test/bds/posts_test.exs @@ -147,13 +147,13 @@ defmodule BDS.PostsTest do assert file_contents =~ "excerpt: Summary\n" assert file_contents =~ "author: Writer\n" assert file_contents =~ "language: en\n" - assert file_contents =~ "do_not_translate: true\n" - assert file_contents =~ "template_slug: article\n" + assert file_contents =~ "doNotTranslate: true\n" + assert file_contents =~ "templateSlug: article\n" assert file_contents =~ "tags:\n - alpha\n" assert file_contents =~ "categories:\n - notes\n" - assert file_contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ - assert file_contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ - assert file_contents =~ ~r/published_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ + assert file_contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ + assert file_contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ + assert file_contents =~ ~r/publishedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ assert file_contents =~ "\n---\nHello from markdown\n" refute File.exists?(full_path <> ".tmp") @@ -274,11 +274,11 @@ defmodule BDS.PostsTest do "status: published", "author: Writer", "language: en", - "do_not_translate: true", - "template_slug: article", - "created_at: 2024-03-30T21:20:00.000Z", - "updated_at: 2024-03-31T21:20:00.000Z", - "published_at: 2024-04-01T21:20:00.000Z", + "doNotTranslate: true", + "templateSlug: article", + "createdAt: 2024-03-30T21:20:00.000Z", + "updatedAt: 2024-03-31T21:20:00.000Z", + "publishedAt: 2024-04-01T21:20:00.000Z", "tags:", " - alpha", "categories:", @@ -313,7 +313,7 @@ defmodule BDS.PostsTest do assert post.content == nil end - test "rebuild_posts_from_files imports legacy old-app translation files alongside canonical posts" do + test "rebuild_posts_from_files imports canonical bDS translation files alongside canonical posts" do temp_dir = Path.join(System.tmp_dir!(), "bds-post-rebuild-legacy-#{System.unique_integer([:positive])}") diff --git a/test/bds/scripts_test.exs b/test/bds/scripts_test.exs index 65e7299..bb3632e 100644 --- a/test/bds/scripts_test.exs +++ b/test/bds/scripts_test.exs @@ -65,14 +65,15 @@ defmodule BDS.ScriptsTest do contents = File.read!(full_path) assert contents =~ "---\nid: #{published.id}\n" + assert contents =~ "projectId: #{project.id}\n" assert contents =~ "slug: process-feed\n" assert contents =~ "title: Process Feed\n" assert contents =~ "kind: utility\n" assert contents =~ "entrypoint: main\n" assert contents =~ "enabled: true\n" assert contents =~ "version: 1\n" - assert contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ - assert contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\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---\nfunction main() return 'ok' end\n" refute File.exists?(full_path <> ".tmp") end @@ -139,14 +140,15 @@ defmodule BDS.ScriptsTest do [ "---", "id: script-from-file", + "projectId: #{project.id}", "slug: recovered", "title: Recovered Script", "kind: utility", "entrypoint: main", "enabled: true", "version: 4", - "created_at: 1970-01-01T00:00:00.301Z", - "updated_at: 1970-01-01T00:00:00.404Z", + "createdAt: 1970-01-01T00:00:00.301Z", + "updatedAt: 1970-01-01T00:00:00.404Z", "---", "function main() return 'restored' end", "" diff --git a/test/bds/tags_test.exs b/test/bds/tags_test.exs index 8a47890..def39c2 100644 --- a/test/bds/tags_test.exs +++ b/test/bds/tags_test.exs @@ -30,7 +30,7 @@ defmodule BDS.TagsTest do tags_path = Path.join([temp_dir, "meta", "tags.json"]) assert File.exists?(tags_path) - assert %{"tags" => [%{"name" => "Alpha"}, %{"color" => "#000000", "name" => "Zebra"}]} = + assert [%{"name" => "Alpha"}, %{"color" => "#000000", "name" => "Zebra"}] = Jason.decode!(File.read!(tags_path)) end @@ -59,11 +59,9 @@ defmodule BDS.TagsTest do tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{ - "tags" => [ - %{"name" => "Alpha", "color" => "#112233", "post_template_slug" => "article"} - ] - } = + assert [ + %{"name" => "Alpha", "color" => "#112233", "postTemplateSlug" => "article"} + ] = Jason.decode!(File.read!(tags_path)) end @@ -93,7 +91,7 @@ defmodule BDS.TagsTest do assert contents =~ "\n---\nBody\n" tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path)) + assert [%{"name" => "Beta"}] = Jason.decode!(File.read!(tags_path)) end test "merge_tags moves source tags onto the target, deduplicates post tags, deletes sources, and refreshes tags.json", @@ -126,7 +124,7 @@ defmodule BDS.TagsTest do assert contents =~ "\n---\nBody\n" tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{"tags" => [%{"name" => "Gamma"}]} = Jason.decode!(File.read!(tags_path)) + assert [%{"name" => "Gamma"}] = Jason.decode!(File.read!(tags_path)) end test "delete_tag removes the tag from posts, rewrites published files, deletes the row, and refreshes tags.json", @@ -157,7 +155,7 @@ defmodule BDS.TagsTest do assert contents =~ "\n---\nBody\n" tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path)) + assert [%{"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", @@ -196,17 +194,15 @@ defmodule BDS.TagsTest do 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)) + assert [ + %{"name" => "Another"}, + %{ + "name" => "Existing", + "color" => "#112233", + "postTemplateSlug" => "feature-view" + }, + %{"name" => "Missing"} + ] = Jason.decode!(File.read!(tags_path)) end defp errors_on(changeset) do diff --git a/test/bds/templates_test.exs b/test/bds/templates_test.exs index a41d00a..d02dcf4 100644 --- a/test/bds/templates_test.exs +++ b/test/bds/templates_test.exs @@ -66,13 +66,14 @@ defmodule BDS.TemplatesTest do contents = File.read!(full_path) assert contents =~ "---\nid: #{published.id}\n" + assert contents =~ "projectId: #{project.id}\n" assert contents =~ "slug: landing-page\n" assert contents =~ "title: Landing Page\n" assert contents =~ "kind: list\n" assert contents =~ "enabled: true\n" assert contents =~ "version: 1\n" - assert contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/ - assert contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\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
{{ page_title }}
\n" refute File.exists?(full_path <> ".tmp") end @@ -153,7 +154,7 @@ defmodule BDS.TemplatesTest do assert post_contents =~ "\n---\nBody\n" tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{"tags" => [%{"name" => "Feature"}]} = Jason.decode!(File.read!(tags_path)) + assert [%{"name" => "Feature"}] = Jason.decode!(File.read!(tags_path)) end test "update_template cascades slug changes to posts and tags and renames the published file", @@ -208,12 +209,12 @@ defmodule BDS.TemplatesTest do assert template_contents =~ "\n---\n
{{ content }}
\n" post_contents = File.read!(Path.join(temp_dir, reloaded_post.file_path)) - assert post_contents =~ "template_slug: feature-view\n" + assert post_contents =~ "templateSlug: feature-view\n" assert post_contents =~ "\n---\nBody\n" tags_path = Path.join([temp_dir, "meta", "tags.json"]) - assert %{"tags" => [%{"name" => "Feature", "post_template_slug" => "feature-view"}]} = + assert [%{"name" => "Feature", "postTemplateSlug" => "feature-view"}] = Jason.decode!(File.read!(tags_path)) end @@ -231,13 +232,14 @@ defmodule BDS.TemplatesTest do [ "---", "id: template-from-file", + "projectId: #{project.id}", "slug: recovered-view", "title: Recovered View", "kind: list", "enabled: true", "version: 3", - "created_at: 1970-01-01T00:00:00.101Z", - "updated_at: 1970-01-01T00:00:00.202Z", + "createdAt: 1970-01-01T00:00:00.101Z", + "updatedAt: 1970-01-01T00:00:00.202Z", "---", "
Recovered
", ""