Backpex.Preferences (Backpex v0.20.0)

Copy Markdown View Source

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).

Summary

Functions

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

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

Reads every value under prefix as a nested map.

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

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

Returns the Phoenix session key used by the Session adapter.

Functions

apply_effects_on_conn(conn, effects)

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(ctx_or_session, key, opts \\ [])

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(ctx_or_session, prefix, opts \\ [])

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(target, key, value, opts \\ [])

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(ctx, entries, opts \\ [])

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.