MishkaCmsCore.Runtime.MediaCategory (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Manages media categories in the MishkaCMS runtime system with full versioning support.

This resource represents categories for organizing media files. Each category belongs to a specific site and can have multiple media files associated with it. Categories help organize and filter media content within the CMS.

A category is a folder, and a folder sits inside another one of its site through parent_id, or at the top level when it has none. Its name is unique among the folders beside it, not across the site. A folder never goes inside itself or one of its own subfolders, and is archived or deleted only once nothing is left in it — not a file and not a folder, archived ones included. Moving a folder moves everything under it: the folders below name it, not where it is. trail/2 reads the folders above one, and :path_label names a folder by them.

Media categories feature full version control through PaperTrail integration, tracking all changes with snapshot-based history and the ability to restore previous versions. The resource implements soft deletion via archival and provides comprehensive metadata management including name and description tracking.

The resource implements soft deletion via archival and provides tenant-aware access control. Multi-tenant isolation ensures categories are properly segregated between sites. The resource provides extensive API actions for external integrations and maintains complete audit trails of all modifications including actor tracking and action recording.

Summary

Functions

Apply the full GuardedStruct pipeline (sanitize → validate → derive → main_validator) to attrs and return either {:ok, transformed_attrs} or {:error, errors}. Wire this into an Ash.Resource.Change to plug guardedstruct rules into Ash's changeset pipeline.

O(1) lookup of a field's compile-time metadata by name. Returns nil for unknown fields.

Compile-time-baked MapSet of every field name owned by the guardedstruct block. Used by GuardedStruct.AshResource.Change to decide, in O(1), whether a key in changeset.atomics belongs to the pipeline.

Validates that the keys in the provided input are valid for at least one action on the resource.

Same as input/1, except restricts the keys to values accepted by the action provided.

How many live files sit directly in each of folders, in their order — one query for all of them.

The folders a form's folder picker offers, as its load: those of the site the form is for, each with its :path_label. The site is the one picked on the form, else the edited record's, else the user's own; a form with no site yet is offered no folder.

Each of folders named after the folders above it, outermost first: photos / 2024 / covers.

The whole path of each folder ids names, outermost first, keyed by id: photos / 2024. Read as trail/2 reads, one level at a time for all of them together. A nil, a folder the actor cannot read, and every one when the folders cannot be read, are left out.

What a folder picker shows for folder: its :path_label when that was loaded, its name when not.

The folder id and every folder above it, outermost first, each with its :id, :name, :parent_id and :site_id — what a breadcrumb draws.

Types

t()

@type t() :: %MishkaCmsCore.Runtime.MediaCategory{
  __lateral_join_source__: term(),
  __meta__: term(),
  __metadata__: term(),
  __order__: term(),
  aggregates: term(),
  archived_at: term(),
  calculations: term(),
  description: term(),
  id: term(),
  inserted_at: term(),
  latest_version: term(),
  media: term(),
  media_count: term(),
  name: term(),
  paper_trail_versions: term(),
  parent: term(),
  parent_id: term(),
  path_label: term(),
  recent_versions: term(),
  site: term(),
  site_id: term(),
  sorted_versions: term(),
  updated_at: term(),
  version_count: term(),
  version_preview: term()
}

Functions

__guarded_change__(attrs, error? \\ false)

Apply the full GuardedStruct pipeline (sanitize → validate → derive → main_validator) to attrs and return either {:ok, transformed_attrs} or {:error, errors}. Wire this into an Ash.Resource.Change to plug guardedstruct rules into Ash's changeset pipeline.

The function is named __guarded_change__ because it does more than validate — it can also transform values (trim, downcase, slugify, auto-fill, etc.) before they reach the data layer.

__guarded_field_meta__(name)

O(1) lookup of a field's compile-time metadata by name. Returns nil for unknown fields.

__guarded_field_name_set__()

Compile-time-baked MapSet of every field name owned by the guardedstruct block. Used by GuardedStruct.AshResource.Change to decide, in O(1), whether a key in changeset.atomics belongs to the pipeline.

default_short_name()

input(opts)

@spec input(values :: map() | Keyword.t()) :: map() | no_return()

Validates that the keys in the provided input are valid for at least one action on the resource.

Raises a KeyError error at compile time if not. This exists because generally a struct should only ever be created by Ash as a result of a successful action. You should not be creating records manually in code, e.g %MyResource{value: 1, value: 2}. Generally that is fine, but often with embedded resources it is nice to be able to validate the keys that are being provided, e.g

Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()

input(opts, action)

@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()

Same as input/1, except restricts the keys to values accepted by the action provided.

json_api_match_route(_, _)

media_counts(folders)

@spec media_counts([
  %MishkaCmsCore.Runtime.MediaCategory{
    __lateral_join_source__: term(),
    __meta__: term(),
    __metadata__: term(),
    __order__: term(),
    aggregates: term(),
    archived_at: term(),
    calculations: term(),
    description: term(),
    id: term(),
    inserted_at: term(),
    latest_version: term(),
    media: term(),
    media_count: term(),
    name: term(),
    paper_trail_versions: term(),
    parent: term(),
    parent_id: term(),
    path_label: term(),
    recent_versions: term(),
    site: term(),
    site_id: term(),
    sorted_versions: term(),
    updated_at: term(),
    version_count: term(),
    version_preview: term()
  }
]) :: [non_neg_integer()]

How many live files sit directly in each of folders, in their order — one query for all of them.

offer(query, state)

@spec offer(Ash.Query.t(), map()) :: Ash.Query.t()

The folders a form's folder picker offers, as its load: those of the site the form is for, each with its :path_label. The site is the one picked on the form, else the edited record's, else the user's own; a form with no site yet is offered no folder.

path_labels(folders, opts)

@spec path_labels(
  [
    %MishkaCmsCore.Runtime.MediaCategory{
      __lateral_join_source__: term(),
      __meta__: term(),
      __metadata__: term(),
      __order__: term(),
      aggregates: term(),
      archived_at: term(),
      calculations: term(),
      description: term(),
      id: term(),
      inserted_at: term(),
      latest_version: term(),
      media: term(),
      media_count: term(),
      name: term(),
      paper_trail_versions: term(),
      parent: term(),
      parent_id: term(),
      path_label: term(),
      recent_versions: term(),
      site: term(),
      site_id: term(),
      sorted_versions: term(),
      updated_at: term(),
      version_count: term(),
      version_preview: term()
    }
  ],
  keyword()
) :: [String.t()]

Each of folders named after the folders above it, outermost first: photos / 2024 / covers.

The folders above are read as trail/2 reads them, one level at a time for all of folders together. When they cannot be read, each folder is named by its own name.

paths(ids, opts)

@spec paths([Ecto.UUID.t() | nil], keyword()) :: %{
  required(Ecto.UUID.t()) => String.t()
}

The whole path of each folder ids names, outermost first, keyed by id: photos / 2024. Read as trail/2 reads, one level at a time for all of them together. A nil, a folder the actor cannot read, and every one when the folders cannot be read, are left out.

picker_label(map)

@spec picker_label(map()) :: String.t()

What a folder picker shows for folder: its :path_label when that was loaded, its name when not.

primary_key_matches?(left, right)

trail(id, opts)

@spec trail(Ecto.UUID.t(), keyword()) ::
  {:ok,
   [
     %MishkaCmsCore.Runtime.MediaCategory{
       __lateral_join_source__: term(),
       __meta__: term(),
       __metadata__: term(),
       __order__: term(),
       aggregates: term(),
       archived_at: term(),
       calculations: term(),
       description: term(),
       id: term(),
       inserted_at: term(),
       latest_version: term(),
       media: term(),
       media_count: term(),
       name: term(),
       paper_trail_versions: term(),
       parent: term(),
       parent_id: term(),
       path_label: term(),
       recent_versions: term(),
       site: term(),
       site_id: term(),
       sorted_versions: term(),
       updated_at: term(),
       version_count: term(),
       version_preview: term()
     }
   ]}
  | {:error, term()}

The folder id and every folder above it, outermost first, each with its :id, :name, :parent_id and :site_id — what a breadcrumb draws.

Read through :read_any as opts[:actor], with opts[:authorize?], one level at a time: one small read per folder up the chain. {:ok, []} when the folder is archived, gone, or not one the actor reads.