Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/20252-cross-field-salesforce-examples-cel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@objectstack/spec": patch
---

Every example predicate in `packages/spec/src/data/validation.zod.ts` now reads fields through the `record.` root and describes the violation, so an author or agent who copies one gets a rule that evaluates and fires on the records it names as invalid. The fix covers the three `cross_field` "Salesforce Examples" in the `CrossFieldValidationSchema` TSDoc, the header `script` example, and the `.describe()` example on `CrossFieldValidationSchema.condition` (#20252).

Clause-②: no

- Example 1, "Close Date Must Be In Current or Future Month": `MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())` becomes `date(record.close_date) < addDays(today(), 1 - today().getDate())`. The old string did not parse (`AND` is not CEL, and `close_date` had no `record.` root). It was also inverted: a `cross_field` condition that evaluates TRUE is the violation, and the old condition was TRUE on the records the rule should accept. The new condition is TRUE when the close date falls before the first day of the current month. It uses only the formula stdlib's `date()`, `today()` and `addDays()` plus CEL's built-in `getDate()` timestamp accessor.
- Example 2, "Discount Validation": `discount > (amount * 0.40)` becomes `record.discount > (record.amount * 0.40)`. The bare fields were unknown variables. The direction is unchanged.
- Example 3, "Opportunity Must Have Products": `products = null AND stage = "closed_won"` becomes `isBlank(record.products) && record.stage == "closed_won"`. A lone `=` is a CEL parse error and `AND` is not CEL. The direction is unchanged.
- The header `script` example: `discount_percent > 0.40` becomes `record.discount_percent > 0.40`. The bare field was an unknown variable. The direction is unchanged.
- The `CrossFieldValidationSchema.condition` description: its example `record.end_date > record.start_date` refused every valid end-after-start range, because a TRUE condition is the violation. It becomes `record.end_date < record.start_date`, and the description now says that a TRUE condition fails validation.

The Salesforce-formula side of each example is unchanged. This changes documentation text only (TSDoc and one `.describe()` string, with the generated reference page regenerated to match). There is no schema, behaviour or export change.
10 changes: 5 additions & 5 deletions content/docs/references/data/validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Equivalent ObjectStack rule:
{
type: 'script',
name: 'discount_cannot_exceed_40_percent',
condition: 'discount_percent > 0.40',
condition: 'record.discount_percent > 0.40',
message: 'Discount cannot exceed 40%',
severity: 'error'
}
Expand Down Expand Up @@ -239,7 +239,7 @@ const result = ConditionalValidationSchema.parse(data);
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
| **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. |

### Nested Shape: `ConditionalValidation.then[type='json_schema']`
Expand Down Expand Up @@ -360,7 +360,7 @@ const result = ConditionalValidationSchema.parse(data);
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
| **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. |

### Nested Shape: `ConditionalValidation.otherwise[type='json_schema']`
Expand Down Expand Up @@ -413,7 +413,7 @@ const result = ConditionalValidationSchema.parse(data);
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
| **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. |


Expand Down Expand Up @@ -658,7 +658,7 @@ This schema accepts one of the following structures:
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
| **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. |

---
Expand Down
10 changes: 5 additions & 5 deletions packages/spec/src/data/validation.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
* {
* type: 'script',
* name: 'discount_cannot_exceed_40_percent',
* condition: 'discount_percent > 0.40',
* condition: 'record.discount_percent > 0.40',
* message: 'Discount cannot exceed 40%',
* severity: 'error'
* }
Expand Down Expand Up @@ -237,7 +237,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
* {
* type: 'cross_field',
* name: 'close_date_future',
* condition: 'MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())',
* condition: 'date(record.close_date) < addDays(today(), 1 - today().getDate())',
* fields: ['close_date'],
* message: 'Close Date must be in the current or a future month'
* }
Expand All @@ -254,7 +254,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
* {
* type: 'cross_field',
* name: 'discount_limit',
* condition: 'discount > (amount * 0.40)',
* condition: 'record.discount > (record.amount * 0.40)',
* fields: ['discount', 'amount'],
* message: 'Discount cannot exceed 40% of the amount'
* }
Expand All @@ -271,7 +271,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
* {
* type: 'cross_field',
* name: 'products_required_for_won',
* condition: 'products = null AND stage = "closed_won"',
* condition: 'isBlank(record.products) && record.stage == "closed_won"',
* fields: ['products', 'stage'],
* message: 'Opportunity must have products to be marked as Closed Won'
* }
Expand All @@ -291,7 +291,7 @@ export const CrossFieldValidationSchema = lazySchema(() => strictObject({
}, {
...BASE_VALIDATION_SHAPE,
type: z.literal('cross_field'),
condition: EvaluatedExpressionInputSchema.describe('Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date`'),
condition: EvaluatedExpressionInputSchema.describe('Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date.'),
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.'),
}));

Expand Down
Loading