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.
integerrejects non-integer numbers). - 7 format checks via shared regex map.
- 5 transforms applied before validation; non-applicable transforms (e.g.
lowercaseon a number) are no-ops. - Nested objects + arrays validated recursively with structured paths (
user.profile.name,items[3].sku). enummembership checks.min/max/minLength/maxLength/patternconstraints.- Custom validators registered by field path; return
true,false, or a string error message. - Zod-validated
Schemaat construction →RULES.SCHEMA_INVALIDon rejection. - Pre-aborted
AbortSignalshort-circuits the recursion withRULES.SCHEMA_INVALID. - Returns
{ valid, errors, value }wherevaluecarries 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; raisesRULES.SCHEMA_INVALIDon rejection.options.customValidators: Record<string, CustomValidatorFn>— keyed by field path. Each validator receives(value, ValidatorContext)and returnstrue,false, or astringerror.
validate(data, options?)→Promise<ValidationResult>.options.signalshort-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
validateFieldis recursive. Pre-aborted signals are checked at each top-level field boundary; nested recursion inherits the signal via theValidatorContext.- The transformed value is accumulated into a parallel
transformedtree mirroring the schema shape. Callers consumeresult.valueto get the post-transform view of the input. format: "regex"is a placeholder that always matches. The field-levelpatternoption 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:
numberrejectsNaN;date/datetimeaccept onlyDateinstances with validgetTime(). TheparseDatetransform converts ISO strings intoDatebefore the type check fires.