Views
A view is what a query returns: a read-only shape, free in its fields, built for the screen or the API that needs it. It is read through a query repository without loading the aggregate.
export type OrderSummary = View<{ id: OrderId; total: Money; placedAt: Date | null }>;When to use
Declare a view for each answer a query handler gives: a summary, a list row, a detail page. A view describes what is read, not how the business works: it has no methods and protects no rule. When a use case changes something, it goes through the aggregate, never through a view.
Usage
Declare a view
A view is a type, in domain/views/. It may hold identifiers and value objects, and any shape the reader needs.
import type { View } from "@alveolus/core";
import type { Money } from "../value-objects/money.value-object";
import type { OrderId } from "../value-objects/order-id.identifier";
export type OrderSummary = View<{
id: OrderId;
total: Money;
lineCount: number;
placedAt: Date | null;
}>;Read it
A query repository returns the view; its adapter builds it straight from storage, without loading the aggregate.
export class PgOrderSummaries extends OrderSummaries {
async summaryOf(id: OrderId): Promise<OrderSummary | undefined> {
const row = await this.db.selectFrom("orders").where("id", "=", id.value).selectAll().executeTakeFirst();
if (row === undefined) {
return undefined;
}
return { id: new OrderId(row.id), lineCount: row.line_count, placedAt: row.placed_at, total: Money.of(row.total) };
}
}Return it from a query
async handle({ orderId }: GetOrderSummary): Promise<Result<OrderSummary, OrderNotFound>> {
const summary = await this.summaries.summaryOf(new OrderId(orderId));
return summary === undefined ? err(new OrderNotFound({ orderId })) : ok(summary);
}The driving adapter that answers the request turns the view into its response, or into a representation for another context.
Keep the aggregate out of views
export type OrderDetails = View<{ order: Order; customerName: string }>;export type OrderDetails = View<{ id: OrderId; total: Money; customerName: string }>;Why?
A view that carries the aggregate hands its business methods to the read side: a query could then change it. A view only carries data, so reading can never write.
Reference
type View<Props extends object> = Readonly<Props>;| Type parameter | Description |
|---|---|
Props | The fields of the view. |
Caveats
Viewchanges nothing at runtime: it is the read-only type it wraps. It marks the type as an answer of a query, for you and for your reviewers.Readonlyis shallow: nested arrays and objects stay mutable unless you declare themreadonly.- No separate read model is implied: views are read from the same storage as the aggregates. CQRS with separate stores is out of scope.
Import from @alveolus/core or @alveolus/core/views.
See also
- Repositories, the query repository that returns a view
- Query handlers, which return views
- Published Language, when a view leaves the context