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
docs(spec): document validation governance fields + cross_field/script overlap; tidy stale form mirror (#3198)
Follow-up to the 2026-06 validation liveness audit (#3106 / #3184). No runtime
behavior change — documentation, a form-schema cleanup, and a docs pointer:
- Document that label/description/tags are governance/editor metadata (surfaced
to the Studio rule editor, not evaluated on the write path). Kept, not removed:
they're set by nearly every example rule and feed the /meta/types editor form,
so they're declared on purpose rather than silent no-ops.
- Document that cross_field evaluates identically to script (same CEL predicate);
only fields[0] is read, to target the violation at a field. Noted on the schema,
its fields .describe(), and the validation docs. Variant kept for the
field-targeting affordance + backward compatibility (removing it would break
discriminated-union parsing of existing cross_field metadata).
- Remove dead form-field entries (scope/caseSensitive/url/handler) and the stale
type=unique hint from the hand-written HAND_CRAFTED_SCHEMAS['validation'] fallback
in metadata-protocol — leftovers from the removed unique/async/custom variants.
- Add the missing beforeDelete pointer to the docs' "not a rule type" callout so
delete-time guards aren't stranded (validation has no delete event since #3184).
Regenerated content/docs/references/data/validation.mdx.
Claude-Session: https://claude.ai/code/session_01VCUSMJBsX14C3RQdWFw7N7
Co-authored-by: Claude <noreply@anthropic.com>
Document two validation-rule facts surfaced by the 2026-06 liveness audit (follow-up to #3106 / #3184), and clean up a stale form-schema mirror — no runtime behavior change:
6
+
7
+
-`label` / `description` / `tags` on validation rules are governance / editor metadata (surfaced to the Studio rule editor and rule listings), not evaluated on the write path. Documented as such on `BaseValidationSchema` rather than removed — they are set by nearly every example rule and feed the `/meta/types` editor form, so they are declared on purpose, not silent no-ops.
8
+
-`cross_field` evaluates identically to `script` (same CEL predicate path); only `fields[0]` is read, to target the violation at a field. Documented the overlap on the schema, its `fields``.describe()`, and the validation docs so authors can choose between them; the variant is kept for the field-targeting affordance and backward compatibility.
9
+
- Removed dead form-field entries (`scope`, `caseSensitive`, `url`, `handler`) and the stale `type=unique` hint from the hand-written `HAND_CRAFTED_SCHEMAS['validation']` fallback in `@objectstack/metadata-protocol` — leftovers from the removed `unique`/`async`/`custom` variants.
10
+
- Added the missing `beforeDelete` lifecycle-hook pointer to the validation docs' "not a rule type" callout, so delete-time guards aren't stranded now that validation has no `delete` event (#3184).
Copy file name to clipboardExpand all lines: content/docs/data-modeling/validation.mdx
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ Three patterns that look like validation rules are deliberately **not** rule typ
13
13
-**Uniqueness** → a unique index (`ObjectSchema.indexes`, `{ fields, unique: true }`, with `partial` for a scoped constraint) or field-level `unique: true`. A SELECT-then-INSERT rule is inherently racy (TOCTOU); a DB unique constraint is not.
14
14
-**Async / remote validation** → a client-form concern, and an SSRF/latency hazard on the server write path. Keep it in the form layer, or enforce the invariant with a `unique` index / lifecycle hook.
15
15
-**Custom handler** → a `beforeInsert` / `beforeUpdate` lifecycle hook, the supported extension point for arbitrary validation code.
16
+
-**Delete-time guards** → a `beforeDelete` lifecycle hook. Validation rules run only on insert/update (a delete carries no record payload to validate), so there is no `'delete'` validation event — block or gate deletions from a `beforeDelete` hook.
16
17
</Callout>
17
18
18
19
## Basic Structure
@@ -197,6 +198,10 @@ Validate relationships between multiple fields:
197
198
}
198
199
```
199
200
201
+
<Callouttype="info">
202
+
`cross_field` evaluates identically to `script` — both run the same CEL predicate. The only difference is `fields[0]`, which labels the field the error attaches to (a plain `script` error attaches to the record). The remaining `fields` are advisory/documentation. Reach for `cross_field` when you want the error targeted at a specific field; otherwise `script` is equivalent.
|**fields**|`string[]`| ✅ | Fields involved in the validation|
174
+
|**fields**|`string[]`| ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path.|
175
175
176
176
177
177
---
@@ -360,7 +360,7 @@ This schema accepts one of the following structures:
360
360
|**message**|`string`| ✅ | Error message to display to the user |
|**fields**|`string[]`| ✅ | Fields involved in the validation|
363
+
|**fields**|`string[]`| ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path.|
condition: ExpressionInputSchema.describe('Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date`'),
203
-
fields: z.array(z.string()).describe('Fields involved in the validation'),
216
+
fields: z.array(z.string()).describe('Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path.'),
0 commit comments