# `PhoenixKit.Modules.Storage.Reorganizer.ResourceSource`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.41.6/lib/modules/storage/reorganizer/resource_source.ex#L1)

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
    (`t: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.

# `kind_spec`

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

# `spec`

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

# `plan`

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

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

---

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