AshDispatch (AshDispatch v0.6.4)

View Source

Event-driven notification system for Ash Framework.

AshDispatch provides a declarative way to define and dispatch events across multiple transport types (email, in-app notifications, SMS, webhooks, etc.) with full Ash integration.

Key Components

Configuration

config :ash_dispatch,
  otp_app: :my_app,
  repo: MyApp.Repo,
  mailer: MyApp.Mailer,
  endpoint: MyAppWeb.Endpoint,
  # Must be endpoint-shaped (subscribe/1 + broadcast/3); see
  # `AshDispatch.Config.pubsub_module/0` for the contract. Do NOT
  # use a bare `Phoenix.PubSub` registered name like `MyApp.PubSub`.
  pubsub_module: MyAppWeb.Endpoint,
  user_resource: MyApp.Accounts.User,
  user_domain: MyApp.Accounts

Usage

Inline DSL (in resources)

defmodule MyApp.Orders.ProductOrder do
  use Ash.Resource,
    extensions: [AshDispatch.Resource]

  dispatch do
    event :created do
      trigger_on [:create]
      channels do
        channel :email, :user
        channel :in_app, :user
      end
    end
  end
end

Standalone Event Modules

defmodule MyApp.Events.Orders.Created.Event do
  use AshDispatch.Event

  dispatch do
    id "orders.created"
    domain :orders
    channels do
      channel :email, :user
      channel :email, :admin, variant: :admin
    end
  end
end

Code Generation

Run mix ash_dispatch.gen to generate missing files based on DSL definitions:

  • Templates for email channels
  • Event module stubs for inline events
  • TypeScript types for frontend integration

Summary

Functions

Renders an event's channels without delivering anything.

Functions

preview(event_id, context_data \\ %{}, opts \\ [])

@spec preview(String.t(), map(), keyword()) :: {:ok, [map()]} | {:error, term()}

Renders an event's channels without delivering anything.

This is the preview engine behind the ManualTrigger resource, exposed as a plain function so an app can render a mail into an admin screen, a test, or a snapshot without going through a resource action.

For each channel of the event it loads the event's record, runs the event's callbacks and renders its templates, returning one map per channel:

%{
  channel: %{transport: :email, audience: :user},
  transport: :email,
  audience: :user,
  subject: "Your order is on its way",
  html_body: "<html>…</html>",
  text_body: "Your order …",
  from_address: "orders@example.com",
  recipient: "Alice <alice@example.com>",
  notification_title: nil,   # :in_app channels only
  notification_message: nil  # :in_app channels only
}

A channel whose prepare_template_assigns/2 raises yields the same map with html_body/text_body nil and an extra :error key — one broken channel never takes the preview down.

Parameters

  • event_id - event identifier, e.g. "orders.created"
  • context_data - the event's data. Pass %{<data_key>_id => id} (e.g. %{order_id: id}) to render a real record; pass %{} to fall back to the event module's sample_data/0, or to an arbitrary record from the database if it defines none.
  • opts:
    • :audience - only preview channels for this audience
    • :transport - only preview channels for this transport
    • :actor - actor used when loading the record (defaults to nil, i.e. an unauthorized read)
    • :recipient_email - display value for the :recipient field. Preview never sends, so this only changes what the preview shows.

Returns

  • {:ok, previews} - a list with one map per matching channel ([] when the filters match no channel)
  • {:error, reason} - the event is unknown, or its record could not be loaded

Examples

# Every channel of the event, for one order
{:ok, previews} = AshDispatch.preview("orders.created", %{order_id: order.id})

# Just the customer's email
{:ok, [preview]} =
  AshDispatch.preview("orders.created", %{order_id: order.id},
    transport: :email,
    audience: :user
  )

preview.subject
#=> "Order confirmation #1234"

Note

Preview renders; it does not deliver, create receipts or consult user preferences. To send, use AshDispatch.Dispatcher.dispatch/3.