Packages
phoenix_kit
1.7.64
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 """
Mapper: converts DB records to the map format expected by
Publishing's web layer (LiveViews, templates, controllers).
## Map Shape
The web layer expects 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)
- `: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,
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,
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,
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: %{},
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_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_uuid: PublishingContent.get_featured_image_uuid(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_uuid: 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_uuid: PublishingContent.get_featured_image_uuid(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