Packages

Event-driven notification system for Ash Framework with multiple transport types

Current section

Files

Jump to
ash_dispatch lib setup.ex
Raw

lib/setup.ex

defmodule AshDispatch.Setup do
@moduledoc """
Automatically sets up DeliveryReceipt resource in your consuming app.
Similar to ash_paper_trail, this creates the necessary resources for you with
zero manual module creation needed.
## Usage
In your Deliveries domain module:
defmodule MyApp.Deliveries do
use AshDispatch.Setup,
repo: MyApp.Repo,
user_resource: MyApp.Accounts.User
# Your domain definition continues here...
use Ash.Domain
# ... rest of domain config
end
This will automatically create `MyApp.Deliveries.DeliveryReceipt` with the
user relationship - completely frictionless!
"""
defmacro __using__(opts) do
repo = Keyword.fetch!(opts, :repo)
user_resource = Keyword.fetch!(opts, :user_resource)
# Build the complete resource module AST
delivery_receipt_ast = build_delivery_receipt_ast(repo, user_resource)
quote do
# Get the calling module to namespace the DeliveryReceipt
caller_module = __MODULE__
receipt_module = Module.concat(caller_module, DeliveryReceipt)
# Dynamically create the DeliveryReceipt resource
Module.create(receipt_module, unquote(delivery_receipt_ast), Macro.Env.location(__ENV__))
end
end
defp build_delivery_receipt_ast(repo, user_resource) do
quote do
@moduledoc """
Delivery receipt tracking resource.
Auto-generated by AshDispatch.Setup - tracks all message deliveries
(email, in-app, Discord, SMS, etc.) with full audit trail.
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [
AshStateMachine,
AshTypescript.Resource
]
postgres do
table "delivery_receipts"
repo(unquote(repo))
references do
reference(:notification, on_delete: :nilify)
reference(:user, on_delete: :nilify)
end
end
state_machine do
initial_states([:pending])
default_initial_state(:pending)
state_attribute(:status)
extra_states([:sending, :sent, :failed_permanent, :skipped, :scheduled, :failed])
transitions do
transition(:schedule, from: :pending, to: :scheduled)
transition(:mark_sending, from: [:scheduled, :pending], to: :sending)
transition(:mark_sent, from: [:sending, :scheduled, :pending], to: :sent)
transition(:mark_failed, from: [:sending, :scheduled, :pending], to: :failed)
transition(:mark_failed_permanent,
from: [:sending, :scheduled, :failed],
to: :failed_permanent
)
transition(:skip, from: [:pending, :scheduled, :sending], to: :skipped)
transition(:retry, from: :failed, to: :scheduled)
end
end
relationships do
belongs_to :notification, AshDispatch.Resources.Notification do
source_attribute :notification_id
destination_attribute :id
allow_nil? true
public? true
define_attribute? false
end
# User relationship - works because we're compiling in the consuming app!
belongs_to :user, unquote(user_resource) do
source_attribute :user_id
destination_attribute :id
allow_nil? true
public? true
define_attribute? false
end
end
attributes do
uuid_primary_key :id
attribute :event_id, :string, allow_nil?: false, public?: true
attribute :transport, :atom,
allow_nil?: false,
public?: true,
constraints: [one_of: [:email, :in_app, :discord, :sms, :webhook]]
attribute :user_id, :uuid, allow_nil?: true, public?: true
attribute :notification_id, :uuid, allow_nil?: true, public?: true
attribute :audience, :atom,
allow_nil?: false,
public?: true,
constraints: [one_of: [:user, :admin, :system]]
attribute :status, :atom,
default: :pending,
allow_nil?: false,
public?: true,
constraints: [
one_of: [:pending, :scheduled, :sending, :sent, :failed, :failed_permanent, :skipped]
]
attribute :recipient, :string, allow_nil?: false, public?: true
attribute :provider_id, :string, allow_nil?: true, public?: true
attribute :provider_response, :map, default: %{}, allow_nil?: false, public?: true
attribute :subject, :string, allow_nil?: true, public?: true
attribute :body_text, :string, allow_nil?: true, public?: true
attribute :body_html, :string, allow_nil?: true, public?: true
attribute :content, :map, default: %{}, allow_nil?: false, public?: true
attribute :oban_job_id, :integer, allow_nil?: true, public?: true
attribute :error_message, :string, allow_nil?: true, public?: true
attribute :retry_count, :integer, default: 0, allow_nil?: false, public?: true
attribute :last_retry_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :sent_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :delivered_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :delivery_delayed_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :failed_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :opened_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :clicked_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :bounced_at, :utc_datetime_usec, allow_nil?: true, public?: true
attribute :complained_at, :utc_datetime_usec, allow_nil?: true, public?: true
create_timestamp :inserted_at
update_timestamp :updated_at
end
calculations do
calculate :oban_job, :map, {AshDispatch.Calculations.ObanJob, []} do
public? true
end
end
actions do
default_accept :*
defaults [:read, :destroy]
create :create do
accept [
:event_id,
:transport,
:user_id,
:notification_id,
:audience,
:recipient,
:subject,
:body_text,
:body_html,
:content,
:oban_job_id,
:provider_id,
:provider_response
]
end
read :list_all do
pagination offset?: true, keyset?: true, required?: false
end
read :list_for_user do
argument :user_id, :uuid, allow_nil?: false
filter expr(user_id == ^arg(:user_id))
pagination offset?: true, keyset?: true, required?: false
end
read :get do
argument :id, :uuid, allow_nil?: false
get? true
filter expr(id == ^arg(:id))
end
update :schedule do
accept [:oban_job_id]
change transition_state(:scheduled)
end
update :mark_sending do
change transition_state(:sending)
end
update :mark_sent do
accept [:sent_at]
change transition_state(:sent)
end
update :mark_failed do
accept [:error_message]
change transition_state(:failed)
change increment(:retry_count, amount: 1)
end
update :mark_failed_permanent do
accept [:error_message]
change transition_state(:failed_permanent)
end
update :skip do
change transition_state(:skipped)
end
update :retry do
accept [:oban_job_id]
change transition_state(:scheduled)
end
end
policies do
policy always() do
authorize_if always()
end
end
identities do
identity :oban_job, [:oban_job_id]
identity :notification, [:notification_id]
end
end
end
end