Current section

Files

Jump to
ash_dispatch lib ash_dispatch.ex
Raw

lib/ash_dispatch.ex

defmodule AshDispatch do
  @moduledoc """
  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

  - `AshDispatch.Event` - Behaviour for defining event modules
  - `AshDispatch.Resource` - Ash extension for inline event definitions
  - `AshDispatch.Dispatcher` - Main entry point for dispatching events
  - `AshDispatch.Introspection` - DSL introspection utilities

  ## 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
  """

  alias AshDispatch.Resources.ManualTrigger.Helpers

  @doc """
  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`.
  """
  @spec preview(String.t(), map(), keyword()) :: {:ok, [map()]} | {:error, term()}
  def preview(event_id, context_data \\ %{}, opts \\ []) do
    channel_filter =
      Helpers.build_channel_filter(Keyword.get(opts, :audience), Keyword.get(opts, :transport))

    Helpers.preview_trigger(
      event_id,
      context_data,
      channel_filter,
      Keyword.get(opts, :recipient_email),
      Keyword.get(opts, :actor)
    )
  end
end