@nifrajs/schema 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,12 +20,12 @@ const app = server().post("/users", { body: t.object({ name: t.string(), age: t.
20
20
  const openapi = toOpenAPI(app) // OpenAPI 3.1
21
21
  ```
22
22
 
23
- - **`t` builder** — `string`/`number`/`integer`/`boolean`/`null`/`literal`/`object`/
23
+ - **`t` builder** - `string`/`number`/`integer`/`boolean`/`null`/`literal`/`object`/
24
24
  `array`/`optional`/`union`/`record`, with options (`min`/`max`, `pattern`, `format`,
25
25
  …) that become JSON Schema constraints. Validators are compiled (fast).
26
- - **Validating string formats** — `email`/`uuid`/`date-time`/`date`/`time`/`uri`/`ipv4`
26
+ - **Validating string formats** - `email`/`uuid`/`date-time`/`date`/`time`/`uri`/`ipv4`
27
27
  validate *and* annotate; register more with `registerFormat`.
28
- - **`toOpenAPI`** — richest from a contract (it carries `response` schemas + op names →
28
+ - **`toOpenAPI`** - richest from a contract (it carries `response` schemas + op names →
29
29
  `operationId`s); also works on a live app. Routes using a BYO Standard Schema are
30
30
  emitted without a detailed schema (Standard Schema exposes no JSON Schema).
31
31
 
@@ -52,14 +52,14 @@ It emits `paths`, `parameters` (path + object query), `requestBody`, and `respon
52
52
  - **`servers`**, **top-level `tags`**, and an info **`description`** (document options).
53
53
  - **`securitySchemes`** → `components.securitySchemes`, a document-wide **`security`**, and per-operation
54
54
  `security` (`[]` marks an operation explicitly public).
55
- - **Non-200 responses** and **non-JSON content** — a contract op's `responses` map declares extra status
55
+ - **Non-200 responses** and **non-JSON content** - a contract op's `responses` map declares extra status
56
56
  codes; `requestContentType` / `responseContentType` set media types other than `application/json`.
57
- - **`$ref` reuse** — a schema with a `$id` (`t.object({…}, { $id: "User" })`) is hoisted into
57
+ - **`$ref` reuse** - a schema with a `$id` (`t.object({…}, { $id: "User" })`) is hoisted into
58
58
  `components.schemas` once and referenced by `$ref` everywhere it's used.
59
- - Per-operation **`summary`**, **`description`**, **`tags`**, **`deprecated`** — declared on the contract op.
59
+ - Per-operation **`summary`**, **`description`**, **`tags`**, **`deprecated`** - declared on the contract op.
60
60
 
61
61
  A **contract** is richest (its ops carry all of the above); in **app** mode a route's response is a
62
- generic `200` with no schema — declare a `defineContract` with `response` schemas, or pass
62
+ generic `200` with no schema - declare a `defineContract` with `response` schemas, or pass
63
63
  `options.operations` (keyed by `"METHOD /path"`) to enrich app routes. Routes using a BYO Standard Schema
64
64
  are still emitted without body/response detail (Standard Schema exposes no JSON Schema).
65
65
 
@@ -67,7 +67,7 @@ are still emitted without body/response detail (Standard Schema exposes no JSON
67
67
 
68
68
  ## For AI agents
69
69
 
70
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
70
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
71
71
  one cheap read instead of the whole corpus. For the wider framework: the repo's
72
72
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
73
73
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
package/dist/adapter.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { StandardSchemaV1 } from "@nifrajs/core/schema";
2
2
  import type { Static, TSchema } from "@sinclair/typebox";
3
3
  /**
4
4
  * A `t` schema. It is a Standard Schema (so any nifra route validates it with no
5
- * special-casing) whose raw TypeBox definition stays reachable as `jsonSchema` —
5
+ * special-casing) whose raw TypeBox definition stays reachable as `jsonSchema` -
6
6
  * and because a TypeBox schema *is* a JSON Schema, that field is exactly what lets
7
7
  * `toOpenAPI` emit a real request/response schema for the route. BYO Standard
8
8
  * Schemas (zod/valibot/arktype) validate too but expose no JSON Schema, so only
@@ -16,13 +16,13 @@ export type NifraSchema<T extends TSchema = TSchema> = StandardSchemaV1<Static<T
16
16
  *
17
17
  * **Validation is fast where it can be, and works everywhere.** The first `validate`
18
18
  * builds a compiled `TypeCompiler` validator (codegen via `new Function`) and memoizes
19
- * it — composing schemas (`t.object({ a: t.string() })`) never compiles the inner
19
+ * it - composing schemas (`t.object({ a: t.string() })`) never compiles the inner
20
20
  * pieces; only a schema actually used to validate a request pays the one-time codegen
21
21
  * cost. On Bun and Node this is the only path and it is untouched.
22
22
  *
23
23
  * Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) **forbid dynamic code
24
24
  * generation**, so that first compile throws there. We catch it once per schema and
25
- * fall back to TypeBox's eval-free `Value` checker — same semantics (and same global
25
+ * fall back to TypeBox's eval-free `Value` checker - same semantics (and same global
26
26
  * `FormatRegistry`, so `t.string({ format })` behaves identically), no `new Function`.
27
27
  * The branch is decided once and memoized, so the hot path is a single check either way.
28
28
  *
@@ -31,7 +31,7 @@ export type NifraSchema<T extends TSchema = TSchema> = StandardSchemaV1<Static<T
31
31
  *
32
32
  * `options.coerce` runs TypeBox's `Value.Convert` (string→number/integer/boolean, per the schema)
33
33
  * BEFORE validating. Query values always arrive as strings (`?limit=20` → `"20"`), so a query schema
34
- * with a numeric field can't validate without this — it's how `t.pageQuery` yields a real `number`.
34
+ * with a numeric field can't validate without this - it's how `t.pageQuery` yields a real `number`.
35
35
  * Leave it OFF (the default) for body/JSON schemas: a JSON number is already a number, and coercing
36
36
  * would silently accept `"20"` where the contract said `20`.
37
37
  */
package/dist/adapter.js CHANGED
@@ -18,13 +18,13 @@ function toIssue(error) {
18
18
  *
19
19
  * **Validation is fast where it can be, and works everywhere.** The first `validate`
20
20
  * builds a compiled `TypeCompiler` validator (codegen via `new Function`) and memoizes
21
- * it — composing schemas (`t.object({ a: t.string() })`) never compiles the inner
21
+ * it - composing schemas (`t.object({ a: t.string() })`) never compiles the inner
22
22
  * pieces; only a schema actually used to validate a request pays the one-time codegen
23
23
  * cost. On Bun and Node this is the only path and it is untouched.
24
24
  *
25
25
  * Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) **forbid dynamic code
26
26
  * generation**, so that first compile throws there. We catch it once per schema and
27
- * fall back to TypeBox's eval-free `Value` checker — same semantics (and same global
27
+ * fall back to TypeBox's eval-free `Value` checker - same semantics (and same global
28
28
  * `FormatRegistry`, so `t.string({ format })` behaves identically), no `new Function`.
29
29
  * The branch is decided once and memoized, so the hot path is a single check either way.
30
30
  *
@@ -33,7 +33,7 @@ function toIssue(error) {
33
33
  *
34
34
  * `options.coerce` runs TypeBox's `Value.Convert` (string→number/integer/boolean, per the schema)
35
35
  * BEFORE validating. Query values always arrive as strings (`?limit=20` → `"20"`), so a query schema
36
- * with a numeric field can't validate without this — it's how `t.pageQuery` yields a real `number`.
36
+ * with a numeric field can't validate without this - it's how `t.pageQuery` yields a real `number`.
37
37
  * Leave it OFF (the default) for body/JSON schemas: a JSON number is already a number, and coercing
38
38
  * would silently accept `"20"` where the contract said `20`.
39
39
  */
@@ -48,7 +48,7 @@ export function fromTypeBox(schema, options) {
48
48
  validate: (value) => {
49
49
  // Install the standard string formats before the first Compile/Check. Driven from this
50
50
  // reachable path (not a top-level import side effect) so a production bundle can't
51
- // tree-shake the registration away — see ./formats.ts. Idempotent, ~free after first call.
51
+ // tree-shake the registration away - see ./formats.ts. Idempotent, ~free after first call.
52
52
  ensureDefaultFormats();
53
53
  // Coerce first when asked (query schemas): `Value.Convert` turns "20"→20 per the schema, so the
54
54
  // compiled/eval-free Check below sees the target type. A non-convertible value (e.g. "abc" for an
package/dist/formats.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare function registerFormat(name: string, validate: (value: string) =
6
6
  * This is driven from the validate path (see `./adapter.ts`) rather than run as a bare
7
7
  * top-level import side effect *on purpose*: a production bundler (`Bun.build`, esbuild)
8
8
  * tree-shakes a module that is only imported for its effect, and Bun.build does **not**
9
- * honor the `sideEffects` package.json field — so the old top-level registration loop was
9
+ * honor the `sideEffects` package.json field - so the old top-level registration loop was
10
10
  * dropped from prod server bundles, making every `t.string({ format })` reject with
11
11
  * "Unknown format" while unbundled `nifra dev` worked. Calling this from code the validator
12
12
  * actually reaches keeps the registration in the bundle.
package/dist/formats.js CHANGED
@@ -32,7 +32,7 @@ let defaultsRegistered = false;
32
32
  * This is driven from the validate path (see `./adapter.ts`) rather than run as a bare
33
33
  * top-level import side effect *on purpose*: a production bundler (`Bun.build`, esbuild)
34
34
  * tree-shakes a module that is only imported for its effect, and Bun.build does **not**
35
- * honor the `sideEffects` package.json field — so the old top-level registration loop was
35
+ * honor the `sideEffects` package.json field - so the old top-level registration loop was
36
36
  * dropped from prod server bundles, making every `t.string({ format })` reject with
37
37
  * "Unknown format" while unbundled `nifra dev` worked. Calling this from code the validator
38
38
  * actually reaches keeps the registration in the bundle.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * `@nifrajs/schema` — the optional, batteries-included schema builder for nifra.
2
+ * `@nifrajs/schema` - the optional, batteries-included schema builder for nifra.
3
3
  *
4
4
  * `t` is TypeBox-backed: validators are compiled (fast) and every schema is a JSON
5
5
  * Schema (free OpenAPI). It implements Standard Schema, so `@nifrajs/core` validates
6
- * it through the exact same path as any BYO library — `@nifrajs/core` never depends on
6
+ * it through the exact same path as any BYO library - `@nifrajs/core` never depends on
7
7
  * a validator; this package is the opt-in default.
8
8
  */
9
9
  export { fromTypeBox, type NifraSchema } from "./adapter.js";
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
- * `@nifrajs/schema` — the optional, batteries-included schema builder for nifra.
2
+ * `@nifrajs/schema` - the optional, batteries-included schema builder for nifra.
3
3
  *
4
4
  * `t` is TypeBox-backed: validators are compiled (fast) and every schema is a JSON
5
5
  * Schema (free OpenAPI). It implements Standard Schema, so `@nifrajs/core` validates
6
- * it through the exact same path as any BYO library — `@nifrajs/core` never depends on
6
+ * it through the exact same path as any BYO library - `@nifrajs/core` never depends on
7
7
  * a validator; this package is the opt-in default.
8
8
  */
9
9
  export { fromTypeBox } from "./adapter.js";
10
10
  // `registerFormat` adds/overrides a custom string format. The standard formats (email, uuid, …)
11
- // install themselves lazily from the validate path (see ./adapter.ts) — NOT as an import side
11
+ // install themselves lazily from the validate path (see ./adapter.ts) - NOT as an import side
12
12
  // effect of this module, which a production bundler would tree-shake away.
13
13
  export { registerFormat } from "./formats.js";
14
14
  export { toOpenAPI } from "./openapi.js";
15
- // Cursor-pagination runtime helpers — pair with `t.paginated` / `t.pageQuery`.
15
+ // Cursor-pagination runtime helpers - pair with `t.paginated` / `t.pageQuery`.
16
16
  export { decodeCursor, encodeCursor, paginate } from "./pagination.js";
17
17
  export { t } from "./t.js";
18
18
  //# sourceMappingURL=index.js.map
package/dist/openapi.d.ts CHANGED
@@ -2,13 +2,13 @@ import type { ContractShape } from "@nifrajs/core/contract";
2
2
  import { type JsonSchema } from "@nifrajs/core/reflection";
3
3
  import type { Server } from "@nifrajs/core/server";
4
4
  /**
5
- * OpenAPI 3.1 generation. We model a practical slice of the spec — enough to feed Swagger UI / codegen
5
+ * OpenAPI 3.1 generation. We model a practical slice of the spec - enough to feed Swagger UI / codegen
6
6
  * and to validate structurally: paths, parameters, request bodies, responses (incl. non-200 and
7
7
  * non-JSON), tags, security, servers, and `$ref` reuse via `components.schemas`.
8
8
  *
9
9
  * Schemas carry full detail only for `t`/TypeBox inputs (they expose a JSON Schema); a BYO Standard
10
10
  * Schema validates at runtime but exposes no JSON Schema, so its route is emitted without body/response
11
- * detail. A **contract** is richest — its operations carry `response`, `tags`, `security`, additional
11
+ * detail. A **contract** is richest - its operations carry `response`, `tags`, `security`, additional
12
12
  * `responses`, etc.; an **app** emits request shapes + a generic `200` (an inline handler has no
13
13
  * response *schema* to serialize), enrichable via `options.operations`.
14
14
  */
@@ -33,7 +33,7 @@ export interface ToOpenAPIOptions {
33
33
  readonly description?: string;
34
34
  /** Server URLs the API is served from (OpenAPI `servers`). */
35
35
  readonly servers?: readonly OpenAPIServer[];
36
- /** Tag definitions (OpenAPI top-level `tags`) — names referenced by an operation's `tags`. */
36
+ /** Tag definitions (OpenAPI top-level `tags`) - names referenced by an operation's `tags`. */
37
37
  readonly tags?: readonly OpenAPITag[];
38
38
  /** Reusable security schemes → `components.securitySchemes` (e.g. `{ bearer: { type: "http", scheme: "bearer" } }`). */
39
39
  readonly securitySchemes?: Readonly<Record<string, Record<string, unknown>>>;
@@ -42,7 +42,7 @@ export interface ToOpenAPIOptions {
42
42
  /**
43
43
  * Per-operation overrides, shallow-merged over the generated operation. Keyed by `operationId`
44
44
  * (contract op name) or `"METHOD /path"` (e.g. `"GET /users/:id"`). The escape hatch for detail that
45
- * can't be introspected — richer response bodies, examples, app-route tags/security.
45
+ * can't be introspected - richer response bodies, examples, app-route tags/security.
46
46
  */
47
47
  readonly operations?: Readonly<Record<string, Record<string, unknown>>>;
48
48
  }
package/dist/openapi.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { reflectRoutes, reflectSchema, } from "@nifrajs/core/reflection";
2
2
  /**
3
- * Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place —
3
+ * Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place -
4
4
  * so a schema used by N operations is emitted once. Schemas without a `$id` stay inline (the existing
5
5
  * behavior). The first sighting of an id wins; later ones just `$ref` it.
6
6
  */
@@ -209,9 +209,9 @@ export function toOpenAPI(input, options = {}) {
209
209
  body: route.schema?.body,
210
210
  query: route.schema?.query,
211
211
  params: route.schema?.params,
212
- // A route may now declare a `response` contract — emit it as the 200 body schema.
212
+ // A route may now declare a `response` contract - emit it as the 200 body schema.
213
213
  response: route.schema?.response,
214
- // …and an `errors` contract — emit each as a non-2xx response.
214
+ // …and an `errors` contract - emit each as a non-2xx response.
215
215
  responses: reflectedErrorsToResponses(route.schema?.errors),
216
216
  operationId: undefined,
217
217
  }, store, options.operations);
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Cursor-pagination runtime helpers — an opaque cursor codec + a page builder. Pair with `t.paginated`
2
+ * Cursor-pagination runtime helpers - an opaque cursor codec + a page builder. Pair with `t.paginated`
3
3
  * (the response schema) and `t.pageQuery` (the request schema).
4
4
  *
5
5
  * Edge-safe: `btoa`/`atob` + `TextEncoder`/`TextDecoder` (no `Buffer`), so it runs on Bun/Node/Deno/
6
- * Workers. Cursors are opaque but NOT signed — treat a decoded cursor as untrusted client input: validate
6
+ * Workers. Cursors are opaque but NOT signed - treat a decoded cursor as untrusted client input: validate
7
7
  * its shape before keying a DB query off it (it's a position hint, not an authorization token).
8
8
  */
9
9
  /** Encode any JSON-serializable value (e.g. the last row's sort key) into an opaque cursor string. */
10
10
  export declare function encodeCursor(value: unknown): string;
11
- /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor — treat that
11
+ /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor - treat that
12
12
  * as "start from the beginning" rather than erroring on a client-supplied string. */
13
13
  export declare function decodeCursor<T = unknown>(cursor: string | null | undefined): T | undefined;
14
- /** A cursor-pagination page — matches the shape of `t.paginated(item)`. */
14
+ /** A cursor-pagination page - matches the shape of `t.paginated(item)`. */
15
15
  export interface Page<Item> {
16
16
  readonly items: Item[];
17
17
  readonly nextCursor: string | null;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Cursor-pagination runtime helpers — an opaque cursor codec + a page builder. Pair with `t.paginated`
2
+ * Cursor-pagination runtime helpers - an opaque cursor codec + a page builder. Pair with `t.paginated`
3
3
  * (the response schema) and `t.pageQuery` (the request schema).
4
4
  *
5
5
  * Edge-safe: `btoa`/`atob` + `TextEncoder`/`TextDecoder` (no `Buffer`), so it runs on Bun/Node/Deno/
6
- * Workers. Cursors are opaque but NOT signed — treat a decoded cursor as untrusted client input: validate
6
+ * Workers. Cursors are opaque but NOT signed - treat a decoded cursor as untrusted client input: validate
7
7
  * its shape before keying a DB query off it (it's a position hint, not an authorization token).
8
8
  */
9
9
  /** UTF-8 → URL-safe base64 (no padding). */
@@ -24,7 +24,7 @@ function fromBase64Url(input) {
24
24
  export function encodeCursor(value) {
25
25
  return toBase64Url(JSON.stringify(value));
26
26
  }
27
- /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor — treat that
27
+ /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor - treat that
28
28
  * as "start from the beginning" rather than erroring on a client-supplied string. */
29
29
  export function decodeCursor(cursor) {
30
30
  if (cursor === null || cursor === undefined || cursor === "")
package/dist/t.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type ArrayOptions, type IntegerOptions, type NumberOptions, type Object
2
2
  import { type NifraSchema } from "./adapter.js";
3
3
  type Props = Record<string, NifraSchema>;
4
4
  /**
5
- * The built-in schema builder. Each constructor returns a `NifraSchema` — a
5
+ * The built-in schema builder. Each constructor returns a `NifraSchema` - a
6
6
  * Standard Schema whose validated output type flows into `c.body`/`c.query`, and
7
7
  * whose `jsonSchema` powers `toOpenAPI`. Options (min/max, length, pattern, …)
8
8
  * pass straight through to TypeBox and so become JSON Schema constraints.
@@ -21,7 +21,7 @@ export declare const t: {
21
21
  readonly null: () => NifraSchema<import("@sinclair/typebox").TNull>;
22
22
  readonly literal: <const L extends TLiteralValue>(value: L) => NifraSchema<import("@sinclair/typebox").TLiteral<L>>;
23
23
  readonly object: <P extends Props>(props: P, options?: ObjectOptions) => NifraSchema<import("@sinclair/typebox").TObject<{ [K in keyof P]: P[K]["jsonSchema"]; }>>;
24
- /** Like `t.object` but ACCEPTS (and passes through) unknown fields — the explicit opt-out of the
24
+ /** Like `t.object` but ACCEPTS (and passes through) unknown fields - the explicit opt-out of the
25
25
  * strict default. Prefer `t.object` unless you genuinely need an open object. */
26
26
  readonly looseObject: <P extends Props>(props: P, options?: ObjectOptions) => NifraSchema<import("@sinclair/typebox").TObject<{ [K in keyof P]: P[K]["jsonSchema"]; }>>;
27
27
  readonly array: <T extends TSchema>(item: NifraSchema<T>, options?: ArrayOptions) => NifraSchema<import("@sinclair/typebox").TArray<T>>;
@@ -35,7 +35,7 @@ export declare const t: {
35
35
  nextCursor: TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>;
36
36
  }>>;
37
37
  /** A request query schema for cursor pagination: `{ cursor?: string; limit?: number }`. `maxLimit`
38
- * caps `limit` — a larger value fails validation (a 422), so a client can't request an unbounded page.
38
+ * caps `limit` - a larger value fails validation (a 422), so a client can't request an unbounded page.
39
39
  * `coerce` is on because query values arrive as strings (`?limit=20` → `"20"`); it's what makes `limit`
40
40
  * a real `number` in the handler (`c.query.limit`), not a string. */
41
41
  readonly pageQuery: (options?: {
@@ -47,7 +47,7 @@ export declare const t: {
47
47
  /** A request-query schema with string->scalar COERCION on. Query values always arrive as strings
48
48
  * (`?limit=20` -> `"20"`), so a plain `t.object({ limit: t.integer() })` in a `query` slot fails to
49
49
  * validate; use `t.query` and `t.integer()`/`t.number()`/`t.boolean()` fields become real numbers/
50
- * booleans in `c.query`. **Open by default** (unknown query fields pass through — `additionalProperties:
50
+ * booleans in `c.query`. **Open by default** (unknown query fields pass through - `additionalProperties:
51
51
  * true`): query params are read by name, not spread into a DB write, so unknown params (UTM tracking,
52
52
  * `fbclid`, etc.) passing through is safe, and rejecting them is a production-only footgun (ad/social
53
53
  * traffic appends params your schema never declares, causing 422s that never appear in dev/CI). Pass
package/dist/t.js CHANGED
@@ -9,7 +9,7 @@ function unwrap(props) {
9
9
  return out;
10
10
  }
11
11
  /**
12
- * The built-in schema builder. Each constructor returns a `NifraSchema` — a
12
+ * The built-in schema builder. Each constructor returns a `NifraSchema` - a
13
13
  * Standard Schema whose validated output type flows into `c.body`/`c.query`, and
14
14
  * whose `jsonSchema` powers `toOpenAPI`. Options (min/max, length, pattern, …)
15
15
  * pass straight through to TypeBox and so become JSON Schema constraints.
@@ -27,12 +27,12 @@ export const t = {
27
27
  boolean: () => fromTypeBox(Type.Boolean()),
28
28
  null: () => fromTypeBox(Type.Null()),
29
29
  literal: (value) => fromTypeBox(Type.Literal(value)),
30
- // `t.object` REJECTS unknown fields by default (`additionalProperties: false`) — the trust-boundary
30
+ // `t.object` REJECTS unknown fields by default (`additionalProperties: false`) - the trust-boundary
31
31
  // rule. A body with extra keys fails validation (a 422), so `c.body` never carries attacker-supplied
32
32
  // properties (no mass-assignment). Use `t.looseObject` (or pass `{ additionalProperties: true }`) to
33
33
  // opt into an open object; an explicit `options.additionalProperties` always wins over the default.
34
34
  object: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: false, ...options })),
35
- /** Like `t.object` but ACCEPTS (and passes through) unknown fields — the explicit opt-out of the
35
+ /** Like `t.object` but ACCEPTS (and passes through) unknown fields - the explicit opt-out of the
36
36
  * strict default. Prefer `t.object` unless you genuinely need an open object. */
37
37
  looseObject: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: true, ...options })),
38
38
  array: (item, options) => fromTypeBox(Type.Array(item.jsonSchema, options)),
@@ -40,13 +40,13 @@ export const t = {
40
40
  optional: (schema) => fromTypeBox(Type.Optional(schema.jsonSchema)),
41
41
  // `const S` captures the argument as a tuple; the explicit return type then maps
42
42
  // over that captured tuple (`S[K]["jsonSchema"]`) so the union's `Static` is
43
- // `A | B`, not `unknown` — the value-level `.map` can't preserve per-element
43
+ // `A | B`, not `unknown` - the value-level `.map` can't preserve per-element
44
44
  // types, so the output type is derived from `S` and the result cast to match
45
45
  // (order/length are preserved by `map`, so the tuple shape is sound).
46
46
  union: (schemas) => fromTypeBox(Type.Union(schemas.map((schema) => schema.jsonSchema))),
47
47
  record: (value, options) => fromTypeBox(Type.Record(Type.String(), value.jsonSchema, options)),
48
48
  // Composed from TypeBox directly (not `t.object`/`t.array`) so the `t` literal doesn't reference
49
- // itself during inference. Cursor pagination — not OFFSET — is the production default: stable under
49
+ // itself during inference. Cursor pagination - not OFFSET - is the production default: stable under
50
50
  // concurrent inserts and O(1) per page. Build pages with `paginate()` + `encodeCursor`/`decodeCursor`.
51
51
  /** A cursor-pagination response envelope: `{ items: T[]; nextCursor: string | null }` (`null` = last page). */
52
52
  paginated: (item, options) => fromTypeBox(Type.Object({
@@ -54,7 +54,7 @@ export const t = {
54
54
  nextCursor: Type.Union([Type.String(), Type.Null()]),
55
55
  }, { additionalProperties: false, ...options })),
56
56
  /** A request query schema for cursor pagination: `{ cursor?: string; limit?: number }`. `maxLimit`
57
- * caps `limit` — a larger value fails validation (a 422), so a client can't request an unbounded page.
57
+ * caps `limit` - a larger value fails validation (a 422), so a client can't request an unbounded page.
58
58
  * `coerce` is on because query values arrive as strings (`?limit=20` → `"20"`); it's what makes `limit`
59
59
  * a real `number` in the handler (`c.query.limit`), not a string. */
60
60
  pageQuery: (options) => fromTypeBox(Type.Object({
@@ -64,7 +64,7 @@ export const t = {
64
64
  /** A request-query schema with string->scalar COERCION on. Query values always arrive as strings
65
65
  * (`?limit=20` -> `"20"`), so a plain `t.object({ limit: t.integer() })` in a `query` slot fails to
66
66
  * validate; use `t.query` and `t.integer()`/`t.number()`/`t.boolean()` fields become real numbers/
67
- * booleans in `c.query`. **Open by default** (unknown query fields pass through — `additionalProperties:
67
+ * booleans in `c.query`. **Open by default** (unknown query fields pass through - `additionalProperties:
68
68
  * true`): query params are read by name, not spread into a DB write, so unknown params (UTM tracking,
69
69
  * `fbclid`, etc.) passing through is safe, and rejecting them is a production-only footgun (ad/social
70
70
  * traffic appends params your schema never declares, causing 422s that never appear in dev/CI). Pass
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/schema",
3
- "version": "2.1.0",
4
- "description": "Built-in schema builder `t` — TypeBox-backed (free JSON Schema + compiled validators), implements Standard Schema so nifra routes validate it natively.",
3
+ "version": "2.3.0",
4
+ "description": "Built-in schema builder `t` - TypeBox-backed (free JSON Schema + compiled validators), implements Standard Schema so nifra routes validate it natively.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -35,7 +35,7 @@
35
35
  "dist",
36
36
  "src"
37
37
  ],
38
- "comment:sideEffects": "No module here runs work at import time: the standard string formats install lazily from the validate path (src/adapter.ts → src/formats.ts ensureDefaultFormats), not as a top-level import side effect. The previous `sideEffects: [formats]` array did not survive bundling — Bun.build ignores the field — which is why formats were dropped from prod bundles. `false` is accurate now and lets consumers tree-shake freely.",
38
+ "comment:sideEffects": "No module here runs work at import time: the standard string formats install lazily from the validate path (src/adapter.ts → src/formats.ts ensureDefaultFormats), not as a top-level import side effect. The previous `sideEffects: [formats]` array did not survive bundling - Bun.build ignores the field - which is why formats were dropped from prod bundles. `false` is accurate now and lets consumers tree-shake freely.",
39
39
  "sideEffects": false,
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
@@ -48,7 +48,7 @@
48
48
  "@sinclair/typebox": "^0.34.49"
49
49
  },
50
50
  "peerDependencies": {
51
- "@nifrajs/core": "^2.1.0"
51
+ "@nifrajs/core": "^2.3.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@nifrajs/core": "workspace:*",
package/src/adapter.ts CHANGED
@@ -11,7 +11,7 @@ import { ensureDefaultFormats } from "./formats.ts"
11
11
 
12
12
  /**
13
13
  * A `t` schema. It is a Standard Schema (so any nifra route validates it with no
14
- * special-casing) whose raw TypeBox definition stays reachable as `jsonSchema` —
14
+ * special-casing) whose raw TypeBox definition stays reachable as `jsonSchema` -
15
15
  * and because a TypeBox schema *is* a JSON Schema, that field is exactly what lets
16
16
  * `toOpenAPI` emit a real request/response schema for the route. BYO Standard
17
17
  * Schemas (zod/valibot/arktype) validate too but expose no JSON Schema, so only
@@ -39,13 +39,13 @@ function toIssue(error: { readonly message: string; readonly path: string }): St
39
39
  *
40
40
  * **Validation is fast where it can be, and works everywhere.** The first `validate`
41
41
  * builds a compiled `TypeCompiler` validator (codegen via `new Function`) and memoizes
42
- * it — composing schemas (`t.object({ a: t.string() })`) never compiles the inner
42
+ * it - composing schemas (`t.object({ a: t.string() })`) never compiles the inner
43
43
  * pieces; only a schema actually used to validate a request pays the one-time codegen
44
44
  * cost. On Bun and Node this is the only path and it is untouched.
45
45
  *
46
46
  * Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) **forbid dynamic code
47
47
  * generation**, so that first compile throws there. We catch it once per schema and
48
- * fall back to TypeBox's eval-free `Value` checker — same semantics (and same global
48
+ * fall back to TypeBox's eval-free `Value` checker - same semantics (and same global
49
49
  * `FormatRegistry`, so `t.string({ format })` behaves identically), no `new Function`.
50
50
  * The branch is decided once and memoized, so the hot path is a single check either way.
51
51
  *
@@ -54,7 +54,7 @@ function toIssue(error: { readonly message: string; readonly path: string }): St
54
54
  *
55
55
  * `options.coerce` runs TypeBox's `Value.Convert` (string→number/integer/boolean, per the schema)
56
56
  * BEFORE validating. Query values always arrive as strings (`?limit=20` → `"20"`), so a query schema
57
- * with a numeric field can't validate without this — it's how `t.pageQuery` yields a real `number`.
57
+ * with a numeric field can't validate without this - it's how `t.pageQuery` yields a real `number`.
58
58
  * Leave it OFF (the default) for body/JSON schemas: a JSON number is already a number, and coercing
59
59
  * would silently accept `"20"` where the contract said `20`.
60
60
  */
@@ -72,7 +72,7 @@ export function fromTypeBox<T extends TSchema>(
72
72
  validate: (value: unknown): StandardResult<Static<T>> => {
73
73
  // Install the standard string formats before the first Compile/Check. Driven from this
74
74
  // reachable path (not a top-level import side effect) so a production bundle can't
75
- // tree-shake the registration away — see ./formats.ts. Idempotent, ~free after first call.
75
+ // tree-shake the registration away - see ./formats.ts. Idempotent, ~free after first call.
76
76
  ensureDefaultFormats()
77
77
  // Coerce first when asked (query schemas): `Value.Convert` turns "20"→20 per the schema, so the
78
78
  // compiled/eval-free Check below sees the target type. A non-convertible value (e.g. "abc" for an
package/src/formats.ts CHANGED
@@ -36,7 +36,7 @@ let defaultsRegistered = false
36
36
  * This is driven from the validate path (see `./adapter.ts`) rather than run as a bare
37
37
  * top-level import side effect *on purpose*: a production bundler (`Bun.build`, esbuild)
38
38
  * tree-shakes a module that is only imported for its effect, and Bun.build does **not**
39
- * honor the `sideEffects` package.json field — so the old top-level registration loop was
39
+ * honor the `sideEffects` package.json field - so the old top-level registration loop was
40
40
  * dropped from prod server bundles, making every `t.string({ format })` reject with
41
41
  * "Unknown format" while unbundled `nifra dev` worked. Calling this from code the validator
42
42
  * actually reaches keeps the registration in the bundle.
package/src/index.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * `@nifrajs/schema` — the optional, batteries-included schema builder for nifra.
2
+ * `@nifrajs/schema` - the optional, batteries-included schema builder for nifra.
3
3
  *
4
4
  * `t` is TypeBox-backed: validators are compiled (fast) and every schema is a JSON
5
5
  * Schema (free OpenAPI). It implements Standard Schema, so `@nifrajs/core` validates
6
- * it through the exact same path as any BYO library — `@nifrajs/core` never depends on
6
+ * it through the exact same path as any BYO library - `@nifrajs/core` never depends on
7
7
  * a validator; this package is the opt-in default.
8
8
  */
9
9
  export { fromTypeBox, type NifraSchema } from "./adapter.ts"
10
10
  // `registerFormat` adds/overrides a custom string format. The standard formats (email, uuid, …)
11
- // install themselves lazily from the validate path (see ./adapter.ts) — NOT as an import side
11
+ // install themselves lazily from the validate path (see ./adapter.ts) - NOT as an import side
12
12
  // effect of this module, which a production bundler would tree-shake away.
13
13
  export { registerFormat } from "./formats.ts"
14
14
  export { type OpenAPIDocument, type OpenAPIInfo, toOpenAPI } from "./openapi.ts"
15
- // Cursor-pagination runtime helpers — pair with `t.paginated` / `t.pageQuery`.
15
+ // Cursor-pagination runtime helpers - pair with `t.paginated` / `t.pageQuery`.
16
16
  export { decodeCursor, encodeCursor, type Page, paginate } from "./pagination.ts"
17
17
  export { t } from "./t.ts"
package/src/openapi.ts CHANGED
@@ -9,13 +9,13 @@ import {
9
9
  import type { Server } from "@nifrajs/core/server"
10
10
 
11
11
  /**
12
- * OpenAPI 3.1 generation. We model a practical slice of the spec — enough to feed Swagger UI / codegen
12
+ * OpenAPI 3.1 generation. We model a practical slice of the spec - enough to feed Swagger UI / codegen
13
13
  * and to validate structurally: paths, parameters, request bodies, responses (incl. non-200 and
14
14
  * non-JSON), tags, security, servers, and `$ref` reuse via `components.schemas`.
15
15
  *
16
16
  * Schemas carry full detail only for `t`/TypeBox inputs (they expose a JSON Schema); a BYO Standard
17
17
  * Schema validates at runtime but exposes no JSON Schema, so its route is emitted without body/response
18
- * detail. A **contract** is richest — its operations carry `response`, `tags`, `security`, additional
18
+ * detail. A **contract** is richest - its operations carry `response`, `tags`, `security`, additional
19
19
  * `responses`, etc.; an **app** emits request shapes + a generic `200` (an inline handler has no
20
20
  * response *schema* to serialize), enrichable via `options.operations`.
21
21
  */
@@ -44,7 +44,7 @@ export interface ToOpenAPIOptions {
44
44
  readonly description?: string
45
45
  /** Server URLs the API is served from (OpenAPI `servers`). */
46
46
  readonly servers?: readonly OpenAPIServer[]
47
- /** Tag definitions (OpenAPI top-level `tags`) — names referenced by an operation's `tags`. */
47
+ /** Tag definitions (OpenAPI top-level `tags`) - names referenced by an operation's `tags`. */
48
48
  readonly tags?: readonly OpenAPITag[]
49
49
  /** Reusable security schemes → `components.securitySchemes` (e.g. `{ bearer: { type: "http", scheme: "bearer" } }`). */
50
50
  readonly securitySchemes?: Readonly<Record<string, Record<string, unknown>>>
@@ -53,7 +53,7 @@ export interface ToOpenAPIOptions {
53
53
  /**
54
54
  * Per-operation overrides, shallow-merged over the generated operation. Keyed by `operationId`
55
55
  * (contract op name) or `"METHOD /path"` (e.g. `"GET /users/:id"`). The escape hatch for detail that
56
- * can't be introspected — richer response bodies, examples, app-route tags/security.
56
+ * can't be introspected - richer response bodies, examples, app-route tags/security.
57
57
  */
58
58
  readonly operations?: Readonly<Record<string, Record<string, unknown>>>
59
59
  }
@@ -107,7 +107,7 @@ export interface OpenAPIDocument {
107
107
  }
108
108
 
109
109
  /**
110
- * Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place —
110
+ * Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place -
111
111
  * so a schema used by N operations is emitted once. Schemas without a `$id` stay inline (the existing
112
112
  * behavior). The first sighting of an id wins; later ones just `$ref` it.
113
113
  */
@@ -211,11 +211,11 @@ interface OperationInput {
211
211
  readonly path: string
212
212
  readonly body: SchemaReflection | undefined
213
213
  readonly query: SchemaReflection | undefined
214
- /** Reflected params schema — per-field constraints merge into path parameters. */
214
+ /** Reflected params schema - per-field constraints merge into path parameters. */
215
215
  readonly params: SchemaReflection | undefined
216
216
  readonly response: SchemaReflection | undefined
217
217
  readonly operationId: string | undefined
218
- // `| undefined` (not just `?`) so a contract op's optional fields — `string | undefined` etc. — are
218
+ // `| undefined` (not just `?`) so a contract op's optional fields - `string | undefined` etc. - are
219
219
  // assignable under `exactOptionalPropertyTypes` when spread into this literal.
220
220
  readonly summary?: string | undefined
221
221
  readonly description?: string | undefined
@@ -356,9 +356,9 @@ export function toOpenAPI(
356
356
  body: route.schema?.body,
357
357
  query: route.schema?.query,
358
358
  params: route.schema?.params,
359
- // A route may now declare a `response` contract — emit it as the 200 body schema.
359
+ // A route may now declare a `response` contract - emit it as the 200 body schema.
360
360
  response: route.schema?.response,
361
- // …and an `errors` contract — emit each as a non-2xx response.
361
+ // …and an `errors` contract - emit each as a non-2xx response.
362
362
  responses: reflectedErrorsToResponses(route.schema?.errors),
363
363
  operationId: undefined,
364
364
  },
package/src/pagination.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Cursor-pagination runtime helpers — an opaque cursor codec + a page builder. Pair with `t.paginated`
2
+ * Cursor-pagination runtime helpers - an opaque cursor codec + a page builder. Pair with `t.paginated`
3
3
  * (the response schema) and `t.pageQuery` (the request schema).
4
4
  *
5
5
  * Edge-safe: `btoa`/`atob` + `TextEncoder`/`TextDecoder` (no `Buffer`), so it runs on Bun/Node/Deno/
6
- * Workers. Cursors are opaque but NOT signed — treat a decoded cursor as untrusted client input: validate
6
+ * Workers. Cursors are opaque but NOT signed - treat a decoded cursor as untrusted client input: validate
7
7
  * its shape before keying a DB query off it (it's a position hint, not an authorization token).
8
8
  */
9
9
 
@@ -27,7 +27,7 @@ export function encodeCursor(value: unknown): string {
27
27
  return toBase64Url(JSON.stringify(value))
28
28
  }
29
29
 
30
- /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor — treat that
30
+ /** Decode a cursor back to its value. Returns `undefined` for a null/empty/malformed cursor - treat that
31
31
  * as "start from the beginning" rather than erroring on a client-supplied string. */
32
32
  export function decodeCursor<T = unknown>(cursor: string | null | undefined): T | undefined {
33
33
  if (cursor === null || cursor === undefined || cursor === "") return undefined
@@ -38,7 +38,7 @@ export function decodeCursor<T = unknown>(cursor: string | null | undefined): T
38
38
  }
39
39
  }
40
40
 
41
- /** A cursor-pagination page — matches the shape of `t.paginated(item)`. */
41
+ /** A cursor-pagination page - matches the shape of `t.paginated(item)`. */
42
42
  export interface Page<Item> {
43
43
  readonly items: Item[]
44
44
  readonly nextCursor: string | null
package/src/t.ts CHANGED
@@ -22,7 +22,7 @@ function unwrap<P extends Props>(props: P): { [K in keyof P]: P[K]["jsonSchema"]
22
22
  }
23
23
 
24
24
  /**
25
- * The built-in schema builder. Each constructor returns a `NifraSchema` — a
25
+ * The built-in schema builder. Each constructor returns a `NifraSchema` - a
26
26
  * Standard Schema whose validated output type flows into `c.body`/`c.query`, and
27
27
  * whose `jsonSchema` powers `toOpenAPI`. Options (min/max, length, pattern, …)
28
28
  * pass straight through to TypeBox and so become JSON Schema constraints.
@@ -41,13 +41,13 @@ export const t = {
41
41
  null: () => fromTypeBox(Type.Null()),
42
42
  literal: <const L extends TLiteralValue>(value: L) => fromTypeBox(Type.Literal(value)),
43
43
 
44
- // `t.object` REJECTS unknown fields by default (`additionalProperties: false`) — the trust-boundary
44
+ // `t.object` REJECTS unknown fields by default (`additionalProperties: false`) - the trust-boundary
45
45
  // rule. A body with extra keys fails validation (a 422), so `c.body` never carries attacker-supplied
46
46
  // properties (no mass-assignment). Use `t.looseObject` (or pass `{ additionalProperties: true }`) to
47
47
  // opt into an open object; an explicit `options.additionalProperties` always wins over the default.
48
48
  object: <P extends Props>(props: P, options?: ObjectOptions) =>
49
49
  fromTypeBox(Type.Object(unwrap(props), { additionalProperties: false, ...options })),
50
- /** Like `t.object` but ACCEPTS (and passes through) unknown fields — the explicit opt-out of the
50
+ /** Like `t.object` but ACCEPTS (and passes through) unknown fields - the explicit opt-out of the
51
51
  * strict default. Prefer `t.object` unless you genuinely need an open object. */
52
52
  looseObject: <P extends Props>(props: P, options?: ObjectOptions) =>
53
53
  fromTypeBox(Type.Object(unwrap(props), { additionalProperties: true, ...options })),
@@ -58,7 +58,7 @@ export const t = {
58
58
  fromTypeBox(Type.Optional(schema.jsonSchema)),
59
59
  // `const S` captures the argument as a tuple; the explicit return type then maps
60
60
  // over that captured tuple (`S[K]["jsonSchema"]`) so the union's `Static` is
61
- // `A | B`, not `unknown` — the value-level `.map` can't preserve per-element
61
+ // `A | B`, not `unknown` - the value-level `.map` can't preserve per-element
62
62
  // types, so the output type is derived from `S` and the result cast to match
63
63
  // (order/length are preserved by `map`, so the tuple shape is sound).
64
64
  union: <const S extends readonly NifraSchema[]>(schemas: S) =>
@@ -69,7 +69,7 @@ export const t = {
69
69
  fromTypeBox(Type.Record(Type.String(), value.jsonSchema, options)),
70
70
 
71
71
  // Composed from TypeBox directly (not `t.object`/`t.array`) so the `t` literal doesn't reference
72
- // itself during inference. Cursor pagination — not OFFSET — is the production default: stable under
72
+ // itself during inference. Cursor pagination - not OFFSET - is the production default: stable under
73
73
  // concurrent inserts and O(1) per page. Build pages with `paginate()` + `encodeCursor`/`decodeCursor`.
74
74
  /** A cursor-pagination response envelope: `{ items: T[]; nextCursor: string | null }` (`null` = last page). */
75
75
  paginated: <T extends TSchema>(item: NifraSchema<T>, options?: ObjectOptions) =>
@@ -83,7 +83,7 @@ export const t = {
83
83
  ),
84
84
  ),
85
85
  /** A request query schema for cursor pagination: `{ cursor?: string; limit?: number }`. `maxLimit`
86
- * caps `limit` — a larger value fails validation (a 422), so a client can't request an unbounded page.
86
+ * caps `limit` - a larger value fails validation (a 422), so a client can't request an unbounded page.
87
87
  * `coerce` is on because query values arrive as strings (`?limit=20` → `"20"`); it's what makes `limit`
88
88
  * a real `number` in the handler (`c.query.limit`), not a string. */
89
89
  pageQuery: (options?: { maxLimit?: number }) =>
@@ -100,7 +100,7 @@ export const t = {
100
100
  /** A request-query schema with string->scalar COERCION on. Query values always arrive as strings
101
101
  * (`?limit=20` -> `"20"`), so a plain `t.object({ limit: t.integer() })` in a `query` slot fails to
102
102
  * validate; use `t.query` and `t.integer()`/`t.number()`/`t.boolean()` fields become real numbers/
103
- * booleans in `c.query`. **Open by default** (unknown query fields pass through — `additionalProperties:
103
+ * booleans in `c.query`. **Open by default** (unknown query fields pass through - `additionalProperties:
104
104
  * true`): query params are read by name, not spread into a DB write, so unknown params (UTM tracking,
105
105
  * `fbclid`, etc.) passing through is safe, and rejecting them is a production-only footgun (ad/social
106
106
  * traffic appends params your schema never declares, causing 422s that never appear in dev/CI). Pass