Spector.Events behaviour (Spector v0.6.0)
View SourceDefine an event log table for storing events from evented schemas.
Basic Usage
defmodule MyApp.Events do
use Spector.Events,
table: "events",
schemas: [MyApp.User, MyApp.Post],
repo: MyApp.Repo
endOptions
:table(required) - The database table name for storing events:schemas(required) - List of schemas that will log events to this table:repo(required) - The Ecto repo module to use for database operations:hashed- Enable hash chain integrity (default:false). See "Hash Chain Integrity" below:aliases- Action aliases for refactoring. See "Action Aliases" below:shard- Sharding function name (atom). See "Table Sharding" below:links- List of link associations for many-to-many relationships. See "Event Links" below
Table Sharding
For high-volume event logs, you can shard events across multiple tables using
the :shard option. Specify a function name (atom) that takes a parent_id
and returns the table name:
defmodule MyApp.Events do
use Spector.Events,
table: "events", # base table name (used for schema definition)
schemas: [MyApp.User],
repo: MyApp.Repo,
shard: :shard_table
def shard_table(parent_id) do
# Shard based on first byte of UUID
<<first_byte, _rest::binary>> = parent_id
"events_#{rem(first_byte, 4)}"
end
endYou'll need to create migrations for each shard table. The sharding function
must be deterministic - the same parent_id must always map to the same table.
When combining sharding with hash chain integrity (hashed: true), each shard
table maintains its own independent hash chain.
Event Links
Enable event linking with the :links option to create many-to-many relationships
between events:
use Spector.Events,
table: "events",
schemas: [MyApp.Chat],
repo: MyApp.Repo,
links: [ancestors: {"event_ancestors", :ancestor_id}]Each link creates a join table and a many_to_many association on the events module.
You can define multiple links:
links: [
ancestors: {"event_ancestors", :ancestor_id},
categories: {"event_categories", :category_id}
]For links that use an Ecto schema module, pass the module instead of a table name:
links: [links: {MyApp.EventLink, :linked_id}]The two approaches differ in what the association returns:
Table name string: Creates a
many_to_manyassociation. Preloading returns the linked events directly. You cannot useput_assocto set fields on the join table.Schema module: Creates a
has_manyassociation. Preloading returns the link records themselves (e.g.,%EventLink{event_id: ..., linked_id: ..., type: ...}). You can useput_associnprepare_event/3to create links with custom fields.
Use a schema module when you need to store metadata on links (types, timestamps, etc.)
or need to create links via put_assoc. Use a table name string for simple joins where
you only care about which events are connected.
See the Event Links Guide for details.
Sharding with Links
When combining sharding with links, a separate link table is created for each shard.
Use link_table_for/2 to get the correct link table name for a given parent_id:
def prepare_event(event_changeset, _previous_events, attrs) do
parent_id = Ecto.Changeset.get_field(event_changeset, :parent_id)
link_table = MyEvents.link_table_for(parent_id, "ancestors")
link = %MyLink{ancestor_id: attrs[:ancestor_id]}
link = Ecto.put_meta(link, source: link_table)
Ecto.Changeset.put_assoc(event_changeset, :ancestors, [link])
endSchema Indexing
Schemas are stored as integers in the database. By default, schemas are auto-indexed starting from 0. To ensure stability when adding/removing schemas, you can specify explicit indexes:
schemas: [MyApp.User, MyApp.Post, {MyApp.Comment, 10}]In this example, User gets index 0, Post gets index 1, and Comment gets
index 10. This allows you to remove Post later without breaking existing data.
Hash Chain Integrity
Enable hashed: true to create a cryptographic hash chain linking all events:
use Spector.Events,
table: "events",
schemas: [MyApp.User],
repo: MyApp.Repo,
hashed: trueEach event's hash includes the previous event's hash, creating a tamper-evident chain. Any modification to historical events will break the chain.
Note: Hash chain integrity requires PostgreSQL due to the use of
LOCK TABLE ... IN EXCLUSIVE MODE for serialization.
Action Aliases
When refactoring action names, use aliases to maintain backwards compatibility with existing events in the database:
use Spector.Events,
table: "events",
schemas: [MyApp.Item],
repo: MyApp.Repo,
aliases: [soft_delete: :archive] # soft_delete uses archive's hashThis allows renaming :archive to :soft_delete in your code while still
reading old events that used :archive.
Compilation Dependencies
Using this module creates a compilation dependency on all schema modules
listed in the :schemas option. This means changes to those schema modules
will trigger recompilation of the events module.
Generated Functions
Using this module generates the following functions:
changeset/1,changeset/2- Build an event changesettable_for/1- Get the table name for a given parent_idshard/2- Apply sharding to a changeset__spector__/1- Internal metadata accessor
Summary
Callbacks
Build an event changeset from attributes.
Build an event changeset from an existing struct and attributes.
Get the link table name for a given parent_id and base link table name.
Apply sharding to a changeset based on the parent_id.
Get the table name for a given parent_id.
Callbacks
@callback changeset(attrs :: map()) :: Ecto.Changeset.t()
Build an event changeset from attributes.
@callback changeset(struct :: struct(), attrs :: map()) :: Ecto.Changeset.t()
Build an event changeset from an existing struct and attributes.
Get the link table name for a given parent_id and base link table name.
With sharding, link tables are prefixed with the shard table name. Without sharding, returns the link table name unchanged.
@callback shard(changeset :: Ecto.Changeset.t(), parent_id :: binary()) :: Ecto.Changeset.t()
Apply sharding to a changeset based on the parent_id.
Updates the changeset's data source to the appropriate shard table.
Get the table name for a given parent_id.
For non-sharded tables, returns the configured table name. For sharded tables, calls the shard function to determine the table.