Abaks: Views
The last part, in order to make our API is to be able to query it, that's why we have to introduce Views.
Since our project is rather simple, we have made opinionated design choices in our CQRS implementation, we need a View Model, but instead of having Materialized Views or Projections, we'll based our implementation directly on the Events.
Noticeably, we want to be able to get the Period:
data Period = Period
  { periodId :: PeriodId,
  }
We could then define a View:
type View a r = Events a -> r
viewPeriod =
  withStartedEvent $ \started ->
    foldl' go $
      Period
        { periodId = started.periodId,
          name = started.name,
          from = started.from,
          to = started.to,
          balance = started.initialBalance,
          initialBalance = started.initialBalance,
          entries = mempty
        }
  where
    go acc =
      \case
        Started _ -> acc
        EntryAdded e ->
          adjustAmount (Amount 0) e.entry.amount $
            withEntries acc $
              Map.insert e.entry.entryId e.entry
        EntryAmountChanged e ->
          adjustAmount (maybe (Amount 0) (.amount) $ Map.lookup e.entryId acc.entries) e.amount $
            withEntry acc e.entryId $ \entry ->
              entry {amount = e.amount}
        EntryValidated e ->
          withEntry acc e.entryId $ \entry ->
            entry {state = Validated}
        EntryCommented e ->
          withEntry acc e.entryId $ \entry ->
            entry {comment = e.comment}
        EntryMarkedInConflict e ->
          withEntry acc e.entryId $ \entry ->
            entry {state = InConflict e.reason}
        EntryDeleted e ->
          withEntries acc $
            Map.delete e.entryId
    withEntries period f = period {entries = f period.entries}
    withEntry period entryId f = withEntries period $ Map.adjust f entryId
    adjustAmount previous new period =
      period
        { balance = Amount $ period.balance.getAmountInCents - previous.getAmountInCents + new.getAmountInCents
        }
Clearly, than a lot of code, let's break this down:
- withStartedEventsplits the stream, extracting- Started
- Then we foldover thenEvents withPeriodinitialized withStarted's data
- For each Eventwe have to updatePeriod
The great thing with this approach is the freedom we have to define per-query independent piece of code, having a fine-grain interpretation of each Event, meaning, the precision of our Views is bound to the precision of our Events.