Sourced.Middleware.Domain.Event behaviour (sourced v0.2.0)

Copy Markdown View Source

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]
end

Events 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
end

Non-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"
end

Tags

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}"]
end

Tags 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

from_map(map)

@callback from_map(map()) :: struct()

Rebuilds the event struct from a string-keyed map produced by a serializing store.

to_tags(event)

@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.

to_type()

@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

cast(module, map)

@spec cast(module :: module(), map :: map()) :: struct()

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