Packages
phoenix_kit
1.7.49
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/publishing/db_storage/mapper.ex
defmodule PhoenixKit.Modules.Publishing.DBStorage.Mapper do
@moduledoc """
Transitional mapper: converts DB records to the legacy map format
expected by Publishing's web layer (LiveViews, templates, controllers).
This exists so the web layer doesn't need changes during the transition.
Once the DB migration is complete and verified, the web layer can be
updated to use DB records directly and this mapper can be removed.
## Legacy Map Shape
The filesystem `Storage` module returns maps with these keys:
- `:group` - group slug
- `:slug` - post directory slug
- `:url_slug` - per-language URL slug
- `:date` - Date struct (timestamp mode)
- `:time` - Time struct (timestamp mode)
- `:path` - relative path
- `:full_path` - absolute path
- `:mode` - :timestamp or :slug atom
- `:language` - current language code
- `:available_languages` - list of language codes
- `:language_statuses` - %{language => status}
- `:version` - current version number
- `:available_versions` - list of version numbers
- `:version_statuses` - %{version_number => status}
- `:version_dates` - %{version_number => date_string}
- `:content` - markdown/PHK body
- `:metadata` - map with :title, :description, :status, :slug, etc.
- `:primary_language` - primary language code
"""
alias PhoenixKit.Modules.Publishing.PublishingContent
alias PhoenixKit.Modules.Publishing.PublishingPost
alias PhoenixKit.Modules.Publishing.PublishingVersion
@doc """
Converts a full post read (post + version + content + all contents + all versions)
into the legacy map format expected by the web layer.
"""
def to_legacy_map(
%PublishingPost{} = post,
%PublishingVersion{} = version,
%PublishingContent{} = content,
all_contents,
all_versions
) do
available_languages = Enum.map(all_contents, & &1.language) |> Enum.sort()
language_statuses =
Map.new(all_contents, fn c -> {c.language, c.status} end)
available_versions = Enum.map(all_versions, & &1.version_number) |> Enum.sort()
version_statuses =
Map.new(all_versions, fn v -> {v.version_number, v.status} end)
version_dates =
Map.new(all_versions, fn v ->
{v.version_number, format_datetime(v.inserted_at)}
end)
version_languages =
Map.new(all_versions, fn v ->
{v.version_number, available_languages}
end)
group_slug = get_group_slug(post)
%{
uuid: post.uuid,
group: group_slug,
slug: post.slug,
url_slug: presence(content.url_slug) || post.slug,
date: post.post_date,
time: post.post_time,
path: build_path(post, version.version_number, content.language),
full_path: nil,
mode: String.to_existing_atom(post.mode),
language: content.language,
available_languages: available_languages,
language_statuses: language_statuses,
language_slugs: build_language_slugs(all_contents, post.slug),
language_previous_slugs: build_language_previous_slugs(all_contents),
version: version.version_number,
available_versions: available_versions,
version_statuses: version_statuses,
version_dates: version_dates,
version_languages: version_languages,
is_legacy_structure: false,
content: content.content,
metadata: build_metadata(post, version, content),
primary_language: post.primary_language
}
end
@doc """
Converts a post to a listing-format map (no content body, just metadata).
Used for listing pages where full content isn't needed.
"""
def to_listing_map(%PublishingPost{} = post, version, all_contents, all_versions) do
available_languages = Enum.map(all_contents, & &1.language) |> Enum.sort()
language_statuses =
Map.new(all_contents, fn c -> {c.language, c.status} end)
available_versions = Enum.map(all_versions, & &1.version_number) |> Enum.sort()
version_statuses =
Map.new(all_versions, fn v -> {v.version_number, v.status} end)
version_dates =
Map.new(all_versions, fn v ->
{v.version_number, format_datetime(v.inserted_at)}
end)
primary_content =
Enum.find(all_contents, fn c -> c.language == post.primary_language end) ||
List.first(all_contents)
group_slug = get_group_slug(post)
current_version = if version, do: version.version_number, else: 1
%{
uuid: post.uuid,
group: group_slug,
slug: post.slug,
url_slug: presence(primary_content && primary_content.url_slug) || post.slug,
date: post.post_date,
time: post.post_time,
path: build_path(post, current_version, primary_content && primary_content.language),
full_path: nil,
mode: String.to_existing_atom(post.mode),
language: post.primary_language,
available_languages: available_languages,
language_statuses: language_statuses,
language_slugs: build_language_slugs(all_contents, post.slug),
language_previous_slugs: build_language_previous_slugs(all_contents),
version: current_version,
available_versions: available_versions,
version_statuses: version_statuses,
version_dates: version_dates,
version_languages: %{},
is_legacy_structure: false,
content: primary_content && extract_excerpt(primary_content),
metadata: build_listing_metadata(post, primary_content),
primary_language: post.primary_language,
# Per-language data for listing pages (so language switching shows correct titles)
language_titles: Map.new(all_contents, fn c -> {c.language, c.title} end),
language_excerpts: Map.new(all_contents, fn c -> {c.language, extract_excerpt(c)} end)
}
end
# ===========================================================================
# Private Helpers
# ===========================================================================
defp get_group_slug(%PublishingPost{group: %{slug: slug}}), do: slug
defp get_group_slug(%PublishingPost{} = _post), do: nil
defp build_path(
%PublishingPost{mode: "timestamp", post_date: date, post_time: time},
version_number,
language
)
when not is_nil(date) do
date_str = Date.to_iso8601(date)
time_str = if time, do: time |> Time.to_string() |> String.slice(0, 5), else: "00:00"
version = version_number || 1
lang = language || "en"
"#{date_str}/#{time_str}/v#{version}/#{lang}.phk"
end
defp build_path(post, version_number, language) do
slug = post.slug
version = version_number || 1
lang = language || post.primary_language || "en"
"#{slug}/v#{version}/#{lang}.phk"
end
defp build_metadata(post, version, content) do
%{
title: content.title,
description: PublishingContent.get_description(content),
status: content.status,
slug: post.slug,
version: version.version_number,
allow_version_access: PublishingPost.allow_version_access?(post),
url_slug: content.url_slug,
previous_url_slugs: PublishingContent.get_previous_url_slugs(content),
published_at: format_datetime(post.published_at),
featured_image_id: PublishingContent.get_featured_image_id(content),
primary_language: post.primary_language
}
end
defp build_listing_metadata(post, nil) do
%{
title: nil,
description: nil,
status: post.status,
slug: post.slug,
published_at: format_datetime(post.published_at),
featured_image_id: nil,
primary_language: post.primary_language
}
end
defp build_listing_metadata(post, content) do
%{
title: content.title,
description: PublishingContent.get_description(content),
status: content.status,
slug: post.slug,
published_at: format_datetime(post.published_at),
featured_image_id: PublishingContent.get_featured_image_id(content),
primary_language: post.primary_language
}
end
defp build_language_slugs(all_contents, default_slug) do
Map.new(all_contents, fn c ->
{c.language, presence(c.url_slug) || default_slug}
end)
end
defp build_language_previous_slugs(all_contents) do
Map.new(all_contents, fn c ->
{c.language, PublishingContent.get_previous_url_slugs(c)}
end)
end
defp extract_excerpt(%PublishingContent{} = content) do
# Use custom excerpt from data, or description, or first paragraph
case PublishingContent.get_excerpt(content) do
excerpt when is_binary(excerpt) and excerpt != "" ->
excerpt
_ ->
case PublishingContent.get_description(content) do
desc when is_binary(desc) and desc != "" ->
desc
_ ->
extract_first_paragraph(content.content)
end
end
end
defp extract_first_paragraph(nil), do: nil
defp extract_first_paragraph(content) when is_binary(content) do
content
|> String.split(~r/\n\n+/)
|> Enum.reject(&String.starts_with?(&1, "#"))
|> List.first()
|> case do
nil -> ""
text -> text |> String.trim() |> String.slice(0, 300)
end
end
defp format_datetime(nil), do: nil
defp format_datetime(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
defp format_datetime(other), do: to_string(other)
# Returns nil for nil and empty string, otherwise the value
defp presence(nil), do: nil
defp presence(""), do: nil
defp presence(value), do: value
end