A single, per-recipient, read/unread notification row (cms_notifications).
One row per recipient (fan-out on write), so privacy is a one-line policy
(user_id == actor(:id)) and the feed is a single indexed lookup. Notifications are
transient personal state — no archival, no PaperTrail versions (like Accounts.Token).
Tenancy: site_id is the recipient's site (nil for master users, who read
unscoped and are still confined to their own rows by the policy). The site an event
concerns is carried in metadata for display, never in site_id.
Two nullable timestamps drive the standard UX with zero extra machinery:
seen_at—nilcounts toward the bell badge; cleared when the dropdown opens.read_at—nilrenders bold/unread; cleared per item or via "mark all read".
type is an open string validated by format (like a permission name), not an enum —
so a new extension can deliver an unknown type without a code change. Whether a site
receives a given type is a per-site boolean Setting (MishkaCmsCore.Notify.setting_key/1),
checked once at delivery — not stored here.
Delivered exclusively through MishkaCmsCore.Notify.deliver/3 (system-internal,
authorize?: false). See MishkaCmsCore.Notify, MishkaCmsCore.Notify.Delivery.
Summary
Functions
Apply the full GuardedStruct pipeline (sanitize → validate → derive →
main_validator) to attrs and return either {:ok, transformed_attrs}
or {:error, errors}. Wire this into an Ash.Resource.Change to plug
guardedstruct rules into Ash's changeset pipeline.
O(1) lookup of a field's compile-time metadata by name. Returns
nil for unknown fields.
Compile-time-baked MapSet of every field name owned by the
guardedstruct block. Used by GuardedStruct.AshResource.Change
to decide, in O(1), whether a key in changeset.atomics
belongs to the pipeline.
Validate + sanitize notification attrs through the GuardedStruct pipeline (strip HTML, trim,
validate) WITHOUT a DB write — the same pipeline :deliver runs on write. Returns
{:ok, sanitized_map} or {:error, errors}; producers can call it to check/clean a notification
before Notify.deliver/3. GuardedStruct.AshResource omits the Codegen transformer (Ash uses
changesets), so this exposes the pipeline under the usual builder/1 name.
Validates that the keys in the provided input are valid for at least one action on the resource.
Same as input/1, except restricts the keys to values accepted by the action provided.
Types
@type t() :: %MishkaCmsCore.Notify.Notification{ __lateral_join_source__: term(), __meta__: term(), __metadata__: term(), __order__: term(), actor: term(), actor_id: term(), aggregates: term(), body: term(), calculations: term(), icon: term(), id: term(), inserted_at: term(), link: term(), metadata: term(), read?: term(), read_at: term(), seen_at: term(), severity: term(), site: term(), site_id: term(), source: term(), title: term(), type: term(), updated_at: term(), user: term(), user_id: term() }
Functions
Apply the full GuardedStruct pipeline (sanitize → validate → derive →
main_validator) to attrs and return either {:ok, transformed_attrs}
or {:error, errors}. Wire this into an Ash.Resource.Change to plug
guardedstruct rules into Ash's changeset pipeline.
The function is named __guarded_change__ because it does more than
validate — it can also transform values (trim, downcase, slugify,
auto-fill, etc.) before they reach the data layer.
O(1) lookup of a field's compile-time metadata by name. Returns
nil for unknown fields.
Compile-time-baked MapSet of every field name owned by the
guardedstruct block. Used by GuardedStruct.AshResource.Change
to decide, in O(1), whether a key in changeset.atomics
belongs to the pipeline.
Validate + sanitize notification attrs through the GuardedStruct pipeline (strip HTML, trim,
validate) WITHOUT a DB write — the same pipeline :deliver runs on write. Returns
{:ok, sanitized_map} or {:error, errors}; producers can call it to check/clean a notification
before Notify.deliver/3. GuardedStruct.AshResource omits the Codegen transformer (Ash uses
changesets), so this exposes the pipeline under the usual builder/1 name.
Notification.builder(%{type: "blog:x", title: " <b>Hi</b> "}) #=> {:ok, %{title: "Hi", …}}
Validates that the keys in the provided input are valid for at least one action on the resource.
Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g %MyResource{value: 1, value: 2}. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
Same as input/1, except restricts the keys to values accepted by the action provided.