AshHooks (AshHooks v2.0.3)

Copy Markdown View Source

Webhooks for Ash Framework — inbound (receive, verify, dedup → domain events) and outbound (sign, deliver, retry, track).

Attach to a resource:

use Ash.Resource,
  extensions: [AshHooks]

webhooks do
  # convention-resolves to AshHooks.Provider.ComplyCube
  inbound :comply_cube do
    secret {:app_env, [:my_app, :complycube_secret]}
    # optional: extract a stable event id (payload -> {:ok, id} | :error);
    # otherwise the provider identity callback, then raw-body digest, applies
  end

  # convention-resolves to AshHooks.Provider.HubSpotV3 — the
  # vendor-default 300s replay window applies; override it either way
  inbound :hub_spot_v3 do
    secret {:app_env, [:my_app, :hubspot_client_secret]}
  end

  outbound :order_paid do
    signing_mode :dual
  end
end

Both halves are independently consumable: inbound-only consumers pull no queue infrastructure (ADR-0004).

Multi-tenant? Declare Ash attribute multitenancy on the four resources (subscription, endpoint, both ledgers) and pass :tenant / ctx[:tenant] to every entry point — every query, write, dedup identity, sweep, and job arg is tenant-scoped, and a tenant-less call against a multitenant resource fails closed with {:error, :tenant_required} before any data access. Single-tenant apps change nothing. The adoption checklist walks the ordered transition (ADR-0011).

Summary

Types

The per-tenant sweep totals: one result per tenant (error-isolated — one tenant's failure never stops the others) plus the sum of the successful counts.

Functions

Fans an outbound event out to its subscribed endpoints — the outbound entry point, delegating to AshHooks.Dispatcher.dispatch/4 (the AshHooks.Ingress.ingest/4 twin).

Maps the retention prune over tenants: sequentially (routing by the resource's extension: inbound ledgers through AshHooks.Ingress.prune/2, outbound deliveries through AshHooks.Delivery.prune/2) — the same per-tenant isolation and sequencing as reap_all/2.

Maps AshHooks.Ingress.reap/2 over tenants: SEQUENTIALLY, per-tenant error isolation — a tenant-less global reap over a multitenant ledger is impossible by construction (each tenant's call carries its own pre-flight). Sequential by design: the shared repo pool is the one cross-tenant starvation surface, so per-tenant sweeps do not contend.

Recovers eligible outbound rows for a declaration and tenant, including interrupted enqueue, due retries, and expired send leases. Schedule this periodically for each outbound declaration and tenant; it preserves retry timing and attempt counts. See AshHooks.Dispatcher.reconcile_pending/3.

Schema validator for the secret option — accepts only secret SOURCES, rejecting a literal binary at DSL parse time (ADR-0005).

Types

sweep_totals()

@type sweep_totals() :: %{
  results: %{optional(term()) => {:ok, non_neg_integer()} | {:error, term()}},
  total: non_neg_integer()
}

The per-tenant sweep totals: one result per tenant (error-isolated — one tenant's failure never stops the others) plus the sum of the successful counts.

Functions

dispatch(resource, name, event, opts \\ [])

@spec dispatch(module(), atom(), AshHooks.Event.t() | term(), keyword()) ::
  {:ok, [AshHooks.Dispatcher.dispatch_result()]} | {:error, term()}

Fans an outbound event out to its subscribed endpoints — the outbound entry point, delegating to AshHooks.Dispatcher.dispatch/4 (the AshHooks.Ingress.ingest/4 twin).

prune_all(resource, opts)

@spec prune_all(module(), keyword()) :: {:ok, sweep_totals()} | {:error, term()}

Maps the retention prune over tenants: sequentially (routing by the resource's extension: inbound ledgers through AshHooks.Ingress.prune/2, outbound deliveries through AshHooks.Delivery.prune/2) — the same per-tenant isolation and sequencing as reap_all/2.

{:ok, %{results: %{"org_a" => {:ok, 2}}, total: 2}} =
  AshHooks.prune_all(Delivery, older_than: cutoff, tenants: ["org_a"])

reap_all(resource, opts)

@spec reap_all(module(), keyword()) :: {:ok, sweep_totals()}

Maps AshHooks.Ingress.reap/2 over tenants: SEQUENTIALLY, per-tenant error isolation — a tenant-less global reap over a multitenant ledger is impossible by construction (each tenant's call carries its own pre-flight). Sequential by design: the shared repo pool is the one cross-tenant starvation surface, so per-tenant sweeps do not contend.

{:ok, %{results: %{"org_a" => {:ok, 3}, "org_b" => {:ok, 0}}, total: 3}} =
  AshHooks.reap_all(Ledger, tenants: ["org_a", "org_b"])

reconcile_pending(resource, name, opts \\ [])

@spec reconcile_pending(module(), atom(), keyword()) ::
  {:ok, [AshHooks.Dispatcher.dispatch_result()]} | {:error, term()}

Recovers eligible outbound rows for a declaration and tenant, including interrupted enqueue, due retries, and expired send leases. Schedule this periodically for each outbound declaration and tenant; it preserves retry timing and attempt counts. See AshHooks.Dispatcher.reconcile_pending/3.

validate_secret_source(source)

@spec validate_secret_source(term()) :: {:ok, term()} | {:error, String.t()}

Schema validator for the secret option — accepts only secret SOURCES, rejecting a literal binary at DSL parse time (ADR-0005).

webhooks(body)

(macro)