PhoenixKitDocumentCreator.Taxonomy (PhoenixKitDocumentCreator v0.9.13)

Copy Markdown View Source

Context module for managing the Document Creator Category → Type hierarchy.

Provides CRUD, cascade soft-delete/restore, reorder, and picker helpers for Category and Type schemas. Modeled on PhoenixKitCatalogue.Catalogue.

Soft-Delete Cascade

  • trash_category/1 — soft-deletes the category, all its types, and all templates reachable via category_uuid or type_uuid. Records affected template uuids in the activity log so restore_category/1 can scope its restore precisely.
  • trash_type/1 — soft-deletes the type and templates whose type_uuid points at it.
  • Documents are never cascaded; they keep pointing at their (now trashed) category and remain usable.

PubSub

Every successful write broadcasts {:doc_taxonomy_changed, level, uuid} on the shared PhoenixKit PubSub. level is :category or :type. Consumers subscribe via PhoenixKit.PubSubHelper.subscribe/1.

Summary

Functions

Returns [{label, value}] for all active categories, ordered by position, preceded by a {"No category", nil} empty option.

Counts categories, applying the same :status filter semantics as list_categories/1. Counts in SQL instead of loading rows — use when only the number is needed (e.g. a "Trash (N)" badge).

Counts published templates per group, keyed by type uuid.

Counts types for a category, applying the same :status filter semantics as list_types_for_category/2. Counts in SQL instead of loading rows.

Returns the canonical ANDI group names, in order.

Seeds (idempotently) the canonical ANDI group set under category_uuid, positioned 0..8 in default_group_order/0 order.

Fetches a type by UUID, but only when it is currently active. Returns nil for trashed types as well as missing uuids.

Fetches a category by UUID. Returns nil if not found.

Fetches a category by UUID. Raises Ecto.NoResultsError if not found.

Fetches a type by UUID. Returns nil if not found.

Fetches a type by UUID. Raises Ecto.NoResultsError if not found.

Lists categories ordered by position then name.

Returns a list of {category, [types]} tuples for all active categories, ordered by position. Types within each category are ordered by position.

Lists a template's category memberships (one row per category, each with an optional group type_uuid), oldest first.

Lists types for a category ordered by position then name.

Returns the display name of a Category or Type record translated into locale, reading overrides from the multilang data JSONB via PhoenixKit.Utils.Multilang.get_language_data/2.

Batch variant of list_memberships_for_template/1 for a grid render: returns %{template_uuid => [%{category_uuid: _, type_uuid: _}, ...]} for the given template uuids in a single query (avoids a per-row query).

Permanently deletes a category and all its types from the database.

Permanently deletes a type from the database.

Reorders categories by assigning positions from the given ordered list.

Reorders types within a category by assigning positions from the given ordered list.

Restores a soft-deleted category.

Restores a soft-deleted type and templates trashed by its cascade.

Replaces a template's full set of category memberships in one transaction.

Subscribes the caller to taxonomy change events.

Soft-deletes a category by setting its status to "deleted".

Soft-deletes a type by setting its status to "deleted".

Returns [{label, value}] for all active types within a category, ordered by position, preceded by a {"No type", nil} empty option.

Updates a category with the given attributes.

Updates a type with the given attributes.

Functions

category_options(locale \\ nil)

@spec category_options(String.t() | nil) :: [{String.t(), Ecto.UUID.t() | nil}]

Returns [{label, value}] for all active categories, ordered by position, preceded by a {"No category", nil} empty option.

Suitable for options_for_select/2. The empty option lets users clear the FK (which is nullable).

locale selects the translated label via localized_name/2; omit (or pass nil) to fall back to the denormalized name column.

Drop-in replacement for the hard-coded category_options/0 in documents_live.ex.

count_categories(opts \\ [])

@spec count_categories(keyword()) :: non_neg_integer()

Counts categories, applying the same :status filter semantics as list_categories/1. Counts in SQL instead of loading rows — use when only the number is needed (e.g. a "Trash (N)" badge).

count_published_templates_by_type(type_uuids)

@spec count_published_templates_by_type([Ecto.UUID.t()]) :: %{
  required(Ecto.UUID.t()) => pos_integer()
}

Counts published templates per group, keyed by type uuid.

Counted through the phoenix_kit_doc_template_taxonomy memberships: published templates filed under the type's current category, which is what PhoenixKitDocumentCreator.Documents.list_templates_for_category/1 returns for that category. So a template filed under several categories counts once in each category's group, and trashed, lost or unfiled templates are not counted. A membership left under the old category when a type moves to another one is not counted: the new category's listing does not return it, and the old category no longer shows the type (its listing still returns the row, which this count deliberately ignores). One query for the whole list; a type with no published template is absent from the map.

count_types_for_category(category_uuid, opts \\ [])

@spec count_types_for_category(Ecto.UUID.t(), keyword()) :: non_neg_integer()

Counts types for a category, applying the same :status filter semantics as list_types_for_category/2. Counts in SQL instead of loading rows.

create_category(attrs, opts \\ [])

Creates a category.

Required attributes

  • :name — category name (1-255 chars)

Optional attributes

  • :description, :position (default: appended after the last active category), :status (default "active"), :data

create_type(attrs, opts \\ [])

Creates a type.

Required attributes

  • :name — type name (1-255 chars)
  • :category_uuid — parent category

Optional attributes

  • :description, :position (default: appended after the last active type in the same category), :status, :data

default_group_order()

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

Returns the canonical ANDI group names, in order.

ensure_default_group_order(category_uuid, opts \\ [])

@spec ensure_default_group_order(Ecto.UUID.t(), keyword()) :: :ok | {:error, term()}

Seeds (idempotently) the canonical ANDI group set under category_uuid, positioned 0..8 in default_group_order/0 order.

  • A missing canonical group is created at its canonical position.
  • An existing active group with a canonical name is repositioned (never duplicated).
  • Any other active groups under the category are pushed after the canonical block (positions continue from 9) so the canonical order is collision-free.

Runs in one transaction and broadcasts a single :type change. This is the code seed for part А — call it per category (e.g. from Admin → Documents → Categories) instead of dragging the 9 groups into order by hand.

get_active_type(uuid)

@spec get_active_type(Ecto.UUID.t()) ::
  PhoenixKitDocumentCreator.Schemas.Type.t() | nil

Fetches a type by UUID, but only when it is currently active. Returns nil for trashed types as well as missing uuids.

get_type/1 returns the row regardless of status, which is correct for callers that need the record itself (e.g. restoring or permanently deleting it) but wrong for anywhere a type's name is rendered as a live label for end users — that would surface a soft-deleted type's name as if it were still a normal, selectable option. Use get_active_type/1 in those spots instead.

get_category(uuid)

Fetches a category by UUID. Returns nil if not found.

get_category!(uuid)

Fetches a category by UUID. Raises Ecto.NoResultsError if not found.

get_type(uuid)

Fetches a type by UUID. Returns nil if not found.

get_type!(uuid)

Fetches a type by UUID. Raises Ecto.NoResultsError if not found.

list_categories(opts \\ [])

@spec list_categories(keyword()) :: [PhoenixKitDocumentCreator.Schemas.Category.t()]

Lists categories ordered by position then name.

Options

  • :status — when provided, returns only categories with this exact status (e.g. "active", "deleted"). When nil (default), returns all non-deleted categories.

list_category_tree()

Returns a list of {category, [types]} tuples for all active categories, ordered by position. Types within each category are ordered by position.

Useful for building grouped pickers in LiveViews.

list_memberships_for_template(template_uuid)

@spec list_memberships_for_template(Ecto.UUID.t()) :: [
  PhoenixKitDocumentCreator.Schemas.TemplateTaxonomy.t()
]

Lists a template's category memberships (one row per category, each with an optional group type_uuid), oldest first.

list_types_for_category(category_uuid, opts \\ [])

@spec list_types_for_category(Ecto.UUID.t(), keyword()) :: [
  PhoenixKitDocumentCreator.Schemas.Type.t()
]

Lists types for a category ordered by position then name.

Options

  • :status — when provided, returns only types with this exact status. Defaults to non-deleted types.

localized_name(record, locale)

Returns the display name of a Category or Type record translated into locale, reading overrides from the multilang data JSONB via PhoenixKit.Utils.Multilang.get_language_data/2.

Mirrors PhoenixKitCatalogue.Catalogue.get_translation/2 — falls back to the primary-language value, then to the denormalized name column. Returns record.name unchanged when locale is nil or no translation data exists yet.

memberships_by_templates(template_uuids)

@spec memberships_by_templates([Ecto.UUID.t()]) :: %{
  optional(Ecto.UUID.t()) => [
    %{category_uuid: Ecto.UUID.t(), type_uuid: Ecto.UUID.t() | nil}
  ]
}

Batch variant of list_memberships_for_template/1 for a grid render: returns %{template_uuid => [%{category_uuid: _, type_uuid: _}, ...]} for the given template uuids in a single query (avoids a per-row query).

permanently_delete_category(category, opts \\ [])

@spec permanently_delete_category(
  PhoenixKitDocumentCreator.Schemas.Category.t(),
  keyword()
) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Category.t()} | {:error, term()}

Permanently deletes a category and all its types from the database.

Relies on ON DELETE CASCADE for child types and ON DELETE SET NULL for template/document FK columns.

permanently_delete_type(type, opts \\ [])

@spec permanently_delete_type(PhoenixKitDocumentCreator.Schemas.Type.t(), keyword()) ::
  {:ok, PhoenixKitDocumentCreator.Schemas.Type.t()} | {:error, term()}

Permanently deletes a type from the database.

Relies on ON DELETE SET NULL for template/document FK columns.

reorder_categories(ordered_uuids, opts \\ [])

@spec reorder_categories([Ecto.UUID.t()], keyword()) :: :ok | {:error, term()}

Reorders categories by assigning positions from the given ordered list.

Each uuid in the list gets position = index. UUIDs not present keep their existing positions.

reorder_types(category_uuid, ordered_uuids, opts \\ [])

@spec reorder_types(Ecto.UUID.t(), [Ecto.UUID.t()], keyword()) ::
  :ok | {:error, term()}

Reorders types within a category by assigning positions from the given ordered list.

restore_category(category, opts \\ [])

Restores a soft-deleted category.

Cascades in one transaction:

  1. Category status → "active"
  2. Types whose uuids were recorded in the trash activity log status → "active" (only those, so types the user had trashed manually before the cascade stay trashed)
  3. Templates whose uuids were recorded in the trash activity log status → "published" (only those, to avoid restoring manually trashed templates)

When PhoenixKit.Activity is not loaded (or no matching activity entry exists), cascade-trashed types and templates are not restored — they must be restored manually.

restore_type(type, opts \\ [])

Restores a soft-deleted type and templates trashed by its cascade.

set_template_memberships(template_uuid, memberships, opts \\ [])

@spec set_template_memberships(Ecto.UUID.t(), [map()], keyword()) ::
  {:ok, [PhoenixKitDocumentCreator.Schemas.TemplateTaxonomy.t()]}
  | {:error, term()}

Replaces a template's full set of category memberships in one transaction.

memberships is a list of maps with a :category_uuid (required) and an optional :type_uuid group (atom or string keys accepted). The write is replace-all: existing rows for the template are deleted first, then the given set is inserted. An empty list clears all memberships.

After the swap, the primary membership — the one whose category has the lowest Category.position — is mirrored into the legacy templates.category_uuid/type_uuid columns so backward-compatible single-binding readers keep working (an empty set clears the mirror).

Broadcasts {:doc_taxonomy_changed, :template, template_uuid}.

subscribe()

@spec subscribe() :: :ok | {:error, term()}

Subscribes the caller to taxonomy change events.

trash_category(category, opts \\ [])

Soft-deletes a category by setting its status to "deleted".

Cascades in one transaction:

  1. Category status → "deleted"
  2. Its currently-active types status → "deleted"
  3. All templates reachable via category_uuid (directly) or via any of the category's type_uuid values → status → "trashed"

Documents are NOT cascaded.

Affected type and template uuids are stored in the activity log payload so restore_category/1 restores only what this cascade trashed — types the user had already trashed manually stay trashed.

trash_type(type, opts \\ [])

Soft-deletes a type by setting its status to "deleted".

Cascades to templates whose type_uuid points at this type. Affected template uuids are stored in the activity log.

type_options(category_uuid, locale \\ nil)

@spec type_options(Ecto.UUID.t() | nil, String.t() | nil) :: [
  {String.t(), Ecto.UUID.t() | nil}
]

Returns [{label, value}] for all active types within a category, ordered by position, preceded by a {"No type", nil} empty option.

Pass nil as category_uuid (or when no category is selected) to get only the empty option. locale selects the translated label via localized_name/2.

Suitable for options_for_select/2.

update_category(category, attrs, opts \\ [])

Updates a category with the given attributes.

update_type(type, attrs, opts \\ [])

Updates a type with the given attributes.