Query handlers
A query handler is the application service of one read. It returns a view read through a query repository, without loading the aggregate. It saves nothing and publishes nothing.
export class GetOrderSummaryHandler extends QueryHandler<GetOrderSummary, OrderSummary, OrderNotFound> {
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);
}
}When to use
Write one query handler per read a client needs: an order summary, a list of orders. A view has the shape the reader wants, whatever the shape of the aggregate. Requests that change state go through a command handler.
Usage
Declare the query and its handler
The query is a plain type named after the request, in the same file as its handler.
import { err, ok, QueryHandler, type Result } from "@alveolus/core";
import { OrderNotFound } from "../../domain/errors/order-not-found.error";
import { OrderSummaries } from "../../domain/repositories/order-summaries.repository";
import { OrderId } from "../../domain/value-objects/order-id.identifier";
import type { OrderSummary } from "../../domain/views/order-summary.view";
export interface GetOrderSummary {
readonly orderId: string;
}
export class GetOrderSummaryHandler extends QueryHandler<GetOrderSummary, OrderSummary, OrderNotFound> {
constructor(private readonly summaries: OrderSummaries) {
super();
}
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);
}
}Return a list
A query that cannot fail keeps the default Error of never.
export class ListOrdersHandler extends QueryHandler<void, readonly OrderSummary[]> {
constructor(private readonly summaries: OrderSummaries) {
super();
}
async handle(): Promise<Result<readonly OrderSummary[], never>> {
return ok(await this.summaries.all());
}
}Keep to the query side
A query handler reads views through query repositories. It never receives a command repository, an outbox, a unit of work or an event publisher: a read must not change anything.
constructor(
private readonly orders: Orders,
private readonly unitOfWork: UnitOfWork,
) {
super();
}constructor(private readonly summaries: OrderSummaries) {
super();
}Checked by command-query-separation.
Reference
abstract class QueryHandler<Input, Output, Error extends AnyDomainError = never> {
abstract handle(query: Input): Promise<Result<Output, Error>>;
}| Type parameter | Description |
|---|---|
Input | The query: the data the handler needs. |
Output | What the read returns, usually a view or a list of views. |
Error | Union of the domain errors the handler may return. Defaults to never. |
| Member | Type | Description |
|---|---|---|
handle(query) | Promise<Result<Output, Error>> | Runs the read and returns its outcome. |
Caveats
- There is no separate read model: views are read from the same storage, through a query repository.
- Call
super()in the constructor of your handler.
Import from @alveolus/core or @alveolus/core/query-handlers.
See also
- Views, what a query returns
- Repositories, for
QueryRepository - Command handlers, for requests that change state
command-query-separation