# `MishkaCmsCore.Notify.Notification`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/notify/notification.ex#L5)

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` — `nil` counts toward the bell badge; cleared when the dropdown opens.
  * `read_at` — `nil` renders 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`.

# `t`

```elixir
@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()
}
```

# `__guarded_change__`

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.

# `__guarded_field_meta__`

O(1) lookup of a field's compile-time metadata by name. Returns
`nil` for unknown fields.

# `__guarded_field_name_set__`

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.

# `builder`

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", …}}

# `default_short_name`

# `input`

```elixir
@spec input(values :: map() | Keyword.t()) :: map() | no_return()
```

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

```elixir
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
```

# `input`

```elixir
@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()
```

Same as `input/1`, except restricts the keys to values accepted by the action provided.

# `primary_key_matches?`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
