Macro for defining Chronicle read models with embedded model-bound projections.
Use Chronicle.ReadModels.ReadModel in a struct module to define both the read model
shape and how Chronicle should project events into it. The projection
definition lives right next to the struct fields — no separate projection
module is needed.
Quick Example
defmodule MyApp.ReadModels.Account do
use Chronicle.ReadModels.ReadModel
defstruct account_id: nil, owner_name: nil, balance: 0, transaction_count: 0
from MyApp.Events.AccountOpened,
set: [
account_id: :event_source_id,
owner_name: :owner_name,
balance: :initial_balance
]
from MyApp.Events.FundsDeposited,
add: [balance: :amount, transaction_count: 1]
from MyApp.Events.FundsWithdrawn,
subtract: [balance: :amount],
add: [transaction_count: 1]
endProjection Macros
from/2
Maps properties from an event onto the read model.
from MyApp.Events.AccountOpened,
set: [account_id: :event_source_id, owner_name: :owner_name],
count: :transaction_countOptions:
:key— key expression identifying the model instance. Defaults to"$eventSourceId"when omitted. Use{:composite, parts}for an ordered list of key-part mappings, and{:event_context, "Occurred.Week"}to map a key part to an event-context property path.:parent_key— parent key for nested models:set— keyword list offield: expressionpairs to set directly:add— keyword list offield: expressionpairs to add to:subtract— keyword list offield: expressionpairs to subtract from:count— field atom to increment by 1 on each event occurrence
join/2
Joins a secondary event onto the model by a matching field.
join MyApp.Events.AccountVerified,
on: "accountId",
set: [verified: true]Options:
:on— (required) the field name in the event to join on:key— key expression (defaults to"$eventSourceId"):set,:add,:subtract— property mappings
removed_with/2
Removes the model instance when the given event occurs.
removed_with MyApp.Events.AccountClosed,
[]Options:
:key— key expression (defaults to"$eventSourceId"):parent_key— parent key for nested models
removed_with_join/2
Removes the model instance when a secondary event — from a different event source — matches it
by a property instead of by the model's own key. Mirrors the .NET client's
RemovedWithJoin<TEvent> attribute.
removed_with_join MyApp.Events.AccountFlaggedForClosure,
key: :account_idOptions:
:key— the property on the event that identifies the instance to remove. Defaults to"$eventSourceId".
clear_with/2
Sets a field back to no value when the given event occurs — sugar for
from event, clear: [...].
clear_with MyApp.Events.DiscountRemoved, [:discount_code]Equivalent to:
from MyApp.Events.DiscountRemoved, clear: [:discount_code]Options:
:key,:parent_key— same asfrom/2
from_every/1
Applies property mappings on every event, regardless of type.
from_every set: [last_activity: "Occurred"]For dictionary properties keyed by event context values:
from_every increment: [event_counts: {:event_context, :type}]This increments a map field event_counts with keys derived from the event type
identifier, producing one counter per distinct event type.
Options:
:set,:add,:subtract— property mappings applied to every event:count— field atom to increment by 1 on each event occurrence:increment— keyword list for incrementing fields; use{field: {:event_context, :property}}for dictionary fields keyed by event context values:decrement— keyword list for decrementing fields; use{field: {:event_context, :property}}for dictionary fields keyed by event context values
Composite Keys
Key parts use the same event-field atoms, explicit expression strings, and
literals as other keys. Target names are preserved and parts are emitted in
list order. A composite key requires a non-empty list of {name, part} pairs
with string or atom names; nested composite keys are not supported.
Event-context paths must be strings containing only letters, dots and
parentheses, matching the kernel's accepted characters. Invalid keys raise
ArgumentError identifying the read model and offending part.
Paths are passed to the kernel unchanged, without adding method parentheses:
from MyApp.Events.AccountOpened,
key: {:composite, [
{"Year", {:event_context, "Occurred.Year"}},
{"Week", {:event_context, "Occurred.Week"}}
]},
count: :transaction_countThis emits
$composite(Year=$eventContext(Occurred.Year),Week=$eventContext(Occurred.Week)),
matching the .NET client. The kernel evaluates the date parts; the client does
not calculate them. Occurred.Week is the ISO 8601 week number (1–53): weeks
start on Monday and week 1 contains the first Thursday.
Occurred.Year is the calendar year, not the ISO week-based year. Pairing it
with Occurred.Week can split a week around New Year. A week-based-year
accessor is tracked in https://github.com/Cratis/Chronicle/issues/4505.
These key options also work in Chronicle.Projections.Projection declarations.
Property Expressions
Values in set:, add:, and subtract: lists can be atoms, strings, or
literals:
- Use atoms for event fields (preferred):
:owner_name,:amount - Use
:event_source_idand:occurredfor built-in context values - Use strings for advanced Chronicle expressions when needed
- Use numbers and booleans as literal values
| Expression | Meaning |
|---|---|
:owner_name | The owner_name field from the event |
:event_source_id | The event source identifier |
:occurred | When the event was recorded |
1 | A literal integer constant |
"$add(amount, balance)" | An explicit Chronicle expression |
Registering with Chronicle.Client
{Chronicle.Client,
...
read_models: [MyApp.ReadModels.Account]}Introspection
MyApp.ReadModels.Account.__chronicle_read_model__(:id)
MyApp.ReadModels.Account.__chronicle_read_model__(:from)
MyApp.ReadModels.Account.__chronicle_read_model__(:join)
MyApp.ReadModels.Account.__chronicle_read_model__(:removed_with)
MyApp.ReadModels.Account.__chronicle_read_model__(:from_every)
MyApp.ReadModels.Account.__chronicle_read_model__(:has_projection?)
MyApp.ReadModels.Account.__chronicle_read_model__(:passive?)Passive Read Models
Pass passive: true to mark a read model as passive. A passive projection does not
actively observe events and never writes to a materialized sink — its instances are
resolved on demand via immediate projection when looked up by id. This is useful for
read models that only need to be queried by key (for example, from within a reactor).
defmodule MyApp.ReadModels.Application do
use Chronicle.ReadModels.ReadModel, passive: true
defstruct id: nil, name: nil
from MyApp.Events.ApplicationCreated,
set: [id: :event_source_id, name: :name]
end
Summary
Functions
Sets a field back to no value when the given event occurs. Sugar for from event, clear: [...].
Names an event that may create or resurrect this read model variant. Repeatable — a variant
may enter on more than one event. Every other event this variant projects from (whether
declared with from/2 directly, or merged in from a Chronicle.Projections.GlobalHandler) is
automatically reclassified into an update-only join: it can bring an already-active instance
up to date, but it can never create or resurrect one.
Declares how an event maps onto this read model.
Applies property mappings on every event, regardless of type.
Declares a join from a secondary event onto this read model.
Disables Chronicle's automatic mapping of matching event property names onto this read model.
Excludes specific fields from Chronicle's automatic property mapping.
Declares that this model is removed when the given event occurs.
Declares that this model is removed when a secondary event matches it by a property, instead of by its own key.
Resolves accumulated no_auto_map declarations into the AutoMap setting and the excluded fields.
Declares this read model to be one of several mutually exclusive representations of the same logical entity. Entering one variant removes the entity from every sibling variant of the same identity.
Functions
Sets a field back to no value when the given event occurs. Sugar for from event, clear: [...].
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Names an event that may create or resurrect this read model variant. Repeatable — a variant
may enter on more than one event. Every other event this variant projects from (whether
declared with from/2 directly, or merged in from a Chronicle.Projections.GlobalHandler) is
automatically reclassified into an update-only join: it can bring an already-active instance
up to date, but it can never create or resurrect one.
enters_on MyApp.Events.IssueCreatedOptions:
:key— event property used as the key for this entering event. Defaults to:event_source_id.
Declares how an event maps onto this read model.
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Applies property mappings on every event, regardless of type.
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Declares a join from a secondary event onto this read model.
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Disables Chronicle's automatic mapping of matching event property names onto this read model.
Called with no arguments it turns AutoMap off for the whole projection, so every property has to be mapped explicitly. Called with a list of fields it leaves AutoMap on but excludes just those fields, which is what you want when a single property is set from something other than the event property that happens to share its name.
no_auto_map()
no_auto_map([:balance, :status])Without either, AutoMap is left at the kernel's default and cannot be turned off.
Excludes specific fields from Chronicle's automatic property mapping.
See no_auto_map/0 for the whole-projection form.
Declares that this model is removed when the given event occurs.
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Declares that this model is removed when a secondary event matches it by a property, instead of by its own key.
See the Chronicle.ReadModels.ReadModel module documentation for full options.
Resolves accumulated no_auto_map declarations into the AutoMap setting and the excluded fields.
Returns {:disabled, []} when AutoMap is off for the whole projection, {:enabled, fields}
when specific fields are excluded, and {:inherit, []} when nothing was declared - which
leaves the kernel's own default in place, exactly as before this could be declared at all.
Declares this read model to be one of several mutually exclusive representations of the same logical entity. Entering one variant removes the entity from every sibling variant of the same identity.
variant_of MyApp.ReadModels.WorkItem, key: :idOptions:
:key— (required) the field on this variant used as its own key, and as the correlation property for reclassified joins.