Files
bDS2/lib/bds/mcp/queries.ex
2026-05-01 15:33:50 +02:00

202 lines
6.3 KiB
Elixir

defmodule BDS.MCP.Queries do
@moduledoc false
import Ecto.Query
alias BDS.MCP.Util
alias BDS.PostLinks
alias BDS.Posts.Post
alias BDS.Posts.Translation, as: PostTranslation
alias BDS.Projects
alias BDS.Repo
@page_size 50
@spec page_size() :: pos_integer()
def page_size, do: @page_size
@spec active_project!() :: Projects.Project.t()
def active_project! do
case Enum.find(Projects.list_projects(), & &1.is_active) do
nil -> raise "no active project"
project -> project
end
end
@spec post_summary(Post.t()) :: map()
def post_summary(%Post{} = post) do
%{
"id" => post.id,
"title" => post.title,
"slug" => post.slug,
"status" => Atom.to_string(post.status),
"tags" => post.tags || [],
"categories" => post.categories || [],
"created_at" => post.created_at,
"backlinks" => linked_posts(post.id, :incoming),
"links_to" => linked_posts(post.id, :outgoing)
}
end
@spec post_detail(Post.t()) :: map()
def post_detail(%Post{} = post) do
post
|> Util.sanitize()
|> Map.put("content", post_body(post))
|> Map.put("backlinks", linked_posts(post.id, :incoming))
|> Map.put("links_to", linked_posts(post.id, :outgoing))
|> Map.put("available_languages", available_languages(post.id, post.language))
end
@spec translated_post_detail(Post.t(), String.t()) :: map()
def translated_post_detail(%Post{} = post, language) do
normalized_language = Util.normalize_term(language)
case Repo.get_by(PostTranslation, translation_for: post.id, language: normalized_language) do
nil ->
post_detail(post)
%PostTranslation{} = translation ->
post_detail(post)
|> Map.put("title", translation.title)
|> Map.put("excerpt", translation.excerpt)
|> Map.put("content", translation_body(translation))
|> Map.put("language", translation.language)
|> Map.put("canonical_language", post.language)
end
end
@spec search_filters(map()) :: map()
def search_filters(params) do
%{}
|> Util.maybe_put(:category, Util.map_get(params, :category))
|> Util.maybe_put(:tags, Util.map_get(params, :tags))
|> Util.maybe_put(:language, Util.map_get(params, :language))
|> Util.maybe_put(:missing_translation_language, Util.map_get(params, :missingTranslationLanguage))
|> Util.maybe_put(:year, Util.map_get(params, :year))
|> Util.maybe_put(:month, Util.map_get(params, :month))
|> Util.maybe_put(:status, parse_status(Util.map_get(params, :status)))
|> Map.put(:offset, Util.map_get(params, :offset, 0))
|> Map.put(:limit, Util.map_get(params, :limit, @page_size))
end
@spec parse_status(term()) :: atom() | nil
def parse_status(nil), do: nil
def parse_status(status) when is_atom(status), do: status
def parse_status(status) when is_binary(status), do: String.to_existing_atom(status)
@spec group_rows(Post.t(), [String.t()]) :: [map()]
def group_rows(_post, []), do: [%{}]
def group_rows(post, [dimension | rest]) do
values = group_values(post, dimension)
for value <- values,
tail <- group_rows(post, rest) do
Map.put(tail, dimension, value)
end
end
defp group_values(post, "year") do
[BDS.Persistence.from_unix_ms!(post.created_at).year]
end
defp group_values(post, "month") do
[BDS.Persistence.from_unix_ms!(post.created_at).month]
end
defp group_values(post, "tag") do
if Enum.empty?(post.tags || []), do: [nil], else: post.tags
end
defp group_values(post, "category") do
if Enum.empty?(post.categories || []), do: [nil], else: post.categories
end
defp group_values(post, "status"), do: [Atom.to_string(post.status)]
@spec available_languages(String.t(), String.t() | nil) :: [String.t()]
def available_languages(post_id, canonical_language) do
languages =
Repo.all(
from translation in PostTranslation,
where: translation.translation_for == ^post_id,
select: translation.language
)
([canonical_language] ++ languages)
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.uniq()
end
@spec linked_posts(String.t(), :incoming | :outgoing) :: [map()]
def linked_posts(post_id, :incoming) do
PostLinks.list_incoming_links(post_id)
|> Enum.map(&load_linked_post(&1.source_post_id))
|> Enum.reject(&is_nil/1)
end
def linked_posts(post_id, :outgoing) do
PostLinks.list_outgoing_links(post_id)
|> Enum.map(&load_linked_post(&1.target_post_id))
|> Enum.reject(&is_nil/1)
end
defp load_linked_post(post_id) do
case Repo.get(Post, post_id) do
%Post{} = post -> %{"id" => post.id, "title" => post.title, "slug" => post.slug}
nil -> nil
end
end
@spec post_body(Post.t()) :: String.t()
def post_body(%Post{content: content}) when is_binary(content), do: content
def post_body(%Post{} = post) do
project = Projects.get_project!(post.project_id)
full_path = Path.join(Projects.project_data_dir(project), post.file_path || "")
case File.read(full_path) do
{:ok, contents} ->
case String.split(contents, "\n---\n", parts: 2) do
[_frontmatter, body] -> String.trim_trailing(body, "\n")
_parts -> contents
end
{:error, _reason} ->
""
end
end
@spec translation_body(PostTranslation.t()) :: String.t()
def translation_body(%PostTranslation{content: content}) when is_binary(content), do: content
def translation_body(%PostTranslation{} = translation) do
project = Projects.get_project!(translation.project_id)
full_path = Path.join(Projects.project_data_dir(project), translation.file_path || "")
case File.read(full_path) do
{:ok, contents} ->
case String.split(contents, "\n---\n", parts: 2) do
[_frontmatter, body] -> String.trim_trailing(body, "\n")
_parts -> contents
end
{:error, _reason} ->
""
end
end
@spec tag_post_count(String.t(), String.t()) :: non_neg_integer()
def tag_post_count(project_id, tag_name) do
Repo.all(from post in Post, where: post.project_id == ^project_id)
|> Enum.count(&(tag_name in (&1.tags || [])))
end
@spec category_post_count(String.t(), String.t()) :: non_neg_integer()
def category_post_count(project_id, category) do
Repo.all(from post in Post, where: post.project_id == ^project_id)
|> Enum.count(&(category in (&1.categories || [])))
end
end