# `Backpex.Preferences`
[🔗](https://github.com/naymspace/backpex/blob/0.20.0/lib/backpex/preferences.ex#L1)

Unified preference management for Backpex.

Reads and writes UI state (theme, sidebar open/closed, sidebar section
expansion, per-resource column visibility, metric toggles, and user-defined
keys) through a configurable **adapter**. The adapter is selected per key by
a longest-prefix match against the configured routes (see
`Backpex.Preferences.Router`), so different prefixes can live in different
storage backends — e.g. `global.*` in the Phoenix session and `resource.*`
in a scoped database table.

## Zero-config defaults

With no `:backpex, Backpex.Preferences` config set, every key routes to
`Backpex.Preferences.Adapters.Session`.

## Configuring per-prefix routing

    config :backpex, Backpex.Preferences,
      adapters: [
        {"global.*",   Backpex.Preferences.Adapters.Session, []},
        {"resource.*", Backpex.Preferences.Adapters.Ecto,
         repo: MyApp.Repo, schema: MyApp.Preference, scope_fields: [:user_id, :tenant_id]},
        {:default,     Backpex.Preferences.Adapters.Session, []}
      ],
      scope: {MyAppWeb.PreferencesScope, :resolve, []}

## Key format

- `global.*` — application-wide preferences (theme, sidebar, ...).
- `resource:<Module>:*` — per-resource preferences (columns, metrics, ...).
  Uses `:` as a separator so module-name dots don't split into extra
  segments (`Backpex.Preferences.Key`).
- `custom.*` — user-defined preferences.

## Public API

  * `get/3` — read a single preference, with a `:default` fallback.
  * `get_map/3` — read every value under a prefix as a nested map.
  * `put/4` — write from a LiveView socket or `%Plug.Conn{}`.
  * `put_batch/3` — dispatch a list of writes (best-effort, first-error-wins;
    see the function docs for the partial-success semantics).

# `apply_effects_on_conn`

Applies a list of adapter side effects to a `%Plug.Conn{}`.

Takes a list because a batch write collects one effect per entry (see
`put_batch/3`); a single `put/4` yields at most one.

Exposed for the preferences controller; not intended for general callers.

# `get`

Reads a preference. Falls back to `opts[:default]` when the value is
missing or the adapter cannot resolve the current preference scope.

Accepts a `%Backpex.Preferences.Context{}` or a bare Phoenix session map.
The session-map form is convenient for call sites that only have a
session on hand.

## Options

  * `:default` — returned when nothing is stored for `key` (default: `nil`).

Extra options are forwarded to the adapter.

## Distinguishing "never set"

With no `:default`, a missing value reads as `nil` — which separates "the
user never set this" from "the user deliberately stored an empty value".
An explicitly cleared `%{}` or `[]` is a real preference and must not be
overwritten by an application default; this is how `persist: [:filters]`
decides whether to apply a resource's filter defaults.

The default Session adapter reserves `nil` for “not found”, so it cannot
distinguish a stored `nil` from a missing key. Store a tagged value such as
`%{"value" => nil}` when that distinction matters. A custom adapter that
returns `{:ok, nil}` may preserve `nil`; with such an adapter, a sentinel
default (for example `default: :__unset__`) distinguishes the missing case.

## Examples

    iex> session = %{"backpex_preferences" => %{"global" => %{"theme" => "dark"}}}
    iex> Backpex.Preferences.get(session, "global.theme")
    "dark"

    iex> Backpex.Preferences.get(%{}, "global.theme", default: "light")
    "light"

# `get_map`

Reads every value under `prefix` as a nested map.

Keys in the returned map are relative to `prefix` (i.e. segments that
follow the prefix). The adapter is free to store values however it likes,
but the shape returned here matches what a single nested `get/3` at that
prefix would have produced.

Returns `%{}` when nothing is stored, the adapter cannot resolve the scope,
or the adapter fails for any other reason.

Client-overlay descendants are reconstructed only for dot-form keys by
matching `prefix <> "."`. Adapter-backed colon-form subtrees remain readable,
but pending client values for keys such as `resource:MyApp.PostLive:columns`
are not merged into a `get_map/3` result. Use `get/3` for colon-form keys when
client-overlay precedence is required.

## Examples

    iex> session = %{
    ...>   "backpex_preferences" => %{
    ...>     "global" => %{"sidebar_section" => %{"blog" => true, "users" => false}}
    ...>   }
    ...> }
    iex> Backpex.Preferences.get_map(session, "global.sidebar_section")
    %{"blog" => true, "users" => false}

    iex> Backpex.Preferences.get_map(%{}, "global.sidebar_section")
    %{}

# `put`

Persists a preference from within a LiveView socket or Plug controller.

Resolves the adapter for `key`, asks it to persist the value, and applies
the side effect it returns (e.g. `put_session`) to the caller. An adapter
that persisted on its own returns `{:ok, :persisted}` and the caller is
handed back unchanged.

When the chosen adapter refuses a non-HTTP write with `:requires_http`
(default behavior of the Session adapter outside a controller), falls back
to `push_event/3` so the browser can retry via the preferences controller
on its next paint.

A socket target supplies `socket.assigns` but no mount session to the
adapter/scope context (`ctx.session` is `%{}`). Scope resolution for
server-originated LiveView writes must therefore work from assigns. A conn
target supplies both `conn.assigns` and the current session.

Returns one of:

  * `{:ok, socket_or_conn}` — write accepted.
  * `{:error, reason}` — the adapter refused the write for a non-transport
    reason. Callers typically ignore the failure (preferences are best
    effort) but can surface it if needed.

## Options

  * `:mirror` — `:session` to have the browser mirror the value into
    `sessionStorage`. Only consulted on the `push_event` fallback: it is a
    property of that round-trip, not of the value, and an adapter that
    persists server-side is read fresh at every mount and needs no mirror.
    See `Backpex.Preferences.LiveView.push_write/4` for when a key needs it.

Every other option is forwarded to the adapter, on top of the adapter's
configured options.

## Examples

From a Plug controller (session is updated in-place):

    Backpex.Preferences.put(conn, "global.theme", "dark")
    #=> {:ok, %Plug.Conn{}}

From a LiveView `handle_event` (session adapter returns `:requires_http`,
so the dispatcher falls back to a `push_event` for the browser to retry):

    Backpex.Preferences.put(socket, "global.theme", "dark")
    #=> {:ok, %Phoenix.LiveView.Socket{}}

# `put_batch`

Dispatches a batch of writes through their adapters and returns the
collected side effects, or the first error encountered.

Used by `Backpex.PreferencesController` to dispatch cross-adapter batch
writes.

Each entry's adapter returns at most one side effect, so the returned list
holds one entry per write that needs the caller to do something — adapters
that persisted on their own (`{:ok, :persisted}`) contribute nothing.

Threads the accumulated session state through each adapter call so that
writes under the same session key compose correctly. The caller applies
the returned effects in order; for `:put_session` effects targeting the
same key, the last effect holds the fully-merged value.

## Semantics

This is **best-effort, first-error-wins**. On the first adapter that
returns `{:error, reason}` the loop halts and returns
`{:error, {key, reason}}` — subsequent entries are not dispatched. Earlier
successful writes may already have been committed by their adapters (e.g.
a DB-backed adapter that writes eagerly). The adapter behaviour has no
rollback primitive, so callers should treat partial success as possible.

## Examples

    ctx = Backpex.Preferences.Context.from_conn(conn)

    Backpex.Preferences.put_batch(ctx, [
      {"global.theme", "dark"},
      {"global.sidebar_open", false}
    ])
    #=> {:ok, [{:put_session, "backpex_preferences", %{...}}]}

# `session_key`

Returns the Phoenix session key used by the Session adapter.

Convenience passthrough to `Backpex.Preferences.Adapters.Session.session_key/0`.

---

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