318 lines
22 KiB
Plaintext
318 lines
22 KiB
Plaintext
page wrnexusvalidation {
|
|
seo {
|
|
title = "@wrnexus/validation"
|
|
description = "Typed schemas, coercion, validation, and browser descriptors."
|
|
}
|
|
|
|
view {
|
|
<div class="docs-shell">
|
|
<a class="skip-link" href="#main">Skip to content</a>
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.2.19</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
|
</header>
|
|
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
|
<main class="page package-page">
|
|
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Security</span><h2>@wrnexus/validation</h2><p>Typed schemas, coercion, validation, and browser descriptors.</p><span class="status status-beta">Private preview · 0.2.19</span><nav><a href="#access">Access</a><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
|
<article id="main" class="documentation"><section class="doc-intro"><span class="eyebrow">Security · Preview</span><h1>@wrnexus/validation</h1><p>Typed schemas, coercion, validation, and browser descriptors.</p><section id="access" class="access-callout"><h2>Private registry access required</h2><p>This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:</p><pre><code>bun add @wrnexus/validation@0.2.19</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide" class="prose"><blockquote>One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms.</blockquote>
|
|
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
|
<h3 id="overview">Overview</h3>
|
|
<p>Define a schema once with the fluent <code>v</code> builder, then reuse it in three places: <code>.parse()</code> runs server-side and returns coerced values plus per-field errors; <code>.describe()</code> emits a plain-JSON <code>SchemaDescriptor</code> that the browser runtime interprets (no <code>eval</code>, no bundled validator); and helpers like <code>parseBody</code> and <code>parseEnv</code> wire schemas straight into API routes and startup config. The server rule logic (<code>applyRule</code>/<code>checkField</code>) and the client runtime (<code>VALIDATE_RUNTIME</code>) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in <code>app/schemas/</code>.</p>
|
|
<pre data-language="bash"><code>bun add @wrnexus/validation</code></pre>
|
|
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
|
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
|
<h3 id="api">API</h3>
|
|
<h4 id="the-v-builder">The <code>v</code> builder</h4>
|
|
<pre data-language="ts"><code>import { v } from "@wrnexus/validation";</code></pre>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Factory</th><th>Returns</th><th>Field methods</th></tr></thead>
|
|
<tbody><tr><td><code>v.string()</code></td><td><code>StringSchema</code></td><td><code>email()</code>, <code>url()</code>, <code>uuid()</code>, <code>date()</code>, <code>length(n)</code>, <code>oneOf(string[])</code>, <code>pattern(re)</code>, <code>trim()</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.number()</code></td><td><code>NumberSchema</code></td><td><code>integer()</code>, <code>positive()</code>, <code>oneOf(number[])</code>, <code>min(n)</code>, <code>max(n)</code></td></tr><tr><td><code>v.boolean()</code></td><td><code>BooleanSchema</code></td><td>(base methods only)</td></tr><tr><td><code>v.object(fields)</code></td><td><code>ObjectSchema</code></td><td><code>parse(input)</code>, <code>describe()</code></td></tr></tbody></table></div>
|
|
<p>Every field schema is chainable and shares these base methods:</p>
|
|
<ul>
|
|
<li><code>min(n, message?)</code> / <code>max(n, message?)</code> — for strings, bounds the length; for numbers, bounds the value.</li>
|
|
<li><code>optional()</code> — an empty/missing value passes instead of erroring <code>"Required"</code>.</li>
|
|
<li><code>label(text)</code> — human label carried into the descriptor.</li>
|
|
<li><code>default(value)</code> — value substituted when the field is absent (implies <code>optional</code>).</li>
|
|
<li><code>refine(fn, message?)</code> — <strong>server-only</strong> predicate. <code>fn</code> returns <code>true</code> (ok), <code>false</code> (use <code>message</code>), or a <code>string</code> (that error). Not serialized to the client.</li>
|
|
</ul>
|
|
<p>Each string rule accepts an optional trailing <code>message</code> to override the default error text.</p>
|
|
<h4 id="objectschema"><code>ObjectSchema</code></h4>
|
|
<pre data-language="ts"><code>schema.parse(input: unknown): ParseResult
|
|
schema.describe(): SchemaDescriptor</code></pre>
|
|
<p><code>parse</code> coerces each field (strings stay strings, <code>v.number()</code> runs <code>Number()</code>, <code>v.boolean()</code> treats <code>true</code> / <code>"true"</code> / <code>"on"</code> as true), applies its rules and refinements, fills in <code>default()</code> values, and returns:</p>
|
|
<pre data-language="ts"><code>interface ParseResult<T = Record<string, unknown>> {
|
|
ok: boolean; // true when errors is empty
|
|
value: T; // coerced values (present pass or fail)
|
|
errors: Record<string, string>; // field name → first failing message
|
|
}</code></pre>
|
|
<p><code>describe()</code> returns the JSON bridge for the client:</p>
|
|
<pre data-language="ts"><code>interface SchemaDescriptor {
|
|
type: "object";
|
|
fields: Record<string, FieldDescriptor>;
|
|
}
|
|
interface FieldDescriptor {
|
|
type: "string" | "number" | "boolean";
|
|
optional?: boolean;
|
|
label?: string;
|
|
trim?: boolean; // strings only
|
|
rules: RuleDescriptor[];
|
|
}</code></pre>
|
|
<h4 id="rules-and-coercion">Rules and coercion</h4>
|
|
<p><code>RuleDescriptor</code> is a discriminated union of the serializable rules — <code>min</code>, <code>max</code>, <code>length</code>, <code>email</code>, <code>url</code>, <code>uuid</code>, <code>date</code>, <code>oneOf</code>, <code>pattern</code>, <code>integer</code>. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic):</p>
|
|
<ul>
|
|
<li><code>applyRule(type, rule, value): string | null</code> — validate one already-coerced value against one rule.</li>
|
|
<li><code>checkField(desc, raw): { value, error }</code> — coerce and validate one field. Empty input (<code>undefined</code>/<code>null</code>/<code>""</code>) is <code>"Required"</code> unless <code>optional</code>. Strings with <code>trim</code> are trimmed first. Numbers that fail <code>Number()</code> yield <code>"Must be a number"</code>.</li>
|
|
</ul>
|
|
<p>Notes on specific rules: <code>email</code>/<code>url</code>/<code>uuid</code> test built-in regexes; <code>date</code> uses <code>Date.parse</code>; <code>pattern</code> reconstructs a <code>RegExp</code> from its <code>source</code>/<code>flags</code> and passes silently if the pattern is invalid; <code>integer</code> requires <code>Number.isInteger</code>; <code>positive()</code> is implemented as <code>min(Number.MIN_VALUE)</code>.</p>
|
|
<h4 id="api-helpers">API helpers</h4>
|
|
<pre data-language="ts"><code>invalid(errors: Record<string, string>): Response // ready 400 { ok:false, errors }
|
|
|
|
parseBody<T>(schema, req):
|
|
Promise<{ ok: true; value: T } | { ok: false; response: Response }></code></pre>
|
|
<p><code>parseBody</code> reads the request body from JSON, <code>application/x-www-form-urlencoded</code>, or <code>multipart/form-data</code>, validates it, and on failure hands back a ready 400 <code>Response</code>.</p>
|
|
<h4 id="environment-config">Environment config</h4>
|
|
<pre data-language="ts"><code>parseEnv<T>(schema: ObjectSchema, source?): T</code></pre>
|
|
<p>Validates env vars (from <code>Bun.env</code>, falling back to <code>process.env</code>) against a schema and coerces them (<code>PORT</code> → number, <code>DEBUG</code> → boolean). On any problem it throws <strong>one</strong> error listing every offending variable, so misconfiguration fails fast at startup.</p>
|
|
<h4 id="client-runtime-from-runtime-ts">Client runtime (from <code>runtime.ts</code>)</h4>
|
|
<pre data-language="ts"><code>renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string
|
|
VALIDATE_RUNTIME: string</code></pre>
|
|
<ul>
|
|
<li><code>renderSchemasScript</code> produces <code>window.__wireSchemas = { name: descriptor, … };</code> to inline in the page.</li>
|
|
<li><code>VALIDATE_RUNTIME</code> is a self-contained, eval-free IIFE string. Injected as a <code><script></code>, it binds every <code>form[data-schema]</code> and validates on submit and blur, writing messages into <code>[data-error="<field>"]</code> elements and toggling <code>aria-invalid</code> / <code>.wire-invalid</code>. On a valid submit it <code>fetch</code>es the form <code>action</code> as JSON (attaching the <code>wire-csrf</code> cookie as an <code>x-csrf-token</code> header), then follows <code>data-redirect</code> / a <code>redirect</code> in the response, surfaces server-side field errors, and fires <code>wire:success</code> / <code>wire:error</code> events. It exposes <code>window.__wireValidate.init(root)</code> and self-initializes on <code>DOMContentLoaded</code>.</li>
|
|
</ul>
|
|
<h3 id="usage">Usage</h3>
|
|
<p>Define a schema and validate an API body:</p>
|
|
<pre data-language="ts"><code>import { v, parseBody } from "@wrnexus/validation";
|
|
|
|
export const signupSchema = v.object({
|
|
email: v.string().trim().email(),
|
|
password: v.string().min(8).max(200),
|
|
age: v.number().integer().min(13).max(120).optional(),
|
|
role: v.string().oneOf(["user", "admin"]).default("user"),
|
|
agree: v.boolean(),
|
|
});
|
|
|
|
// inside a route handler
|
|
const result = await parseBody(signupSchema, req);
|
|
if (!result.ok) return result.response; // ready 400 with field errors
|
|
const { email, password, role } = result.value;</code></pre>
|
|
<p>Server-only refinement:</p>
|
|
<pre data-language="ts"><code>const schema = v.object({
|
|
username: v
|
|
.string()
|
|
.min(3)
|
|
.refine((name) => !RESERVED.has(String(name)), "That name is taken"),
|
|
});</code></pre>
|
|
<p>Validate environment at startup:</p>
|
|
<pre data-language="ts"><code>import { v, parseEnv } from "@wrnexus/validation";
|
|
|
|
export const env = parseEnv(
|
|
v.object({
|
|
DATABASE_URL: v.string().min(1),
|
|
PORT: v.number().integer().default(3000),
|
|
DEBUG: v.boolean().optional(),
|
|
}),
|
|
);
|
|
// throws one readable error listing every bad variable if misconfigured</code></pre>
|
|
<p>Wire the same schema into the browser:</p>
|
|
<pre data-language="ts"><code>import { renderSchemasScript, VALIDATE_RUNTIME } from "@wrnexus/validation";
|
|
import { signupSchema } from "./app/schemas/signup.ts";
|
|
|
|
const head = `<script>${renderSchemasScript({ signup: signupSchema.describe() })}</script>
|
|
<script>${VALIDATE_RUNTIME}</script>`;
|
|
// render a <form data-schema="signup"> with [data-error="email"] etc.</code></pre>
|
|
<h3 id="requirements-notes">Requirements / Notes</h3>
|
|
<ul>
|
|
<li><strong>Bun-only.</strong> <code>parseEnv</code> reads <code>Bun.env</code> (falling back to <code>process.env</code>); <code>parseBody</code> and <code>invalid</code> use the Web <code>Request</code>/<code>Response</code> APIs that back <code>Bun.serve</code>.</li>
|
|
<li>Refinements (<code>refine</code>) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same <code>RuleDescriptor</code> list.</li>
|
|
<li>No runtime dependencies. Ships as TypeScript source (<code>src/index.ts</code>) executed directly by Bun.</li>
|
|
<li>Pairs with the WRNexusJS server (<code>@wrnexus/core</code>) for route handlers and the SSR layer that injects <code>renderSchemasScript</code> / <code>VALIDATE_RUNTIME</code>.</li>
|
|
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>/**
|
|
* Client-side validation. `renderSchemasScript` bakes the discovered schema
|
|
* descriptors into `window.__wireSchemas`; `VALIDATE_RUNTIME` is a generic,
|
|
* eval-free validator that reads them and validates every `form[data-schema]`
|
|
* on submit and blur, writing messages into `[data-error="<field>"]` elements.
|
|
* The rule logic mirrors `checkField`/`applyRule` in index.ts.
|
|
*/
|
|
|
|
/** `window.__wireSchemas = { name: descriptor, ... }` for the client validator. */
|
|
declare function renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string;
|
|
declare const VALIDATE_RUNTIME: string;
|
|
|
|
/**
|
|
* @wrnexus/validation — one schema, validated on the server (API) and the browser
|
|
* (forms). A schema is a fluent builder; `.parse()` runs server-side and returns
|
|
* coerced values + field errors, while `.describe()` emits a JSON descriptor the
|
|
* eval-free client validator interprets. Define schemas once in `app/schemas/`.
|
|
*/
|
|
type RuleDescriptor = {
|
|
kind: "min";
|
|
n: number;
|
|
message?: string;
|
|
} | {
|
|
kind: "max";
|
|
n: number;
|
|
message?: string;
|
|
} | {
|
|
kind: "length";
|
|
n: number;
|
|
message?: string;
|
|
} | {
|
|
kind: "email";
|
|
message?: string;
|
|
} | {
|
|
kind: "url";
|
|
message?: string;
|
|
} | {
|
|
kind: "uuid";
|
|
message?: string;
|
|
} | {
|
|
kind: "date";
|
|
message?: string;
|
|
} | {
|
|
kind: "oneOf";
|
|
values: (string | number)[];
|
|
message?: string;
|
|
} | {
|
|
kind: "pattern";
|
|
source: string;
|
|
flags?: string;
|
|
message?: string;
|
|
} | {
|
|
kind: "integer";
|
|
message?: string;
|
|
};
|
|
interface FieldDescriptor {
|
|
type: "string" | "number" | "boolean";
|
|
optional?: boolean;
|
|
label?: string;
|
|
/** Trim string input before validating. */
|
|
trim?: boolean;
|
|
rules: RuleDescriptor[];
|
|
}
|
|
interface SchemaDescriptor {
|
|
type: "object";
|
|
fields: Record<string, FieldDescriptor>;
|
|
}
|
|
interface ParseResult<T = Record<string, unknown>> {
|
|
ok: boolean;
|
|
/** Coerced values (present whether or not validation passed). */
|
|
value: T;
|
|
/** Field name → message, only for fields that failed. */
|
|
errors: Record<string, string>;
|
|
}
|
|
/**
|
|
* Apply one rule to an already-coerced value. Shared by the server; the client
|
|
* runtime (runtime.ts) mirrors this exactly. Returns an error message or null.
|
|
*/
|
|
declare function applyRule(type: string, rule: RuleDescriptor, value: unknown): string | null;
|
|
/** Coerce + validate one field against its descriptor. */
|
|
declare function checkField(desc: FieldDescriptor, raw: unknown): {
|
|
value: unknown;
|
|
error: string | null;
|
|
};
|
|
/** A server-only refinement (a predicate that can't be serialized to the client). */
|
|
type Refinement = {
|
|
fn: (value: unknown) => boolean | string;
|
|
message?: string;
|
|
};
|
|
declare abstract class FieldSchema {
|
|
abstract readonly type: "string" | "number" | "boolean";
|
|
protected _optional: boolean;
|
|
protected _label?: string;
|
|
protected _default?: unknown;
|
|
protected rules: RuleDescriptor[];
|
|
protected refinements: Refinement[];
|
|
optional(): this;
|
|
label(label: string): this;
|
|
/** Value used when the field is absent (implies optional). */
|
|
default(value: unknown): this;
|
|
min(n: number, message?: string): this;
|
|
max(n: number, message?: string): this;
|
|
/**
|
|
* Custom SERVER-side validation. `fn` returns true (ok), false (use `message`),
|
|
* or a string (that error). Not mirrored to the client validator.
|
|
*/
|
|
refine(fn: (value: unknown) => boolean | string, message?: string): this;
|
|
getDefault(): unknown;
|
|
runRefinements(value: unknown): string | null;
|
|
describe(): FieldDescriptor;
|
|
}
|
|
declare class StringSchema extends FieldSchema {
|
|
readonly type: "string";
|
|
private _trim;
|
|
email(message?: string): this;
|
|
url(message?: string): this;
|
|
uuid(message?: string): this;
|
|
date(message?: string): this;
|
|
length(n: number, message?: string): this;
|
|
oneOf(values: string[], message?: string): this;
|
|
trim(): this;
|
|
pattern(re: RegExp, message?: string): this;
|
|
describe(): FieldDescriptor;
|
|
}
|
|
declare class NumberSchema extends FieldSchema {
|
|
readonly type: "number";
|
|
integer(message?: string): this;
|
|
positive(message?: string): this;
|
|
oneOf(values: number[], message?: string): this;
|
|
}
|
|
declare class BooleanSchema extends FieldSchema {
|
|
readonly type: "boolean";
|
|
}
|
|
declare class ObjectSchema {
|
|
private readonly fields;
|
|
constructor(fields: Record<string, FieldSchema>);
|
|
/** Validate an input object; returns coerced values + per-field errors. */
|
|
parse(input: unknown): ParseResult;
|
|
describe(): SchemaDescriptor;
|
|
}
|
|
/** The fluent schema builder. */
|
|
declare const v: {
|
|
string: () => StringSchema;
|
|
number: () => NumberSchema;
|
|
boolean: () => BooleanSchema;
|
|
object: (fields: Record<string, FieldSchema>) => ObjectSchema;
|
|
};
|
|
/**
|
|
* Validate environment variables against a schema at startup. Values are read
|
|
* from `Bun.env` / `process.env` by default and coerced by the schema (so
|
|
* `PORT` becomes a number, `DEBUG` a boolean). On any problem it throws ONE
|
|
* readable error listing every offending variable, so misconfiguration fails
|
|
* fast with an actionable message instead of surfacing deep inside the app.
|
|
*
|
|
* export const env = parseEnv(v.object({
|
|
* DATABASE_URL: v.string().min(1),
|
|
* PORT: v.number(),
|
|
* }));
|
|
*/
|
|
declare function parseEnv<T = Record<string, unknown>>(schema: ObjectSchema, source?: Record<string, string | undefined>): T;
|
|
/** A 400 response carrying field errors, for API routes. */
|
|
declare function invalid(errors: Record<string, string>): Response;
|
|
/**
|
|
* Parse a request's JSON body against a schema. On failure returns
|
|
* `{ ok: false, response }` (a ready 400); on success `{ ok: true, value }`.
|
|
*/
|
|
declare function parseBody<T = Record<string, unknown>>(schema: ObjectSchema, req: Request): Promise<{
|
|
ok: true;
|
|
value: T;
|
|
} | {
|
|
ok: false;
|
|
response: Response;
|
|
}>;
|
|
|
|
export { type FieldDescriptor, ObjectSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, VALIDATE_RUNTIME, applyRule, checkField, invalid, parseBody, parseEnv, renderSchemasScript, v };
|
|
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/validation</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>import { v } from "@wrnexus/validation";</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>schema.parse(input: unknown): ParseResult
|
|
schema.describe(): SchemaDescriptor</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>interface ParseResult<T = Record<string, unknown>> {
|
|
ok: boolean; // true when errors is empty
|
|
value: T; // coerced values (present pass or fail)
|
|
errors: Record<string, string>; // field name → first failing message
|
|
}</code></pre></article></div></section></article>
|
|
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#the-v-builder">The v builder</a><a class="toc-level-4" href="#objectschema">ObjectSchema</a><a class="toc-level-4" href="#rules-and-coercion">Rules and coercion</a><a class="toc-level-4" href="#api-helpers">API helpers</a><a class="toc-level-4" href="#environment-config">Environment config</a><a class="toc-level-4" href="#client-runtime-from-runtime-ts">Client runtime (from runtime.ts)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
|
</main>
|
|
<footer>WRNexusJS 0.2.19 · Private Developer Preview · Bun-native · Documentation generated from installed package APIs.</footer>
|
|
</div>
|
|
}
|
|
}
|