Chronicle.ReadModels.ReadModel (cratis_chronicle v3.16.1)

Copy Markdown View Source

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]
end

Projection 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_count

Options:

  • :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 of field: expression pairs to set directly
  • :add — keyword list of field: expression pairs to add to
  • :subtract — keyword list of field: expression pairs 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_id

Options:

  • :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 as from/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_count

This 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_id and :occurred for built-in context values
  • Use strings for advanced Chronicle expressions when needed
  • Use numbers and booleans as literal values
ExpressionMeaning
:owner_nameThe owner_name field from the event
:event_source_idThe event source identifier
:occurredWhen the event was recorded
1A 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

clear_with(event_module, fields, opts \\ [])

(macro)

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.

enters_on(event_module, opts \\ [])

(macro)

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.IssueCreated

Options:

  • :key — event property used as the key for this entering event. Defaults to :event_source_id.

from(event_module, opts \\ [])

(macro)

Declares how an event maps onto this read model.

See the Chronicle.ReadModels.ReadModel module documentation for full options.

from_every(opts)

(macro)

Applies property mappings on every event, regardless of type.

See the Chronicle.ReadModels.ReadModel module documentation for full options.

join(event_module, opts)

(macro)

Declares a join from a secondary event onto this read model.

See the Chronicle.ReadModels.ReadModel module documentation for full options.

no_auto_map()

(macro)

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.

no_auto_map(fields)

(macro)

Excludes specific fields from Chronicle's automatic property mapping.

See no_auto_map/0 for the whole-projection form.

removed_with(event_module, opts \\ [])

(macro)

Declares that this model is removed when the given event occurs.

See the Chronicle.ReadModels.ReadModel module documentation for full options.

removed_with_join(event_module, opts \\ [])

(macro)

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.

resolve_no_auto_map(declarations)

@spec resolve_no_auto_map([:all | [atom()]]) ::
  {:inherit | :enabled | :disabled, [atom()]}

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.

variant_of(identity_module, opts)

(macro)

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: :id

Options:

  • :key — (required) the field on this variant used as its own key, and as the correlation property for reclassified joins.