// `orderStats` — a materialized aggregate (`*.aggregate.ts`). // // The framework owns the lifecycle: it runs `build` on the `refresh` // interval, stores the rows in an auto-managed backing table, and serves // them via `ctx.aggregates.orderStats.read(...)`. Use an aggregate when // the SOURCE query is expensive but the RESULT is small + bounded — a // per-status order roll-up here. // // `output` MUST match each returned row's shape. `build` runs as the // system (no per-request subject), so it reads across every tenant; the // `tenantId` column is part of the grouping so reads stay per-tenant. import { defineAggregate } from '@voltro/runtime' import { column, count } from '@voltro/database' import { Schema } from 'effect' import { database } from '../database/schema' export const OrderStat = Schema.Struct({ tenantId: Schema.String, status: Schema.String, orders: Schema.Number, }) export type OrderStat = Schema.Schema.Type export default defineAggregate({ name: 'orderStats', refresh: '1m', // re-run every minute (interval shorthand) output: OrderStat, build: async (ctx) => { const rows = await ctx.store.query( database.orders .groupBy(['tenantId', 'status']) // Grouped columns must be EXPLICITLY projected via `column(...)` // in the aggregate spec — they are NOT auto-included (SQL rule: // a selected non-aggregate column must be in GROUP BY). .aggregate({ tenantId: column('tenantId'), status: column('status'), orders: count(), }) .descriptor, ) return rows.map((r) => ({ tenantId: (r as { tenantId: string }).tenantId, status: (r as { status: string }).status, orders: (r as { orders: number }).orders, })) }, })