# `PhoenixKitEntities`
[🔗](https://github.com/BeamLabEU/phoenix_kit_entities/blob/v0.4.6/lib/phoenix_kit_entities.ex#L1)

Dynamic entity system for PhoenixKit.

This module provides both the Ecto schema definition and business logic for
managing custom content types (entities) with flexible field schemas.

## Schema Fields

- `name`: Unique identifier for the entity (e.g., "brand", "product")
- `display_name`: Human-readable singular name shown in UI (e.g., "Brand")
- `display_name_plural`: Human-readable plural name (e.g., "Brands")
- `description`: Description of what this entity represents
- `icon`: Icon identifier for UI display (hero icons)
- `status`: Workflow status string - one of "draft", "published", or "archived"
- `fields_definition`: JSONB array of field definitions
- `settings`: JSONB map of entity-specific settings
- `created_by`: User ID of the admin who created the entity
- `date_created`: When the entity was created
- `date_updated`: When the entity was last modified

## Field Definition Structure

Each field in `fields_definition` is a map with:
- `type`: Field type (text, textarea, number, boolean, date, select, etc.)
- `key`: Unique field identifier (snake_case)
- `label`: Display label for the field
- `required`: Whether the field is required
- `default`: Default value
- `validation`: Map of validation rules
- `options`: Array of options (for select, radio, checkbox types)

## Core Functions

### Entity Management
- `list_entities/0` - Get all entities
- `list_active_entities/0` - Get only active entities
- `get_entity!/1` - Get an entity by ID (raises if not found)
- `get_entity_by_name/1` - Get an entity by its name
- `create_entity/1` - Create a new entity
- `update_entity/2` - Update an existing entity
- `delete_entity/1` - Delete an entity (and all its data)
- `change_entity/2` - Get changeset for forms

### System Settings
- `enabled?/0` - Check if entities system is enabled
- `enable_system/0` - Enable the entities system
- `disable_system/0` - Disable the entities system
- `get_config/0` - Get current system configuration
- `get_max_per_user/0` - Get max entities per user limit
- `validate_user_entity_limit/1` - Check if user can create more entities

## Usage Examples

    # Check if system is enabled
    if PhoenixKitEntities.enabled?() do
      # System is active
    end

    # Create a brand entity
    # Note: fields_definition requires string keys, not atom keys
    {:ok, entity} = PhoenixKitEntities.create_entity(%{
      name: "brand",
      display_name: "Brand",
      display_name_plural: "Brands",
      description: "Brand content type for company profiles",
      icon: "hero-building-office",
      created_by_uuid: admin_user.uuid,
      fields_definition: [
        %{"type" => "text", "key" => "name", "label" => "Name", "required" => true},
        %{"type" => "textarea", "key" => "tagline", "label" => "Tagline"},
        %{"type" => "rich_text", "key" => "description", "label" => "Description", "required" => true},
        %{"type" => "select", "key" => "industry", "label" => "Industry",
          "options" => ["Technology", "Manufacturing", "Retail"]},
        %{"type" => "date", "key" => "founded_date", "label" => "Founded Date"},
        %{"type" => "boolean", "key" => "featured", "label" => "Featured Brand"}
      ]
    })

    # Get entity by name
    entity = PhoenixKitEntities.get_entity_by_name("brand")

    # List all active entities
    entities = PhoenixKitEntities.list_active_entities()

# `t`

```elixir
@type t() :: %PhoenixKitEntities{
  __meta__: term(),
  created_by_uuid: term(),
  creator: term(),
  date_created: term(),
  date_updated: term(),
  description: term(),
  display_name: term(),
  display_name_plural: term(),
  entity_data: term(),
  fields_definition: term(),
  icon: term(),
  name: term(),
  position: term(),
  settings: term(),
  status: term(),
  uuid: term()
}
```

# `change_entity`

```elixir
@spec change_entity(t(), map()) :: Ecto.Changeset.t()
```

Returns an `%Ecto.Changeset{}` for tracking entity changes.

## Examples

    iex> PhoenixKitEntities.change_entity(entity)
    %Ecto.Changeset{data: %PhoenixKit.Entities{}}

# `changeset`

```elixir
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
```

Creates a changeset for entity creation and updates.

Validates that name is unique, fields_definition is valid, and all required fields are present.
Automatically sets date_created on new records.

# `count_all_entity_data`

```elixir
@spec count_all_entity_data() :: non_neg_integer()
```

Counts the total number of entity data records across all entities.

## Examples

    iex> PhoenixKitEntities.count_all_entity_data()
    243

# `count_entities`

```elixir
@spec count_entities() :: non_neg_integer()
```

Counts the total number of entities in the system.

## Examples

    iex> PhoenixKitEntities.count_entities()
    15

# `count_user_entities`

```elixir
@spec count_user_entities(String.t()) :: non_neg_integer()
```

Counts the total number of entities created by a user.

## Examples

    iex> PhoenixKitEntities.count_user_entities(1)
    5

# `create_entity`

```elixir
@spec create_entity(
  map(),
  keyword()
) :: {:ok, t()} | {:error, Ecto.Changeset.t() | :managed_blueprint}
```

Creates an entity.

## Examples

    iex> PhoenixKitEntities.create_entity(%{name: "brand", display_name: "Brand"})
    {:ok, %PhoenixKit.Entities{}}

    iex> PhoenixKitEntities.create_entity(%{name: ""})
    {:error, %Ecto.Changeset{}}

Note: `created_by` is auto-filled with the first admin or user ID if not provided,
but only if at least one user exists in the system. If no users exist, the changeset
will fail with a validation error on `created_by`.

# `delete_entity`

```elixir
@spec delete_entity(
  t(),
  keyword()
) :: {:ok, t()} | {:error, Ecto.Changeset.t() | term()}
```

Deletes an entity.

Note: This will also delete all associated entity_data records due to the
ON DELETE CASCADE constraint defined in the database migration (V17).

## Examples

    iex> PhoenixKitEntities.delete_entity(entity)
    {:ok, %PhoenixKit.Entities{}}

    iex> PhoenixKitEntities.delete_entity(entity)
    {:error, %Ecto.Changeset{}}

# `disable_all_data_mirror`

```elixir
@spec disable_all_data_mirror() :: {:ok, non_neg_integer()}
```

Disables data mirroring for all entities.

## Examples

    iex> PhoenixKitEntities.disable_all_data_mirror()
    {:ok, count}

# `disable_all_definitions_mirror`

```elixir
@spec disable_all_definitions_mirror() :: {:ok, non_neg_integer()}
```

Disables definition mirroring for all entities.

## Examples

    iex> PhoenixKitEntities.disable_all_definitions_mirror()
    {:ok, count}

# `disable_system`

```elixir
@spec disable_system(keyword()) :: {:ok, term()} | {:error, term()}
```

Disables the entities system.

Sets the "entities_enabled" setting to false and logs a
`module.entities.disabled` activity row.

## Options

  * `:actor_uuid` — see `enable_system/1`.

## Examples

    iex> PhoenixKitEntities.disable_system(actor_uuid: admin.uuid)
    {:ok, %Setting{}}

# `enable_all_data_mirror`

```elixir
@spec enable_all_data_mirror() :: {:ok, non_neg_integer()}
```

Enables data mirroring for all entities.

## Examples

    iex> PhoenixKitEntities.enable_all_data_mirror()
    {:ok, count}

# `enable_all_definitions_mirror`

```elixir
@spec enable_all_definitions_mirror() :: {:ok, non_neg_integer()}
```

Enables definition mirroring for all entities.

## Examples

    iex> PhoenixKitEntities.enable_all_definitions_mirror()
    {:ok, count}

# `enable_system`

```elixir
@spec enable_system(keyword()) :: {:ok, term()} | {:error, term()}
```

Enables the entities system.

Sets the "entities_enabled" setting to true and logs a
`module.entities.enabled` activity row.

## Options

  * `:actor_uuid` — UUID of the user toggling the system. Threaded
    through to the activity log entry. `nil` is allowed when the
    caller doesn't have a scope (system jobs).

## Examples

    iex> PhoenixKitEntities.enable_system(actor_uuid: admin.uuid)
    {:ok, %Setting{}}

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Checks if the entities system is enabled.

Returns true if the "entities_enabled" setting is true.

## Examples

    iex> PhoenixKitEntities.enabled?()
    false

# `entities_children`

```elixir
@spec entities_children(any()) :: [PhoenixKit.Dashboard.Tab.t()]
```

# `entities_children`

```elixir
@spec entities_children(any(), String.t() | nil) :: [PhoenixKit.Dashboard.Tab.t()]
```

Dynamic children function for Entities sidebar tabs.

Supports both arities:
- `entities_children(scope, locale)` — preferred when phoenix_kit core
  (>= pending `dynamic_children/2` release) passes the current locale
  explicitly to the sidebar callback.
- `entities_children(scope)` — fallback that reads the locale from
  `Gettext.get_locale/1`. Older core releases dispatch this form.

# `get_config`

```elixir
@spec get_config() :: map()
```

Gets the current entities system configuration.

Returns a map with the current settings.

## Examples

    iex> PhoenixKitEntities.get_config()
    %{enabled: false, max_per_user: 100, allow_relations: true, file_upload: false, entity_count: 0, total_data_count: 0}

Count queries are wrapped in `safe_count/1` so `get_config/0` works
outside of a sandbox checkout (e.g. unit-test contexts that don't
use `DataCase`) — same defensive pattern as `enabled?/0`.

# `get_entity`

```elixir
@spec get_entity(
  term(),
  keyword()
) :: t() | nil
```

Gets a single entity by integer ID or UUID.

Returns the entity if found, nil otherwise.

Accepts:
- Integer ID (e.g., 123)
- UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000")
- Integer string (e.g., "123")

## Examples

    iex> PhoenixKitEntities.get_entity(123)
    %PhoenixKit.Entities{}

    iex> PhoenixKitEntities.get_entity("550e8400-e29b-41d4-a716-446655440000")
    %PhoenixKit.Entities{}

    iex> PhoenixKitEntities.get_entity(456)
    nil

# `get_entity!`

```elixir
@spec get_entity!(
  term(),
  keyword()
) :: t()
```

Gets a single entity by integer ID or UUID.

Raises `Ecto.NoResultsError` if the entity does not exist.

## Examples

    iex> PhoenixKitEntities.get_entity!(123)
    %PhoenixKit.Entities{}

    iex> PhoenixKitEntities.get_entity!(456)
    ** (Ecto.NoResultsError)

# `get_entity_by_name`

```elixir
@spec get_entity_by_name(
  String.t(),
  keyword()
) :: t() | nil
```

Gets a single entity by its unique name.

Returns the entity if found, nil otherwise.

## Examples

    iex> PhoenixKitEntities.get_entity_by_name("brand")
    %PhoenixKit.Entities{}

    iex> PhoenixKitEntities.get_entity_by_name("invalid")
    nil

# `get_entity_translation`

```elixir
@spec get_entity_translation(t(), String.t()) :: map() | nil
```

Gets the translation for a specific language on an entity definition.

Returns the translated fields merged with the primary language values
as defaults. Returns primary language values if no translation exists.

## Examples

    iex> get_entity_translation(entity, "es-ES")
    %{"display_name" => "Productos", "display_name_plural" => "Productos", "description" => "..."}

# `get_entity_translations`

```elixir
@spec get_entity_translations(t()) :: %{optional(String.t()) =&gt; map()}
```

Gets all translations for an entity definition.

Returns a map of language codes to translated fields.
Only includes languages that have at least one translated field.

## Examples

    iex> get_entity_translations(entity)
    %{
      "es-ES" => %{"display_name" => "Productos", "display_name_plural" => "Productos"},
      "fr-FR" => %{"display_name" => "Produits"}
    }

    iex> get_entity_translations(entity_without_translations)
    %{}

# `get_max_per_user`

```elixir
@spec get_max_per_user() :: non_neg_integer()
```

Gets the maximum number of entities a single user can create.

Returns the system-wide limit for entity creation per user.
Defaults to 100 if not set.

## Examples

    iex> PhoenixKitEntities.get_max_per_user()
    100

# `get_mirror_settings`

```elixir
@spec get_mirror_settings(t()) :: %{
  mirror_definitions: boolean(),
  mirror_data: boolean()
}
```

Gets the mirror settings for an entity.

Returns a map with mirror_definitions and mirror_data booleans.
Defaults to false if not explicitly set.

## Examples

    iex> PhoenixKitEntities.get_mirror_settings(entity)
    %{mirror_definitions: true, mirror_data: false}

# `get_sort_mode`

```elixir
@spec get_sort_mode(t()) :: String.t()
```

Gets the sort mode for an entity.

Returns `"auto"` (sort by creation date, default) or `"manual"` (sort by position).

## Examples

    iex> PhoenixKitEntities.get_sort_mode(entity)
    "auto"

# `get_sort_mode_by_uuid`

```elixir
@spec get_sort_mode_by_uuid(binary()) :: String.t()
```

Gets the sort mode for an entity by UUID.

Convenience wrapper that looks up the entity first.
Returns `"auto"` if the entity is not found.

## Examples

    iex> PhoenixKitEntities.get_sort_mode_by_uuid(entity_uuid)
    "manual"

# `get_system_stats`

```elixir
@spec get_system_stats() :: map()
```

Gets summary statistics for the entities system.

Returns counts and metrics useful for admin dashboards.

## Examples

    iex> PhoenixKitEntities.get_system_stats()
    %{total_entities: 5, active_entities: 4, total_data_records: 150}

# `invalidate_entities_cache`

```elixir
@spec invalidate_entities_cache() :: :ok
```

Invalidates the cached entity summaries in the Dashboard Registry's ETS table.
Called when entity lifecycle PubSub events are received.

# `list_active_entities`

```elixir
@spec list_active_entities(keyword()) :: [t()]
```

Returns the list of active (published) entities.

## Examples

    iex> PhoenixKitEntities.list_active_entities()
    [%PhoenixKit.Entities{status: "published"}, ...]

# `list_entities`

```elixir
@spec list_entities(keyword()) :: [t()]
```

Returns the list of entities ordered by creation date.

## Examples

    iex> PhoenixKitEntities.list_entities()
    [%PhoenixKit.Entities{}, ...]

# `list_entities_with_mirror_status`

```elixir
@spec list_entities_with_mirror_status() :: [map()]
```

Lists all entities with their mirror status and data counts.

Returns a list of maps suitable for the settings UI.

## Examples

    iex> PhoenixKitEntities.list_entities_with_mirror_status()
    [%{id: 1, name: "test", display_name: "Test", data_count: 8, mirror_definitions: true, mirror_data: false}, ...]

# `list_entity_summaries`

```elixir
@spec list_entity_summaries(keyword()) :: [map()]
```

Returns a lightweight list of published entity summaries for sidebar display.

Selects only sidebar-relevant fields without preloading associations.
Supports `:lang` option for translation resolution.

# `manual_sort?`

```elixir
@spec manual_sort?(t()) :: boolean()
```

Checks if an entity uses manual sorting.

## Examples

    iex> PhoenixKitEntities.manual_sort?(entity)
    true

# `mirror_data_enabled?`

```elixir
@spec mirror_data_enabled?(t()) :: boolean()
```

Checks if data mirroring is enabled for this entity.

## Examples

    iex> PhoenixKitEntities.mirror_data_enabled?(entity)
    false

# `mirror_definitions_enabled?`

```elixir
@spec mirror_definitions_enabled?(t()) :: boolean()
```

Checks if definition mirroring is enabled for this entity.

## Examples

    iex> PhoenixKitEntities.mirror_definitions_enabled?(entity)
    true

# `multilang_enabled?`

```elixir
@spec multilang_enabled?() :: boolean()
```

Checks if multilang is globally enabled (Languages module has 2+ languages).

Convenience wrapper around `Multilang.enabled?/0`.

## Examples

    iex> PhoenixKitEntities.multilang_enabled?()
    true

# `next_entity_position`

```elixir
@spec next_entity_position() :: integer()
```

Returns the next available `position` for a new entity — i.e. one
past the highest currently used. Falls back to `1` when the table is
empty.

# `phoenix_kit_project_extensions`

# `remove_entity_translation`

```elixir
@spec remove_entity_translation(t(), String.t()) ::
  {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Removes all translations for a specific language from an entity definition.

## Examples

    iex> remove_entity_translation(entity, "es-ES")
    {:ok, %PhoenixKitEntities{}}

# `reorder_entities`

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

Re-indexes the supplied list of entity UUIDs into positions `1..N`
in the order given.

This is the entry point for the drag-and-drop reorder event from the
Entities admin LV. UUIDs not in the list are left at their current
positions; missing UUIDs in the table are silently skipped (the LV
always sends the full visible list, so partial sends are a stale-DOM
artifact and not worth blowing up over).

Wraps both passes in a single transaction. Returns `:ok` on success
or `{:error, reason}` on transaction failure.

# `resolve_language`

```elixir
@spec resolve_language(t(), String.t() | nil) :: t()
@spec resolve_language(t(), String.t()) :: t()
```

Resolves translated fields on an entity struct for a given language.

Merges translations from `settings["translations"][lang_code]` onto the
entity's `display_name`, `display_name_plural`, and `description` fields.

For the primary language (or when no translation exists), returns the entity
unchanged. For secondary languages, applies override values where they exist
and keeps primary values as defaults.

## Examples

    iex> resolve_language(entity, "es-ES")
    %PhoenixKitEntities{display_name: "Productos", ...}

    iex> resolve_language(entity, "en-US")  # primary language
    %PhoenixKitEntities{display_name: "Products", ...}

# `resolve_languages`

```elixir
@spec resolve_languages([t()], String.t() | nil) :: [t()]
@spec resolve_languages([t()], String.t()) :: [t()]
```

Resolves translations on a list of entity structs.

## Examples

    iex> resolve_languages(entities, "es-ES")
    [%PhoenixKitEntities{display_name: "Productos"}, ...]

# `set_entity_translation`

```elixir
@spec set_entity_translation(t(), String.t(), map()) ::
  {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Sets the translation for a specific language on an entity definition.

Merges the provided fields into the existing translation for that language.
Empty string values are treated as "remove override" (field falls back to primary).

## Examples

    iex> set_entity_translation(entity, "es-ES", %{
    ...>   "display_name" => "Productos",
    ...>   "display_name_plural" => "Productos"
    ...> })
    {:ok, %PhoenixKitEntities{}}

# `update_entity`

```elixir
@spec update_entity(t(), map(), keyword()) ::
  {:ok, t()} | {:error, Ecto.Changeset.t() | :managed_blueprint | :locked_key}
```

Updates an entity.

## Examples

    iex> PhoenixKitEntities.update_entity(entity, %{display_name: "Updated"})
    {:ok, %PhoenixKit.Entities{}}

    iex> PhoenixKitEntities.update_entity(entity, %{name: ""})
    {:error, %Ecto.Changeset{}}

# `update_mirror_settings`

```elixir
@spec update_mirror_settings(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Updates the mirror settings for an entity.

## Parameters
  - `entity` - The entity to update
  - `mirror_settings` - Map with keys "mirror_definitions" and/or "mirror_data"

## Examples

    iex> PhoenixKitEntities.update_mirror_settings(entity, %{"mirror_definitions" => true})
    {:ok, %PhoenixKit.Entities{}}

# `update_sort_mode`

```elixir
@spec update_sort_mode(t(), String.t()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Updates the sort mode for an entity.

Valid modes: `"auto"` (sort by creation date) or `"manual"` (sort by position).

When switching to manual mode, existing records retain their auto-populated
positions from creation order. Admins can then reorder as needed.

## Examples

    iex> PhoenixKitEntities.update_sort_mode(entity, "manual")
    {:ok, %PhoenixKitEntities{}}

# `validate_user_entity_limit`

```elixir
@spec validate_user_entity_limit(String.t()) ::
  {:ok, :valid} | {:error, {:user_entity_limit_reached, non_neg_integer()}}
```

Validates that a user hasn't exceeded their entity creation limit.

Checks the current number of entities created by the user against the system limit.
Returns `{:ok, :valid}` if within limits, `{:error, reason}` if limit exceeded.

## Examples

    iex> PhoenixKitEntities.validate_user_entity_limit(1)
    {:ok, :valid}

    iex> PhoenixKitEntities.validate_user_entity_limit(1)
    {:error, {:user_entity_limit_reached, 100}}

Error tuples flow through `PhoenixKitEntities.Errors.message/1` for
user-facing strings.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
