Packages

phoenix_kit

2.65.0
3.0.0 2.65.0 2.64.1 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. A system library
  is `site`, except a **hidden** one a module keeps its own files in, which is
  `private` (`PhoenixKit.Modules.Storage.Libraries.hidden?/1`).

  `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

  alias PhoenixKit.Modules.Storage.Libraries

  @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. Always
  `site`; `Libraries.create_system_library/2` makes a hidden one `private`.
  """
  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

  defp requested_visibility(attrs) when is_map(attrs),
    do: Map.get(attrs, :visibility) || Map.get(attrs, "visibility")

  @doc """
  A new user library: a name, its owner, a URL slug (unique among the
  owner's libraries) and an object-key prefix. `private` unless the caller says
  `visibility: "site"` (a public library, see `Libraries.create_user_library/3`,
  which is where that is allowed or refused: this changeset trusts its attrs).
  """
  def create_user_changeset(library, attrs) do
    # Read from the attrs, never from the changeset: the struct's own default is
    # "site", so a changeset that was not asked for "site" would still say it.
    visibility = if requested_visibility(attrs) == "site", do: "site", else: "private"

    library
    |> cast(attrs, [:name, :owner_uuid, :key_prefix, :slug, :is_default])
    |> put_change(:kind, "user")
    |> put_change(:visibility, visibility)
    |> 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

  @doc """
  Changes a user library's URL slug. Its name, owner and kind never change here. The slug
  keeps the shape `create_user_changeset/2` demands, is unique among the owner's libraries,
  and is not one the host has reserved (`PhoenixKit.Modules.Storage.Libraries.reserved_slug?/1`).
  """
  def slug_changeset(library, attrs) do
    library
    |> cast(attrs, [:slug])
    |> validate_required([:slug])
    |> validate_format(:slug, ~r/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
    |> validate_length(:slug, max: 64)
    |> validate_change(:slug, fn :slug, slug ->
      if Libraries.reserved_slug?(slug),
        do: [slug: "is reserved"],
        else: []
    end)
    |> unique_constraint(:slug, name: :phoenix_kit_storage_libraries_owner_slug_index)
  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