From 1aa7b4dcbf7cd63a30009b34e504ba0affed6079 Mon Sep 17 00:00:00 2001 From: evgenovalov Date: Mon, 20 Jul 2026 12:13:04 +0200 Subject: [PATCH 1/2] docs: document parameterized computed fields Add a "Parameterized Computed Fields" section to the ORM computed-fields page: the ZModel parameter syntax, the 3-argument implementation signature, how to supply `args` across orderBy / where / select / include / aggregate / groupBy, and a note on grouping by a correlated-subquery computed field. Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/orm/computed-fields.md | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docs/orm/computed-fields.md b/docs/orm/computed-fields.md index e0fae58f..d1447c76 100644 --- a/docs/orm/computed-fields.md +++ b/docs/orm/computed-fields.md @@ -80,6 +80,90 @@ type ComputedFieldCallback = ( ) => OperandExpression<...>; ``` +## Parameterized Computed Fields + +A computed field can declare typed **parameters**, with the arguments supplied at query time wherever the field is used. This lets a single field express a database-side computation that depends on a runtime value — for example, "count a user's posts created since a given date", or the motivating case of "sort products by their tag name in a chosen category". + +Declare the parameters right after the field name in the ZModel schema (a parameterized field reads just like a regular one, with a parameter list added): + +```zmodel +model User { + id Int @id + posts Post[] + recentPostCount(since: DateTime) Int @computed +} +``` + +The implementation receives the arguments as a **third** parameter, after `eb` and `context`: + +```ts +import { sql } from '@zenstackhq/orm/helpers'; + +const db = new ZenStackClient(schema, { + ... + computedFields: { + User: { + // `args` is typed from the field's declared parameters: `{ since: Date }` + recentPostCount: (eb, { modelAlias }, args) => + eb.selectFrom('Post') + .whereRef('Post.authorId', '=', sql.ref(`${modelAlias}.id`)) + .where('Post.createdAt', '>=', args.since) + .select(({ fn }) => fn.countAll().as('count')), + }, + }, +}); +``` + +Because the arguments are **plain data** (not a callback), they serialize over the wire — so a frontend can drive the query through the auto-CRUD API while it stays a single, policy-checked, `select`-narrowed statement. + +### Supplying arguments + +The `args` object travels with the field wherever it is used. Note that a parameterized field is **not** returned by default (it needs arguments), so a plain `findMany()` won't include it — you request it explicitly via `select`/`include`. + +```ts +const since = new Date('2024-01-01'); + +// orderBy — `{ args, sort, nulls? }` +await db.user.findMany({ orderBy: { recentPostCount: { args: { since }, sort: 'desc' } } }); + +// where (and `having`) — `args` alongside the filter operators +await db.user.findMany({ where: { recentPostCount: { args: { since }, gte: 5 } } }); + +// select / include +await db.user.findMany({ include: { recentPostCount: { args: { since } } } }); +await db.user.findFirst({ select: { id: true, recentPostCount: { args: { since } } } }); + +// aggregate — `_count` / `_sum` / `_avg` / `_min` / `_max` +await db.user.aggregate({ _sum: { recentPostCount: { args: { since } } } }); + +// groupBy — a keyed `{ field, args }` entry in `by` +await db.user.groupBy({ + by: [{ field: 'recentPostCount', args: { since } }], + _count: { _all: true }, +}); +``` + +The full signature of a parameterized computed field implementation adds the `args` parameter: + +```ts +import { OperandExpression, ExpressionBuilder } from 'kysely'; + +type ParameterizedComputedFieldCallback = ( + eb: ExpressionBuilder<...>, + context: { + modelAlias: string + }, + args: { + // derived from the field's declared parameters, e.g. `since: DateTime` -> `since: Date` + since: Date + } +) => OperandExpression<...>; +``` + +:::info +Grouping **by** a computed field whose implementation is a *correlated subquery* is subject to your database's own rules for grouping by a correlated expression (PostgreSQL rejects it; SQLite allows it) — the same constraint that applies to any correlated `GROUP BY`. Computed fields defined by a row-local expression can be grouped on all databases. +::: + ## Samples From beca7acd8140d6a306f8aabf7e0e299d4692b6d3 Mon Sep 17 00:00:00 2001 From: evgenovalov Date: Mon, 20 Jul 2026 12:17:31 +0200 Subject: [PATCH 2/2] docs: add AvailableSince v3.9.0 marker to parameterized computed fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New-feature docs must carry an release marker (review feedback). Imported from `../_components/AvailableSince` to match this file's other component imports (the suggested `./_components` path is incorrect — the component lives at docs/_components/). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/orm/computed-fields.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/orm/computed-fields.md b/docs/orm/computed-fields.md index d1447c76..7b2e4736 100644 --- a/docs/orm/computed-fields.md +++ b/docs/orm/computed-fields.md @@ -5,6 +5,7 @@ description: Computed fields in ZModel import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; +import AvailableSince from '../_components/AvailableSince'; # Computed Fields @@ -82,6 +83,8 @@ type ComputedFieldCallback = ( ## Parameterized Computed Fields + + A computed field can declare typed **parameters**, with the arguments supplied at query time wherever the field is used. This lets a single field express a database-side computation that depends on a runtime value — for example, "count a user's posts created since a given date", or the motivating case of "sort products by their tag name in a chosen category". Declare the parameters right after the field name in the ZModel schema (a parameterized field reads just like a regular one, with a parameter list added):