guides/basic_chat.md

# Building a Basic Chat with Spector

This guide shows how to use Spector to build a simple chat with edit history tracking using event links.

## Overview

Basic chat applications need:
- Message history with user identification
- Ability to edit messages while preserving previous versions
- Simple linear conversation flow (no branching)

Spector's event sourcing with links tracks every edit as a linked event chain.

## Setup

### 1. Define the Chat Schema

```elixir
defmodule MyApp.BasicChat do
  use Spector.Evented, events: MyApp.BasicChatEvents, actions: [:edit]
  use Ecto.Schema
  alias Ecto.Changeset
  import Ecto.Query

  defmodule Message do
    use Ecto.Schema

    @primary_key false
    embedded_schema do
      field :id, :binary_id
      field :user_id, :binary_id
      field :content, :string
    end

    def changeset(message \\ %__MODULE__{}, attrs) do
      message
      |> Changeset.cast(attrs, [:user_id, :content])
      |> Spector.changeset_put_event_id(attrs)
      |> Changeset.validate_required([:id, :user_id, :content])
    end
  end

  @primary_key {:id, :binary_id, autogenerate: false}
  embedded_schema do
    embeds_many :messages, Message
  end

  @impl true
  def prepare_event(event_changeset, previous_events, %{edits: previous_id}) do
    previous_events
    |> Enum.find(&(&1.id == previous_id))
    |> then(&Changeset.put_assoc(event_changeset, :edits, [&1]))
  end

  def prepare_event(event_changeset, _previous_events, _attrs) do
    event_changeset
  end

  def changeset(chat, %{edits: message_id, content: new_content}) when chat.action == :edit do
    chat = Changeset.change(chat)
    current_messages = Changeset.get_field(chat, :messages, [])

    updated_messages =
      Enum.map(current_messages, fn message ->
        if message.id == message_id do
          %{message | content: new_content}
        else
          message
        end
      end)

    Changeset.put_change(chat, :messages, updated_messages)
  end

  def changeset(chat \\ %__MODULE__{}, attrs) do
    chat = Changeset.change(chat)

    message =
      attrs
      |> Message.changeset()
      |> Changeset.apply_action!(:insert)

    current_messages = Changeset.get_field(chat, :messages, [])
    Changeset.put_change(chat, :messages, [message | current_messages])
  end

  def list_messages(chat_id) do
    edited_ids =
      from(link in "basic_chat_edits", select: link.previous_id)
      |> MyApp.Repo.all()

    Spector.all_events(__MODULE__, chat_id)
    |> Enum.reject(&(&1.id in edited_ids))
  end

  def get_edit_history(message_id) do
    previous_ids =
      from(link in "basic_chat_edits",
        where: link.event_id == type(^message_id, :binary_id),
        select: link.previous_id
      )

    MyApp.Repo.all(
      from(e in MyApp.BasicChatEvents,
        where: e.id == ^message_id or e.id in subquery(previous_ids),
        order_by: [asc: e.inserted_at]
      )
    )
  end
end
```

### 2. Define the Events Module

```elixir
defmodule MyApp.BasicChatEvents do
  use Spector.Events,
    table: "basic_chat_events",
    links: [edits: {"basic_chat_edits", :previous_id}],
    schemas: [MyApp.BasicChat],
    repo: MyApp.Repo
end
```

The `edits` link creates a relationship from the edited message to its previous version.

### 3. Create the Migration

```elixir
defmodule MyApp.Repo.Migrations.CreateBasicChatEvents do
  use Ecto.Migration

  def up do
    Spector.Migration.up(
      table: "basic_chat_events",
      links: [{"basic_chat_edits", :previous_id}]
    )
  end

  def down do
    Spector.Migration.down(
      table: "basic_chat_events",
      links: [{"basic_chat_edits", :previous_id}]
    )
  end
end
```

## Usage

### Starting a Conversation

```elixir
user_id = Ecto.UUID.generate()

assert {:ok, %MyApp.BasicChat{messages: [%{user_id: ^user_id, content: "Hello everyone!"}]} = chat} =
  Spector.insert(MyApp.BasicChat, %{
    user_id: user_id,
    content: "Hello everyone!"
  })
```

### Adding Messages

Note that messages are prepended, so the most recent message appears first:

```elixir
other_user_id = Ecto.UUID.generate()

assert {:ok, %{messages: [%{user_id: ^other_user_id, content: "Hi there!"}, %{content: "Hello everyone!"}]} = chat} =
  Spector.update(chat, %{
    user_id: other_user_id,
    content: "Hi there!"
  })
```

### Editing a Message

Use the `:edit` action with `:edits` pointing to the message ID being edited:

```elixir
[_, %{id: first_message_id}] = chat.messages

assert {:ok, %{messages: [%{content: "Hi there!"}, %{content: "Hello everyone! (edited)"}]} = chat} =
  Spector.execute(chat, :edit, %{
    edits: first_message_id,
    content: "Hello everyone! (edited)"
  })
```


## How It Works

1. **Insert**: Creates the first event with the initial message.

2. **Update**: Adds new messages to the conversation.

3. **Edit**: Creates a new event linked to the previous version via `basic_chat_edits`. The original remains in the event log.

4. **History**: Follow the `edits` links backward to see all versions of a message.

## Tips

- The `user_id` in each message tracks the author
- Edits preserve the original `user_id` - add an `edited_by` field if you need to track who made edits
- For real-time updates, combine with Phoenix PubSub
- Add `inserted_at` from the event for message timestamps