README.md

# Commanded Utils

[![Hex.pm](https://img.shields.io/hexpm/v/commanded_utils.svg)](https://hex.pm/packages/commanded_utils)
[![Documentation](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/commanded_utils)
[![License](https://img.shields.io/hexpm/l/commanded_utils.svg)](LICENSE)

> Essential utilities and helpers for building CQRS/Event Sourcing applications with Commanded

**Commanded Utils** is a comprehensive toolkit that simplifies building event-sourced applications with [Commanded](https://github.com/commanded/commanded). It provides elegant DSLs, type-safe commands and events, automatic validation, enrichment pipelines, error handling, and more - with minimal boilerplate.

[δΈ­ζ–‡ζ–‡ζ‘£](README_zh-CN.md)

## Features

- 🎨 **Elegant DSL** - `defcommand` and `defevent` macros for clean, declarative definitions
- βœ… **Type Safety** - Built on Ecto schemas with automatic type casting and validation
- πŸ”„ **Enrichment Pipeline** - Clean, functional command enrichment with convention over configuration
- πŸ“¦ **Event Versioning** - Built-in upcasting support for seamless event evolution
- πŸ›‘οΈ **Error Handling** - Configurable retry logic and failure handling for event handlers
- 🧰 **Helper Functions** - 20+ utility functions for type conversions and decimal math
- πŸ“ **Minimal Boilerplate** - No more verbose `defimpl` blocks - just define `enrich/1` functions
- 🏒 **Multi-tenancy Ready** - Optional tenant and creator fields support

## Installation

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

```elixir
def deps do
  [
    {:commanded, "~> 1.4"},
    {:commanded_utils, "~> 0.1"}
  ]
end
```

Run `mix deps.get` to install.

## Quick Start

### Define a Command

```elixir
defmodule CreateUser do
  use CommandedUtils.Command

  @required_fields ~w(email)a

  defcommand do
    field :user_id, Ecto.UUID
    field :email, :string
    field :name, :string
    field :age, :integer
  end

  # Optional: enrich before dispatch
  def enrich(cmd) do
    %{cmd | user_id: cmd.user_id || Ecto.UUID.generate()}
  end

  # Optional: custom validation
  def changeset(struct, attrs) do
    struct
    |> super(attrs)
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
  end
end
```

### Define an Event

```elixir
defmodule UserCreated do
  use CommandedUtils.Event

  defevent do
    field :user_id, Ecto.UUID
    field :email, :string
    field :name, :string
  end
end
```

### Use Commands

```elixir
# Validate and create
{:ok, cmd} = CreateUser.new(%{
  email: "john@example.com",
  name: "John Doe",
  age: 30
})

# Dispatch to aggregate
MyApp.Router.dispatch(cmd)
```

## Core Concepts

### Commands

Commands represent intentions to change state. They are validated before dispatch and can be enriched with additional data.

```elixir
defmodule PlaceOrder do
  use CommandedUtils.Command

  @required_fields ~w(customer_id items)a

  defcommand do
    field :order_id, Ecto.UUID
    field :customer_id, Ecto.UUID
    field :items, {:array, :map}
    field :total_amount, :decimal
  end

  # Multi-step enrichment
  def enrich(cmd) do
    cmd
    |> ensure_order_id()
    |> calculate_total()
    |> validate_inventory()
  end

  defp ensure_order_id(%{order_id: nil} = cmd) do
    %{cmd | order_id: Ecto.UUID.generate()}
  end
  defp ensure_order_id(cmd), do: cmd

  defp calculate_total(cmd) do
    import CommandedUtils.Helpers
    
    total = 
      cmd.items
      |> Enum.map(&decimal_mult(&1.price, &1.quantity))
      |> Enum.reduce(Decimal.new(0), &Decimal.add/2)
    
    %{cmd | total_amount: total}
  end

  defp validate_inventory(cmd) do
    # Return {:error, reason} to halt dispatch
    # Return {:ok, cmd} or cmd to continue
    if all_items_in_stock?(cmd.items) do
      {:ok, cmd}
    else
      {:error, :insufficient_inventory}
    end
  end
end
```

### Events with Versioning

Events can evolve over time. Commanded Utils provides automatic upcasting:

```elixir
defmodule OrderPlaced do
  use CommandedUtils.Event

  # Current version
  defevent version: 2 do
    field :order_id, Ecto.UUID
    field :customer_id, Ecto.UUID
    field :items, {:array, :map}
    field :payment_method, :string  # Added in v2
  end

  # Upgrade v1 events to v2
  def upcast(%{"version" => 1} = params, _metadata, 2) do
    Map.put(params, "payment_method", "credit_card")
  end

  # No transformation needed for v1
  def upcast(params, _metadata, 1), do: params
end
```

When old events are read from the event store, they're automatically upcasted to the latest version.

### Middleware Setup

Add Commanded Utils middleware to your router:

```elixir
defmodule MyApp.Router do
  use Commanded.Commands.Router

  # Add middleware
  middleware CommandedUtils.Middleware.Logger
  middleware CommandedUtils.Middleware.Enrich

  # Define aggregates and dispatch
  identify MyApp.UserAggregate, by: :user_id, prefix: "user-"
  dispatch CreateUser, to: MyApp.UserAggregate
end
```

The `Enrich` middleware automatically calls `enrich/1` if defined in your command module.

### Projectors with Error Handling

```elixir
defmodule MyApp.UserProjector do
  use Commanded.Projections.Ecto,
    application: MyApp.CommandedApp,
    repo: MyApp.Repo,
    name: __MODULE__

  # Automatic retry with configurable backoff
  use CommandedUtils.EventHandlerFailureContext,
    max_retries: 5,
    retry_after: 1000,
    skip: true

  import CommandedUtils.Helpers

  alias MyApp.User

  project %UserCreated{} = evt, _meta, fn multi ->
    user = %User{
      id: evt.user_id,
      email: to_string(evt.email),
      name: to_string(evt.name),
      age: to_integer(evt.age)
    }
    
    Ecto.Multi.insert(multi, :user, user)
  end

  project %UserUpdated{} = evt, meta, fn multi ->
    set_fields = [
      name: to_string(evt.name),
      updated_at: meta.created_at
    ]

    Ecto.Multi.update_all(
      multi,
      :user_updated,
      user_query(evt.user_id),
      set: set_fields
    )
  end

  defp user_query(user_id) do
    from(u in User, where: u.id == ^user_id)
  end
end
```

## Helper Functions

Commanded Utils provides 30+ utility functions for common operations:

```elixir
import CommandedUtils.Helpers

# Type conversions
to_decimal("10.50")           # #Decimal<10.50>
to_atom("active")             # :active
to_integer("42")              # 42
to_boolean("true")            # true
to_string(:active)            # "active"

# DateTime parsing (using Ecto.Type.cast)
{:ok, dt} = parse_utc_datetime("2024-01-01 12:00:00")  # ~U[2024-01-01 12:00:00Z]
{:ok, date} = parse_date("2024-01-01")                 # ~D[2024-01-01]
{:ok, time} = parse_time("12:30:00")                   # ~T[12:30:00]
{:ok, ndt} = parse_naive_datetime("2024-01-01 12:00") # ~N[2024-01-01 12:00:00]

# DateTime parsing (raising on error)
parse_utc_datetime!("2024-01-01 12:00:00")  # ~U[...] or raise
parse_date!("2024-01-01")                    # ~D[...] or raise

# Decimal operations
decimal_add(10, 5)            # #Decimal<15>
decimal_sub(20, 5)            # #Decimal<15>
decimal_mult(price, quantity) # Safe multiplication
decimal_div(total, count)     # Safe division

# Decimal comparisons
decimal_gt_zero?(balance)     # true/false
decimal_lt_zero?(balance)     # true/false
decimal_eq_zero?(balance)     # true/false

# UUID helpers
ensure_uuid(nil)              # Generates new UUID
ensure_uuid("existing")       # "existing"
```

All helpers handle `nil` values gracefully and provide safe type conversions.

### Usage in Projectors

```elixir
project %OrderCreated{} = evt, _meta, fn multi ->
  # Parse dates using Ecto.Type.cast
  {:ok, order_date} = parse_date(evt.order_date)
  {:ok, due_date} = parse_date(evt.due_date)
  {:ok, created_at} = parse_utc_datetime(evt.created_at)

  order = %Order{
    id: evt.order_id,
    amount: to_decimal(evt.amount),
    status: to_atom(evt.status),
    order_date: order_date,
    due_date: due_date,
    created_at: created_at
  }
  
  Ecto.Multi.insert(multi, :order, order)
end
```

## Configuration

Configure optional fields globally:

```elixir
# config/config.exs
config :commanded_utils,
  # Fields always included
  required_fields: [:version],
  # Optional fields (can be enabled per module)
  optional_fields: [:tenant_id, :creator, :metadata]
```

Include optional fields in specific modules:

```elixir
defmodule CreateTenantUser do
  use CommandedUtils.Command, include: [:tenant_id, :creator]

  defcommand do
    # tenant_id and creator automatically included
    field :email, :string
  end
end
```

### Aggregates

Define aggregates using the same type system:

```elixir
defmodule MyApp.ItemAggregate do
  @required_fields []
  
  use CommandedUtils.Type  # Reuse the type system
  import CommandedUtils.Helpers
  
  alias Commanded.Aggregate.Multi
  
  # Define aggregate state
  deftype do
    field :item_id, Ecto.UUID
    field :name, :string
    field :price, :decimal
    field :stock, :integer, default: 0
    field :deleted?, :boolean, default: false
  end
  
  # Command handlers
  def execute(%__MODULE__{item_id: nil}, %CreateItem{} = cmd) do
    %ItemCreated{
      item_id: cmd.item_id,
      name: cmd.name,
      price: cmd.price
    }
  end
  
  def execute(%__MODULE__{item_id: id} = state, %AdjustStock{item_id: id} = cmd) do
    new_stock = state.stock + cmd.quantity
    
    if new_stock < 0 do
      {:error, :insufficient_stock}
    else
      %StockAdjusted{
        item_id: id,
        quantity: cmd.quantity,
        new_stock: new_stock
      }
    end
  end
  
  # Return multiple events using Multi
  def execute(%__MODULE__{item_id: id} = state, %UpdateItem{item_id: id} = cmd) do
    state
    |> Multi.new()
    |> Multi.execute(fn _ ->
      %ItemUpdated{
        item_id: id,
        name: cmd.name,
        price: cmd.price
      }
    end)
    |> Multi.execute(fn _ ->
      # Automatically publish price change if price changed
      if decimal_gt_zero?(decimal_sub(cmd.price, state.price)) do
        %PriceIncreased{item_id: id, old_price: state.price, new_price: cmd.price}
      else
        []
      end
    end)
  end
  
  # State mutators
  def apply(%__MODULE__{} = state, %ItemCreated{} = evt) do
    %__MODULE__{
      state
      | item_id: evt.item_id,
        name: evt.name,
        price: to_decimal(evt.price)
    }
  end
  
  def apply(%__MODULE__{} = state, %StockAdjusted{} = evt) do
    %__MODULE__{state | stock: evt.new_stock}
  end
  
  def apply(%__MODULE__{} = state, %ItemUpdated{} = evt) do
    %__MODULE__{
      state
      | name: evt.name,
        price: to_decimal(evt.price)
    }
  end
  
  # Lifecycle management
  def after_event(%ItemDeleted{}), do: :stop
  def after_event(_), do: :timer.hours(1)
end
```

**Key Features:**
- βœ… Reuse `CommandedUtils.Type` for aggregate state
- βœ… Use `Helpers` for safe type conversions
- βœ… Return multiple events with `Commanded.Aggregate.Multi`
- βœ… Business logic validation in `execute/2`
- βœ… Aggregate lifecycle management

## Examples

See the [Bank Account Example](lib/commanded_utils/examples/bank_account.ex) for a complete working implementation including:

- Commands with validation and enrichment
- Events with versioning
- Aggregate implementation with Multi
- Business logic and state management
- Lifecycle management

## Documentation

- [API Documentation](https://hexdocs.pm/commanded_utils)
- [Getting Started Guide](guides/getting-started.md)
- [Commands and Events Guide](guides/commands-and-events.md)

## Comparison with Plain Commanded

### Before (Plain Commanded)

```elixir
# Verbose defimpl for enrichment
defmodule CreateUser do
  @derive Jason.Encoder
  defstruct [:user_id, :email, :name]
  
  defimpl Commanded.Middleware.Enrichable, for: __MODULE__ do
    def enrich(%CreateUser{} = cmd, _) do
      # 40+ lines of boilerplate...
    end
  end
end
```

### After (With Commanded Utils)

```elixir
# Clean and focused
defmodule CreateUser do
  use CommandedUtils.Command

  @required_fields ~w(email)a

  defcommand do
    field :user_id, Ecto.UUID
    field :email, :string
    field :name, :string
  end

  def enrich(cmd) do
    %{cmd | user_id: cmd.user_id || Ecto.UUID.generate()}
  end
end
```

**Reduction: ~70% less boilerplate code**

## Testing

```bash
# Run all tests
mix test

# Run with coverage
mix test --cover

# Run specific test file
mix test test/commanded_utils/command_test.exs
```

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Credits

Built with ❀️ by [zven21](https://github.com/zven21)

Powered by [Commanded](https://github.com/commanded/commanded)

## Related Projects

- [Commanded](https://github.com/commanded/commanded) - CQRS/ES framework for Elixir
- [EventStore](https://github.com/commanded/eventstore) - Event store for Commanded
- [Commanded Ecto Projections](https://github.com/commanded/commanded-ecto-projections) - Ecto projections for Commanded

## Support

- πŸ“– [Documentation](https://hexdocs.pm/commanded_utils)
- πŸ’¬ [GitHub Issues](https://github.com/zven21/commanded_utils/issues)
- πŸ› [Bug Reports](https://github.com/zven21/commanded_utils/issues/new)

---

**Star ⭐ this repository if you find it helpful!**