Backpex.PreferencesController (Backpex v0.20.0)

Copy Markdown View Source

HTTP endpoint for persisting user preferences.

Accepts JSON requests from the BackpexPreferences JS hook. Each call routes through Backpex.Preferences, which dispatches to the adapter configured for the key's prefix (see Backpex.Preferences.Router).

Contracts

Single write:

POST /backpex_preferences
{"key": "global.theme", "value": "dark"}

Batch write:

POST /backpex_preferences
{"preferences": [
  {"key": "global.theme", "value": "dark"},
  {"key": "global.sidebar_open", "value": true}
]}

The batch form is best-effort, first-error-wins: if any adapter refuses a write, the dispatcher halts at that entry, no further adapters are called, and the response is 422 {ok: false, error: %{key: _, reason: _}}. Session-backed effects from earlier successful entries in the same batch are also dropped (the controller never applies them on the error path), so the session cookie is left unchanged. However, adapters that persist eagerly (e.g. a DB-backed adapter that wrote via Repo.insert!) may have already committed earlier writes — the adapter behaviour has no rollback primitive, so callers should treat partial success as possible.

An adapter that refuses a write because its store cannot hold it returns {:error, :too_large}, which surfaces as 422 {ok: false, error: %{key: _, reason: "too_large"}} — see the size limit section of Backpex.Preferences.Adapters.Session. The refusal is the designed outcome, not a bug: the alternative is a CookieOverflowError 500 on this and every later request.

Single-write :unscoped is treated as a no-op rather than an error: the response is 200 {ok: false, error: %{reason: "unscoped"}} and no warning is logged. The JS hook fires writes from anonymous visitors whenever the session lapses — this avoids surfacing them as 4xx noise. Batches always halt on any error (including :unscoped) and return 422.

Entries in a batch are retained only when they are maps containing a binary "key" and a "value" field. Other members are silently discarded; an empty or all-invalid list is therefore a successful no-op (200 {ok: true}). A payload that matches neither the single nor batch shape returns 400 {ok: false, error: "missing key/value"}.

Backpex.Preferences.put_batch/3 refuses a value that the built-in reader for its key cannot consume (Backpex.Preferences.Keys.valid_value?/2), returning 422 {ok: false, error: %{key: _, reason: "invalid_value"}}. Keys Backpex does not own (custom.*, unknown resource: suffixes) have no known shape and pass through unchecked — an adapter that needs constraints on those enforces its own.

That gate is a shape check, not authorization. This controller does not ask whether the caller may write the key, only whether the value would break a later render. Authorization belongs in the pipeline the route is mounted in, or in the adapter.