PhoenixKit.Modules.Storage.Reorganizer.ResourceSource (phoenix_kit v2.40.1)

Copy Markdown View Source

A reorganizer PhoenixKit.Modules.Storage.Reorganizer.Source built from a declaration, for a module that keeps one media folder per record through PhoenixKit.Modules.Storage.ResourceFolders. The module states what its records are; this module applies the whole Source contract to them — one implementation of the rules instead of one per module:

defmodule MyModule.MediaReorganizer do
  alias PhoenixKit.Modules.Storage.Reorganizer.ResourceSource

  def plan(actor_uuid, opts \\ []), do: ResourceSource.plan(spec(), actor_uuid, opts)

  defp spec do
    %{
      source: "my_module",
      app: :my_module,
      pending_prefix: "my-module-attachment-pending-",
      kinds: [
        %{
          kind: :item,
          schema: MyModule.Item,
          prefix: "my-module-item-",
          pointer: {:data, "files_folder_uuid"},
          live: &where(&1, [r], r.status != "deleted")
        }
      ]
    }
  end
end

The declaration

  • :source — the source name on every action
  • :app — the OTP app whose :attachments_parent_folder / :attachments_folder_name hooks decide where folders belong
  • :kinds — the record kinds, parents first (their folders move in this order)
  • :pending_prefix — the name prefix of folders made for unsaved records, when the module makes any; stale empty ones are trashed
  • :noun — what a record is called in reports (default "record")
  • :name_hook — false for a module whose runtime never asks the :attachments_folder_name hook (it finds folders by the deterministic name only): the plan then never proposes a host name its uploads would not follow (default true)
  • :extra — fun(actor_uuid, opts) -> [action] for reports only this module can make, appended to the plan

Each kind:

  • :kind, :schema — the atom the parent hook receives, the schema
  • :prefix — the deterministic folder name is prefix <> uuid
  • :pointer — where the record stores its folder uuid (PhoenixKit.Modules.Storage.ResourceFolders.pointer/0), or nil for a module that finds folders by name only
  • :live — fun(query) -> query keeping the records that are live (default: every row); a record that is not live is an orphan's owner
  • :subject — what the parent hook's third argument is: :record (the full row, the default) or :uuid
  • :label — the field naming a record in reports (default :name)
  • :fields — further columns candidate detection reads (default [])
  • :order — fun(records) -> records reordering a kind's records (e.g. parents before children within the kind)
  • :orphan — :not_live (default) reports the folder of a missing or not-live record; :missing only a missing one

Rules applied

The Source moduledoc is the contract; how each case is decided:

  • No parent hook configured → report-only (orphans, pending folders); a configured hook that is not callable is one :hook_error.
  • A candidate is a record with a live folder — through its pointer or by its deterministic name anywhere; nothing else reaches a hook, and candidates are reloaded as full rows before any hook sees them.
  • The parent hook failing (raise, throw, exit, anything but {:ok, uuid} / {:ok, nil} / nil) skips the record into one :hook_error; so does a failing name hook. Logs name the failure's shape only.
  • A pointer-found folder keeps its name unless it still has the deterministic or a pending name; then the host name is proposed.
  • By name: the host name directly under the parent (not adopted when another record's pointer claims it — the deterministic name is wanted instead), then the deterministic name under the parent, then at the root. Two of these live at once is a :duplicate; a copy anywhere else is :relocated, never moved. With a root answer and no copy at the root, a single copy elsewhere is adopted in place (:hook_nil) and several are a :duplicate.
  • A root answer never moves a folder that has a parent (:hook_nil).
  • Two records on one folder, or two moves onto one target, are one :duplicate each; neither moves.
  • Orphans: deterministic-named folders at the root or under a parent a hook named, whose record is missing or not live, and no record claims. Pending folders: empty and older than pending_days → :trash (report-only without a hook); not empty → a report naming their files.
  • A module with a pointer back-fills it (after_move) and renames a taken target "name (N)"; one without reports the conflict.

Summary

Functions

The plan for spec (see the moduledoc) — Source.plan/2's answer.

Types

kind_spec()

@type kind_spec() :: %{
  :kind => atom(),
  :schema => module(),
  :prefix => String.t(),
  optional(:pointer) =>
    PhoenixKit.Modules.Storage.ResourceFolders.pointer() | nil,
  optional(:live) => (Ecto.Queryable.t() -> Ecto.Query.t()),
  optional(:subject) => :record | :uuid,
  optional(:label) => atom(),
  optional(:fields) => [atom()],
  optional(:order) => ([struct()] -> [struct()]),
  optional(:orphan) => :not_live | :missing
}

spec()

@type spec() :: %{
  :source => String.t(),
  :app => atom(),
  :kinds => [kind_spec()],
  optional(:pending_prefix) => String.t() | nil,
  optional(:noun) => String.t(),
  optional(:name_hook) => boolean(),
  optional(:extra) => (String.t() | nil, keyword() -> [map()])
}

Functions

plan(spec, actor_uuid, opts \\ [])

@spec plan(spec(), String.t() | nil, keyword()) :: [map()]

The plan for spec (see the moduledoc) — Source.plan/2's answer.