PhoenixKit.Modules.Publishing.DBStorage (PhoenixKitPublishing v0.1.7)

Copy Markdown View Source

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.

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

clear_url_slug_from_post(group_slug, post_slug, url_slug_to_clear)

@spec clear_url_slug_from_post(String.t(), String.t(), String.t()) :: [String.t()]

Clears a specific url_slug from all content rows of a post. Returns cleared language codes.

count_posts(group_slug)

@spec count_posts(String.t()) :: non_neg_integer()

Counts non-trashed posts in a group.

create_content(attrs)

@spec create_content(map()) ::
  changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())

Creates content for a version/language.

create_group(attrs)

@spec create_group(map()) ::
  changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingGroup.t())

Creates a publishing group.

create_post(attrs)

@spec create_post(map()) ::
  changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingPost.t())

Creates a post within a group.

create_version(attrs)

@spec create_version(map()) ::
  changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingVersion.t())

Creates a new version for a post.

create_version_from(post_uuid, source_version_number, opts \\ %{})

@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}.

delete_content(content)

Deletes content.

delete_group(group)

Deletes a group and all its posts (cascade).

delete_post(post)

Hard-deletes a post and all its versions/contents (cascade).

find_by_previous_url_slug(group_slug, language, url_slug)

@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.

find_by_url_slug(group_slug, language, url_slug)

Finds content by URL slug across all versions in a group. Excludes trashed posts.

find_post_by_date_time(group_slug, date, time)

@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).

get_active_version(post)

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.

get_content(version_uuid, language)

Gets content for a specific version and language.

get_group(uuid)

Gets a group by UUID.

get_group_by_slug(slug)

@spec get_group_by_slug(String.t()) ::
  PhoenixKit.Modules.Publishing.PublishingGroup.t() | nil

Gets a group by slug.

get_latest_version(post_uuid)

@spec get_latest_version(String.t()) ::
  PhoenixKit.Modules.Publishing.PublishingVersion.t() | nil

Gets the latest version for a post.

get_post(group_slug, post_slug)

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.

get_post_by_datetime(group_slug, date, time)

@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.

get_post_by_uuid(uuid, preloads \\ [])

@spec get_post_by_uuid(String.t(), [atom() | tuple()]) ::
  PhoenixKit.Modules.Publishing.PublishingPost.t() | nil

Gets a post by UUID with preloads.

get_version(post_uuid, version_number)

Gets a specific version by post and version number.

list_contents(version_uuid)

Lists all content rows for a version.

list_groups(status \\ "active")

@spec list_groups(String.t() | nil) :: [
  PhoenixKit.Modules.Publishing.PublishingGroup.t()
]

Lists groups ordered by position. Filters by status (default: active only).

list_languages(version_uuid)

@spec list_languages(String.t()) :: [String.t()]

Lists available languages for a version.

list_posts(group_slug, status \\ nil)

@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.

list_posts_for_listing(group_slug)

@spec list_posts_for_listing(String.t()) :: [map()]

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.

list_posts_slug_mode(group_slug, status \\ nil)

@spec list_posts_slug_mode(String.t(), String.t() | nil) :: [
  PhoenixKit.Modules.Publishing.PublishingPost.t()
]

Lists posts in slug mode (ordered by slug asc).

list_posts_timestamp_mode(group_slug, status \\ nil, opts \\ [])

@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)

list_posts_with_metadata(group_slug, status \\ nil)

@spec list_posts_with_metadata(String.t(), String.t() | nil) :: [map()]

Lists all posts in a group with their latest version metadata.

Returns a list of post maps suitable for listing pages.

list_versions(post_uuid)

Lists all versions for a post, ordered by version number.

next_version_number(post_uuid)

@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.

read_post(group_slug, post_slug, language \\ nil, version_number \\ nil)

@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.

read_post_by_datetime(group_slug, date, time, language \\ nil, version_number \\ nil)

@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.

resolve_content(contents, language)

Resolves content for a language from a list of content rows.

Fallback chain: exact language match → site default language → first available.

restore_group(group)

Restores a trashed group by setting status to 'active'.

restore_post(post)

Restores a trashed post by clearing trashed_at.

stream_posts(group_slug)

@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).

trash_group(group)

Trashes a group by setting status to 'trashed'.

trash_post(post)

Trashes a post by setting trashed_at timestamp.

update_content(content, attrs)

Updates content.

update_content_status(version_uuid, new_status)

@spec update_content_status(String.t(), String.t()) :: {non_neg_integer(), nil}

Bulk-updates the status of all content rows for a version.

update_content_status_except(version_uuid, exclude_language, new_status)

@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.

update_group(group, attrs)

Updates a publishing group.

update_post(post, attrs)

Updates a post.

update_version(version, attrs)

Updates a version.

upsert_content(attrs)

@spec upsert_content(map()) ::
  changeset_or_struct(PhoenixKit.Modules.Publishing.PublishingContent.t())

Upserts content by version_id + language using ON CONFLICT.

upsert_group(attrs)

@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.