Skip to content

Domain.View reference

Source: packages/domain/src/View.ts

A read model: events folded into state per key, kept up to date as new events arrive. A view can emit events of its own, route one event to several keys, and decide who may read it.

import * as Domain from "@alchemy.run/domain";
import * as Schema from "effect/Schema";
import { Account } from "./Account.ts";
export class AccountSummary extends Domain.View("AccountSummary", {
from: [Account],
key: Account,
state: Schema.Struct({ balance: Schema.Int }),
initial: { balance: 0 },
evolve: {
MoneyDeposited: (v, e) => ({ ...v, balance: e.balanceAfter }),
MoneyWithdrawn: (v, e) => ({ ...v, balance: e.balanceAfter }),
},
}) {}
import * as Domain from "@alchemy.run/domain";
import * as Schema from "effect/Schema";
import { Account } from "./Account.ts";
export class StatementLine extends Domain.Event("StatementLine", {
kind: Schema.Literals(["deposit", "withdrawal"]),
amount: 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 }),
MoneyWithdrawn: (e) => new StatementLine({ kind: "withdrawal", amount: e.amount }),
},
}) {}
keyOf: {
TransferRequested: {
out: ({ event }) => Account.ref(event.from),
in: ({ event }) => Account.ref(event.to),
},
},
key: OrgId, // a string Schema
keyOf: { Tour: ({ event }) => event.orgId },

The declaration lists the middleware that guards reads and the rejections they may fail with. Its Layer, one validate function, judges each read: on every get, snapshot and list, and on every change while a client watches. A view that declares either needs its Layer.

export class AccountSummary extends Domain.View("AccountSummary", {
from: [Account],
key: Account,
middleware: [Session],
rejects: [NotOwner],
state: Schema.Struct({ owner: Schema.NullOr(Schema.String), balance: Schema.Int }),
initial: { owner: null, balance: 0 },
evolve: { MoneyDeposited: (v, e) => ({ ...v, balance: e.balanceAfter }) },
}) {}
export const AccountSummaryLive = AccountSummary.toLayer(
Effect.succeed(
Effect.fn(function* (_request, summary) {
const actor = yield* CurrentActor;
if (summary?.owner && summary.owner !== actor.customerId) return yield* new NotOwner();
}),
),
);

The last argument is its annotations: data for what is built on the model, read back as X.annotations. Add your own keys by augmenting Domain.Annotations.

An Api’s read tools, CLI help and OpenAPI document show its description after the sentence for each call.

export class AccountSummary extends Domain.View(
"AccountSummary",
{
from: [Account],
key: Account,
state: Schema.Struct({ balance: Schema.Int }),
initial: { balance: 0 },
evolve: { MoneyDeposited: (_, e) => ({ balance: e.balanceAfter }) },
},
{ description: "An account's balance." },
) {}