README.md

# Spector

CQRS-style event sourcing for Ecto schemas.

Spector records all changes to your Ecto schemas as events in a separate event log table. This enables full audit trails, temporal queries, and the ability to replay history. For tamper-evident logs, enable optional hash chain integrity.

## Installation

Add `spector` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:spector, "~> 0.2.0"}
  ]
end
```

## Quick Start

### 1. Define an Events Table

```elixir
defmodule MyApp.Events do
  use Spector.Events,
    table: "events",
    schemas: [MyApp.User, MyApp.Post],
    repo: MyApp.Repo
end
```

### 2. Mark Schemas as Evented

```elixir
defmodule MyApp.User do
  use Spector.Evented, events: MyApp.Events
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
  end

  def changeset(changeset, attrs) do
    changeset
    |> Ecto.Changeset.cast(attrs, [:name, :email])
    |> Ecto.Changeset.validate_required([:name, :email])
  end
end
```

### 3. Create Migrations

```elixir
# For the events table
defmodule MyApp.Repo.Migrations.CreateEvents do
  use Ecto.Migration

  def up, do: Spector.Migration.up(table: "events")
  def down, do: Spector.Migration.down(table: "events")
end
```

### 4. Use Spector Instead of Repo

```elixir
# Insert
{:ok, user} = Spector.insert(MyApp.User, %{name: "Alice", email: "alice@example.com"})

# Update
{:ok, user} = Spector.update(user, %{name: "Alice Smith"})

# Delete
{:ok, user} = Spector.delete(user)
```

## How It Works

When you update or execute an action on a record, Spector "rolls forward" by replaying all stored events through your schema's `changeset/2` function. This means:

- Your changeset function handles both new operations AND historical replay
- Schema migrations happen automatically during replay (using version guards)
- The current state is always reconstructed from the event log
- Stale in-memory objects are never a problem

This design lets you evolve your schema over time while maintaining full compatibility with historical events.

## Features

### Custom Actions

Define domain-specific actions beyond insert/update/delete:

```elixir
defmodule MyApp.Item do
  use Spector.Evented, events: MyApp.Events, actions: [:archive]

  def changeset(changeset, attrs) when changeset.action == :archive do
    Ecto.Changeset.change(changeset, archived_at: attrs[:archived_at])
  end

  def changeset(changeset, attrs) do
    Ecto.Changeset.cast(changeset, attrs, [:name, :value])
  end
end

# Execute custom action
{:ok, item} = Spector.execute(item, :archive, %{archived_at: DateTime.utc_now()})
```

### Schema Versioning

Handle schema migrations with version guards:

```elixir
defmodule MyApp.User do
  use Spector.Evented, events: MyApp.Events, version: 1

  # Migrate v0 events (with :title) to v1 (with :name)
  def changeset(changeset, attrs) when version_is(attrs, 0) do
    attrs = Map.put(attrs, "name", attrs["title"])
    do_changeset(changeset, attrs)
  end

  def changeset(changeset, attrs), do: do_changeset(changeset, attrs)
end
```

### Hash Chain Integrity

Enable tamper-evident event logs with cryptographic hashing:

```elixir
defmodule MyApp.Events do
  use Spector.Events,
    table: "events",
    schemas: [MyApp.User],
    repo: MyApp.Repo,
    hashed: true
end
```

### Explicit Schema Indexing

Ensure stability when adding/removing schemas:

```elixir
schemas: [MyApp.User, MyApp.Post, {MyApp.Comment, 10}]
```

### Action Aliases

Maintain backwards compatibility when renaming actions:

```elixir
use Spector.Events,
  aliases: [soft_delete: :archive]
```

## Database Support

Spector works with any database supported by Ecto for basic functionality.

**Note:** Hashed event tables (`hashed: true`) currently require PostgreSQL. The hash chain integrity feature uses `LOCK TABLE ... IN EXCLUSIVE MODE` which is PostgreSQL-specific.

## Documentation

Full documentation is available at [HexDocs](https://hexdocs.pm/spector).

## License

MIT License. See LICENSE for details.