feat: support parameterized computed fields#2744
Conversation
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds support for parameterized ChangesParameterized Computed Fields
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…unt) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)
85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign this JSDoc with the current API surface.
The new CRUD types intentionally exclude parameterized computed fields from
whereandselect, so this comment is advertising entry points that the type system now rejects. Tightening it toorderByonly would keep the exported contract accurate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the computed field params property in schema.ts so it matches the current API surface: the comment should no longer mention where or select as supported query-time entry points. Keep the documentation aligned with the exported types by describing parameterized computed fields as usable in orderBy only, and ensure the wording around ProcedureParam and params reflects that restriction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410
⛔ Files ignored due to path filters (2)
packages/language/src/generated/ast.tsis excluded by!**/generated/**packages/language/src/generated/grammar.tsis excluded by!**/generated/**
📒 Files selected for processing (8)
packages/language/src/validators/datamodel-validator.tspackages/language/src/zmodel.langiumpackages/orm/src/client/crud-types.tspackages/orm/src/client/crud/dialects/base-dialect.tspackages/orm/src/client/zod/factory.tspackages/schema/src/schema.tspackages/sdk/src/ts-schema-generator.tstests/e2e/orm/client-api/computed-fields.test.ts
- grammar: require at least one parameter when a field declares `(...)`, so `field(): T` no longer parses (empty param lists are meaningless) - runtime: cursor pagination now rejects a parameterized computed field in `orderBy` (its sort key is not a real column), matching the existing relevance-ordering guard - codegen: a param typed with a model/enum/type-def reference maps to `unknown` (those names aren't in scope in the generated schema) — same convention as computed-field return types; zod still validates the value precisely - schema: tighten the FieldDef.params doc to mention `orderBy` only - test: assert cursor + parameterized computed sort is rejected Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — all four addressed in ef09757:
|
|
Besides the tests, I tried this in a real app on a large database, to be sure the parameterized I built this branch as local tarballs, linked it in with pnpm tagNameInCategory(categoryId: Int): String? @computed
// combines a row's tag names in the chosen categorythen sorted a list by it, with the orderBy: { tagNameInCategory: { args: { categoryId }, sort: 'asc' | 'desc', nulls: 'last' } }The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:
So sending the whole |
What
Feature request: #2743
Lets a
@computedfield declare typed parameters, with the arguments supplied at query timewherever the field is used. This first cut wires it end-to-end for
orderBy.The motivating case from #2743 — sort products by their tag name in a chosen category:
Why
Closes the gap raised in #2743. A
@computedfield is evaluated in SQL and is usable inorderBy/where/select, but it takes no arguments — so you can't express a DB-side sortthat depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today (
$qb/raw SQL, or anonKyselyQueryplugin) give up access policies,select-narrowed result types, and/or single-query execution.Because the arguments are plain data (not a function like
where.$expr), they serialize overthe wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.
How
zmodel.langium): aDataFieldmay declare a(params): Typesignature(reusing the existing
FunctionParamshape). A validator rejects parameters on non-@computedfields. Langium AST/grammar regenerated.
ts-schema-generator.ts): the declared params flow into the generatedcomputed-field stub signature, so the implementation type (
ComputedFieldsOptions) and the queryinput types both derive the args type from a single source and can't drift. Params are also
emitted as
FieldDef.paramsmetadata (shape mirrorsProcedureParam) for the runtime + zod.base-dialect.ts): query-time args are forwarded to the implementation as a thirdargument through the single
fieldRefchokepoint; extracted from theorderByvalue inapplyScalarOrderBy.crud-types.ts,zod/factory.ts):orderByaccepts{ args, sort, nulls? }for a parameterized computed field. Since these fields require args,they're excluded from default selection (also at runtime, so a plain
findMany()is safe),explicit
select, andwhere.Scope / follow-ups
Intentionally scoped to
orderBy(the motivating use case).whereandselectwithargsarenatural extensions on the same mechanism (the
fieldRefchokepoint already forwards args; theinput/result types would lift the same exclusions) and can follow in a separate PR.
Testing
tests/e2e/orm/client-api/computed-fields.test.ts— one with anIntparam, one with a
DateTimeparam (recentPostCount) — each verifying that different argsproduce different orderings (proving the arg reaches the SQL), that ascending/descending
behave, and that the field is not auto-returned.
@zenstackhq/language(84), ormclient-apie2e (618 passed; the onlyfailures are the
mysql-timezonetests, which need a live MySQL server unavailable in my sandboxand are unrelated to this change). No type errors reported by the type-tests.
overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real —tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with theorderBysent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:asc/descordered by the tag name, andnulls: 'last'put the untagged rows at the end;countwith the samewheredidn't change);selectnarrowing still applied (no raw SQL).Checklist
devpnpm buildgreen for all touched packages (language,schema,sdk,orm,testtools)Summary by CodeRabbit
orderBy, with argument validation.wherefilters unless args are provided.orderBybehavior using different parameter values (including direction and cursor restrictions).