chore: refactoring of Repo.get/get! usages

This commit is contained in:
2026-05-01 17:20:08 +02:00
parent f6425de51d
commit 3505355980
23 changed files with 192 additions and 89 deletions

View File

@@ -83,9 +83,9 @@ _None._ All modules previously on the queue have been split; refresh the queue i
## 7. Direct `Repo.get` in `BDS.Desktop.ShellLive` ## 7. Direct `Repo.get` in `BDS.Desktop.ShellLive`
**Status:** open. 10 call sites verified. **Status:** ✅ done (2026-05-01). `Repo.get/2` and `Repo.get!/2` no longer appear in `BDS.Desktop.ShellLive` or its submodules. ShellLive entity reads now go through context APIs (`Posts.get_post/1`, `Posts.get_post!/1`, `Media.get_media/1`, `Templates.get_template/1`, `Scripts.get_script/1`, `AI.get_chat_conversation/1`, and `Settings.get_global_setting/1` / `put_global_setting/2`). Added a regression test that scans ShellLive source for direct `Repo.get` calls.
**Plan:** add the missing context functions (`Posts.get_post_with_translations/1`, etc.) and replace each `Repo.get(Schema, id)` with the context call. **Rule:** ShellLive modules may use `Repo` for query-shaped read models where no context abstraction exists yet, but direct primary-key entity fetches belong in context modules.
--- ---
@@ -216,6 +216,8 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search`
### 2026-05-01 ### 2026-05-01
- **Direct `Repo.get` in `BDS.Desktop.ShellLive`**: added context helpers for primary-key reads (`Posts.get_post/1`, `Media.get_media/1`, `Templates.get_template/1`, `Scripts.get_script/1`, `AI.get_chat_conversation/1`) and introduced `BDS.Settings` for global editor settings. Replaced all ShellLive `Repo.get/2` and `Repo.get!/2` calls across the main shell, tab helpers, CLI sync, panel renderer, chat message build, code entity editor, post editor, media editor, overlay components, post metadata, and settings editor. Added a ShellLive regression test that scans source files to keep direct `Repo.get` calls out. Section 7 is closed.
- **God modules**: - **God modules**:
- `BDS.Maintenance` 810 → 141 (83 %). Submodules under `lib/bds/maintenance/`: `Progress` (45), `FileScan` (158), `DiffComputation` (93), `DiffReports` (315), `Repair` (145). Coordinator keeps the 4 public entrypoints (`repair_metadata_diff/4`, `import_metadata_diff_orphans/3`, `rebuild_from_filesystem/3`, `metadata_diff/2`); submodules wired via `import only:`. - `BDS.Maintenance` 810 → 141 (83 %). Submodules under `lib/bds/maintenance/`: `Progress` (45), `FileScan` (158), `DiffComputation` (93), `DiffReports` (315), `Repair` (145). Coordinator keeps the 4 public entrypoints (`repair_metadata_diff/4`, `import_metadata_diff_orphans/3`, `rebuild_from_filesystem/3`, `metadata_diff/2`); submodules wired via `import only:`.
- `BDS.Scripting.Capabilities` 1715 → 194 (89 %). Submodules: `Util` (301), `Posts` (270), `Media` (254), `Crud` (284), `Projects` (204), `AppShell` (134), `Bridges` (176). Public `for_project/2` preserved. - `BDS.Scripting.Capabilities` 1715 → 194 (89 %). Submodules: `Util` (301), `Posts` (270), `Media` (254), `Crud` (284), `Projects` (204), `AppShell` (134), `Bridges` (176). Public `for_project/2` preserved.

View File

@@ -149,6 +149,9 @@ defmodule BDS.AI do
@spec list_chat_conversations() :: [map()] @spec list_chat_conversations() :: [map()]
defdelegate list_chat_conversations(), to: Chat defdelegate list_chat_conversations(), to: Chat
@spec get_chat_conversation(String.t()) :: BDS.AI.ChatConversation.t() | nil
defdelegate get_chat_conversation(conversation_id), to: Chat
@spec available_chat_models(String.t() | nil) :: [map()] @spec available_chat_models(String.t() | nil) :: [map()]
defdelegate available_chat_models(current_model \\ nil), to: Chat defdelegate available_chat_models(current_model \\ nil), to: Chat

View File

@@ -53,6 +53,11 @@ defmodule BDS.AI.Chat do
|> Enum.map(&format_conversation/1) |> Enum.map(&format_conversation/1)
end end
@spec get_chat_conversation(String.t()) :: ChatConversation.t() | nil
def get_chat_conversation(conversation_id) when is_binary(conversation_id) do
Repo.get(ChatConversation, conversation_id)
end
@spec available_chat_models(String.t() | nil) :: [map()] @spec available_chat_models(String.t() | nil) :: [map()]
def available_chat_models(current_model \\ nil) do def available_chat_models(current_model \\ nil) do
endpoint_models = configured_chat_models() endpoint_models = configured_chat_models()

View File

@@ -6,6 +6,15 @@ defmodule BDS.AI.ChatConversation do
@primary_key {:id, :string, autogenerate: false} @primary_key {:id, :string, autogenerate: false}
@type t :: %__MODULE__{
id: String.t(),
title: String.t() | nil,
model: String.t() | nil,
copilot_session_id: String.t() | nil,
created_at: integer() | nil,
updated_at: integer() | nil
}
schema "chat_conversations" do schema "chat_conversations" do
field :title, :string field :title, :string
field :model, :string field :model, :string

View File

@@ -29,7 +29,6 @@ defmodule BDS.Desktop.ShellLive do
parse_integer: 1 parse_integer: 1
] ]
alias BDS.Projects alias BDS.Projects
alias BDS.Repo
alias BDS.Templates alias BDS.Templates
alias BDS.UI.{Commands, MenuBar, Session, Workbench} alias BDS.UI.{Commands, MenuBar, Session, Workbench}
@@ -346,7 +345,7 @@ defmodule BDS.Desktop.ShellLive do
end end
def handle_event("delete_sidebar_template", %{"id" => template_id}, socket) do def handle_event("delete_sidebar_template", %{"id" => template_id}, socket) do
case Repo.get(Templates.Template, template_id) do case Templates.get_template(template_id) do
%Templates.Template{project_id: project_id} when project_id == socket.assigns.projects.active_project_id -> %Templates.Template{project_id: project_id} when project_id == socket.assigns.projects.active_project_id ->
case Templates.delete_template(template_id) do case Templates.delete_template(template_id) do
{:ok, :deleted} -> {:ok, :deleted} ->

View File

@@ -1,13 +1,13 @@
defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
@moduledoc false @moduledoc false
alias BDS.{AI, Repo} alias BDS.AI
alias BDS.AI.ChatConversation alias BDS.AI.ChatConversation
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.ChatEditor.{ModelSelection, ToolSurfaces, ToolTracking} alias BDS.Desktop.ShellLive.ChatEditor.{ModelSelection, ToolSurfaces, ToolTracking}
def build(%{current_tab: %{type: :chat, id: conversation_id}} = assigns) do def build(%{current_tab: %{type: :chat, id: conversation_id}} = assigns) do
case Repo.get(ChatConversation, conversation_id) do case AI.get_chat_conversation(conversation_id) do
nil -> nil ->
nil nil

View File

@@ -3,9 +3,9 @@ defmodule BDS.Desktop.ShellLive.CliSync do
import Phoenix.Component, only: [assign: 3] import Phoenix.Component, only: [assign: 3]
alias BDS.Media.Media alias BDS.{Media, Posts}
alias BDS.Media.Media, as: MediaRecord
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Repo
alias BDS.UI.Workbench alias BDS.UI.Workbench
@doc """ @doc """
@@ -78,8 +78,8 @@ defmodule BDS.Desktop.ShellLive.CliSync do
defp maybe_refresh_tab_meta(socket, "post", post_id, action) when action in [:created, :updated] do defp maybe_refresh_tab_meta(socket, "post", post_id, action) when action in [:created, :updated] do
maybe_put_tab_meta(socket, :post, post_id, fn -> maybe_put_tab_meta(socket, :post, post_id, fn ->
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{} = post -> %{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status || :draft)} %Post{} = post -> %{title: post.title || post.slug || post.id, subtitle: Atom.to_string(post.status)}
_other -> nil _other -> nil
end end
end) end)
@@ -87,8 +87,8 @@ defmodule BDS.Desktop.ShellLive.CliSync do
defp maybe_refresh_tab_meta(socket, "media", media_id, action) when action in [:created, :updated] do defp maybe_refresh_tab_meta(socket, "media", media_id, action) when action in [:created, :updated] do
maybe_put_tab_meta(socket, :media, media_id, fn -> maybe_put_tab_meta(socket, :media, media_id, fn ->
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> %{title: media.title || media.filename || media.id, subtitle: media.filename || media.mime_type || "media"} %MediaRecord{} = media -> %{title: media.title || media.filename || media.id, subtitle: media.filename || media.mime_type || "media"}
_other -> nil _other -> nil
end end
end) end)

View File

@@ -4,7 +4,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
use Phoenix.Component use Phoenix.Component
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.{MCP, Repo, Scripts, Scripting, Templates} alias BDS.{MCP, Scripts, Scripting, Templates}
alias BDS.Scripts.Script alias BDS.Scripts.Script
alias BDS.Templates.Template alias BDS.Templates.Template
@@ -27,7 +27,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def save_script(socket, reload, append_output) do def save_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab %{id: script_id} = socket.assigns.current_tab
case Repo.get(Script, script_id) do case Scripts.get_script(script_id) do
nil -> reload.(socket, socket.assigns.workbench) nil -> reload.(socket, socket.assigns.workbench)
%Script{} = script -> %Script{} = script ->
draft = current_script_draft(socket.assigns, script) draft = current_script_draft(socket.assigns, script)
@@ -57,7 +57,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def check_script(socket, reload, append_output) do def check_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab %{id: script_id} = socket.assigns.current_tab
case Repo.get(Script, script_id) do case Scripts.get_script(script_id) do
nil -> reload.(socket, socket.assigns.workbench) nil -> reload.(socket, socket.assigns.workbench)
%Script{} = script -> %Script{} = script ->
case Scripting.validate(current_script_draft(socket.assigns, script)["content"] || "") do case Scripting.validate(current_script_draft(socket.assigns, script)["content"] || "") do
@@ -70,7 +70,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def run_script(socket, reload, append_output) do def run_script(socket, reload, append_output) do
%{id: script_id} = socket.assigns.current_tab %{id: script_id} = socket.assigns.current_tab
case Repo.get(Script, script_id) do case Scripts.get_script(script_id) do
nil -> reload.(socket, socket.assigns.workbench) nil -> reload.(socket, socket.assigns.workbench)
%Script{} = script -> %Script{} = script ->
draft = current_script_draft(socket.assigns, script) draft = current_script_draft(socket.assigns, script)
@@ -109,7 +109,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def save_template(socket, reload, append_output) do def save_template(socket, reload, append_output) do
%{id: template_id} = socket.assigns.current_tab %{id: template_id} = socket.assigns.current_tab
case Repo.get(Template, template_id) do case Templates.get_template(template_id) do
nil -> reload.(socket, socket.assigns.workbench) nil -> reload.(socket, socket.assigns.workbench)
%Template{} = template -> %Template{} = template ->
draft = current_template_draft(socket.assigns, template) draft = current_template_draft(socket.assigns, template)
@@ -129,7 +129,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def validate_template(socket, reload, append_output) do def validate_template(socket, reload, append_output) do
%{id: template_id} = socket.assigns.current_tab %{id: template_id} = socket.assigns.current_tab
case Repo.get(Template, template_id) do case Templates.get_template(template_id) do
nil -> reload.(socket, socket.assigns.workbench) nil -> reload.(socket, socket.assigns.workbench)
%Template{} = template -> %Template{} = template ->
case MCP.validate_template(current_template_draft(socket.assigns, template)["content"] || "") do case MCP.validate_template(current_template_draft(socket.assigns, template)["content"] || "") do
@@ -149,7 +149,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
end end
def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do def build_script(%{current_tab: %{type: :scripts, id: script_id}} = assigns) do
case Repo.get(Script, script_id) do case Scripts.get_script(script_id) do
nil -> nil nil -> nil
%Script{} = script -> %Script{} = script ->
draft = current_script_draft(assigns, script) draft = current_script_draft(assigns, script)
@@ -171,7 +171,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do
def build_script(_assigns), do: nil def build_script(_assigns), do: nil
def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do def build_template(%{current_tab: %{type: :templates, id: template_id}} = assigns) do
case Repo.get(Template, template_id) do case Templates.get_template(template_id) do
nil -> nil nil -> nil
%Template{} = template -> %Template{} = template ->
draft = current_template_draft(assigns, template) draft = current_template_draft(assigns, template)

View File

@@ -6,10 +6,11 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
import Ecto.Query import Ecto.Query
alias BDS.Desktop.{FilePicker, ShellData} alias BDS.Desktop.{FilePicker, ShellData}
alias BDS.{AI, I18n, Media, Repo} alias BDS.{AI, I18n, Media}
alias BDS.Media.Media, as: MediaRecord alias BDS.Media.Media, as: MediaRecord
alias BDS.Media.Translation alias BDS.Media.Translation
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Repo
alias BDS.UI.Workbench alias BDS.UI.Workbench
embed_templates "media_editor_html/*" embed_templates "media_editor_html/*"
@@ -23,7 +24,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
def update(socket, params, reload) do def update(socket, params, reload) do
case socket.assigns.current_tab do case socket.assigns.current_tab do
%{type: :media, id: media_id} -> %{type: :media, id: media_id} ->
case Repo.get(MediaRecord, media_id) do case Media.get_media(media_id) do
nil -> nil ->
socket socket
@@ -38,7 +39,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end end
def persist_socket(socket, media_id, reload, append_output) do def persist_socket(socket, media_id, reload, append_output) do
case Repo.get(MediaRecord, media_id) do case Media.get_media(media_id) do
nil -> nil ->
socket socket
@@ -111,7 +112,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info") |> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|> reload.(socket.assigns.workbench) |> reload.(socket.assigns.workbench)
else else
case Repo.get(MediaRecord, media_id) do case Media.get_media(media_id) do
nil -> nil ->
socket socket
@@ -184,7 +185,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
def apply_ai_suggestions(socket, media_id, fields, reload, append_output) do def apply_ai_suggestions(socket, media_id, fields, reload, append_output) do
try do try do
case Repo.get(MediaRecord, media_id) do case Media.get_media(media_id) do
nil -> nil ->
socket socket
@@ -375,7 +376,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end end
def build(%{current_tab: %{type: :media, id: media_id}} = assigns) do def build(%{current_tab: %{type: :media, id: media_id}} = assigns) do
case Repo.get(MediaRecord, media_id) do case Media.get_media(media_id) do
nil -> nil ->
nil nil
@@ -468,7 +469,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
"title" => media.title || "", "title" => media.title || "",
"alt" => media.alt || "", "alt" => media.alt || "",
"caption" => media.caption || "", "caption" => media.caption || "",
"tags" => Enum.join(media.tags || [], ", "), "tags" => Enum.join(media.tags, ", "),
"author" => media.author || "", "author" => media.author || "",
"language" => media.language || "" "language" => media.language || ""
} }
@@ -567,4 +568,4 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end end
defp reload_with_assigned_workbench(socket, reload), do: reload.(socket, socket.assigns.workbench) defp reload_with_assigned_workbench(socket, reload), do: reload.(socket, socket.assigns.workbench)
end end

View File

@@ -6,8 +6,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
import Ecto.Query import Ecto.Query
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.{I18n, Metadata, Repo} alias BDS.{I18n, Media, Metadata, Posts, Repo}
alias BDS.Media.Media alias BDS.Media.Media, as: MediaRecord
alias BDS.Media.Translation, as: MediaTranslation alias BDS.Media.Translation, as: MediaTranslation
alias BDS.Posts.{Post, PostMedia, Translation} alias BDS.Posts.{Post, PostMedia, Translation}
alias BDS.Tags.Tag alias BDS.Tags.Tag
@@ -93,7 +93,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
defp media(project_id) do defp media(project_id) do
Repo.all( Repo.all(
from media in Media, from media in MediaRecord,
where: media.project_id == ^project_id, where: media.project_id == ^project_id,
order_by: [desc: media.updated_at, desc: media.created_at], order_by: [desc: media.updated_at, desc: media.created_at],
select: %{id: media.id, title: media.title, original_name: media.original_name, mime_type: media.mime_type, alt: media.alt, caption: media.caption} select: %{id: media.id, title: media.title, original_name: media.original_name, mime_type: media.mime_type, alt: media.alt, caption: media.caption}
@@ -155,7 +155,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
end end
defp source_language(%{type: :post, id: post_id}, metadata) do defp source_language(%{type: :post, id: post_id}, metadata) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{language: language} when is_binary(language) and language != "" -> language %Post{language: language} when is_binary(language) and language != "" -> language
_other -> metadata.main_language || "en" _other -> metadata.main_language || "en"
end end
@@ -164,8 +164,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
end end
defp source_language(%{type: :media, id: media_id}, metadata) do defp source_language(%{type: :media, id: media_id}, metadata) do
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{language: language} when is_binary(language) and language != "" -> language %MediaRecord{language: language} when is_binary(language) and language != "" -> language
_other -> metadata.main_language || "en" _other -> metadata.main_language || "en"
end end
rescue rescue
@@ -190,7 +190,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
end end
defp ai_fields(%{type: :post, id: post_id}, title, subtitle, page_language) do defp ai_fields(%{type: :post, id: post_id}, title, subtitle, page_language) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{} = post -> %Post{} = post ->
[ [
%{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: post.title || title, suggested_value: refine_title(post.title || title), locked: false}, %{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: post.title || title, suggested_value: refine_title(post.title || title), locked: false},
@@ -206,8 +206,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
end end
defp ai_fields(%{type: :media, id: media_id}, title, _subtitle, page_language) do defp ai_fields(%{type: :media, id: media_id}, title, _subtitle, page_language) do
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> %MediaRecord{} = media ->
[ [
%{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: media.title || title, suggested_value: refine_title(media.title || title), locked: false}, %{key: "title", label: ShellData.translate("Title", %{}, page_language), current_value: media.title || title, suggested_value: refine_title(media.title || title), locked: false},
%{key: "alt", label: ShellData.translate("Alt Text", %{}, page_language), current_value: media.alt || "", suggested_value: media.alt || title, locked: false}, %{key: "alt", label: ShellData.translate("Alt Text", %{}, page_language), current_value: media.alt || "", suggested_value: media.alt || title, locked: false},
@@ -225,8 +225,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
defp delete_details(%{type: :media, id: media_id}, page_language) do defp delete_details(%{type: :media, id: media_id}, page_language) do
entity_name = entity_name =
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> media.title || media.original_name || media.id %MediaRecord{} = media -> media.title || media.original_name || media.id
_other -> media_id _other -> media_id
end end

View File

@@ -5,10 +5,11 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Git alias BDS.Git
alias BDS.Media.Media alias BDS.Media
alias BDS.Media.Media, as: MediaRecord
alias BDS.PostLinks alias BDS.PostLinks
alias BDS.Posts
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Repo
@doc "Render the active panel tab body." @doc "Render the active panel tab body."
def render_panel_body(assigns) do def render_panel_body(assigns) do
@@ -208,7 +209,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
defp related_posts(links, key) do defp related_posts(links, key) do
Enum.map(links, fn link -> Enum.map(links, fn link ->
case Repo.get(Post, Map.fetch!(link, key)) do case Posts.get_post(Map.fetch!(link, key)) do
%Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id} %Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id}
_other -> nil _other -> nil
end end
@@ -230,15 +231,15 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
end end
defp git_history_target(%{type: :post, id: post_id}) do defp git_history_target(%{type: :post, id: post_id}) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path} %Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path}
_other -> nil _other -> nil
end end
end end
defp git_history_target(%{type: :media, id: media_id}) do defp git_history_target(%{type: :media, id: media_id}) do
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path} %MediaRecord{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path}
_other -> nil _other -> nil
end end
end end

View File

@@ -3,7 +3,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
use Phoenix.Component use Phoenix.Component
alias BDS.{AI, Posts, Preview, Repo} alias BDS.{AI, Posts, Preview}
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata} alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata}
alias BDS.Posts.Post alias BDS.Posts.Post
@@ -84,7 +84,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
def update(socket, params, reload) do def update(socket, params, reload) do
case socket.assigns.current_tab do case socket.assigns.current_tab do
%{type: :post, id: post_id} -> %{type: :post, id: post_id} ->
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -118,7 +118,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def persist_socket(socket, post_id, action, reload, append_output) do def persist_socket(socket, post_id, action, reload, append_output) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -131,13 +131,13 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
case persist(post, draft, active_language, metadata, action) do case persist(post, draft, active_language, metadata, action) do
{:ok, record} -> {:ok, record} ->
workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id) workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id)
normalized_form = persisted_form(Repo.get!(Post, post_id), metadata, active_language) normalized_form = persisted_form(Posts.get_post!(post_id), metadata, active_language)
socket socket
|> assign(:workbench, workbench) |> assign(:workbench, workbench)
|> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, normalized_form)) |> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, normalized_form))
|> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, save_state_for_action(action))) |> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, save_state_for_action(action)))
|> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: record_title(record, Repo.get!(Post, post_id)), subtitle: Atom.to_string(record_status(record))})) |> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: record_title(record, Posts.get_post!(post_id)), subtitle: Atom.to_string(record_status(record))}))
|> reload.(workbench) |> reload.(workbench)
{:error, reason} -> {:error, reason} ->
@@ -149,7 +149,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def discard_socket(socket, post_id, reload, append_output) do def discard_socket(socket, post_id, reload, append_output) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -206,7 +206,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
normalized_mode = normalize_mode(mode) normalized_mode = normalize_mode(mode)
if normalized_mode == :preview do if normalized_mode == :preview do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{} = post -> %Post{} = post ->
_ = Preview.ensure_preview(post.project_id) _ = Preview.ensure_preview(post.project_id)
@@ -250,7 +250,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info") |> append_output.(translated("Detect Language"), translated("Automatic AI actions stay gated by airplane mode."), nil, "info")
|> reload.(socket.assigns.workbench) |> reload.(socket.assigns.workbench)
else else
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -318,7 +318,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -366,7 +366,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def add_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do def add_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -399,7 +399,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def remove_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do def remove_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
socket socket
@@ -417,7 +417,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
end end
def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
nil -> nil ->
nil nil
@@ -446,7 +446,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
display_title: display_title(form["title"], post.slug, post.id), display_title: display_title(form["title"], post.slug, post.id),
subtitle: nil, subtitle: nil,
slug: post.slug || post.id, slug: post.slug || post.id,
status: post.status || :draft, status: post.status,
dirty?: Workbench.dirty?(assigns.workbench, :post, post.id), dirty?: Workbench.dirty?(assigns.workbench, :post, post.id),
save_state: Map.get(assigns.post_editor_save_states, post.id, :idle), save_state: Map.get(assigns.post_editor_save_states, post.id, :idle),
quick_actions_open?: Map.get(assigns.post_editor_quick_actions_open, post.id, false), quick_actions_open?: Map.get(assigns.post_editor_quick_actions_open, post.id, false),
@@ -454,8 +454,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
excerpt_expanded: Map.get(expanded, :excerpt, false), excerpt_expanded: Map.get(expanded, :excerpt, false),
mode: Map.get(assigns.post_editor_modes, post.id, :markdown), mode: Map.get(assigns.post_editor_modes, post.id, :markdown),
editing_canonical?: editing_canonical_language?(translations, active_language, canonical_language), editing_canonical?: editing_canonical_language?(translations, active_language, canonical_language),
can_publish?: (post.status || :draft) == :draft, can_publish?: post.status == :draft,
can_delete?: (post.status || :draft) == :published, can_delete?: post.status == :published,
has_published_version?: has_published_version?(post), has_published_version?: has_published_version?(post),
discard_label: discard_label(post), discard_label: discard_label(post),
discard_title: discard_title(post), discard_title: discard_title(post),

View File

@@ -3,9 +3,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
import Ecto.Query import Ecto.Query
alias BDS.{I18n, Metadata, PostLinks, Posts, Preview, Repo, Templates} alias BDS.{I18n, Media, Metadata, PostLinks, Posts, Preview, Repo, Templates}
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Media.Media alias BDS.Media.Media, as: MediaRecord
alias BDS.Posts.{Post, PostMedia} alias BDS.Posts.{Post, PostMedia}
def project_metadata(nil), do: %{main_language: "en", blog_languages: []} def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
@@ -56,8 +56,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
) )
Enum.map(rows, fn {media_id, sort_order} -> Enum.map(rows, fn {media_id, sort_order} ->
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> %MediaRecord{} = media ->
%{ %{
media_id: media.id, media_id: media.id,
has_thumbnail: String.starts_with?(to_string(media.mime_type || ""), "image/"), has_thumbnail: String.starts_with?(to_string(media.mime_type || ""), "image/"),
@@ -83,7 +83,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
defp related_posts(links, key) do defp related_posts(links, key) do
Enum.map(links, fn link -> Enum.map(links, fn link ->
case Repo.get(Post, Map.fetch!(link, key)) do case Posts.get_post(Map.fetch!(link, key)) do
%Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id} %Post{} = post -> %{id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id}
_other -> nil _other -> nil
end end

View File

@@ -3,9 +3,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
use Phoenix.Component use Phoenix.Component
alias BDS.Persistence alias BDS.Settings
alias BDS.Repo
alias BDS.Settings.Setting
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
def editor_form do def editor_form do
@@ -46,22 +44,11 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do
end end
def get_global_setting(key) do def get_global_setting(key) do
case Repo.get(Setting, key) do Settings.get_global_setting(key)
%Setting{value: value} -> value
_other -> nil
end
end end
def put_global_setting(key, value) do def put_global_setting(key, value) do
setting = Repo.get(Setting, key) || %Setting{} Settings.put_global_setting(key, value)
setting
|> Setting.changeset(%{key: key, value: to_string(value || ""), updated_at: Persistence.now_ms()})
|> Repo.insert_or_update()
|> case do
{:ok, _setting} -> :ok
{:error, reason} -> {:error, reason}
end
end end
defp editor_attrs(assigns) do defp editor_attrs(assigns) do

View File

@@ -2,9 +2,9 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
@moduledoc false @moduledoc false
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Media.Media alias BDS.{Media, Posts}
alias BDS.Media.Media, as: MediaRecord
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Repo
alias BDS.UI.Registry alias BDS.UI.Registry
def tab_title(nil, _tab_meta), do: translated("Dashboard") def tab_title(nil, _tab_meta), do: translated("Dashboard")
@@ -59,29 +59,29 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
end end
def post_title(post_id) do def post_title(post_id) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{} = post -> post.title || post.slug || post.id %Post{} = post -> post.title || post.slug || post.id
_other -> "Post" _other -> "Post"
end end
end end
def post_subtitle(post_id) do def post_subtitle(post_id) do
case Repo.get(Post, post_id) do case Posts.get_post(post_id) do
%Post{} = post -> post.slug || "draft" %Post{} = post -> post.slug || "draft"
_other -> "draft" _other -> "draft"
end end
end end
def media_title(media_id) do def media_title(media_id) do
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> media.title || media.filename || media.id %MediaRecord{} = media -> media.title || media.filename || media.id
_other -> "Media" _other -> "Media"
end end
end end
def media_subtitle(media_id) do def media_subtitle(media_id) do
case Repo.get(Media, media_id) do case Media.get_media(media_id) do
%Media{} = media -> media.filename || media.mime_type || "media" %MediaRecord{} = media -> media.filename || media.mime_type || "media"
_other -> "media" _other -> "media"
end end
end end

View File

@@ -115,6 +115,9 @@ defmodule BDS.Media do
end end
end end
@spec get_media(String.t()) :: Media.t() | nil
def get_media(media_id), do: Repo.get(Media, media_id)
@spec update_media(String.t(), attrs()) :: @spec update_media(String.t(), attrs()) ::
{:ok, Media.t()} | {:error, :not_found | Ecto.Changeset.t()} {:ok, Media.t()} | {:error, :not_found | Ecto.Changeset.t()}
def update_media(media_id, attrs) do def update_media(media_id, attrs) do

View File

@@ -348,6 +348,9 @@ defmodule BDS.Posts do
end end
@spec get_post!(String.t()) :: Post.t() @spec get_post!(String.t()) :: Post.t()
@spec get_post(String.t()) :: Post.t() | nil
def get_post(post_id), do: Repo.get(Post, post_id)
def get_post!(post_id), do: Repo.get!(Post, post_id) def get_post!(post_id), do: Repo.get!(Post, post_id)
@spec get_post_translation!(String.t()) :: Translation.t() @spec get_post_translation!(String.t()) :: Translation.t()

View File

@@ -38,6 +38,9 @@ defmodule BDS.Scripts do
|> Repo.insert() |> Repo.insert()
end end
@spec get_script(String.t()) :: Script.t() | nil
def get_script(script_id), do: Repo.get(Script, script_id)
def publish_script(script_id) do def publish_script(script_id) do
case Repo.get(Script, script_id) do case Repo.get(Script, script_id) do
nil -> nil ->

View File

@@ -7,6 +7,23 @@ defmodule BDS.Scripts.Script do
@primary_key {:id, :string, autogenerate: false} @primary_key {:id, :string, autogenerate: false}
@foreign_key_type :string @foreign_key_type :string
@type t :: %__MODULE__{
id: String.t(),
project_id: String.t(),
slug: String.t() | nil,
title: String.t() | nil,
kind: :macro | :utility | :transform | nil,
entrypoint: String.t() | nil,
enabled: boolean() | nil,
version: integer() | nil,
file_path: String.t() | nil,
status: :draft | :published | nil,
content: String.t() | nil,
created_at: integer() | nil,
updated_at: integer() | nil,
project: term()
}
schema "scripts" do schema "scripts" do
field :slug, :string field :slug, :string
field :title, :string field :title, :string

28
lib/bds/settings.ex Normal file
View File

@@ -0,0 +1,28 @@
defmodule BDS.Settings do
@moduledoc false
alias BDS.Persistence
alias BDS.Repo
alias BDS.Settings.Setting
@spec get_global_setting(String.t()) :: String.t() | nil
def get_global_setting(key) do
case Repo.get(Setting, key) do
%Setting{value: value} -> value
_other -> nil
end
end
@spec put_global_setting(String.t(), term()) :: :ok | {:error, Ecto.Changeset.t()}
def put_global_setting(key, value) do
setting = Repo.get(Setting, key) || %Setting{}
setting
|> Setting.changeset(%{key: key, value: to_string(value || ""), updated_at: Persistence.now_ms()})
|> Repo.insert_or_update()
|> case do
{:ok, _setting} -> :ok
{:error, reason} -> {:error, reason}
end
end
end

View File

@@ -38,6 +38,9 @@ defmodule BDS.Templates do
|> Repo.insert() |> Repo.insert()
end end
@spec get_template(String.t()) :: Template.t() | nil
def get_template(template_id), do: Repo.get(Template, template_id)
def publish_template(template_id) do def publish_template(template_id) do
case Repo.get(Template, template_id) do case Repo.get(Template, template_id) do
nil -> nil ->

View File

@@ -7,6 +7,22 @@ defmodule BDS.Templates.Template do
@primary_key {:id, :string, autogenerate: false} @primary_key {:id, :string, autogenerate: false}
@foreign_key_type :string @foreign_key_type :string
@type t :: %__MODULE__{
id: String.t(),
project_id: String.t(),
slug: String.t() | nil,
title: String.t() | nil,
kind: :post | :list | :not_found | :partial | nil,
enabled: boolean() | nil,
version: integer() | nil,
file_path: String.t() | nil,
status: :draft | :published | nil,
content: String.t() | nil,
created_at: integer() | nil,
updated_at: integer() | nil,
project: term()
}
schema "templates" do schema "templates" do
field :slug, :string field :slug, :string
field :title, :string field :title, :string

View File

@@ -4,6 +4,29 @@ defmodule BDS.Desktop.ShellLiveTest do
import Phoenix.ConnTest import Phoenix.ConnTest
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
@shell_live_source_root Path.expand("../../../lib/bds/desktop/shell_live", __DIR__)
test "shell live modules use contexts instead of direct Repo.get calls" do
source_files =
[Path.expand("../../../lib/bds/desktop/shell_live.ex", __DIR__) |
Path.wildcard(Path.join(@shell_live_source_root, "**/*.ex"))]
offenders =
source_files
|> Enum.flat_map(fn path ->
path
|> File.read!()
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.filter(fn {line, _line_number} ->
String.contains?(line, "Repo.get(") or String.contains?(line, "Repo.get!(")
end)
|> Enum.map(fn {_line, line_number} -> "#{Path.relative_to_cwd(path)}:#{line_number}" end)
end)
assert offenders == []
end
alias BDS.Persistence alias BDS.Persistence
alias BDS.AI alias BDS.AI
alias BDS.CliSync.Watcher alias BDS.CliSync.Watcher