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

4.9 KiB

FieldValidator — @suaveplan/rules/validators/field

(no submodules)

Purpose

FieldValidator is a chainable, immutable-per-call validator for a single value. Every withX method returns a fresh validator with the new stage appended, so preset chains can be shared across requests without aliasing concerns. validateSync(value) runs purely-sync chains; validate(value, options?) returns a Promise and threads AbortSignal through every stage. The module ships 12 preset chains covering the common shapes (email, password, username, phone, URL, UUID, date, integer, age, currency, percentage, positive number).

The chain is the natural fit when validating one value at a time (form field, query parameter, header). For object-tree validation, prefer SchemaValidator; for plug-in-style reusable validators with a registry, prefer CustomValidator.

Features

  • Immutable chain — every .withX returns a new FieldValidator.
  • Sync + async stages; validateSync throws RULES.VALIDATOR_INVALID when invoked on an async chain.
  • 9 builder methods: required, minLength, maxLength, min, max, pattern, oneOf, custom, customAsync.
  • 12 preset factories.
  • OTel histogram (rules.validator.validate.duration_ms{kind:"field"}) + counter (rules.validator.validations_total{kind:"field",outcome}).

Usage: Basic Example

import { field } from "@suaveplan/rules/validators/field";

const validator = field().required().minLength(3).maxLength(20);
const result = await validator.validate("ada");
result.valid; // true

Usage: Advanced Example

import {
    field,
    passwordFieldValidator,
    usernameFieldValidator,
} from "@suaveplan/rules/validators/field";

// Compose a preset with caller-specific extensions
const adminPasswordValidator = passwordFieldValidator()
    .minLength(12, "admin passwords must be at least 12 chars")
    .customAsync(async (value) => {
        const breached = await checkHibp(value as string);
        return breached ? "password appears in known-breach corpus" : true;
    });

const username = usernameFieldValidator();
expect((await username.validate("ada42")).valid).toBe(true);

declare function checkHibp(password: string): Promise<boolean>;
declare const expect: { (received: unknown): { toBe(v: unknown): void } };

API Reference

class FieldValidator

  • FieldValidator.create() / createFieldValidator() / field() — entry points.
  • required(message?), minLength(min, message?), maxLength(max, message?), min(min, message?), max(max, message?), pattern(re, message?), oneOf(values, message?), custom(fn, message?), customAsync(fn, message?) — each returns a new FieldValidator.
  • validate(value, options?) → Promise<FieldValidatorResult> where FieldValidatorResult = { valid: boolean; errors: readonly string[] }. Honours options.signal.
  • validateSync(value) → FieldValidatorResult. Throws RulesError(RULES.VALIDATOR_INVALID) when the chain has any async stage.
  • isAsync() → true when the chain contains any async stage.

Preset chains

Factory Behaviour
emailFieldValidator() required + email regex
urlFieldValidator() required + ^https?://
uuidFieldValidator() required + RFC-4122 8-4-4-4-12
phoneFieldValidator() required + ^\+?[\d\s\-().]{7,}$
usernameFieldValidator() required + ^[a-z0-9_]{3,32}$ (case-insensitive)
passwordFieldValidator() required + minLength 8 + upper + lower + digit
dateFieldValidator() instanceof Date && !NaN
integerFieldValidator() Number.isInteger
ageValidator() required + range 0..150
currencyValidator() required + non-negative number
percentageValidator() required + range 0..100
positiveNumberValidator() required + > 0

Implementation Notes

  • Every stage runs against the original input value (not the previous stage's output) — FieldValidator is a multi-predicate aggregator, not a transform pipeline. For transforms, use SchemaValidator with the transforms field.
  • validate always records every stage that fails — it does not short-circuit. This makes the error list complete in a single pass, which matches the convention used by Zod and Joi.
  • A pre-aborted signal short-circuits the loop and records the synthetic "validation aborted" error. Subsequent stages do not run.
  • The async-stage detector (isAsync) is O(stages) and runs once on validateSync. Callers that want fast-path sync validation should check isAsync() first.
  • custom accepts a fn returning true | false | string. false falls back to either the supplied message or the default "custom validation failed"; a string return is treated as the error message verbatim.
  • Preset chains are factory functions (not module-level constants) so consumers get an independent instance per call and can chain further without affecting other callers.