You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: field conditional rules (CEL) — spec + server enforcement (B2) (#1649)
* feat(spec): field conditional rules — visibleWhen / readonlyWhen / requiredWhen (CEL)
Add three CEL-predicate field props (over `record`) for field-level conditional
behavior, designed for dual-side evaluation: the client form toggles
visibility/read-only/required live, the server enforces them. `requiredWhen` is
the canonical name; `conditionalRequired` stays as a back-compat alias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(objectql): enforce field requiredWhen + ignore readonlyWhen writes (B2)
Server-side enforcement for the conditional field rules:
- evaluateValidationRules now also enforces field `requiredWhen`/
`conditionalRequired` (CEL over the merged record) → a 'required' error when
the predicate is TRUE and the value is missing. Can't be bypassed by a direct
API write.
- stripReadonlyWhenFields drops update-payload fields whose `readonlyWhen` is
TRUE for the record's state (keeps the persisted value); wired into
engine.update.
- needsPriorRecord accounts for conditional fields so the prior record is
fetched on update (predicates over unchanged fields evaluate correctly).
46 rule-validator tests pass (requiredWhen, alias, readonly-strip, prior-fetch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add three CEL-predicate field props (over `record`) evaluated on both sides. **Spec**: `visibleWhen` / `readonlyWhen` / `requiredWhen` (`requiredWhen` canonical; `conditionalRequired` kept as a back-compat alias). **Server (objectql)**: the validator now enforces `requiredWhen`/`conditionalRequired` over the merged record (so the rule can't be bypassed by a direct API write), and the update path ignores writes to a field whose `readonlyWhen` is TRUE (keeps the persisted value). `needsPriorRecord` accounts for conditional fields so the prior record is fetched on update.
group: z.string().optional().describe('Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system")'),
528
528
529
-
/** Conditional Requirements */
530
-
conditionalRequired: ExpressionInputSchema.optional().describe('Predicate (CEL) — field is required when TRUE. e.g. P`record.status == \'closed_won\'`'),
529
+
/**
530
+
* Conditional field rules (CEL predicates over `record`). Evaluated on BOTH
531
+
* sides: the client form toggles the field's visibility / read-only / required
532
+
* state live as the record changes (UX), and the server enforces
533
+
* `requiredWhen` and ignores writes to a field whose `readonlyWhen` is TRUE
534
+
* (so the rule can't be bypassed). e.g. `P\`record.status == 'paid'\``.
535
+
*/
536
+
visibleWhen: ExpressionInputSchema.optional().describe("Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'`"),
537
+
readonlyWhen: ExpressionInputSchema.optional().describe("Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'`"),
538
+
requiredWhen: ExpressionInputSchema.optional().describe("Predicate (CEL) — field is required when TRUE. Canonical name for `conditionalRequired`."),
539
+
540
+
/** Conditional Requirements
541
+
* @deprecated Alias of `requiredWhen` — kept for back-compat. */
542
+
conditionalRequired: ExpressionInputSchema.optional().describe('Predicate (CEL) — field is required when TRUE. Alias of `requiredWhen`.'),
531
543
532
544
/** Security & Visibility */
533
545
hidden: z.boolean().default(false).describe('Hidden from default UI'),
0 commit comments