Skip to content

Domain.Query reference

Source: packages/domain/src/Query.ts

A question a client asks across keys, answered from a store you own: the table, index or warehouse a Projection writes. Where a view is one key’s read model, a query is a message, like a command: it declares its input, its result, the middleware that guards it, and the rejections it may return. Its Layer is one handler: it checks who may ask, then reads the store, scoped to what the caller may see. List it in the model’s queries, and in an Api to give clients a typed method.

import * as Domain from "@alchemy.run/domain";
import * as Schema from "effect/Schema";
import { AccountId } from "./Account.ts";
import { Ledger } from "./Ledger.ts";
import { NotOwner } from "./NotOwner.ts";
import { Session } from "./Session.ts";
export const LedgerEntry = Schema.Struct({ id: Schema.String, kind: Schema.String, amount: Schema.Int });
export class AccountLedger extends Domain.Query("AccountLedger", {
input: { account: AccountId },
success: LedgerEntry,
paged: true,
waitsFor: [Ledger],
middleware: [Session],
rejects: [NotOwner],
}) {}

The construction Effect runs once and resolves the store and any model member the handler reads. The handler gets the input and, for a paged query, the page asked for. It refuses a caller by failing with one of the query’s rejections, and returns the entries and the cursor of the next page (null on the last, never a short page).

export const AccountLedgerLive = AccountLedger.toLayer(
Effect.gen(function* () {
const store = yield* LedgerStore;
const summaries = yield* AccountSummary;
return Effect.fn(function* ({ account }, page) {
const actor = yield* CurrentActor;
const summary = yield* summaries.get(Account.ref(account)).pipe(Effect.orDie);
const owner = Option.getOrNull(summary)?.customerId;
if (owner && owner !== actor.customerId) return yield* new NotOwner();
return yield* store.forAccount(account, page);
});
}),
);

Most calls are plain reads and get what the store holds now.

A page at a time

const page = yield* bank.AccountLedger({ account: accountId }, { take: 20 });
if (page.cursor) yield* bank.AccountLedger({ account: accountId }, { after: page.cursor });

Right after its own command, a client passes the command’s receipt as atLeast. The query finds the keys to wait on in that command’s events, waits until every read in waitsFor has handled them, then runs the handler. The caller passes receipts, never projections or keys: one, or one per command when it wrote to several aggregate instances.

Reading your own write

const receipt = yield* bank.DepositMoney(accountId, { amount: 100 });
const page = yield* bank.AccountLedger({ account: accountId }, { atLeast: receipt });

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 MCP tools, CLI help and OpenAPI document show its description.

export class AccountLedger extends Domain.Query(
"AccountLedger",
{ input: { account: AccountId }, success: LedgerRow, paged: true },
{ description: "One account's ledger, a page at a time." },
) {}