The history of site settings: what each setting was, and since when.
Why
A stored instant does not say which regime wrote it. When the time_zone
setting moved from an integer offset to an IANA id and several modules
turned out to have added that value to other instants, the rows they had
written could not be repaired: nothing recorded when the setting changed
or what it was before. phoenix_kit_settings.date_updated holds the last
change only, and no settings writer logged anything.
Where it lives
In the activity feed, as setting.changed entries that are permanent
(the pruner keeps them whatever their age), so there is one record of who
did what, and the admin's Activity page shows settings changes beside
everything else. The entry is inserted inside the settings write's
transaction and published to the feed's subscribers only after the commit
— nobody hears of a change that rolled back.
Every write through PhoenixKit.Settings that changes a value records
one entry: metadata carries the key, the value from and to (a
JSON setting as its encoded document), the source ("settings" for the
admin pages, "system" otherwise); actor_uuid is the person when one
made the change; resource_uuid is the setting row. The value before is
read under a row lock inside the write's transaction, so two racing
writers cannot both record the same old value. A write that leaves the
value as it was records nothing. A restricted (secret) setting records
that a change happened — restricted: true, both values withheld.
Reading it
list/2— the changes to one key, newest first.value_at/2— the value a key had at an instant: the answer to "which timezone was this site on when that row was written?".
Summary
Functions
The activity action a settings change is logged under.
The changes to key, newest first. :limit (default 100).
The current row for key, locked for the rest of the transaction — the
"before" a writer hands to record/3. nil when the key does not exist.
Call inside a transaction.
Publishes a recorded entry to the feed's subscribers — call after the
transaction that wrote it has committed. :unchanged publishes nothing.
Records the change a settings write made, or nothing when it changed no value.
The value key had at instant.
Functions
@spec action() :: String.t()
The activity action a settings change is logged under.
@spec list( String.t(), keyword() ) :: [PhoenixKit.Activity.Entry.t()]
The changes to key, newest first. :limit (default 100).
@spec lock_current(String.t()) :: PhoenixKit.Settings.Setting.t() | nil
The current row for key, locked for the rest of the transaction — the
"before" a writer hands to record/3. nil when the key does not exist.
Call inside a transaction.
@spec publish(PhoenixKit.Activity.Entry.t() | :unchanged) :: :ok
Publishes a recorded entry to the feed's subscribers — call after the
transaction that wrote it has committed. :unchanged publishes nothing.
@spec record( PhoenixKit.Settings.Setting.t() | nil, PhoenixKit.Settings.Setting.t(), keyword() ) :: {:ok, PhoenixKit.Activity.Entry.t() | :unchanged} | {:error, Ecto.Changeset.t()}
Records the change a settings write made, or nothing when it changed no value.
before is the row as it was — read under a row lock inside the write's
transaction (lock_current/1) — or nil when the key did not exist;
written the row as stored. Options: :actor_uuid (nil for a module or
a migration), :source ("settings" for the admin pages; default
"system").
Returns {:ok, %Activity.Entry{}}, {:ok, :unchanged} or
{:error, changeset}.
@spec value_at(String.t(), DateTime.t() | NaiveDateTime.t()) :: String.t() | nil
The value key had at instant.
The newest change at or before the instant says what the value became;
with none, the oldest change after it says what the value was before
anything was recorded; with no history at all, the current value — a
setting that was never changed since recording began is ASSUMED to have
always been what it is now. A JSON setting is its encoded document, the
same shape the history holds. A DateTime in any zone is the instant it
names, not its wall clock. A restricted key answers nil for every
instant: its values are withheld from the history and this must not
become the way around that — and a key that WAS restricted when a change
was recorded answers nil for that period even after it stops being
restricted, because the value was never written down.