first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# SchemaValidator — `@suaveplan/rules/validators/schema`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SchemaValidator` validates a `Record<string, unknown>` against a JSON-schema-like `Schema` definition. It covers the 8 primitive `FieldType`s (`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
```ts
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
```ts
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.