Packages
ex_sorcery
0.1.0
A toolkit for building event-driven applications in Elixir with event sourcing and CQRS patterns
Current section
Files
Jump to
Current section
Files
lib/sorcery/event.ex
defmodule Sorcery.Event do
@moduledoc """
Defines the structure of an event in the system.
An event represents something that happened in your system that you want to track.
Required fields:
- type: String identifying what kind of event it is
- data: The event payload
- metadata: Additional context with required domain and instance_id
- domain_sequence_number: Sequence number within the event's domain
Optional fields:
- sequence_number: Global sequence number (set by the store)
- version: Event schema version
- inserted_at: When the event was stored
"""
@type t :: %__MODULE__{
type: String.t(),
data: map(),
metadata: %{
required(:domain) => String.t(),
required(:instance_id) => String.t(),
optional(String.t()) => term()
},
sequence_number: pos_integer() | nil,
version: pos_integer() | nil,
inserted_at: DateTime.t() | nil
}
@enforce_keys [:type, :data, :metadata ]
defstruct [:type, :data, :metadata, :sequence_number, :version, :inserted_at]
@required_keys [:type, :data, :domain, :instance_id]
@optional_keys [:version, :inserted_at, :additional_metadata]
@doc """
Creates a new event.
## Required Parameters
* `:type` - String identifying what kind of event it is
* `:data` - The event payload as a map
* `:domain` - String identifying the domain this event belongs to
* `:instance_id` - String identifying the instance that generated this event
* `:domain_sequence_number` - Sequence number within the event's domain
## Optional Parameters
* `:version` - Event schema version
* `:inserted_at` - When the event was stored
* `:additional_metadata` - Map of additional metadata to merge with required metadata
Note: The global sequence_number is assigned by the event store when the event is persisted.
## Examples
iex> Event.new(%{
...> type: "user_registered",
...> data: %{user_id: "123"},
...> domain: "users",
...> instance_id: "instance_1",
...> domain_sequence_number: 1,
...> version: 1
...> })
%Event{...}
"""
@spec new(map()) :: t() | no_return()
def new(params) when is_map(params) do
params
|> validate_required()
|> validate_only_known_keys()
|> build_metadata()
|> build_struct()
end
defp validate_required(params) do
@required_keys
|> Enum.each(fn key ->
unless Map.has_key?(params, key) do
raise ArgumentError, "Missing required key: #{key}"
end
end)
params
end
defp validate_only_known_keys(params) do
known_keys = MapSet.new(@required_keys ++ @optional_keys)
params
|> Map.keys()
|> Enum.each(fn key ->
unless MapSet.member?(known_keys, key) do
raise ArgumentError, "Unknown key: #{key}"
end
end)
params
end
defp build_metadata(%{domain: domain, instance_id: instance_id} = params) do
metadata = Map.merge(
%{domain: domain, instance_id: instance_id},
Map.get(params, :additional_metadata, %{})
)
Map.put(params, :metadata, metadata)
end
defp build_struct(params) do
struct!(__MODULE__, %{
type: params.type,
data: params.data,
metadata: params.metadata,
version: Map.get(params, :version),
inserted_at: Map.get(params, :inserted_at)
})
end
end