PhoenixKitCatalogue.Catalogue.Attributes (PhoenixKitCatalogue v0.18.0)

Copy Markdown View Source

Attribute groups — reusable, translatable sets of product characteristics.

A group ("Idea doors") owns attributes ("Color", "Trim"), each attribute owns ordered values ("White", "Oak"); an item is linked to one group through phoenix_kit_cat_item_attribute_groups and inherits everything the group defines. This is an evolution of the hand-typed per-item metadata (item.data["meta"]), which stays untouched and is surfaced read/editable by the item form's legacy collapse.

Identity and translations

key slugs (auto-generated from the primary-language name via PhoenixKit.Utils.Slug, immutable after creation) plus row UUIDs are the durable identity — future exclusion rules and parent-app order lines reference them, so editing a translation never changes what old data means. Display names ride the module's multilang data JSONB convention (primary language in the name/value columns, other languages in data); resolve them with resolved_group/2.

Deletion vs archive

All three definition levels carry status ("active" / "archived"). Archive is the path for anything in use: the DB RESTRICTs deleting a group any item references, and delete_attribute_group/2 performs the values → attributes → group cascade explicitly in one transaction (mirroring permanently_delete_catalogue/2's gate-then-cascade shape) rather than trusting a silent DB cascade.

Downstream contract

Order lines in the parent app that record a chosen value must snapshot the resolved labels and keys at order time — the UUID reference alone is identity, not history.

Public surface is re-exported from PhoenixKitCatalogue.Catalogue via defdelegate.

Summary

Functions

How many items are assigned to each of the given groups — one grouped query. Returns %{group_uuid => count}; drives the "in use" gate and the list page's usage column.

Per-group attribute counts for the groups list — one grouped query, active attributes only. Returns %{group_uuid => count}.

Returns a changeset for tracking attribute-group form changes.

Adds an attribute to a group. The stable key slug is generated from the given name (deduped within the group); position appends at the end.

Creates an attribute group.

Adds a value to an attribute. The stable key slug is generated from the display text (deduped within the attribute); position appends at the end; the attribute's first value becomes the default automatically.

Deletes an attribute and its values in one transaction.

Hard-deletes a group with an explicit values → attributes → group cascade in one transaction.

Deletes a value. If it was the default, the lowest-position remaining active value is promoted so a multi attribute never silently loses its default.

Fetches an attribute by UUID (with its group). Returns nil if not found.

Fetches an attribute group by UUID. Returns nil if not found.

Fetches a group with ALL its attributes and values preloaded in position order, archived rows included — the group editor's working set. Consumer paths (item preview, product card) want resolved_group/2 instead.

Fetches a value by UUID (with its attribute). Returns nil if not found.

The item's current assignment (or nil) — one indexed lookup.

Batch map of %{item_uuid => attribute_group_uuid} for the given items — one indexed query; drives the list/card indicator chips with no per-row lookups.

Lists attribute groups ordered by position, then name.

Persists a manual ordering of an attribute's values (same contract as reorder_attributes/2).

Persists a manual ordering of a group's attributes. UUIDs not in the list keep their position; unknown UUIDs are dropped BEFORE any writes — the client list is forgeable, so the write count is bounded by the group's real row count, never by payload length (panel finding).

The programmatic read surface: a group resolved for display in lang — active attributes in position order, each with its active values in position order, names/labels translated with primary-language fallback.

Makes a value its attribute's default. Unset-then-set inside one transaction (the partial unique index allows at most one default); fails with {:error, :not_found} when the value vanished concurrently and {:error, :conflict} when two flips race on the index.

Sets (or clears, with nil) the item's attribute group.

Updates an attribute (name, translations, kind, status, position). key is immutable.

Updates an attribute group (name, translations, status, position).

Updates a value's display text / translations / status. key is immutable.

Functions

assignment_counts(group_uuids)

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

How many items are assigned to each of the given groups — one grouped query. Returns %{group_uuid => count}; drives the "in use" gate and the list page's usage column.

attribute_counts(group_uuids)

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

Per-group attribute counts for the groups list — one grouped query, active attributes only. Returns %{group_uuid => count}.

change_attribute_group(group, attrs \\ %{})

@spec change_attribute_group(PhoenixKitCatalogue.Schemas.AttributeGroup.t(), map()) ::
  Ecto.Changeset.t()

Returns a changeset for tracking attribute-group form changes.

create_attribute(group, attrs, opts \\ [])

Adds an attribute to a group. The stable key slug is generated from the given name (deduped within the group); position appends at the end.

create_attribute_group(attrs, opts \\ [])

@spec create_attribute_group(
  map(),
  keyword()
) ::
  {:ok, PhoenixKitCatalogue.Schemas.AttributeGroup.t()}
  | {:error, Ecto.Changeset.t()}

Creates an attribute group.

create_attribute_value(attribute, attrs)

Adds a value to an attribute. The stable key slug is generated from the display text (deduped within the attribute); position appends at the end; the attribute's first value becomes the default automatically.

delete_attribute(attribute, opts \\ [])

@spec delete_attribute(
  PhoenixKitCatalogue.Schemas.Attribute.t(),
  keyword()
) :: {:ok, PhoenixKitCatalogue.Schemas.Attribute.t()} | {:error, term()}

Deletes an attribute and its values in one transaction.

delete_attribute_group(group, opts \\ [])

@spec delete_attribute_group(
  PhoenixKitCatalogue.Schemas.AttributeGroup.t(),
  keyword()
) ::
  {:ok, PhoenixKitCatalogue.Schemas.AttributeGroup.t()}
  | {:error, :in_use | Ecto.Changeset.t()}

Hard-deletes a group with an explicit values → attributes → group cascade in one transaction.

Gated: returns {:error, :in_use} when any item is assigned to the group (the assignment FK would RESTRICT anyway — the gate turns the constraint error into a domain answer). Archive is the path for groups in use.

delete_attribute_value(value)

@spec delete_attribute_value(PhoenixKitCatalogue.Schemas.AttributeValue.t()) ::
  {:ok, PhoenixKitCatalogue.Schemas.AttributeValue.t()} | {:error, term()}

Deletes a value. If it was the default, the lowest-position remaining active value is promoted so a multi attribute never silently loses its default.

get_attribute(uuid)

@spec get_attribute(Ecto.UUID.t()) :: PhoenixKitCatalogue.Schemas.Attribute.t() | nil

Fetches an attribute by UUID (with its group). Returns nil if not found.

get_attribute_group(uuid)

@spec get_attribute_group(Ecto.UUID.t()) ::
  PhoenixKitCatalogue.Schemas.AttributeGroup.t() | nil

Fetches an attribute group by UUID. Returns nil if not found.

get_attribute_group_full(uuid)

@spec get_attribute_group_full(Ecto.UUID.t()) ::
  PhoenixKitCatalogue.Schemas.AttributeGroup.t() | nil

Fetches a group with ALL its attributes and values preloaded in position order, archived rows included — the group editor's working set. Consumer paths (item preview, product card) want resolved_group/2 instead.

get_attribute_value(uuid)

@spec get_attribute_value(Ecto.UUID.t()) ::
  PhoenixKitCatalogue.Schemas.AttributeValue.t() | nil

Fetches a value by UUID (with its attribute). Returns nil if not found.

get_item_attribute_group_uuid(item_uuid)

@spec get_item_attribute_group_uuid(Ecto.UUID.t()) :: Ecto.UUID.t() | nil

The item's current assignment (or nil) — one indexed lookup.

item_attribute_group_map(item_uuids)

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

Batch map of %{item_uuid => attribute_group_uuid} for the given items — one indexed query; drives the list/card indicator chips with no per-row lookups.

list_attribute_groups(opts \\ [])

@spec list_attribute_groups(keyword()) :: [
  PhoenixKitCatalogue.Schemas.AttributeGroup.t()
]

Lists attribute groups ordered by position, then name.

Options

  • :status — filter by status ("active" / "archived"); nil = all.

reorder_attribute_values(attribute, uuids)

@spec reorder_attribute_values(PhoenixKitCatalogue.Schemas.Attribute.t(), [
  Ecto.UUID.t()
]) :: :ok

Persists a manual ordering of an attribute's values (same contract as reorder_attributes/2).

reorder_attributes(group, uuids)

@spec reorder_attributes(PhoenixKitCatalogue.Schemas.AttributeGroup.t(), [
  Ecto.UUID.t()
]) :: :ok

Persists a manual ordering of a group's attributes. UUIDs not in the list keep their position; unknown UUIDs are dropped BEFORE any writes — the client list is forgeable, so the write count is bounded by the group's real row count, never by payload length (panel finding).

resolved_group(group_uuid, lang)

@spec resolved_group(Ecto.UUID.t() | nil, String.t()) :: map() | nil

The programmatic read surface: a group resolved for display in lang — active attributes in position order, each with its active values in position order, names/labels translated with primary-language fallback.

Returns nil when the group doesn't exist. Shape:

%{
  uuid: ..., key-less group: name: "Idea doors",
  attributes: [
    %{uuid: ..., key: "color", name: "Цвет", kind: "multi",
      values: [%{uuid: ..., key: "oak", value: "Дуб", default?: true}, ...]},
    ...
  ]
}

This is what the item form preview, the product card, and (later) the parent app's order-line picker all consume — module boundaries stay at the context, not at raw table access.

set_default_value(value)

Makes a value its attribute's default. Unset-then-set inside one transaction (the partial unique index allows at most one default); fails with {:error, :not_found} when the value vanished concurrently and {:error, :conflict} when two flips race on the index.

set_item_attribute_group(item, group_uuid, opts \\ [])

@spec set_item_attribute_group(
  PhoenixKitCatalogue.Schemas.Item.t(),
  Ecto.UUID.t() | nil,
  keyword()
) ::
  {:ok, :assigned | :cleared | :unchanged}
  | {:error, :invalid_group | Ecto.Changeset.t()}

Sets (or clears, with nil) the item's attribute group.

Validates the group exists and is active — except that keeping the item's CURRENT group is always allowed even when that group has been archived since (the stale-select rule: an archived assignment renders, it just can't be newly chosen). Returns {:error, :invalid_group} for anything else.

update_attribute(attribute, attrs)

Updates an attribute (name, translations, kind, status, position). key is immutable.

update_attribute_group(group, attrs, opts \\ [])

Updates an attribute group (name, translations, status, position).

update_attribute_value(value, attrs)

Updates a value's display text / translations / status. key is immutable.