Database storage layer for the Publishing module.
Provides CRUD operations for publishing groups, posts, versions, and contents via PostgreSQL with Ecto.
Summary
Functions
Clears a specific url_slug from all content rows of a post. Returns cleared language codes.
Counts non-trashed posts in a group.
Creates content for a version/language.
Creates a publishing group.
Creates a post within a group.
Creates a new version for a post.
Creates a new version by cloning content from a source version.
Deletes content.
Deletes a group and all its posts (cascade).
Hard-deletes a post and all its versions/contents (cascade).
Finds content by a previous URL slug (stored in data.previous_url_slugs JSONB array). Excludes trashed posts.
Finds content by URL slug across all versions in a group. Excludes trashed posts.
Finds a post by date and time (timestamp mode, matches hour:minute only).
Gets the active (published) version for a post via active_version_uuid.
Gets content for a specific version and language.
Gets a group by UUID.
Gets a group by slug.
Gets the latest version for a post.
Gets a post by group slug and post slug. Excludes trashed posts.
Gets a timestamp-mode post by date and time.
Gets a post by UUID with preloads.
Gets a specific version by post and version number.
Lists all content rows for a version.
Lists groups ordered by position. Filters by status (default: active only).
Lists available languages for a version.
Lists posts in a group, optionally filtered by status. Excludes trashed by default.
Lists all posts in a group in listing format (excerpt only, no full content).
Lists posts in slug mode (ordered by slug asc).
Lists posts in timestamp mode (ordered by date/time desc).
Lists all posts in a group with their latest version metadata.
Lists all versions for a post, ordered by version number.
Gets the next version number for a post.
Reads a full post with its latest version and content for a specific language.
Reads a timestamp-mode post by date and time instead of slug.
Resolves content for a language from a list of content rows.
Restores a trashed group by setting status to 'active'.
Restores a trashed post by clearing trashed_at.
Streams every post in a group (including trashed) for batch operations that shouldn't materialise the whole listing in memory.
Trashes a group by setting status to 'trashed'.
Trashes a post by setting trashed_at timestamp.
Updates content.
Bulk-updates the status of all content rows for a version.
Bulk-updates the status of all content rows for a version, excluding a specific language.
Updates a publishing group.
Updates a post.
Updates a version.
Upserts content by version_id + language using ON CONFLICT.
Upserts a group by slug atomically via PostgreSQL ON CONFLICT.
Functions
Clears a specific url_slug from all content rows of a post. Returns cleared language codes.
@spec count_posts(String.t()) :: non_neg_integer()
Counts non-trashed posts in a group.
@spec create_content(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())
Creates content for a version/language.
@spec create_group(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Creates a publishing group.
@spec create_post(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())
Creates a post within a group.
@spec create_version(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingVersion.t())
Creates a new version for a post.
@spec create_version_from(String.t(), pos_integer(), map() | keyword()) :: {:ok, PhoenixKit.Modules.Publishing.PublishingVersion.t()} | {:error, term()}
Creates a new version by cloning content from a source version.
Creates a new version row and copies all content rows from the source. Also copies version-level data (featured_image, tags, seo, etc.). Wrapped in a transaction for atomicity.
Returns {:ok, %PublishingVersion{}} or {:error, reason}.
@spec delete_content(PhoenixKit.Modules.Publishing.PublishingContent.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())
Deletes content.
@spec delete_group(PhoenixKit.Modules.Publishing.PublishingGroup.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Deletes a group and all its posts (cascade).
@spec delete_post(PhoenixKit.Modules.Publishing.PublishingPost.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())
Hard-deletes a post and all its versions/contents (cascade).
@spec find_by_previous_url_slug(String.t(), String.t(), String.t()) :: PhoenixKit.Modules.Publishing.PublishingContent.t() | nil
Finds content by a previous URL slug (stored in data.previous_url_slugs JSONB array). Excludes trashed posts.
@spec find_by_url_slug(String.t(), String.t(), String.t()) :: PhoenixKit.Modules.Publishing.PublishingContent.t() | nil
Finds content by URL slug across all versions in a group. Excludes trashed posts.
@spec find_post_by_date_time(String.t(), Date.t(), Time.t() | nil) :: PhoenixKit.Modules.Publishing.PublishingPost.t() | nil
Finds a post by date and time (timestamp mode, matches hour:minute only).
@spec get_active_version(PhoenixKit.Modules.Publishing.PublishingPost.t()) :: PhoenixKit.Modules.Publishing.PublishingVersion.t() | nil
Gets the active (published) version for a post via active_version_uuid.
Reads from the preloaded :active_version association if present (see
get_post/2), otherwise falls back to a direct lookup. This keeps callers
that received a hand-built struct working while letting the read paths
short-circuit the second round trip.
@spec get_content(String.t(), String.t()) :: PhoenixKit.Modules.Publishing.PublishingContent.t() | nil
Gets content for a specific version and language.
@spec get_group(String.t()) :: PhoenixKit.Modules.Publishing.PublishingGroup.t() | nil
Gets a group by UUID.
@spec get_group_by_slug(String.t()) :: PhoenixKit.Modules.Publishing.PublishingGroup.t() | nil
Gets a group by slug.
@spec get_latest_version(String.t()) :: PhoenixKit.Modules.Publishing.PublishingVersion.t() | nil
Gets the latest version for a post.
@spec get_post(String.t(), String.t()) :: PhoenixKit.Modules.Publishing.PublishingPost.t() | nil
Gets a post by group slug and post slug. Excludes trashed posts.
Preloads :active_version so that downstream get_active_version/1 calls
read from the in-memory association instead of issuing a second query — the
read-then-resolve hot path becomes a single round trip.
@spec get_post_by_datetime(String.t(), Date.t(), Time.t() | nil) :: PhoenixKit.Modules.Publishing.PublishingPost.t() | nil
Gets a timestamp-mode post by date and time.
Truncates seconds from the input time since URLs use HH:MM format only, and new posts are stored with seconds zeroed. For older posts with non-zero seconds, falls back to hour:minute matching.
@spec get_post_by_uuid(String.t(), [atom() | tuple()]) :: PhoenixKit.Modules.Publishing.PublishingPost.t() | nil
Gets a post by UUID with preloads.
@spec get_version(String.t(), pos_integer()) :: PhoenixKit.Modules.Publishing.PublishingVersion.t() | nil
Gets a specific version by post and version number.
@spec list_contents(String.t()) :: [ PhoenixKit.Modules.Publishing.PublishingContent.t() ]
Lists all content rows for a version.
@spec list_groups(String.t() | nil) :: [ PhoenixKit.Modules.Publishing.PublishingGroup.t() ]
Lists groups ordered by position. Filters by status (default: active only).
Lists available languages for a version.
@spec list_posts(String.t(), String.t() | nil) :: [ PhoenixKit.Modules.Publishing.PublishingPost.t() ]
Lists posts in a group, optionally filtered by status. Excludes trashed by default.
Lists all posts in a group in listing format (excerpt only, no full content).
Always uses Mapper.to_listing_map/4 which strips content bodies and includes
only excerpts. Designed for caching in :persistent_term where data is copied
to the reading process heap — keeping entries small matters.
@spec list_posts_slug_mode(String.t(), String.t() | nil) :: [ PhoenixKit.Modules.Publishing.PublishingPost.t() ]
Lists posts in slug mode (ordered by slug asc).
@spec list_posts_timestamp_mode(String.t(), String.t() | nil, keyword()) :: [ PhoenixKit.Modules.Publishing.PublishingPost.t() ]
Lists posts in timestamp mode (ordered by date/time desc).
Options:
:date- Filter to a specific date (Date struct or ISO 8601 string)
Lists all posts in a group with their latest version metadata.
Returns a list of post maps suitable for listing pages.
@spec list_versions(String.t()) :: [ PhoenixKit.Modules.Publishing.PublishingVersion.t() ]
Lists all versions for a post, ordered by version number.
@spec next_version_number(String.t()) :: pos_integer()
Gets the next version number for a post.
Uses SELECT ... FOR UPDATE to lock the row and prevent concurrent reads from getting the same number.
@spec read_post(String.t(), String.t(), String.t() | nil, pos_integer() | nil) :: {:ok, map()} | {:error, :not_found}
Reads a full post with its latest version and content for a specific language.
Returns a post map or nil if not found.
@spec read_post_by_datetime( String.t(), Date.t(), Time.t() | nil, String.t() | nil, pos_integer() | nil ) :: {:ok, map()} | {:error, :not_found}
Reads a timestamp-mode post by date and time instead of slug.
@spec resolve_content( [PhoenixKit.Modules.Publishing.PublishingContent.t()], String.t() | nil ) :: PhoenixKit.Modules.Publishing.PublishingContent.t() | nil
Resolves content for a language from a list of content rows.
Fallback chain: exact language match → site default language → first available.
@spec restore_group(PhoenixKit.Modules.Publishing.PublishingGroup.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Restores a trashed group by setting status to 'active'.
@spec restore_post(PhoenixKit.Modules.Publishing.PublishingPost.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())
Restores a trashed post by clearing trashed_at.
@spec stream_posts(String.t()) :: Enumerable.t()
Streams every post in a group (including trashed) for batch operations that shouldn't materialise the whole listing in memory.
Caller MUST be inside a Repo.checkout/1 (or an explicit transaction) —
Postgres-backed Ecto streams require a checked-out connection. Yields raw
%PublishingPost{} structs with :group preloaded; no version/content
metadata (callers re-read what they need).
@spec trash_group(PhoenixKit.Modules.Publishing.PublishingGroup.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Trashes a group by setting status to 'trashed'.
@spec trash_post(PhoenixKit.Modules.Publishing.PublishingPost.t()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())
Trashes a post by setting trashed_at timestamp.
@spec update_content(PhoenixKit.Modules.Publishing.PublishingContent.t(), map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())
Updates content.
@spec update_content_status(String.t(), String.t()) :: {non_neg_integer(), nil}
Bulk-updates the status of all content rows for a version.
@spec update_content_status_except(String.t(), String.t(), String.t()) :: {non_neg_integer(), nil}
Bulk-updates the status of all content rows for a version, excluding a specific language.
@spec update_group(PhoenixKit.Modules.Publishing.PublishingGroup.t(), map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Updates a publishing group.
@spec update_post(PhoenixKit.Modules.Publishing.PublishingPost.t(), map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())
Updates a post.
@spec update_version(PhoenixKit.Modules.Publishing.PublishingVersion.t(), map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingVersion.t())
Updates a version.
@spec upsert_content(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())
Upserts content by version_id + language using ON CONFLICT.
@spec upsert_group(map()) :: changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())
Upserts a group by slug atomically via PostgreSQL ON CONFLICT.
The previous check-then-act version (get_group_by_slug then create
or update) had a TOCTOU race: two concurrent callers with the same
slug could both observe nil and both attempt to insert, with one
crashing on the unique index. This version delegates conflict
resolution to PostgreSQL and replaces the mutable columns on hit.