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