Skip to content

Queries

A query is a named, typed read over a projection: "give me this student's submissions, newest first." It takes validated input, reads the projection's state, and returns a result. It never touches the write side — reads come from the read model, which is what keeps them fast and free of surprises.

Shape

ts
import { defineQuery } from "@nwire/forge";

export const submissionsByStudent = defineQuery(SubmissionsByStudent, {
  name: "submissions.by-student",
  description: "Avi's submission history, newest first.",
  input: z.object({
    studentId: z.string(),
    status: z.enum(["submitted", "under-review", "graded"]).optional(),
  }),
  execute: (state, { studentId, status }) => {
    const all = state.byStudent[studentId] ?? [];
    return status ? all.filter((s) => s.status === status) : all;
  },
  slo: { p95LatencyMs: 50 },
  cacheable: true,
});

Usage

ts
// Register the query as a handler when building the App:
createApp({ appName: "submissions", registry: defineRegistry({ handlers: [submissionsByStudent] }) });

// HTTP wire — mount the query under a route binding. The query IS the
// handler, so the wire references it directly.
app.wire(
  get("/submissions", { query: submissionsByStudentInput }),
  submissionsByStudent,
);

// From an action handler — projection state read via ctx.query
const reviewSubmission = defineAction({
  name: "submissions.review",
  input: ReviewSubmissionInput,
  handler: async (ctx) => {
    const { input } = ctx;
    const history = await ctx.query(submissionsByStudent, { studentId: input.studentId });
    // …
  },
});

Why queries instead of "just call the projection"

Three reasons:

  1. Validation — query inputs go through Zod, same as any handler input.
  2. Wire ergonomics — wire a query under a route binding and the runtime parses the query-string input through its schema.
  3. Observability — queries show up in the trace view, and observed latency is scored against the slo.p95LatencyMs you declare.

Multi-tenancy

The runtime passes envelope.tenant to the projection store before invoking execute. Tenant data is scoped automatically — you never thread it manually.

See also

MIT licensed.