Part 2: Read it with a view
The account decides, but it doesn’t answer questions. Add two views: a summary with the balance, and a statement with one line per movement.
Show the balance
Section titled “Show the balance”import * as Domain from "alchemy/Domain";import * as Schema from "effect/Schema";import { Account } from "./Account.aggregate.ts";
export class AccountSummary extends Domain.View("AccountSummary", { from: [Account], key: Account, state: Schema.Struct({ owner: Schema.NullOr(Schema.String), balance: Schema.Int }), initial: { owner: null, balance: 0 }, evolve: { AccountOpened: (v, e) => ({ ...v, owner: e.owner }), MoneyDeposited: (v, e) => ({ ...v, balance: e.balanceAfter }), MoneyWithdrawn: (v, e) => ({ ...v, balance: e.balanceAfter }), },}) {}A view folds events into state, one copy per key. key: Account gives every account its own summary.
Host the view
Section titled “Host the view”import * as Domain from "alchemy/Domain";import { Account } from "./Account.aggregate.ts";import { AccountSummary } from "./AccountSummary.view.ts";
export class Bank extends Domain.Model("Bank", { aggregates: [Account], views: [AccountSummary],}) {}Check the balance
Section titled “Check the balance”import { it } from "@effect/vitest";import { Story } from "alchemy/Domain/Story";import { Account } from "../src/Account.aggregate.ts";import { AccountSummary } from "../src/AccountSummary.view.ts";import { Bank } from "../src/Bank.model.ts";import { DepositMoney } from "../src/DepositMoney.command.ts";import { AccountOpened } from "../src/OpenAccount.command.ts";import { WithdrawMoney } from "../src/WithdrawMoney.command.ts";
const story = Story.make(Bank);const account = Account.ref("a-1");
it.effect("the summary follows the balance", () => story( Story.given(account, new AccountOpened({ owner: "sam" })), Story.when(account, new DepositMoney({ amount: 100 })), Story.when(account, new WithdrawMoney({ amount: 30 })), Story.view(AccountSummary, account, { owner: "sam", balance: 70 }), ),);view checks a key’s state after everything before it has been applied. A partial object checks only the fields you give.
Describe a statement line
Section titled “Describe a statement line”import * as Domain from "alchemy/Domain";import * as Schema from "effect/Schema";
export class StatementLine extends Domain.Event("StatementLine", { kind: Schema.Literals(["deposit", "withdrawal"]), amount: Schema.Int, balance: Schema.Int,}) {}A view can record events of its own. One StatementLine describes any movement of money.
Emit a line for every movement
Section titled “Emit a line for every movement”import * as Domain from "alchemy/Domain";import * as Schema from "effect/Schema";import { Account } from "./Account.aggregate.ts";
export class StatementLine extends Domain.Event("StatementLine", { balance: Schema.Int,}) {}
export class Statement extends Domain.View("Statement", { from: [Account], key: Account, events: [StatementLine], emit: { MoneyDeposited: (e) => new StatementLine({ kind: "deposit", amount: e.amount, balance: e.balanceAfter }), MoneyWithdrawn: (e) => new StatementLine({ kind: "withdrawal", amount: e.amount, balance: e.balanceAfter }), },}) {}emit takes each account event and returns the line to record. The statement keeps no state of its own; its lines are its history.
Host the statement
Section titled “Host the statement”import { Account } from "./Account.aggregate.ts";import { AccountSummary } from "./AccountSummary.view.ts";import { Statement } from "./Statement.view.ts";
export class Bank extends Domain.Model("Bank", { aggregates: [Account], views: [AccountSummary], views: [AccountSummary, Statement],}) {}Check the statement
Section titled “Check the statement”import { DepositMoney } from "../src/DepositMoney.command.ts";import { AccountOpened } from "../src/OpenAccount.command.ts";import { Statement } from "../src/Statement.view.ts";import { WithdrawMoney } from "../src/WithdrawMoney.command.ts"; ),);
it.effect("the statement has one line per movement", () => story( Story.given(account, new AccountOpened({ owner: "sam" })), Story.when(account, new DepositMoney({ amount: 100 })), Story.when(account, new WithdrawMoney({ amount: 30 })), Story.emitted(Statement, account, [ { kind: "deposit", amount: 100, balance: 100 }, { kind: "withdrawal", amount: 30, balance: 70 }, ]), ),);emitted checks every event a view key has emitted, in order.
bun vitest runThe account decides, and two views answer: what the balance is, and how it got there.