AshHooks.Subscription (AshHooks v2.0.2)

Copy Markdown View Source

Turns the consumer's resource into the outbound Subscription — which events go to which endpoint, and with which signature envelope.

use Ash.Resource,
  data_layer: AshSqlite.DataLayer,
  extensions: [AshHooks.Subscription]

subscription do
  endpoint_resource(MyApp.WebhookEndpoint)
end

The extension injects: event_types ({:array, :string}, default ["*"]), endpoint_id (uuid — the sole UUID-storage-compatible primary key of the endpoint_resource, under any attribute name), and signing_mode (:legacy | :dual | :standard, NULLABLE — the outbound declaration's mode applies when unset: ADR-0002's per-subscription mode). A subscription's own primary key follows the same single UUID-storage-compatible contract because outbound delivery rows store it in subscription_id. UUIDv7 keys satisfy this contract. Composite and non-UUID keys fail at DSL compilation with the reference column named in the error.

Typing contract

Events carry canonical STRING types (AshHooks.Event canonicalizes at construction). event_types rows match by exact string or the bare wildcard — "*" / :"*" — with entries normalized via to_string/1, so a consumer-typed register ({:array, :atom}, e.g. a closed one_of: enum) matches identically to the injected string array: same wildcard posture, same exact-match semantics, either representation. A closed-enum register is declared by REDECLARING the attribute — your own attribute(:event_types, {:array, :atom}, constraints: [...]) replaces the injection entirely (add_new_attribute stands down), so your enum, your min_length, and your own default apply; the wildcard default ["*"] is NOT forced on you.

Matching is exact strings plus the bare wildcard entry, evaluated IN MEMORY by the dispatcher after reading through the consumer's primary read action — no array-containment SQL, so the semantics are identical on every data layer.

The package injects NO read action and NO read policies: read surfaces are the consumer's to open through their own domain policies — the dispatcher's internal reads run unauthorized, the inbound reaper's precedent (ADR-0005's consumer-governed posture).

Summary

Functions

Whether a subscription row (event_types) matches an event type (a canonical string): the bare "*" entry matches everything; any other entry matches exactly — no glob interpretation. Works on any resource row carrying the injected event_types attribute.

The subscription-level signing modes: :legacy (legacy envelope only), :dual (legacy + Standard Webhooks), :standard (SW only).

Functions

matches?(subscription, type)

@spec matches?(map(), String.t()) :: boolean()

Whether a subscription row (event_types) matches an event type (a canonical string): the bare "*" entry matches everything; any other entry matches exactly — no glob interpretation. Works on any resource row carrying the injected event_types attribute.

Entries are normalized before comparing: atoms to strings (a consumer-typed register — {:array, :atom}, a closed enum — H8), a "*"/:"*" wildcard in either representation. Without the normalization an atom-typed register matched NOTHING — zero deliveries, no error. An entry that is neither binary nor atom simply does not match (total: one exotic row can never abort the fanout).

signing_modes()

@spec signing_modes() :: [atom()]

The subscription-level signing modes: :legacy (legacy envelope only), :dual (legacy + Standard Webhooks), :standard (SW only).

subscription(body)

(macro)