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
4 changes: 4 additions & 0 deletions .changeset/showcase-validation-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

docs(showcase): demonstrate the `format` named-format branch and `conditional` `otherwise` on the Account example. Example-only (private package) — no package release.
41 changes: 33 additions & 8 deletions examples/app-showcase/src/objects/account.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export const Account = ObjectSchema.create({
support_config: Field.json({ label: 'Support Config' }),
// Captured only when an account churns; required by the `conditional` rule.
churn_reason: Field.text({ label: 'Churn Reason', maxLength: 500 }),
// A plain text field (deliberately NOT Field.email) so the `format` rule's
// named `email` format is what enforces validity — demonstrating the named
// format branch rather than the field-type's built-in check.
billing_email: Field.text({ label: 'Billing Email', maxLength: 200 }),
},

// A third `state_machine` example with a different topology than
Expand Down Expand Up @@ -84,6 +88,18 @@ export const Account = ObjectSchema.create({
regex: '^\\d{2}-\\d{7}$',
message: 'Tax ID must look like 12-3456789.',
},
{
// `format` — the *named* format branch (`email` / `url` / `phone` / `json`),
// complementing the regex example above. Validates `billing_email` only
// when the write supplies a non-empty value.
type: 'format' as const,
name: 'billing_email_format',
label: 'Billing Email Format',
description: 'Billing email must be a valid email address.',
field: 'billing_email',
format: 'email' as const,
message: 'Billing Email must be a valid email address.',
},
{
// `json_schema` — validate the JSON `support_config` blob against a
// JSON Schema (compiled by ajv). Accepts a parsed object or a JSON
Expand All @@ -105,16 +121,17 @@ export const Account = ObjectSchema.create({
},
},
{
// `conditional` — only enforce the inner rule when `when` holds. Here:
// an account may only be marked churned if it records why. The nested
// rule supplies the message; this conditional's severity (default
// `error`) decides that it blocks.
// `conditional` with BOTH branches — `when` picks `then` (true) or
// `otherwise` (false). A churned account must record *why*; a
// non-churned account must NOT carry a stale churn reason. The
// `otherwise` branch only flags an explicitly-set reason (it `has()`-
// guards the absent case), so ordinary non-churned writes are untouched.
type: 'conditional' as const,
name: 'churn_requires_reason',
label: 'Churn Requires a Reason',
description: 'A churned account must record a churn reason.',
name: 'churn_reason_consistency',
label: 'Churn Reason Consistency',
description: 'A churned account needs a reason; a non-churned account must not have one.',
when: P`record.status == 'churned'`,
message: 'Churn reason validation.',
message: 'Churn reason consistency.',
then: {
type: 'script' as const,
name: 'churn_reason_present',
Expand All @@ -123,6 +140,14 @@ export const Account = ObjectSchema.create({
// churn_reason); the equality checks catch an explicit null / blank.
condition: P`!has(record.churn_reason) || record.churn_reason == null || record.churn_reason == ''`,
},
otherwise: {
type: 'script' as const,
name: 'churn_reason_absent',
message: 'A churn reason should only be set when the account is churned.',
// Only fires if a non-empty reason is explicitly present on a
// non-churned account — absent/blank is fine.
condition: P`has(record.churn_reason) && record.churn_reason != null && record.churn_reason != ''`,
},
},
],
});
25 changes: 25 additions & 0 deletions examples/app-showcase/test/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ describe('showcase Account validation rules (write-path enforcement)', () => {
});
});

describe('format — billing_email named format', () => {
it('rejects a malformed email', () => {
expect(() => evaluateValidationRules(schema, { billing_email: 'nope' }, 'insert')).toThrow(
ValidationError,
);
});

it('accepts a valid email', () => {
expect(() =>
evaluateValidationRules(schema, { billing_email: 'ar@acme.com' }, 'insert'),
).not.toThrow();
});
});

describe('json_schema — support_config shape', () => {
it('accepts a conforming config', () => {
expect(() =>
Expand Down Expand Up @@ -84,5 +98,16 @@ describe('showcase Account validation rules (write-path enforcement)', () => {
}),
).not.toThrow();
});

it('otherwise-branch: rejects a churn reason set on a non-churned account', () => {
expect(() =>
evaluateValidationRules(
schema,
{ status: 'active', churn_reason: 'left over' },
'update',
{ previous: { status: 'prospect' } },
),
).toThrow(ValidationError);
});
});
});