Files
WRNexusJSDoc/app/pages/packages/validation.wrn
T

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 &#123; v &#125; from &quot;@wrnexus/validation&quot;;</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>&quot;Required&quot;</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>&quot;true&quot;</code> / <code>&quot;on&quot;</code> as true), applies its rules and refinements, fills in <code>default()</code> values, and returns:</p>
<pre data-language="ts"><code>interface ParseResult&lt;T = Record&lt;string, unknown&gt;&gt; &#123;
ok: boolean; // true when errors is empty
value: T; // coerced values (present pass or fail)
errors: Record&lt;string, string&gt;; // field name → first failing message
&#125;</code></pre>
<p><code>describe()</code> returns the JSON bridge for the client:</p>
<pre data-language="ts"><code>interface SchemaDescriptor &#123;
type: &quot;object&quot;;
fields: Record&lt;string, FieldDescriptor&gt;;
&#125;
interface FieldDescriptor &#123;
type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
optional?: boolean;
label?: string;
trim?: boolean; // strings only
rules: RuleDescriptor[];
&#125;</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): &#123; value, error &#125;</code> — coerce and validate one field. Empty input (<code>undefined</code>/<code>null</code>/<code>&quot;&quot;</code>) is <code>&quot;Required&quot;</code> unless <code>optional</code>. Strings with <code>trim</code> are trimmed first. Numbers that fail <code>Number()</code> yield <code>&quot;Must be a number&quot;</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&lt;string, string&gt;): Response // ready 400 &#123; ok:false, errors &#125;
parseBody&lt;T&gt;(schema, req):
Promise&lt;&#123; ok: true; value: T &#125; | &#123; ok: false; response: Response &#125;&gt;</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&lt;T&gt;(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&lt;string, SchemaDescriptor&gt;): string
VALIDATE_RUNTIME: string</code></pre>
<ul>
<li><code>renderSchemasScript</code> produces <code>window.__wireSchemas = &#123; name: descriptor, … &#125;;</code> to inline in the page.</li>
<li><code>VALIDATE_RUNTIME</code> is a self-contained, eval-free IIFE string. Injected as a <code>&lt;script&gt;</code>, it binds every <code>form[data-schema]</code> and validates on submit and blur, writing messages into <code>[data-error=&quot;&lt;field&gt;&quot;]</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 &#123; v, parseBody &#125; from &quot;@wrnexus/validation&quot;;
export const signupSchema = v.object(&#123;
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([&quot;user&quot;, &quot;admin&quot;]).default(&quot;user&quot;),
agree: v.boolean(),
&#125;);
// inside a route handler
const result = await parseBody(signupSchema, req);
if (!result.ok) return result.response; // ready 400 with field errors
const &#123; email, password, role &#125; = result.value;</code></pre>
<p>Server-only refinement:</p>
<pre data-language="ts"><code>const schema = v.object(&#123;
username: v
.string()
.min(3)
.refine((name) =&gt; !RESERVED.has(String(name)), &quot;That name is taken&quot;),
&#125;);</code></pre>
<p>Validate environment at startup:</p>
<pre data-language="ts"><code>import &#123; v, parseEnv &#125; from &quot;@wrnexus/validation&quot;;
export const env = parseEnv(
v.object(&#123;
DATABASE_URL: v.string().min(1),
PORT: v.number().integer().default(3000),
DEBUG: v.boolean().optional(),
&#125;),
);
// 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 &#123; renderSchemasScript, VALIDATE_RUNTIME &#125; from &quot;@wrnexus/validation&quot;;
import &#123; signupSchema &#125; from &quot;./app/schemas/signup.ts&quot;;
const head = `&lt;script&gt;$&#123;renderSchemasScript(&#123; signup: signupSchema.describe() &#125;)&#125;&lt;/script&gt;
&lt;script&gt;$&#123;VALIDATE_RUNTIME&#125;&lt;/script&gt;`;
// render a &lt;form data-schema=&quot;signup&quot;&gt; with [data-error=&quot;email&quot;] 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=&quot;&lt;field&gt;&quot;]` elements.
* The rule logic mirrors `checkField`/`applyRule` in index.ts.
*/
/** `window.__wireSchemas = &#123; name: descriptor, ... &#125;` for the client validator. */
declare function renderSchemasScript(descriptors: Record&lt;string, SchemaDescriptor&gt;): 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 = &#123;
kind: &quot;min&quot;;
n: number;
message?: string;
&#125; | &#123;
kind: &quot;max&quot;;
n: number;
message?: string;
&#125; | &#123;
kind: &quot;length&quot;;
n: number;
message?: string;
&#125; | &#123;
kind: &quot;email&quot;;
message?: string;
&#125; | &#123;
kind: &quot;url&quot;;
message?: string;
&#125; | &#123;
kind: &quot;uuid&quot;;
message?: string;
&#125; | &#123;
kind: &quot;date&quot;;
message?: string;
&#125; | &#123;
kind: &quot;oneOf&quot;;
values: (string | number)[];
message?: string;
&#125; | &#123;
kind: &quot;pattern&quot;;
source: string;
flags?: string;
message?: string;
&#125; | &#123;
kind: &quot;integer&quot;;
message?: string;
&#125;;
interface FieldDescriptor &#123;
type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
optional?: boolean;
label?: string;
/** Trim string input before validating. */
trim?: boolean;
rules: RuleDescriptor[];
&#125;
interface SchemaDescriptor &#123;
type: &quot;object&quot;;
fields: Record&lt;string, FieldDescriptor&gt;;
&#125;
interface ParseResult&lt;T = Record&lt;string, unknown&gt;&gt; &#123;
ok: boolean;
/** Coerced values (present whether or not validation passed). */
value: T;
/** Field name → message, only for fields that failed. */
errors: Record&lt;string, string&gt;;
&#125;
/**
* 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): &#123;
value: unknown;
error: string | null;
&#125;;
/** A server-only refinement (a predicate that can't be serialized to the client). */
type Refinement = &#123;
fn: (value: unknown) =&gt; boolean | string;
message?: string;
&#125;;
declare abstract class FieldSchema &#123;
abstract readonly type: &quot;string&quot; | &quot;number&quot; | &quot;boolean&quot;;
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) =&gt; boolean | string, message?: string): this;
getDefault(): unknown;
runRefinements(value: unknown): string | null;
describe(): FieldDescriptor;
&#125;
declare class StringSchema extends FieldSchema &#123;
readonly type: &quot;string&quot;;
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;
&#125;
declare class NumberSchema extends FieldSchema &#123;
readonly type: &quot;number&quot;;
integer(message?: string): this;
positive(message?: string): this;
oneOf(values: number[], message?: string): this;
&#125;
declare class BooleanSchema extends FieldSchema &#123;
readonly type: &quot;boolean&quot;;
&#125;
declare class ObjectSchema &#123;
private readonly fields;
constructor(fields: Record&lt;string, FieldSchema&gt;);
/** Validate an input object; returns coerced values + per-field errors. */
parse(input: unknown): ParseResult;
describe(): SchemaDescriptor;
&#125;
/** The fluent schema builder. */
declare const v: &#123;
string: () =&gt; StringSchema;
number: () =&gt; NumberSchema;
boolean: () =&gt; BooleanSchema;
object: (fields: Record&lt;string, FieldSchema&gt;) =&gt; ObjectSchema;
&#125;;
/**
* 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(&#123;
* DATABASE_URL: v.string().min(1),
* PORT: v.number(),
* &#125;));
*/
declare function parseEnv&lt;T = Record&lt;string, unknown&gt;&gt;(schema: ObjectSchema, source?: Record&lt;string, string | undefined&gt;): T;
/** A 400 response carrying field errors, for API routes. */
declare function invalid(errors: Record&lt;string, string&gt;): Response;
/**
* Parse a request's JSON body against a schema. On failure returns
* `&#123; ok: false, response &#125;` (a ready 400); on success `&#123; ok: true, value &#125;`.
*/
declare function parseBody&lt;T = Record&lt;string, unknown&gt;&gt;(schema: ObjectSchema, req: Request): Promise&lt;&#123;
ok: true;
value: T;
&#125; | &#123;
ok: false;
response: Response;
&#125;&gt;;
export &#123; type FieldDescriptor, ObjectSchema, type ParseResult, type RuleDescriptor, type SchemaDescriptor, VALIDATE_RUNTIME, applyRule, checkField, invalid, parseBody, parseEnv, renderSchemasScript, v &#125;;
</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 &#123; v &#125; from &quot;@wrnexus/validation&quot;;</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&lt;T = Record&lt;string, unknown&gt;&gt; &#123;
ok: boolean; // true when errors is empty
value: T; // coerced values (present pass or fail)
errors: Record&lt;string, string&gt;; // field name → first failing message
&#125;</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>
}
}