MishkaCmsCore.Activity.Behaviours.Source behaviour (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Behaviour for activity-feed data sources. Each adapter answers four questions:

  • topics/1 — which PubSub topics the parent LiveView must subscribe to so the feed receives live updates.
  • handles?/1 — whether a given broadcast belongs to this source.
  • to_entry/1 — turn a live broadcast into a feed entry.
  • initial_fetch/2 — fetch the most recent entries for first render.
  • in_scope?/2 — (optional) whether a live broadcast belongs in a resource-scoped feed; only needed by sources on a global topic.

Adapters live under MishkaCmsCore.Activity.Sources.* (e.g. PaperTrail, Oban). The ActivityFeed LiveComponent composes any list of sources without knowing what each one does.

Summary

Types

One activity entry. Every key here is one a renderer reads, so a source that omits one hands a template a map entry.that_key raises on — MishkaCmsCore.Activity.Helpers.complete_entry/1 fills the gap at both seams, and entry_contract_test.exs fails the source that left it. The optional tail is the pipeline's own: :actor_label and :site_label are resolved downstream by MishkaCmsCore.Activity.ActivityEntry, :host by the dashboard's Recent activity widget.

Callbacks

Whether a live broadcast belongs in a feed of the given scope. Sources whose topics/1 already encode the scope (per-resource topics, e.g. PaperTrail / Oban) can omit it — the subscription alone filters them. A source on a GLOBAL topic (e.g. EventLog's "event_log:created") implements it so a resource-scoped feed drops out-of-scope broadcasts. A missing callback is treated as always in scope.

Types

entry()

@type entry() :: %{
  :id => String.t(),
  :resource_label => String.t(),
  :action => atom(),
  :severity => :info | :warn | :error,
  :tone => :created | :updated | :destroyed | nil,
  :badge_label => String.t() | nil,
  :badge_tone => atom() | nil,
  :inserted_at => DateTime.t(),
  :source_id => String.t() | nil,
  :site_id => String.t() | nil,
  :user_id => String.t() | nil,
  :message => String.t(),
  optional(atom()) => any()
}

One activity entry. Every key here is one a renderer reads, so a source that omits one hands a template a map entry.that_key raises on — MishkaCmsCore.Activity.Helpers.complete_entry/1 fills the gap at both seams, and entry_contract_test.exs fails the source that left it. The optional tail is the pipeline's own: :actor_label and :site_label are resolved downstream by MishkaCmsCore.Activity.ActivityEntry, :host by the dashboard's Recent activity widget.

scope()

@type scope() :: %{
  optional(:current_user) => map() | nil,
  optional(:resources) => [module()] | nil
}

Callbacks

handles?(t)

@callback handles?(Phoenix.Socket.Broadcast.t()) :: boolean()

in_scope?(t, scope)

(optional)
@callback in_scope?(Phoenix.Socket.Broadcast.t(), scope()) :: boolean()

Whether a live broadcast belongs in a feed of the given scope. Sources whose topics/1 already encode the scope (per-resource topics, e.g. PaperTrail / Oban) can omit it — the subscription alone filters them. A source on a GLOBAL topic (e.g. EventLog's "event_log:created") implements it so a resource-scoped feed drops out-of-scope broadcasts. A missing callback is treated as always in scope.

initial_fetch(scope, limit)

@callback initial_fetch(scope(), limit :: pos_integer()) :: [entry()]

to_entry(t)

@callback to_entry(Phoenix.Socket.Broadcast.t()) :: entry() | nil

topics(scope)

@callback topics(scope()) :: [String.t()]