Packages

phoenix_kit

2.52.2
2.64.0 2.63.0 2.62.2 2.62.1 2.62.0 2.61.0 2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage schemas library.ex
Raw

lib/modules/storage/schemas/library.ex

defmodule PhoenixKit.Modules.Storage.Library do
  @moduledoc """
  A storage library: a partition of the file store (V202).

  Every file, media folder and folder link belongs to exactly one library.
  **System** libraries are site-wide and managed by admins; everything that
  existed before V202 lives in the default one, **Media**
  (`PhoenixKit.Modules.Storage.Libraries.media_uuid/0`). **User** libraries
  (V203) belong to a user, are `private`, and may have members
  (`PhoenixKit.Modules.Storage.LibraryMember`). A trashed user library may
  outlive its owner (`owner_uuid` NULL) until it is purged.

  `key_prefix` is the first segment of the object key for files stored in the
  library from now on. `nil` keeps the historical layout, which is what Media
  uses, so nothing about an existing install's keys changes.

  Go through `PhoenixKit.Modules.Storage.Libraries` rather than this schema.
  """

  use Ecto.Schema
  use PhoenixKit.SchemaPrefix
  import Ecto.Changeset

  @primary_key {:uuid, UUIDv7, autogenerate: true}
  @foreign_key_type UUIDv7

  @kinds ~w(system user)
  @visibilities ~w(site private)

  @type t :: %__MODULE__{
          uuid: UUIDv7.t() | nil,
          name: String.t() | nil,
          kind: String.t(),
          owner_uuid: UUIDv7.t() | nil,
          visibility: String.t(),
          key_prefix: String.t() | nil,
          slug: String.t() | nil,
          settings: map(),
          is_default: boolean(),
          trashed_at: DateTime.t() | nil,
          storage_profile_uuid: UUIDv7.t() | nil,
          variant_set_uuid: UUIDv7.t() | nil,
          inserted_at: DateTime.t() | nil,
          updated_at: DateTime.t() | nil
        }

  schema "phoenix_kit_storage_libraries" do
    field :name, :string
    field :kind, :string, default: "system"
    field :visibility, :string, default: "site"
    field :key_prefix, :string
    # The URL name (`/admin/media/library/<slug>`); nil for the default
    # library, which is the bare `/admin/media`. Kept when the library is
    # renamed, so its links keep working.
    field :slug, :string
    field :settings, :map, default: %{}
    field :is_default, :boolean, default: false
    field :trashed_at, :utc_datetime
    # Where the library's bytes live and which sizes its uploads get (V205).
    # nil means the Default of each; set through `Storage.Profiles` and
    # `Storage.VariantSets`, never cast by the changesets here.
    field :storage_profile_uuid, UUIDv7
    field :variant_set_uuid, UUIDv7

    belongs_to :owner, PhoenixKit.Users.Auth.User,
      foreign_key: :owner_uuid,
      references: :uuid

    timestamps(type: :utc_datetime)
  end

  @doc "A new system library: a name, a URL slug and an object-key prefix."
  def create_system_changeset(library, attrs) do
    library
    |> cast(attrs, [:name, :key_prefix, :slug])
    |> put_change(:kind, "system")
    |> put_change(:visibility, "site")
    |> validate_name()
    |> validate_required([:key_prefix, :slug])
    |> validate_format(:key_prefix, ~r/\A[a-z0-9][a-z0-9_-]{0,63}\z/)
    |> validate_format(:slug, ~r/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
    |> validate_length(:slug, max: 64)
    |> unique_constraint(:key_prefix, name: :phoenix_kit_storage_libraries_key_prefix_index)
    |> unique_constraint(:slug, name: :phoenix_kit_storage_libraries_owner_slug_index)
  end

  @doc """
  A new user library: a name, its owner, a URL slug (unique among the
  owner's libraries) and an object-key prefix. Always `private`.
  """
  def create_user_changeset(library, attrs) do
    library
    |> cast(attrs, [:name, :owner_uuid, :key_prefix, :slug, :is_default])
    |> put_change(:kind, "user")
    |> put_change(:visibility, "private")
    |> validate_name()
    |> validate_required([:owner_uuid, :key_prefix, :slug])
    |> validate_format(:key_prefix, ~r/\A[a-z0-9][a-z0-9_-]{0,63}\z/)
    |> validate_format(:slug, ~r/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
    |> validate_length(:slug, max: 64)
    |> unique_constraint(:key_prefix, name: :phoenix_kit_storage_libraries_key_prefix_index)
    |> unique_constraint(:slug, name: :phoenix_kit_storage_libraries_owner_slug_index)
    |> unique_constraint(:is_default, name: :phoenix_kit_storage_libraries_default_index)
  end

  @doc """
  The slug a name gives: lower-case letters and digits, hyphen-separated
  (`"Brand Assets 2026"` → `"brand-assets-2026"`). Letters outside ASCII
  are dropped with their accents where they have one; a name with nothing
  left is `"library"`.
  """
  @spec slugify(String.t()) :: String.t()
  def slugify(name) do
    name
    |> String.normalize(:nfd)
    |> String.replace(~r/\p{Mn}/u, "")
    |> String.downcase()
    |> String.replace(~r/[^a-z0-9]+/, "-")
    |> String.trim("-")
    |> String.slice(0, 58)
    |> String.trim("-")
    |> case do
      "" -> "library"
      slug -> slug
    end
  end

  @doc "Renames a library. Its kind, owner and key prefix never change here."
  def rename_changeset(library, attrs) do
    library
    |> cast(attrs, [:name])
    |> validate_name()
  end

  defp validate_name(changeset) do
    changeset
    |> update_change(:name, &String.trim/1)
    |> validate_required([:name])
    |> validate_length(:name, min: 1, max: 255)
    |> validate_inclusion(:kind, @kinds)
    |> validate_inclusion(:visibility, @visibilities)
    |> unique_constraint(:name,
      name: :phoenix_kit_storage_libraries_owner_name_index,
      message: "is already the name of another library"
    )
  end
end