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), do: BDS.BoundedAtoms.post_status(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