Behaviour for domain event structs registered in a
Sourced.Middleware.Domain.
Serializing stores (such as Sourced.EventStore.Postgres) persist event data
as JSON and read it back as a plain string-keyed map. from_map/1 rebuilds
the domain struct from that map; encoding is handled separately by the JSON
library's encoder protocol (e.g. JSON.Encoder).
use Sourced.Middleware.Domain.Event adopts the behaviour and
injects defaults for both callbacks: to_type/0 derives the stored
type from the module name, and from_map/1 copies the struct's known keys
out of the map — no atoms are created dynamically. Flat events need nothing
else:
defmodule OrderShipped do
use Sourced.Middleware.Domain.Event
@derive JSON.Encoder
defstruct [:order_id, :address]
endEvents with nested structs must override from_map/1, since JSON cannot
know that "order_items" should become OrderItem structs:
defmodule OrderPlaced do
use Sourced.Middleware.Domain.Event
@derive JSON.Encoder
defstruct [:id, :order_items]
@impl Sourced.Middleware.Domain.Event
def from_map(map) do
%__MODULE__{
id: map["id"],
order_items: Enum.map(map["order_items"], &OrderItem.from_map/1)
}
end
endNon-serializing stores (such as Sourced.EventStore.InMemory) keep the
struct as-is, so from_map/1 is never invoked for them.
Stored type
Each event has a stable stored type string, persisted in the type column and
used to look the module back up. to_type/0 defaults to the full
module name (MyApp.OrderShipped becomes "MyApp.OrderShipped"); override it
to persist a different string:
defmodule OrderCancelled do
use Sourced.Middleware.Domain.Event
@derive JSON.Encoder
defstruct [:order_id]
@impl Sourced.Middleware.Domain.Event
def to_type, do: "order_voided"
endTags
to_tags/1 derives the event's Sourced.StoredEvent.tag/0s from the struct,
and is what Sourced.EventStore.Query items match on. It defaults to [],
so an event with no tags is only ever reachable by its type:
defmodule OrderPlaced do
use Sourced.Middleware.Domain.Event
@derive JSON.Encoder
defstruct [:id, :customer_id]
@impl Sourced.Middleware.Domain.Event
def to_tags(event), do: ["order:#{event.id}", "customer:#{event.customer_id}"]
endTags are derived on append, when a bare struct is wrapped into a
event map. Writing that map yourself sets them directly and
to_tags/1 is not consulted, so tag a hand-built event explicitly:
%{
type: OrderPlaced.to_type(),
data: %OrderPlaced{id: 1, customer_id: 42},
tags: ["order:1", "customer:42"],
occurred_at: DateTime.utc_now()
}
Summary
Callbacks
Rebuilds the event struct from a string-keyed map produced by a serializing store.
Returns the event's tags.
Returns the event's stored type string.
Functions
Default decoding: builds module's struct from the string-keyed map,
copying only the struct's defined keys.
Callbacks
Rebuilds the event struct from a string-keyed map produced by a serializing store.
@callback to_tags(event :: struct()) :: [Sourced.StoredEvent.tag()]
Returns the event's tags.
Defaults to []; override it to tag the event with the domain concepts it
concerns, derived from the struct's own fields.
@callback to_type() :: String.t()
Returns the event's stored type string.
Defaults to the full module name; override it to persist a different, stable string.
Functions
Default decoding: builds module's struct from the string-keyed map,
copying only the struct's defined keys.
Keys absent from the map keep the struct's default value; map keys that are
not struct fields are ignored. Useful as a starting point inside a custom
from_map/1:
def from_map(map) do
event = Sourced.Middleware.Domain.Event.cast(__MODULE__, map)
%{event | order_items: Enum.map(event.order_items, &OrderItem.from_map/1)}
end