Files
skills/docs/foundation/flow/rules/src/validators/schema/schema.md
2026-05-29 08:20:56 +01:00

6.2 KiB

SchemaValidator — @suaveplan/rules/validators/schema

(no submodules)

Purpose

SchemaValidator validates a Record<string, unknown> against a JSON-schema-like Schema definition. It covers the 8 primitive FieldTypes (string, number, integer, boolean, object, array, date, datetime, any), 7 string formats (email, url, uuid, date, datetime, ipv4, ipv6, regex), 5 transforms (trim, lowercase, uppercase, parseNumber, parseDate), enum checks, min/max/minLength/maxLength/pattern constraints, recursive nested objects, recursive array items, and caller-supplied custom validators keyed by field path.

Every input schema is Zod-validated at construction — malformed schemas raise RulesError(RULES.SCHEMA_INVALID). The validate method returns a ValidationResult rather than throwing for any normal failure mode (missing required, wrong type, format miss, custom-refinement failure). OTel histogram and counter fire per invocation.

Features

  • 8 field types with strict matching (e.g. integer rejects non-integer numbers).
  • 7 format checks via shared regex map.
  • 5 transforms applied before validation; non-applicable transforms (e.g. lowercase on a number) are no-ops.
  • Nested objects + arrays validated recursively with structured paths (user.profile.name, items[3].sku).
  • enum membership checks.
  • min / max / minLength / maxLength / pattern constraints.
  • Custom validators registered by field path; return true, false, or a string error message.
  • Zod-validated Schema at construction → RULES.SCHEMA_INVALID on rejection.
  • Pre-aborted AbortSignal short-circuits the recursion with RULES.SCHEMA_INVALID.
  • Returns { valid, errors, value } where value carries the transformed data tree.

Usage: Basic Example

import { SchemaValidator } from "@suaveplan/rules/validators/schema";

const validator = new SchemaValidator({
    email: { type: "string", required: true, format: "email" },
    age: { type: "integer", min: 18 },
});

const result = await validator.validate({ email: "ada@example.com", age: 30 });
result.valid; // true

Usage: Advanced Example

import { SchemaValidator } from "@suaveplan/rules/validators/schema";

const validator = new SchemaValidator(
    {
        username: {
            type: "string",
            required: true,
            minLength: 3,
            maxLength: 20,
            transforms: ["trim", "lowercase"],
        },
        profile: {
            type: "object",
            properties: {
                bio: { type: "string", maxLength: 240 },
                tags: {
                    type: "array",
                    items: { type: "string", minLength: 2 },
                },
            },
        },
        registeredAt: { type: "date", transforms: ["parseDate"], required: true },
    },
    {
        customValidators: {
            username: async (value) =>
                typeof value === "string" && !value.startsWith("admin")
                    ? true
                    : "reserved username prefix",
        },
    },
);

const result = await validator.validate({
    username: "  ADA  ",
    profile: { bio: "Mathematician", tags: ["math", "logic"] },
    registeredAt: "1815-12-10",
});

if (!result.valid) {
    for (const err of result.errors) console.warn(err.path, err.code, err.message);
}
console.log(result.value); // { username: "ada", profile: {...}, registeredAt: Date }

API Reference

class SchemaValidator

  • new SchemaValidator(schema, options?) — Zod-validates the schema; raises RULES.SCHEMA_INVALID on rejection.
    • options.customValidators: Record<string, CustomValidatorFn> — keyed by field path. Each validator receives (value, ValidatorContext) and returns true, false, or a string error.
  • validate(data, options?) → Promise<ValidationResult>. options.signal short-circuits.

interface Schema

Record<string, FieldSchema> where FieldSchema carries type, required, format, min, max, minLength, maxLength, pattern, enum, items (recursive), properties (recursive), transforms, default.

Format dispatch

Format Regex
email ^[^\s@]+@[^\s@]+\.[^\s@]+$
url ^https?://.+ (case-insensitive)
uuid RFC-4122 8-4-4-4-12 hex
date ^\d{4}-\d{2}-\d{2}$
datetime ISO 8601 prefix YYYY-MM-DDTHH:MM:SS
ipv4 dotted-quad shape
ipv6 hex-and-colons (not strictly compliant; pre-filter only)
regex accepts anything (.*) — placeholder slot for future tightening

Transform dispatch

Transform Applies when Effect
trim string value.trim()
lowercase string value.toLowerCase()
uppercase string value.toUpperCase()
parseNumber string Number(value) if not NaN; else leave
parseDate string new Date(value) if valid; else leave

Implementation Notes

  • validateField is recursive. Pre-aborted signals are checked at each top-level field boundary; nested recursion inherits the signal via the ValidatorContext.
  • The transformed value is accumulated into a parallel transformed tree mirroring the schema shape. Callers consume result.value to get the post-transform view of the input.
  • format: "regex" is a placeholder that always matches. The field-level pattern option is the way to assert a specific regex; future hardening can replace the format slot with a per-pattern compile-cache.
  • The validator deliberately does NOT short-circuit on first error — it accumulates every issue across the tree so callers can present them all at once. Tests assert specific codes against the first error for clarity; production callers should iterate result.errors.
  • The CustomValidatorFn return type boolean | string | Promise<boolean | string> matches the convention used by zxcvbn and Joi: a string return is treated as a custom error message, otherwise a falsy return generates the default message.
  • Type matching is strict: number rejects NaN; date / datetime accept only Date instances with valid getTime(). The parseDate transform converts ISO strings into Date before the type check fires.