# `DDD.Repository.Ecto`
[🔗](https://github.com/blackuprise/vicander/blob/v0.1.1/lib/ddd/persistence/repository/ecto.ex#L1)

Auto-generates a full Ecto repository adapter from a `DDD.Repository` port.

Reads aggregate field metadata and port `find_by` declarations to generate:
  - Embedded Ecto schema (no separate schema module needed)
  - `find_by_id/1`
  - `save/1`  — aggregate upsert + outbox event inserts in ONE transaction
  - `delete/1`
  - `find_by_<field>/1` for every `find_by` on the port
  - `to_domain/1` and `from_domain/1`

The whole aggregate is one row. Nested entities (`{:list, Entity}` / `{:one, Entity}`)
are stored as `jsonb` and rebuilt into sealed structs on load; keys missing from
old rows take the field default.

`save/1` refuses structs that are not sealed (`DDD.Error` with code `:unsealed`).

## Usage

    defmodule MyApp.Infrastructure.OrderRepository do
      use DDD.Repository.Ecto,
        port:  MyApp.OrderRepository,
        repo:  MyApp.Repo,
        table: "orders"

      # Custom queries use schema() and repo() helpers
      def find_recent(limit \\ 10) do
        import Ecto.Query
        from(o in schema(), order_by: [desc: o.created_at], limit: ^limit)
        |> repo().all()
        |> Enum.map(&to_domain/1)
      end
    end

## Your own schema

To store an aggregate in a table you already have, pass the Ecto schema that maps it:

    use DDD.Repository.Ecto,
      port:   MyApp.OrderRepository,
      repo:   MyApp.Repo,
      schema: MyApp.Infra.OrderRecord       # instead of `table:`; giving both is an error

Each aggregate field is read from, and written to, the schema field of the same name (through
`DDD.Type`). For anything else override `to_domain/1` and `from_domain/1`; the two-argument
versions take the values for the fields that differ, so the rest stays by name:

    def to_domain(record), do: to_domain(record, customer_id: record.cust_ref)
    def from_domain(order), do: from_domain(order, cust_ref: order.customer_id)

The save is the framework's: your schema's `changeset/2` is never called, and the upsert,
the optimistic lock and the outbox insert stay in one transaction. The schema must have a
`:version` field, the aggregate's identity as primary key (an `:identity` string column for a
composite one), and `DDD.Type.Json` for nested entities, lists and maps
(see `DDD.Type.ecto_type/1`). It is checked when the adapter is compiled; overriding
`to_domain/1` or `from_domain/1` turns off the by-name checks (unmapped fields, JSON types).

You own the table: `mix vic.gen.migration` skips the aggregate, and
`DDD.Migration.schema_drift/2` / `DDD.Migration.table_drift/1` report a mismatch for a test.

## Migrations

`mix vic.gen.migration MyApp.Order` generates and diffs table migrations;
`mix vic.gen.outbox_migration` creates the outbox table.

# `ecto_type`

Ecto type for a domain field type (see `DDD.Type`).

# `schema_problems`

```elixir
@spec schema_problems(module(), module(), {atom(), Ecto.Type.t()}, boolean()) :: [
  String.t()
]
```

What is wrong with `schema` as the storage of `aggregate`, as a list of messages (empty: fine).

`pk` is the `{field, ecto_type}` the aggregate's identity needs. With `overridden?` (you
wrote `to_domain/1` or `from_domain/1`) the by-name mapping is yours, so unmapped fields and
JSON column types are not checked; the primary key and the `:version` column always are.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
