From a8de060147bb533ee48ef77a4bfab7ddda36c37e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:17:12 +0800 Subject: [PATCH] docs(showcase): demonstrate format named-format + conditional otherwise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enriches the app-showcase Account validation examples so each enforced rule type shows more than one shape: - `format`: adds a *named* `email` format on a new plain-text `billing_email` field (deliberately not Field.email, so the rule itself enforces validity) — complements the existing regex example. - `conditional`: the churn rule gains an `otherwise` branch — a churned account needs a reason; a non-churned one must not carry a stale one. The otherwise `has()`-guards the absent case so ordinary writes are untouched. Both verified in test/validation.test.ts against the real evaluator (20 pass). Co-Authored-By: Claude Opus 4.8 --- .changeset/showcase-validation-examples.md | 4 ++ .../src/objects/account.object.ts | 41 +++++++++++++++---- examples/app-showcase/test/validation.test.ts | 25 +++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 .changeset/showcase-validation-examples.md diff --git a/.changeset/showcase-validation-examples.md b/.changeset/showcase-validation-examples.md new file mode 100644 index 000000000..b576d65c7 --- /dev/null +++ b/.changeset/showcase-validation-examples.md @@ -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. diff --git a/examples/app-showcase/src/objects/account.object.ts b/examples/app-showcase/src/objects/account.object.ts index 166927350..40c34d384 100644 --- a/examples/app-showcase/src/objects/account.object.ts +++ b/examples/app-showcase/src/objects/account.object.ts @@ -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 @@ -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 @@ -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', @@ -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 != ''`, + }, }, ], }); diff --git a/examples/app-showcase/test/validation.test.ts b/examples/app-showcase/test/validation.test.ts index 95a89641c..adeddc8ed 100644 --- a/examples/app-showcase/test/validation.test.ts +++ b/examples/app-showcase/test/validation.test.ts @@ -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(() => @@ -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); + }); }); });