feat(observability): config API for the mappers dictionary and notification reads
The two resources whose absence is visible in the product: the mappers screen
fails outright because its table was never created in the store it read from,
and the notification bell has nowhere to record what a user has read.
Six routes under `/alerts/config`, behind the same internal API key and
answering in the same `{ "status": ... }` envelope the notifier uses. They join
the definition and enablement resources under that scope rather than sitting
beside the delivery routes, so the tree says one thing about itself: a route
under `/config` is a row, and a route under `/chat` or `/email` is a message.
`alerts_dicts` keeps history: a delete retires the live row rather than removing
it, and the unique index is partial — one enabled row per `(name, key_)`, with
superseded rows kept. A save is therefore a single
`INSERT … ON CONFLICT (name, key_) WHERE is_enabled IS TRUE DO UPDATE`, spelling
the index's own predicate so Postgres can infer the arbiter. Both halves are
load-bearing: an upsert assuming a plain unique constraint fails outright, and
one matching on `(name, key_)` without the predicate finds a retired row and
brings it back carrying its old value. Diesel expresses this through
`filter_target`; a bound parameter would not, since the planner cannot prove
`is_enabled = $1` implies the index.
`product`, `values_` and `metadata` are `json` rather than `jsonb` because the
dashboard serializes them itself and the mappers screen parses some of them
twice. `RawJson` carries the bytes from the wire to the column and back without
building a tree: parsing into a `serde_json::Value` and serializing it again
re-encodes the document, and the screen would be handed back something it did
not save. That is the opposite trade from a definition, whose json columns are
typed because the alert manager reads them; nothing reads a dictionary entry but
the screen that wrote it. An entry's JSON is capped by
`dictionary.max_entry_bytes`, since one oversized save otherwise becomes a row
nothing can read back — a broken page long after the save that caused it. The
refusal is `IR_08`: `IR_05` through `IR_07` already name conditions the
definition and enablement routes report.
The watermark is per user, but the internal API key names the service, not a
person, so the routes read `X-User-Name` and nothing authenticates it. Local
accounts are disabled in both environments, so every request arrives with no
name, the table holds one shared row, and dictionary saves fall to the column's
default. Keeping the name on the request makes that a data fact rather than a
schema one — the day the portal authenticates, rows appear per person with no
route and no migration to change. A header rather than a path segment: the empty
name every caller sends today has no spelling as a path segment, and a user name
is an email address wherever there is one, which a path would write into every
access log.
A read that finds nothing is a `200` with `status: "absent"`, not a `404`. The
mappers screen and the bell both have a defined behaviour for "nothing saved
yet", and making that an HTTP error would mean the alert manager has to treat an
error response as normal — the habit that hides a real one. `404` keeps the
meaning the routes beside these give it: a path naming something this service
does not have.
`diesel_models` gains `diesel/uuid`, which the observability schema's surrogate
keys need and no table in `hyperswitch_db` has, and both tables are registered
for `GetPrimaryKey` so the list query can go through `generic_filter`.
Verified against a migrated local Postgres: 122 tests green across the crate,
including the 13 `#[ignore]`d ones that store rows — a repeated save leaves one
row rather than two, a retired entry stays as history and is not revived by the
next save, and the JSON columns come back byte-identical through both the write
and a fresh read. Clippy clean; `diesel_models` checks under `v1` and under
`v2,kv_store`.