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/1save/1— aggregate upsert + outbox event inserts in ONE transactiondelete/1find_by_<field>/1for everyfind_byon the portto_domain/1andfrom_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
endYour 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 errorEach 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.
Summary
Functions
Ecto type for a domain field type (see DDD.Type).
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.