# shelving Shelving is a TypeScript toolkit for working with typed data. At its core it is a schema validation library — every schema has a `Schema.validate()` method that returns a typed value or throws a human-readable error. On top of that it provides a database provider abstraction, an API provider abstraction, observable state stores, React integration, and a large set of typed utility functions. > Note: Shelving is in active development and does not yet follow semver. ## Installation ```sh npm install shelving ``` Shelving is an ES module. Import from the main package or from individual module subpaths: ```ts import { STRING, DataSchema } from "shelving" import { MemoryDBProvider } from "shelving/db" ``` ## For AI agents The docs are also published as Markdown for LLMs and coding agents: - is the index. has every page in one file. - Add `.md` to any docs page path to get that page as Markdown, for example . - The npm package includes the same files in `node_modules/shelving/docs/`, so an agent can read the docs for the installed version without network access. - The npm package also includes an [Agent Skill](https://agentskills.io/) at `node_modules/shelving/skills/shelving/SKILL.md`. It tells an agent where the docs are and which conventions to follow. To use it with Claude Code, link it into your project: ```sh mkdir -p .claude/skills && ln -s ../../node_modules/shelving/skills/shelving .claude/skills/shelving ``` ## Changelog See [Releases](https://github.com/dhoulb/shelving/releases) on GitHub. ## Modules ## Pages - [Guides](guides.md): Guides that explain how to use Shelving in a real project, and how to write code in the Shelving style. ## Modules - [shelving/api](api.md): Typed, provider-based framework for HTTP API access. Define your routes as Endpoint definitions, then call them through a composable provider stack — the same pattern the shelving/db module uses for databases. - [shelving/bun](bun.md): DBProvider implementation for PostgreSQL using Bun's built-in Bun.sql API. No external database driver is required. - [shelving/cloudflare](cloudflare.md): DBProvider implementations for Cloudflare Workers. Two backends are available: Workers KV for simple key-value storage and D1 for relational SQL. - [shelving/db](db.md): Typed database provider abstraction. Define your schema once as a Collection, then swap providers (in-memory, SQLite, Firestore, Cloudflare D1, …) without changing any call sites. Providers are composable wrappers — add validation, logging, or caching by stacking them in a chain. - [shelving/error](error.md): Typed error classes for system and transport failures. - [shelving/extract](extract.md): Turn a folder of source files into a navigable tree of documentation. extract is the engine behind the Shelving documentation site itself. - [shelving/firebase](firebase.md): FirestoreProvider — a Cloud Firestore DBProvider that talks directly to the Firestore REST API using fetch. - [shelving/icon/AddIcon](icon/AddIcon.md) - [shelving/icon/CloseIcon](icon/CloseIcon.md) - [shelving/icon/DownIcon](icon/DownIcon.md) - [shelving/icon/ErrorIcon](icon/ErrorIcon.md) - [shelving/icon/ExpandIcon](icon/ExpandIcon.md) - [shelving/icon/InfoIcon](icon/InfoIcon.md) - [shelving/icon/LeftIcon](icon/LeftIcon.md) - [shelving/icon/LoadingIcon](icon/LoadingIcon.md) - [shelving/icon/MenuIcon](icon/MenuIcon.md) - [shelving/icon/RemoveIcon](icon/RemoveIcon.md) - [shelving/icon/RetryIcon](icon/RetryIcon.md) - [shelving/icon/RightIcon](icon/RightIcon.md) - [shelving/icon/ShrinkIcon](icon/ShrinkIcon.md) - [shelving/icon/SuccessIcon](icon/SuccessIcon.md) - [shelving/icon/UpIcon](icon/UpIcon.md) - [shelving/icon/WarningIcon](icon/WarningIcon.md) - [shelving/markup](markup.md): A rule-based Markdown renderer that turns user-facing text into React nodes. - [shelving/react](react.md): React hooks and context helpers for integrating Shelving shelving/store, async sequences, and API/DB providers into React components. The module is built on useSyncExternalStore and standard React patterns — no magic, no global state. - [shelving/schema](schema.md): Schema validation for TypeScript. A Schema describes how to coerce and validate an unknown value into a typed T. Schemas are the foundation of shelving — database collections, API endpoints, and form handlers all build on them. - [shelving/sequence](sequence.md): Async-iterator primitives used throughout the shelving library. A "sequence" is an AsyncIterable — something you can consume with for await...of. This module provides the building blocks for producing and connecting sequences, most importantly DeferredSequence, which lets external code drive an async iterator by resolving or rejecting it on demand. - [shelving/store](store.md): Observable value containers for reactive state. A Store holds a single current value, broadcasts changes to all active consumers, and integrates with React Suspense out of the box. Stores suppress duplicate emissions using deep equality, so consumers only see genuine changes. - [shelving/test](test.md): Shared test fixtures and assertion helpers for Shelving's own unit tests. - [shelving/ui](ui.md): A React component library for building Shelving apps — forms, content, layout, routing, dialogs, and the documentation-site components, all in one place. - [shelving/util/ansi](util/ansi.md): Constants and a helper for wrapping terminal output in ANSI escape codes. Use these when writing CLI tools or test reporters that need colour and style without pulling in a third-party library. - [shelving/util/array](util/array.md): Typed helpers for reading, validating, and immutably transforming arrays. All immutable operations return the original reference when nothing changed, which keeps React state and memoisation stable. - [shelving/util/async](util/async.md): Helpers for working with promises and async values: detecting sync/async values, running concurrent promises safely, creating deferreds, and handling cancellation with AbortSignal. - [shelving/util/base64](util/base64.md): Encode and decode data to and from standard Base64 and URL-safe Base64 (Base64URL). Useful for binary payloads, JWTs, cryptographic keys, and any context that needs ASCII-safe binary representation. - [shelving/util/boolean](util/boolean.md): Type guards and assertion functions for boolean values. These follow the standard is* / assert* naming convention and pair naturally with schema validation and runtime checks. - [shelving/util/buffer](util/buffer.md): Type guards for ArrayBuffer, ArrayBufferView, and typed arrays. Use these when a function receives an unknown binary value and needs to distinguish between a raw buffer and its various view types. - [shelving/util/bytes](util/bytes.md): Type guards, conversion, and assertion for Uint8Array byte sequences. Used throughout the library wherever raw binary data is accepted — cryptographic operations, Base64 encoding, and binary I/O. - [shelving/util/class](util/class.md): Type utilities and runtime helpers for working with class constructors and instances. Useful when you need to check whether a value is a constructor, whether an object is an instance of a particular class, or when you need to inspect property descriptors. - [shelving/util/color](util/color.md): A Color class and accompanying helpers for parsing, representing, and converting CSS hex colours. Useful for theming, accessibility checks, and anywhere a component or tool needs to inspect or manipulate a colour value. - [shelving/util/constants](util/constants.md): Named constants for common numeric magnitudes, time durations, whitespace characters, and control symbols. Import these instead of magic numbers to keep code readable and consistent. - [shelving/util/crypto](util/crypto.md): Secure password hashing and verification using PBKDF2-SHA-512, plus a helper for generating cryptographically random bytes. Uses the Web Crypto API so it works in browsers, Node.js, and edge runtimes without additional dependencies. - [shelving/util/currency](util/currency.md): Validate ISO 4217 currency codes and retrieve display metadata (symbol, smallest unit step) using the runtime's built-in Intl API. The list of supported codes comes from Intl.supportedValuesOf("currency") so it stays current without a bundled lookup table. - [shelving/util/data](util/data.md): The Data type and its helpers are the foundation of Shelving's data layer. A Data object is a plain Record — a JSON-safe object with no class prototype. Every database document, query result, and schema-validated value is a Data. - [shelving/util/date](util/date.md): Parse, validate, format, and manipulate dates. The central idea is PossibleDate — a value that could be a date — which the helpers accept everywhere so you rarely need to call new Date() yourself. - [shelving/util/debug](util/debug.md) - [shelving/util/dictionary](util/dictionary.md): Typed helpers for working with { [key: string]: T } objects — called dictionaries to distinguish them from the more permissive Data type. Mirrors the naming conventions of the array and object helpers: with* for immutable updates, set*/delete* for mutable by-reference mutations. - [shelving/util/diff](util/diff.md): Compute the minimal transformation needed to turn one value into another. Used internally by the update and sync layers to avoid sending unchanged data. Returns the special SAME symbol when the two values are deeply equal. - [shelving/util/dispose](util/dispose.md) - [shelving/util/duration](util/duration.md): These helpers measure the distance between two dates — in milliseconds, seconds, minutes, hours, days, weeks, months, or years — and format that distance for display. They exist so callers never have to do raw date arithmetic or wrangle Intl.DurationFormat directly. - [shelving/util/element](util/element.md): These types and helpers describe a React-compatible element structure (type, props, key) and the generic tools for walking, filtering, and flattening collections of them. The tree-shaped layer built on top of Element — TreeElement, path resolution, and tree flattening — lives in a sibling file; see shelving/util/tree. - [shelving/util/entity](util/entity.md): An entity is a compact "type:id" string that encodes both the kind of resource and its identifier in one value, e.g. "challenge:a1b2c3". These helpers parse and validate those strings so callers never need to split on : themselves. - [shelving/util/entry](util/entry.md): An entry is a readonly [key, value] pair — the shape produced by Map.entries(), Object.entries(), and Array.entries(). These helpers provide a uniform way to iterate, extract, and type entries across all of those sources. - [shelving/util/env](util/env.md): These helpers read process.env safely across Node, edge runtimes, and browser environments, where process may not exist. They follow the standard get* / require* pattern: get* returns undefined on miss, require* throws RequiredError. - [shelving/util/equal](util/equal.md): Strict and structural equality predicates used throughout the library. The three tiers — exact, shallow, and deep — map to the same left is T type-guard pattern so they can be used interchangeably anywhere a Match function is expected. - [shelving/util/error](util/error.md): Low-level helpers for working with error messages as plain strings. They exist to give call-site code a uniform way to extract, split, join, and annotate messages without caring whether the source is an Error object, a plain string, or any other value. - [shelving/util/file](util/file.md): Tiny helpers for splitting filenames into base name and extension. They exist so callers never need to hand-roll lastIndexOf(".") logic or handle the dotfile edge case themselves. - [shelving/util/filter](util/filter.md): Lightweight generic filter utilities for synchronous iterables, arrays, and async sequences. They exist to avoid having to repeat the for…of / if match / yield pattern everywhere a match function is used as a predicate. - [shelving/util/focus](util/focus.md): Browser utility for locating the first focusable element inside a DOM subtree. Useful when you need to programmatically move keyboard focus into a dialog, panel, or other container after it becomes visible. - [shelving/util/format](util/format.md): A collection of helpers for converting typed values into user-readable strings. All formatters respect the browser's locale by default and accept an optional locale override — they delegate to the Intl APIs where possible so internationalisation and translation are handled automatically. - [shelving/util/function](util/function.md): Core type definitions and small utilities for working with functions. This file is the source of the AnyCaller type that threads through the whole library to produce accurate stack traces, and provides BLACKHOLE() and PASSTHROUGH() — two no-op sentinels used throughout as safe default callbacks. - [shelving/util/geo](util/geo.md): Country codes, names, and address formatting. Provides the COUNTRIES lookup (ISO 3166-1 alpha-2), typed Country and AddressData types, and helpers for parsing and displaying country information. - [shelving/util/hash](util/hash.md): Lightweight, deterministic string-to-number hashing. Useful for consistently mapping a string to a bucket — for example picking a stable colour, avatar, or index for a user or entity name without storing anything. - [shelving/util/http](util/http.md): Typed utilities for building and parsing Request and Response objects using the standard Fetch API. The helpers handle content-type negotiation, body serialisation, and error-to-response conversion so you don't have to repeat that logic in every endpoint or client. - [shelving/util/hydrate](util/hydrate.md) - [shelving/util/index](util/index.md) - [shelving/util/item](util/item.md): The Item type and its helpers. An Item is a Data object — a plain JSON-safe record — that also carries an id of type string | number. Every database collection in Shelving operates on items, so this is the fundamental unit of persisted data. - [shelving/util/iterate](util/iterate.md): General-purpose utilities for working with any Iterable — arrays, sets, generators, and custom iterables alike. These complement the array-specific helpers by operating lazily on any sequence without forcing materialisation. - [shelving/util/jwt](util/jwt.md): Encode, decode, and verify JSON Web Tokens signed with HMAC SHA-512. Also provides convenience helpers for reading and writing Bearer tokens on Request objects. - [shelving/util/lazy](util/lazy.md) - [shelving/util/link](util/link.md): Resolve any link-shaped value — a site path, relative reference, or full URI — into an absolute URI string. Useful when building components or routing logic that must handle all three forms uniformly. - [shelving/util/log](util/log.md): Three thin wrappers around console.log/console.error that format HTTP Request and Response objects with ANSI arrow decorators. Intended for server-side request logging in development and production. - [shelving/util/map](util/map.md): Type definitions and helper functions for working with Map instances. Covers type guards, conversions from plain objects or iterables, mutable set/remove operations, and safe key lookups. - [shelving/util/merge](util/merge.md): Immutably merge two values — objects, arrays, or primitives. All merge functions return the left reference unchanged when nothing actually changed, making them safe to use in memoised or reactive contexts. - [shelving/util/null](util/null.md): Type guards, assertions, and utility types for null and nullish (null | undefined) values. Use these instead of inline === null checks to get proper TypeScript type narrowing and consistent error messages. - [shelving/util/number](util/number.md): Type guards, conversions, rounding, clamping, and arithmetic utilities for numbers. The conversion functions (getNumber(), getInteger()) accept strings and Date instances as well as raw numbers, and always return a finite value or undefined — never NaN or Infinity. - [shelving/util/object](util/object.md): Core immutable helpers for reading and updating plain objects, plus utility types for mutable/readonly/partial transformations. These are the building blocks for safe state management — every mutating helper returns the original reference when nothing changed. - [shelving/util/path](util/path.md): Typed helpers for working with filesystem and URL paths. Used throughout the router to represent and manipulate AbsolutePath values — strings that always start with /. - [shelving/util/query](util/query.md): A Query is a plain object that describes filters, ordering, and a result limit for a set of data items. These helpers parse, apply, and paginate queries — the same Query type that shelving/db uses for all collection reads. - [shelving/util/random](util/random.md): Small utilities for generating random numbers, characters, and short human-readable keys. Useful for placeholder IDs, test data, and UI token generation. - [shelving/util/regexp](util/regexp.md): Typed wrappers and combinators for working with regular expressions. They let you test, escape, compose, and extract matches with cleaner types than the raw RegExp API. - [shelving/util/sequence](util/sequence.md): Helpers for working with AsyncIterable sequences — repeating, driving, merging, and side-effecting streams of values. Used internally for real-time database subscriptions and polling loops. - [shelving/util/serialise](util/serialise.md) - [shelving/util/set](util/set.md): Typed helpers for working with JavaScript Set objects. Covers type guards, conversion, membership checks, and mutable add/delete operations. - [shelving/util/sort](util/sort.md): A quicksort implementation and type-aware comparators that handle mixed-type values gracefully. Used internally by shelving/util/query when applying $order to items. - [shelving/util/source](util/source.md): Two helpers for traversing a chain of Sourceable objects — objects with a .source property — and returning the first instance of a given class. Used to unwrap layered or decorated objects back to their underlying implementation. - [shelving/util/start](util/start.md): Manage processes that have a start callback and an optional stop callback. Starter prevents double-starts and wraps errors, making it easy to build safe, disposable lifecycle objects. - [shelving/util/string](util/string.md): Core helpers for checking, converting, sanitising, and transforming strings. Used throughout Shelving wherever user input is cleaned or URLs are generated. - [shelving/util/template](util/template.md): Match placeholders out of a string, or render values into one. Useful when a full regular expression is overkill and you just want to extract named segments from a path or pattern. - [shelving/util/timeout](util/timeout.md): Timeout is a thin wrapper around setTimeout that tracks its own reference and cancels any in-flight timer before setting a new one. It removes the boilerplate of saving and clearing timeout handles manually. - [shelving/util/transform](util/transform.md): Map and transform values across arrays, objects, dictionaries, and async sequences. These are higher-order utilities that apply a function to every element or property of a collection. - [shelving/util/tree](util/tree.md): These types and helpers describe a tree of elements — a hierarchical structure built on top of shelving/util/element, used to represent a documentation site's content hierarchy. The shelving/extract module builds trees of TreeElement nodes from source files; the shelving/ui component renders them. - [shelving/util/types](util/types.md) - [shelving/util/undefined](util/undefined.md): Small set of guards and assertions for undefined. Useful in filter pipelines and for writing intent-revealing code where a plain === undefined check would be less clear. - [shelving/util/units](util/units.md): These helpers model physical units of measure — mass, length, speed, area, volume, temperature, angles, percentages, and more — and let you convert amounts between units and format them for display. They exist so callers never have to hard-code conversion factors or wrangle Intl.NumberFormat unit options directly. - [shelving/util/update](util/update.md): Updates is the mutation language for Shelving data objects. An updates object uses encoded key prefixes to describe how each field should change — set, increment, or add/remove array items — and updateData() applies those changes immutably. This is the format consumed by the shelving/db layer when writing documents. - [shelving/util/uri](util/uri.md): These helpers work with any valid URI — including non-hierarchical ones like urn:isbn:... or mailto: — and provide typed wrappers, query-param manipulation, and safe conversion utilities. They exist because the built-in URL class has loose typing and is mutable, which can cause subtle bugs. - [shelving/util/url](util/url.md): These helpers work specifically with hierarchical scheme://host URLs (a strict subset of URIs), and add path-matching, base-URL resolution, and active/proud link detection. They exist to handle the common navigation and routing tasks that plain URI helpers do not cover. - [shelving/util/uuid](util/uuid.md): Three small helpers for generating and validating UUIDs. They exist to provide a normalised, dash-free UUID format and a safe way to parse untrusted input strings. - [shelving/util/validate](util/validate.md): These helpers define the Validator interface and utility functions for applying validators to single values, arrays, dictionaries, and full data objects. They exist to give all Shelving validators a common contract and to collect field-level errors before throwing — rather than stopping at the first failure. - [shelving/util/xml](util/xml.md): Two small helpers for producing XML strings from data objects. They exist for cases where you need to serialise a plain data structure to XML without pulling in a full XML library. --- # Guides Guides that explain how to use Shelving in a real project, and how to write code in the Shelving style. ## Pages - [Building with Shelving](guides/building-with-shelving.md): A guide to structuring a full-stack TypeScript project on Shelving. It describes a proven layout for a project with a React frontend, a Bun HTTP API, and a shared contract package between them — where Shelving provides the schema system, endpoint contracts, request handling, data providers, and UI component library. - [Code styleguide](guides/styleguide.md): The canonical code style for the shelving codebase and for any project built in its style. AGENTS.md signposts this file — read it fully before writing any code. Rules are stated as requirements — deviate only with a stated reason. --- # Building with Shelving A guide to structuring a full-stack TypeScript project on [Shelving](https://github.com/dhoulb/shelving). It describes a proven layout for a project with a React frontend, a Bun HTTP API, and a shared contract package between them — where Shelving provides the schema system, endpoint contracts, request handling, data providers, and UI component library. Follow the file and folder names in this guide exactly. The layout works because every kind of file has exactly one place it can live, and because the client and server communicate only through typed contracts defined in a shared package. ## Principles 1. **Three top-level packages.** All source code lives in `shared/`, `api/`, or `app/`. `shared/` is the contract layer; `api/` is the server; `app/` is the client. 2. **The contract lives in `shared/`.** Database schemas, endpoint definitions, domain logic, and utilities used by both sides are defined once in `shared/` and imported by both `api/` and `app/`. Neither side ever redeclares a type or a validation rule the other side also needs. 3. **Schema-first.** Data shapes are defined as Shelving schemas (runtime validators), and TypeScript types are *derived* from them with `ValidatorType`. You never write a type by hand and a validator separately. 4. **Hard boundaries, mechanically enforced.** `app/` must never import from `api/`, and `api/` must never import from `app/`. This is enforced by the linter, not by convention (see [Boundaries](#boundaries)). 5. **Dependency injection via a context object, owned by the composition root.** Handlers and services never construct their own providers (database, external APIs). They receive a single context object, so the same code runs against real providers in production, debug/in-memory providers locally, and mock providers in tests. The full context type (`APIContext`) is defined in `api/` — never in `shared/`. Shared code stays generic over a context type `C`, and declares only small named context slices — reusable ones like `DatabaseContext` in `shared/util/context.ts`, adapter-specific ones alongside each adapter (see [Dependency injection](#dependency-injection-context-slices-and-the-composition-root)). 6. **Group by domain, not by file type.** Feature code sits in a folder named after the domain entity it handles, with its components, styles, helpers, and tests together. 7. **Every code PR gets a disposable preview environment.** CI deploys a full app + API stack per pull request at deterministic URLs, and tears it down when the PR closes. Reviewing against a real deployment — not just reading the diff — is a core part of the workflow (see [CI](#ci-the-test-gate-preview-environments-and-deployment)). 8. **Document the repo for agents.** An `AGENTS.md` at the repo root is the operating manual LLM coding agents read before working; keeping it current is part of the definition of done (see [AGENTS.md](#agentsmd--make-the-repo-legible-to-llms)). ## Top-level layout ``` project/ ├── shared/ # Contract layer: schemas, endpoints, domain logic, shared utils │ ├── collection/ # Database collection schemas + derived types │ ├── endpoint/ # Typed endpoint contracts (URL + request/response schemas) │ ├── config/ # Shared configuration data (e.g. form definitions) built from schemas │ ├── integration/ # Clients/adapters for external third-party APIs │ ├── util/ # Generic shared utilities (each file = one topic) │ ├── img/ # Shared static images │ └── font/ # Shared font files + their @font-face CSS ├── api/ # Server package │ ├── start.ts # Local dev server entry (Bun.serve) │ ├── worker.ts # Production serverless/edge entry │ ├── index.ts # Request dispatch + top-level error handling │ ├── handlers/ # Endpoint handlers (thin: attach services to endpoints) │ ├── services/ # Domain behaviour (the actual work) │ ├── config/ # Server-side configuration (processors, mappings) │ └── util/ # Server-only utilities: DI context, secrets env, middleware, test helpers ├── app/ # Client package │ ├── index.html # HTML shell (production build input) │ ├── start.html # HTML shell (local dev, without third-party scripts) │ ├── index.tsx # React root mount │ ├── App.tsx # Top-level app: route table + global providers │ ├── start.ts # Local dev server entry (serves SPA + static assets) │ ├── style.css # Global stylesheet entry │ ├── util/ # Client-only utilities (API client, app context) │ └── / # One folder per functional area (see below) ├── e2e/ # Playwright end-to-end tests (bun:test based) ├── scripts/ # Build/CI helper scripts ├── docs/ # Project documentation ├── .github/ │ └── workflows/ # CI: test gate + per-PR preview deploy, production deploy, smoke, preview teardown ├── AGENTS.md # Operating manual for LLM coding agents (see the AGENTS.md section) ├── CLAUDE.md # One line: points agents at AGENTS.md ├── global.d.ts # Ambient module declarations (CSS modules, image imports) ├── tsconfig.json # Includes the path aliases for shared/* ├── biome.json # Lint/format config, including the boundary rules ├── package.json # Scripts follow the test:* / build:* / start:* pattern └── test.env # Committed fake-but-valid env for deterministic test runs ``` ## The `shared/` package `shared/` is imported by both `api/` and `app/`, so **everything in it must be safe to ship to the browser**. No secrets, no server-only dependencies. ### `shared/collection/` — database schemas One file per collection, plus a barrel `index.ts`. Each file defines: 1. The collection schema with `COLLECTION(...)` from `shelving`. 2. Derived variants with `.pick(...)` etc. (e.g. the "create" payload). 3. Response-shape schemas with `DATA(...)`, built from the collection's own result schemas (`ORDER.item`, `ORDER.items`, `ORDER.queryItems`) so the client can hydrate its local database from them. 4. A function for each query that the server answers in full (e.g. `getMyOrdersQuery()`), so the server and the client build the exact same query. 5. The derived TypeScript types via `ValidatorType`. ```ts // shared/collection/order.ts import { COLLECTION, DATA, DICTIONARY, OPTIONAL, type Query, STRING, StringSchema, TIMESTAMP, UNKNOWN, type ValidatorType } from "shelving"; /** Represent an order in the database. */ export const ORDER = COLLECTION("order", STRING, { user: STRING, form: new StringSchema({ title: "Form", min: 1 }), date: TIMESTAMP, data: DICTIONARY(UNKNOWN), }); export type Order = ValidatorType; export type OrderItem = ValidatorType; /** Represent an order that's being created. */ export const CREATE_ORDER = ORDER.pick("form", "data"); export type CreateOrder = ValidatorType; /** Response type for a submitted order. */ export const ORDER_RESULT = DATA({ order: ORDER.item, redirect: OPTIONAL(STRING) }); export type OrderResult = ValidatorType; /** Response type for my orders: the query it answers, and every order that matches it. */ export const MY_ORDERS_RESULT = DATA({ orders: ORDER.queryItems }); /** The query that `MY_ORDERS_RESULT` answers. The server and the client both call this. */ export function getMyOrdersQuery(user: string): Query { return { user, $order: "!date", $limit: 20 }; } ``` Rules: - Schema constants are `UPPER_SNAKE_CASE`; derived types are `PascalCase` with the same name. - The type is always derived from the schema — never written independently. - `shared/collection/index.ts` is a barrel: `export * from "./order.js";`, one line per module, alphabetically sorted. - Results that return rows use the collection's own schemas, so `HydratingAPIProvider` can find the collection: - `ORDER.item` for one row. `NULLABLE(ORDER.item)` deletes the row on `null`, using the `id` from the payload. - `ORDER.items` for a list of rows. Never `ARRAY(ORDER.item)`: it validates the same value, but it does not hydrate. - `ORDER.queryItems` for `{ query, items }`, when `items` is the complete answer to `query`. The client then knows the query has loaded, even with no rows. - `DATA({ orders: ORDER.items, people: PERSON.items })` to return several collections in one response (one level deep). - Build each `queryItems` query in one shared function. Query times match by `JSON.stringify(query)`, so the server and the client must write the keys in the same order. - The `query` in a `queryItems` value is validated in full by `ORDER.query`: unknown keys and bad values throw. ### `shared/endpoint/` — endpoint contracts One file per domain area, plus a barrel `index.ts`. An endpoint is a **value** created with `GET(...)`/`POST(...)` from `shelving`, binding a URL pattern to a request schema and a response schema: ```ts // shared/endpoint/order.ts import { CREATE_ORDER, ORDER_RESULT } from "shared/collection"; import { POST } from "shelving"; export const ORDER_ENDPOINT = POST("/order", CREATE_ORDER, ORDER_RESULT); ``` This single constant is the entire client/server contract: - The **server** imports it and attaches a handler: `ORDER_ENDPOINT.handler(...)`. - The **client** imports it and calls it through the API provider — with the request and response fully typed and validated on both ends. Rules: - Endpoint constants are named `_ENDPOINT`. - Endpoint files contain only endpoint definitions — no logic. - Request/response schemas come from `shared/collection` (or `DATA(...)` shapes defined alongside them); endpoints never define ad-hoc inline shapes. ### `shared/config/` — shared configuration data Configuration that both sides need — e.g. multi-step form definitions built from schema classes. Each config area is a folder with an `index.ts` barrel that also asserts completeness: ```ts // shared/config//index.ts export const FORMS = { alpha, beta, } satisfies ImmutableDictionary; ``` The `satisfies` clause is the pattern to copy: barrels that aggregate config use it to type-check that every entry conforms (and, on the API side, that every shared config entry has a matching server-side counterpart — see `api/config/` below). ### `shared//` — shared domain logic A domain area that needs classes/logic on both sides (e.g. a schema subclass for multi-step forms plus a processor that validates and forwards submissions) gets its own folder: one class per file, named after the class (`FooSchema.ts` exports `FooSchema`), plus a barrel `index.ts`. Any shared class that receives providers at call time (a processor, a forwarder-style "port" class) is **generic over a context type `C`** — it never names the application's full context. See [Dependency injection](#dependency-injection-context-slices-and-the-composition-root). ### `shared/integration/` — external API adapters One folder per third-party service: `shared/integration//` containing the provider class (`Provider.ts`), a `types.ts` for the wire types, optional `util.ts`, tests, and a barrel `index.ts`. Integrations that hold secrets take them as **constructor arguments** — the secret values themselves are read only in `api/` (see env split below). Each integration also exports its **context slice**: a small interface declaring only the piece of the DI context this integration needs at call time. A forwarder class that sends submissions to the service extends the shared base class parameterised by that slice, never by the application's full context: ```ts // shared/integration/crm/index.ts (excerpt) import { BaseForwarder } from "shared/order"; import type { APIProvider, Data } from "shelving"; /** Slice of the DI context that the CRM integration requires. */ export interface CRMContext { crm: APIProvider; } export class CRMForwarder extends BaseForwarder { // ...reads `context.crm` and nothing else. } ``` The composition root's `APIContext` satisfies `CRMContext` structurally, so the forwarder plugs in with zero casts — see [Dependency injection](#dependency-injection-context-slices-and-the-composition-root). ### `shared/util/` — shared utilities Flat folder, one file per topic (`phone.ts`, `cookie.ts`, `env.ts`, …), each with a co-located `.test.ts`. Two files deserve special mention: - **`shared/util/env.ts`** — parses *public* environment variables only. It starts with a comment warning that these values are inlined into client bundles. Required values throw at module load: `if (!process.env.API_URL) throw new ReferenceError(...)`. Export parsed values (e.g. `URL` objects via `requireURL`), not raw strings. - **`shared/util/context.ts`** — declares small, descriptive, **reusable context slices** — `DatabaseContext { db: DBProvider }` and the like — that shared service callbacks reuse when they need context. What it must **never** declare is the full server context: the "large bundle" shape is owned by `api/util/context.ts`, where `APIContext` composes these slices into its final shape. Nothing in `app/` ever imports the full server context, and defining it in `shared/` forces generic shared code to depend on the whole application environment. See [Dependency injection](#dependency-injection-context-slices-and-the-composition-root). ## Dependency injection: context slices and the composition root Everything server-side receives its dependencies through a single context object, but **who names that object's type matters as much as the injection itself**. The rules: ### The composition root owns the context type The server's full DI context is defined in **`api/util/context.ts`**, named `APIContext`, with `APIHandlers = EndpointHandlers` alongside. It does **not** live in `shared/`. This mirrors `app/util/context.ts` — each side owns its own context file. Nothing in `app/` ever imports the server context, and defining it in `shared/` forces generic shared code to depend on the whole application environment. What `shared/` *does* declare is **slices**: small, descriptive interfaces that each name one capability. The full context only ever exists at the composition root, as a composition of slices. ### Shared code never names the full context Any shared class that receives providers at call time — a forwarder-style "port" class, a processor — is **generic over a context type `C`**. This is exactly the idiom Shelving itself uses for `EndpointHandlers` and `Endpoint.handler()`. The rule: **if a shared module hard-codes the application's context type, it is mis-layered** — make it generic over `C`. ### Code declares the context slice it needs Slices come from two places: - **Cross-cutting, reusable slices** live in `shared/util/context.ts` — `DatabaseContext { db: DBProvider }` and similar. Shared service callbacks that need a capability name the slice, not the application context: ```ts // shared/util/context.ts (excerpt) import type { DBProvider } from "shelving"; /** Slice of the DI context that provides database access. */ export interface DatabaseContext { db: DBProvider; } ``` - **Adapter-specific slices** live with their adapter — e.g. `shared/integration/crm` exports `CRMContext { crm: APIProvider }` and its forwarder extends `BaseForwarder`. The composition root then composes the full context **by extension**: ```ts // api/util/context.ts import type { OrderProcessors, OrderRedirectContext } from "shared/order"; import type { CRMContext } from "shared/integration/crm"; import type { DatabaseContext } from "shared/util/context"; import type { EndpointHandlers } from "shelving"; /** Dependency-injection context assembled at each API entry point. */ export interface APIContext extends DatabaseContext, CRMContext, OrderRedirectContext { processors: OrderProcessors; } export type APIHandlers = EndpointHandlers; ``` Structural typing plus contravariance does the assembly with **zero casts**: a `Forwarder` is assignable wherever a `Forwarder` is expected, because `APIContext` satisfies the slice. Note the self-referential `processors: OrderProcessors` — a context containing consumers of itself is a genuinely recursive relationship, and stating it once at the root keeps it out of the shared files, where the same relationship degenerates into an import cycle. ### Method-specific requirements accumulate as intersections When one method needs more context than the class's `C` — e.g. redirect resolution needs `{ websiteURL: URL }` — declare a slice for it (`OrderRedirectContext`, exported next to the code that needs it) and type **that method's parameter** as `C & OrderRedirectContext`. Do **not** constrain the whole class: `C` is inferred from constructor arguments (the list of forwarders), and the inferred slice won't satisfy an unrelated constraint, so a class-level constraint breaks inference. ### Default generic contexts to `C = unknown`, never `C = void` When a class is constructed with an empty list (a processor with no forwarders), there is no inference site, so `C` falls back to the default — and `void` fails assignability against any real context, while `unknown` correctly means "works with any context". ### Keep mapper/transform callbacks pure Mapping callback types stay `input → output`. Don't thread the context through mapping callbacks speculatively — in practice no mapper uses it, and pure mappers are a stronger contract. Add the context parameter only when a real need appears. ### Make required-ness honest A context field that every real composition root sets (like `websiteURL`) is **required, not optional**. If only tests omit a field, the field isn't optional — the tests are incomplete; have them supply mocks instead. ## The `api/` package ### Entry points — three composition roots The API has **no global state**. All providers are assembled into an `APIContext` (defined in `api/util/context.ts` — see [Dependency injection](#dependency-injection-context-slices-and-the-composition-root)) at an entry point and passed down. There are exactly three places that construct a context: 1. **`api/start.ts`** — local dev server. Runs `Bun.serve` on the port derived from the `API_URL` env var. Builds a context from debug/in-memory providers (`MemoryDBProvider` wrapped in `DebugDBProvider`, mock external APIs wrapped in `DebugAPIProvider`), wraps the handler in logging + CORS middleware. 2. **`api/worker.ts`** — production serverless/edge entry. Builds a context from real providers (persistent KV/database provider, real external API clients wrapped in `LoggingAPIProvider`) using secrets from `api/util/env.ts`. 3. **`api/util/test.ts`** — test helper exporting `createMockAPIContext()`, which builds a context entirely from `Mock*` providers so tests exercise handlers in-process with no network I/O. Because the context interface is the same in all three, handlers and services are identical across dev, production, and test. ### `api/index.ts` — dispatch One exported function, `handleAPI(request, context)`, which delegates to Shelving's `handleEndpoints(API_URL, HANDLERS, request, context, handleAPI)` and converts anything thrown into an error `Response` with `getErrorResponse(thrown, DEBUG)` (the `DEBUG` flag prevents leaking error details in production). All top-level error handling lives here and nowhere else. ### `api/handlers/` — thin handler lists One file per domain area plus an `index.ts` that assembles the master list. A handler file imports the endpoint from `shared/endpoint`, attaches `.handler(...)`, and immediately delegates to a service: ```ts // api/handlers/order.ts import { ORDER_ENDPOINT } from "shared/endpoint"; import { createOrder } from "../services/order.js"; import type { APIHandlers } from "../util/context.js"; export const ORDER_HANDLERS: APIHandlers = [ ORDER_ENDPOINT.handler((create, _request, context) => { return createOrder(create, context); }), ]; ``` `api/handlers/index.ts` exports `HANDLERS: APIHandlers` — a flat array spreading each area's handlers, plus tiny inline handlers for infrastructure routes (`GET("/")` health check, `robots.txt`, `favicon.ico`). Rules: - Handlers contain **no domain logic** — they unpack the request and call a service. - Handler arrays are named `_HANDLERS`. - Handlers return plain data; Shelving validates it against the endpoint's response schema. ### `api/services/` — domain behaviour One file per domain entity; functions named as verbs (`createOrder`, not `orderCreate`). A service receives validated payload + `APIContext`, performs the work (validate deeper, write via `context.db`, forward via context providers), and returns the response-shape data. Co-located `*.test.ts` files test services through `createMockAPIContext()`. ### `api/config/` — server-side configuration Mirrors `shared/config/` folder-for-folder. Where `shared/config//` defines *what the data looks like* (schemas — needed by the client to render forms), `api/config//` defines *what the server does with it* (processors, forwarding/mapping to external systems — server-only because it encodes business rules and touches secret-bearing providers). The `api/config//index.ts` barrel uses `satisfies { [K in keyof typeof SHARED_CONFIG]: ... }` to force a compile error if a shared config entry is missing its server counterpart. ### `api/util/` — server-only utilities - **`api/util/context.ts`** — defines `APIContext` (the DI context interface, composed by extending the reusable slices from `shared/util/context.ts` and each adapter's own slice) and `APIHandlers = EndpointHandlers`. See [Dependency injection](#dependency-injection-context-slices-and-the-composition-root). - **`api/util/env.ts`** — parses *secret* environment variables (API keys etc.). Starts with a comment: "these values are server-side secrets — only import this file in API code, never in APP code." - **`api/util/middleware.ts`** — request middleware as higher-order functions named `with*`: `withCORS(handler)`, `withLogging(handler)`. Each takes a `RequestHandler` and returns a new one. - **`api/util/test.ts`** — the mock-context factory described above, plus shared test fixtures. ## The `app/` package ### Entry points - **`app/index.html`** — production HTML shell. References `./style.css` and `./index.tsx`; the bundler resolves and bundles from here. - **`app/start.html`** — identical shell for local dev, minus any third-party scripts you don't want running locally. - **`app/index.tsx`** — mounts `` into `#root`. Nothing else. - **`app/App.tsx`** — the composition root of the client: the `ROUTES` table, the app-level context value, and the provider tree (Shelving's `App`, `Navigation`, `PageCatcher`, `Router`, `Dialogs`, `Notices`). Every page component is registered here. - **`app/start.ts`** — local dev server: `Bun.serve` on the port derived from `APP_URL`, serving the HTML shell for all SPA routes and static files (e.g. `/img/*` mapped to `shared/img/`). ### Routing Routes are declared as a plain object mapping path patterns to page components, using Shelving's `Router`: ```tsx const ROUTES: Routes = { "/": HomePage, "/order/{form}": OrderPage, "/debug/{path}": DEBUG && DebugPage, // conditionally registered routes use && with an env flag "/**": () => , }; ``` ### Feature folders Everything below the entry points is grouped **by functional area**, one folder per domain entity: `app//`. A feature folder holds, together: - The page component: `Page.tsx` (+ `Page.module.css` if it needs page-specific layout). - Feature-local components, each `PascalCase.tsx` with a matching `.module.css` when styled. - Feature-local hooks and helpers. - Co-located `*.test.ts` files. Never create `app/components/`, `app/hooks/`, or other type-based folders. If a component is generic enough to leave its feature folder, it belongs in the Shelving UI library (`shelving/ui`), not in a app-level shared folder. ### `app/util/` Client-only cross-feature utilities. Two standard files: - **`app/util/api.ts`** — the local database and the single API client instance. All features call endpoints through `API`, and read rows from `DB`: ```ts export const DB = new MemoryDBProvider(); export const API = new HydratingAPIProvider(new ClientAPIProvider({ url: API_URL }), DB); export const { DBContext, useItem, useQuery } = createDBContext(DB); export const { APIContext, useAPI } = createAPIContext(API); ``` - **`app/util/context.ts`** — the app-level React context: an `AppContextData` interface, the `createContext` value, and a `requireAppContext()` accessor that throws if used outside the provider. ### Data flow The client has one local database (`DB`, a plain `MemoryDBProvider`). API results fill it, and components read from it. - **Fetch with `useAPI()`, read with `useQuery()` / `useItem()`.** A component calls the endpoint with `useAPI(endpoint, payload)` and ignores the result. `HydratingAPIProvider` writes the rows into `DB` before the call resolves, and the `useQuery()` / `useItem()` stores update from `DB`. - **Loading.** A store over `DB` is loading until `DB` has the rows, or has a time for the query or item. A `queryItems` result sets the query time, so an empty list shows as empty and not as loading. A `NULLABLE(X.item)` result of `null` marks the item as missing. - **Exact queries.** `useQuery()` must use the same query (from the shared function) as the server sends in `queryItems`, or the store waits for a query time that never comes. - **Paging is an API feature.** `useQuery()` shows every row in `DB` that matches, with no `$limit`. To load more, call the endpoint with `getQueryItemsAfter(page)` and keep the newest page in state: ```tsx const [page, setPage] = useState(first); const next = getQueryItemsAfter(page.orders); // `undefined` when there are no more. {next && } ``` - **Writes are commands.** Send writes through endpoints. `DB` is a read model: a write to it changes only this client. An optimistic edit in `DB` is fine, and the next response that returns the row replaces it. - **Deleted rows.** A list result never deletes rows. A row the server deleted stays in `DB` until the client deletes it locally (e.g. an optimistic delete) or the page reloads. - **Partial rows.** Do not hydrate part of a row into a collection. Use a separate collection for a projection (e.g. `PEOPLE` for the public part of `PERSON`) and accept that the two can drift. ### Component conventions - Components are named function declarations returning `ReactElement` — not arrow functions, not `JSX.Element`. - Page components use the `*Page` suffix and are registered in `App.tsx`. Layout components use the `*Layout` suffix. - **No inline `style` props, ever.** All styling lives in `.module.css` files named after their component (`OrderPage.module.css` for `OrderPage.tsx`). - Reusable components expose styling options as **variants**: boolean props mapped to class names in the CSS module. Pages compose existing `shelving/ui` primitives (`Button`, `Notice`, `Page`, `Row`, layouts, transitions) rather than styling raw elements. ## Boundaries Three mechanisms keep the packages honest: 1. **Linter-enforced import bans** (Biome `noRestrictedImports` overrides): - Files under `app/**` may not import `api/**` — "Client bundles must not import server-only modules". - Files under `api/**` may not import `app/**` — "API code must not import client modules". - `shared/**` imports neither, by construction (nothing in it references the other packages). 2. **The env split.** Public env vars are parsed only in `shared/util/env.ts`; secret env vars only in `api/util/env.ts`. A secret can never leak into a client bundle because the only file that reads it is un-importable from `app/`. 3. **Path aliases.** Cross-package imports of `shared/` go through bare aliases defined in `tsconfig.json` `paths`, which also enforce that folders with barrels are imported *as* the barrel: ```jsonc "paths": { "shared/collection": ["./shared/collection/index.ts"], "shared/config/*": ["./shared/config/*/index.ts"], "shared/endpoint": ["./shared/endpoint/index.ts"], "shared/": ["./shared//index.ts"], // one alias per shared domain folder, e.g. "shared/order" "shared/integration/*": ["./shared/integration/*/index.ts"], "shared/img/*": ["./shared/img/*"], "shared/font/*": ["./shared/font/*.css"], "shared/util/*": ["./shared/util/*.ts"] } ``` Import rules that follow: - Cross-package imports use the alias, no `.js` suffix: `import { ORDER_ENDPOINT } from "shared/endpoint";`. - Relative imports within a package use explicit `.js` extensions: `import { createOrder } from "../services/order.js";`. - Type-only imports use `import type` (enforced by `verbatimModuleSyntax`). - Core types and utilities come from `shelving` (and `shelving/ui` for components) — don't reimplement what the library provides. A `global.d.ts` at the root declares the ambient module types for `*.module.css`, `*.css`, and image imports so the aliases type-check. ## Testing layout Three tiers, all on `bun:test`: 1. **Unit tests** — co-located with source as `.test.ts`, in all three packages. The unit test script runs `bun test` across `app api shared`. 2. **In-process API tests** — service/handler tests build a context with `createMockAPIContext()` from `api/util/test.ts` and call services directly. No server, no network. 3. **E2E tests** — in the top-level `e2e/` folder: `*.test.ts` files driving Playwright, plus support modules (`device.ts` for browser/device setup, `steps.ts` for reusable step runners, `env.ts`, `debug.ts`). Tests safe to run against a live deployment include `@smoke` in their name, so a smoke run is just `bun test e2e -t @smoke`. Environment handling for tests: - **`test.env`** is a committed, fake-but-valid env file. Test scripts load it explicitly with `--no-env-file --env-file=test.env` so runs are deterministic regardless of local `.env`. - Do **not** name it `.env.test` — Bun auto-loads that during `bun test`, which makes smoke/manual runs target the wrong URLs. - **`scripts/startWhile.ts`** boots the app + API on per-run derived ports, waits until they're reachable, runs the wrapped command (the e2e suite), then tears them down. This gives the e2e script a self-contained one-command run. ## Scripts and commands `package.json` scripts follow a strict prefix pattern so groups can be run with `--parallel`/`--sequential` wildcards: ```jsonc "scripts": { "test": "bun run --parallel test:*", // the full local gate "test:lint": "biome check .", "test:type": "tsgo --noEmit", "test:unit": "bun --no-env-file --env-file=test.env test --concurrent app api shared", "test:e2e": "bun --no-env-file --env-file=test.env run scripts/startWhile.ts bun test e2e", "fix": "bunx biome check --write", "build": "bun run --sequential build:api build:app", "build:app": "bun build app/index.html --target=browser --env=inline --minify --outdir=.build/app", "build:api": "bun build api/worker.ts --env=inline --outfile=.build/api/worker.js", "start": "bun run --parallel start:*", "start:app": "bun run app/start.ts", "start:api": "bun --watch run api/start.ts" } ``` Conventions: - `test:*` scripts are the CI gate; plain `test` runs them all. - Builds output to `.build/` (git-ignored, never hand-edited). `postbuild:*` scripts can verify the bundle (e.g. grep the output to confirm no secret env values were inlined into the client bundle — a cheap, high-value safety check). - Servers derive their ports from `APP_URL`/`API_URL` env vars rather than hardcoding or taking `--port` flags, so every environment (dev, test, CI, prod) is configured the same way. Required public env vars follow the same shape: `DEBUG`, `APP_URL`, `API_URL` (plus any project-specific public URLs), validated eagerly in `shared/util/env.ts` so a misconfigured environment crashes at boot, not mid-request. ## CI: the test gate, preview environments, and deployment This is a **key part of the workflow**, not an afterthought. Every code pull request gets its own fully deployed, disposable preview environment — real app, real API, at URLs derived from the PR number — and production deploys automatically on merge to the default branch. Review happens against a live deployment, not just a diff. The setup is four workflows in `.github/workflows/`. The hosting below is described using Cloudflare (Pages for the static app bundle, Workers for the API) as the example technology — any host that supports named/branch deployments for static sites and named serverless deployments for the API works the same way. | Workflow | Trigger | What it does | |---|---|---| | `deploy-preview.yml` | PR `opened` / `synchronize` / `reopened` | Full test gate, then deploy a per-PR preview environment and post a sticky PR comment with the URLs | | `deploy-prod.yml` | Push to `main` | Full test gate, then deploy to production | | `smoke.yml` | `deployment_status` success (+ manual dispatch) | Run `@smoke`-tagged e2e tests against the freshly deployed URL | | `teardown-preview.yml` | PR `closed` | Delete the per-PR deployments and update the sticky comment | ### The preview workflow (`deploy-preview.yml`) Structure — two jobs, deploy gated on test: ```yaml name: Deploy Preview on: pull_request: types: [opened, synchronize, reopened] paths-ignore: - '**/*.md' - 'docs/**' permissions: contents: read deployments: write pull-requests: write concurrency: group: preview-pr-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 # Cache the package-manager cache AND the e2e browser binaries, both keyed on the lockfile. - uses: actions/cache@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} - uses: actions/cache@v5 id: playwright-cache with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }} - run: bun install --frozen-lockfile - if: steps.playwright-cache.outputs.cache-hit != 'true' run: bunx playwright install chromium - run: bun run test # the FULL gate: lint + type + unit + e2e deploy: needs: test runs-on: ubuntu-latest env: SLUG: pr-${{ github.event.pull_request.number }} WORKER_NAME: pr-${{ github.event.pull_request.number }}-api APP_URL: https://pr-${{ github.event.pull_request.number }}.${{ vars.PAGES_SUBDOMAIN }}.pages.dev API_URL: https://pr-${{ github.event.pull_request.number }}-api.${{ vars.WORKERS_SUBDOMAIN }}.workers.dev CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} environment: name: preview url: https://pr-${{ github.event.pull_request.number }}.${{ vars.PAGES_SUBDOMAIN }}.pages.dev steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 # ... same bun cache + install steps ... # Env values are BAKED INTO the bundles at build time (--env=inline), # so write .env from a single multi-line repo secret before building. - name: Write .env run: | cat > .env <<'EOF' ${{ secrets.ENV_PREVIEW }} EOF [ -s .env ] || (echo "ENV_PREVIEW is empty" && exit 1) - run: bun run build:app - run: bun run build:api # Static bundle → branch-named deployment; API bundle → per-PR named worker. - run: bunx wrangler@4.71.0 pages deploy .build/app --project-name ${{ vars.PAGES_SUBDOMAIN }} --branch ${{ env.SLUG }} - run: bunx wrangler@4.71.0 deploy .build/api/worker.js --name ${{ env.WORKER_NAME }} - name: Post PR comment uses: actions/github-script@v8 with: script: | # Sticky-comment pattern: first line is a fixed marker; find a comment # starting with the marker and update it, otherwise create it. # Body: a small table with App URL, API URL, worker name, short commit SHA. ``` The load-bearing decisions, each worth copying: 1. **Deterministic URLs derived from the PR number.** The slug `pr-` names everything: the Pages branch alias (`pr-..pages.dev`), the Worker (`pr--api`), the concurrency group. Anyone — human or LLM — can compute the preview URLs from the PR number alone, without reading the PR comment or querying the host. 2. **`paths-ignore` for docs.** Both deploy workflows ignore `'**/*.md'` and `'docs/**'`. Deploying because a README changed is pure waste. The trade-off (see gotchas below) is that docs-only PRs get *no* preview, which agents must know about. 3. **Deploy gated on the full test gate.** The `deploy` job has `needs: test`, and `test` runs the *entire* local gate (`bun run test` = lint + type + unit + e2e), not a subset. Partial gates let e2e regressions reach deployed previews. 4. **Per-PR concurrency with `cancel-in-progress`.** Group `preview-pr-` means a new push cancels the previous in-flight run — only the latest commit's preview survives, and runs for different PRs never queue behind each other. 5. **One multi-line env secret per environment.** The whole `.env` file lives in a single repo secret (`ENV_PREVIEW`, `ENV_PROD`) that the workflow writes to disk before building. One place to rotate, and the `[ -s .env ]` guard fails fast if the secret is missing/empty. Because builds use `--env=inline`, env values are frozen into the bundle — changing an env value means rebuilding and redeploying, not editing host config. 6. **The sticky PR comment.** The comment starts with a fixed marker line (`### Preview Environment`); each deploy finds the existing comment by that marker and updates it rather than posting a new one. The PR thread stays clean and always shows the current state. 7. **GitHub `environment:` on the deploy job.** Declaring `environment: {name, url}` creates a GitHub Deployment, which (a) shows the preview link directly on the PR, and (b) emits the `deployment_status` event that triggers the smoke workflow. 8. **Pinned deploy CLI.** `bunx wrangler@` — an unpinned deploy tool is a supply-chain and reproducibility risk in CI. 9. **Cache the package cache and browser binaries keyed on the lockfile.** Two `actions/cache` entries (package-manager cache, Playwright browsers), both keyed on `hashFiles('bun.lock')`, keep the test job fast. ### The production workflow (`deploy-prod.yml`) Identical shape, different trigger and targets: - Trigger: `push` to `main`, with the same `paths-ignore` for `**/*.md` and `docs/**`. - Same two-job `test` → `deploy` structure, same `.env`-from-secret pattern (`ENV_PROD`). - Deploys the static bundle to the production branch alias (`--branch main`) and the API to the fixed production worker name (`api`). - Concurrency group `deploy-prod` with `cancel-in-progress: true` — rapid merges collapse into one deploy of the latest commit. - `environment: {name: production, url: }` so production deploys also trigger smoke. ### The smoke workflow (`smoke.yml`) Runs the `@smoke` subset of the e2e suite against whatever was just deployed: ```yaml on: deployment_status: # fired by any deploy job that declares `environment:` workflow_dispatch: # manual runs against the configured APP_URL jobs: smoke: if: github.event_name == 'workflow_dispatch' || github.event.deployment_status.state == 'success' steps: # ... checkout, bun + playwright setup with the same caches ... - name: Use deployment URL as APP_URL if: github.event.deployment_status.environment_url run: echo "APP_URL=${{ github.event.deployment_status.environment_url }}" >> "$GITHUB_ENV" - run: bun run smoke # bun test e2e -t @smoke, against $APP_URL ``` Notes: - `deployment_status` fires for **both** preview and production deployments — one workflow smoke-tests everything, because the target URL comes from the event (`environment_url`), not from config. - Only e2e tests with `@smoke` in the test name run. Keep `@smoke` on exactly the cases that are safe and meaningful against a live deployment (idempotent flows, test-mode forms) — never destructive ones. - If production sits behind bot protection / a WAF, give the smoke runner a shared-secret bypass header (sent on every Playwright request, matched by a WAF "skip" rule) via a secret env var; leave it unset for local/preview runs that aren't behind the WAF. ### The teardown workflow (`teardown-preview.yml`) Previews are disposable — tear them down when the PR closes (merged or not): - Trigger: `pull_request: types: [closed]`, same per-PR concurrency group. - Delete the per-PR worker by name (`wrangler delete pr--api --force || true` — `|| true` because a docs-only PR never deployed one). - Delete the static-host deployments for the PR: list the project's deployments as JSON, filter to those matching the PR slug (by branch name, alias, or URL), delete each by id. This matching-by-slug step is the fiddliest part — deterministic slugs (decision 1 above) are what make it possible. - Mark the PR's GitHub Deployments `inactive` and delete them, so the PR doesn't show stale "Active" environments. - Update the sticky PR comment to say the preview has been torn down. ### Gotchas (read these before touching CI) - **`.md` and `docs/**` changes do not trigger previews or deploys.** This is the `paths-ignore` working as intended, but it has consequences an LLM must anticipate: a docs-only PR gets no preview environment and its deploy-gated checks may show as skipped; a docs-only merge to `main` does **not** redeploy production. If you genuinely need a preview for a docs-only PR, include a code change or run the deploy manually. Conversely: don't wait for a preview URL to appear on a docs-only PR — it never will. - **A new push cancels the in-flight deploy.** If you push twice quickly, the first run is cancelled mid-way by `cancel-in-progress` — a cancelled run is not a failure. Judge preview state by the *latest* run and the commit SHA in the sticky comment. - **Env is frozen at build time.** With `--env=inline` builds, changing `ENV_PREVIEW`/`ENV_PROD` does nothing to already-deployed bundles. Rebuild and redeploy (push a commit, or re-run the deploy workflow). - **Preview and production must not share state.** The per-PR worker gets its own bindings; keep preview database/KV namespaces separate from production (e.g. a `preview_id` binding alongside the production id in the worker config). - **Smoke failures against previews can be opaque** (process kills with no output). The diagnostic loop: reduce the failing e2e test to its first steps, add `console.debug` checkpoints around suspect boundaries, re-push to re-trigger the preview + smoke run, and read which checkpoints printed in the Actions logs. ## AGENTS.md — make the repo legible to LLMs **Create an `AGENTS.md` file at the repository root so LLM coding agents can understand the codebase.** This is not optional documentation — for a project maintained with agent assistance, `AGENTS.md` is the agents' operating manual, and an agent that hasn't read it will misplace files, skip the test gate, and violate conventions it had no way to know about. Pair it with a minimal `CLAUDE.md` that forces the read: ```md # CLAUDE.md Read `AGENTS.md` at the start of every session before doing any work. ``` ### Required sections Write `AGENTS.md` with these sections, in this order. Keep every claim current — a stale map is worse than no map, because agents trust it over re-discovering the code. 1. **Project Snapshot** — runtime/tooling, the three packages, where reusable components come from, and a pointer to this guide for structural rationale. 2. **Fast Start** — install command, run command, local URLs, and the required/optional env vars (with what breaks when each is missing). 3. **Canonical Commands** — the exact commands for lint, type-check, unit tests, the full gate, e2e/smoke variants, and build. Agents should never have to guess a script name. 4. **CI & Deployment** — the workflow list from the [CI section](#ci-the-test-gate-preview-environments-and-deployment) above, condensed to the facts an agent needs mid-task: what triggers a preview, the deterministic preview URL scheme, that `.md`/docs changes don't trigger deploys, that env is baked at build time, and that concurrent pushes cancel in-flight deploys. 5. **Current Known Baseline** — dated statement of which gate commands currently pass. When an agent's change breaks this, updating the section is part of the change. 6. **Repository Map** — one line per important file/folder: entry points, handler/service/config folders, the shared contract folders, the env-parsing files (flagging which is public and which is secret). 7. **File Organization Rules** — the placement rules from this guide, restated imperatively ("`api/handlers`: request handlers only…"). This is the section that stops files landing in the wrong place. 8. **Component / Coding Conventions** — naming, styling rules (no inline styles, CSS-module variants), import rules (aliases vs `.js` relative paths), TypeScript settings that bite (`verbatimModuleSyntax`, `exactOptionalPropertyTypes`), error-handling and logging conventions. 9. **Architecture Notes** — the handful of decisions everything hangs off: routing, endpoint contracts + context injection, the enforced import boundaries, how servers derive ports from URLs. 10. **Agent Workflow Recommendations** — what to run before coding and before finishing (the full gate, autofix), and any UI-verification steps. 11. **Common Pitfalls** — every trap that has actually cost time (env auto-loading quirks, port assumptions, cache issues). This section is earned, not written up front. ### Maintenance rules Write these into `AGENTS.md` itself, so agents keep it alive: - Treat `AGENTS.md` as **ongoing memory**: when the user says "add instructions", "remember to do this", or "always do this", the rule goes in this file. - When a convention is discovered that isn't written down, write it down in the matching section. - When the baseline changes (a gate command starts failing), update **Current Known Baseline** with the exact failing command and file. - Refactoring the file for clarity is encouraged; letting it drift from reality is the one failure mode to avoid — an agent reading a stale map will confidently do the wrong thing. ## Style essentials The broader coding style is a project choice, but these rules interact directly with the layout above: - **Naming:** `PascalCase` for types/classes/components; `camelCase` for functions/variables; `UPPER_SNAKE_CASE` for exported constants (schemas, endpoints, handler lists, config dictionaries). Module-private functions and constants get a `_` prefix and are not exported. - **One component per file**, file named after its export. Test files co-located as `*.test.ts`. - **Barrels:** every multi-file `shared/` folder has an `index.ts` of alphabetised `export * from "./Module.js";` lines, and the tsconfig alias points at it. - **Verb prefixes carry contracts:** `require*` throws when missing; `get*` returns `undefined` instead; `with*` returns a wrapped/updated copy (including middleware); `handle*` is a top-level request entry point; `format*`/`parse*` are display/parse pairs. Pick the prefix that matches the behaviour. - **Errors:** user-facing errors are thrown as plain strings (`throw "Unknown form";`) and surfaced by the UI notice system; unexpected errors use Shelving's typed error classes (`RequiredError`, `UnexpectedError`, `ResponseError`). - **Logging:** `console.log` is banned by the linter; use `console.debug`/`console.warn`/`console.error`. ## Checklist for adding a new domain feature Adding a feature called `order` touches these files, in this order: 1. `shared/collection/order.ts` — collection schema, create/pick variants, result schema, derived types. Add to `shared/collection/index.ts`. 2. `shared/endpoint/order.ts` — `export const ORDER_ENDPOINT = POST("/order", CREATE_ORDER, ORDER_RESULT);`. Add to `shared/endpoint/index.ts`. 3. *(If both sides need config/domain classes)* `shared/config/order/` and/or `shared/order/`, with barrels. 4. `api/services/order.ts` — `createOrder(create, context)` doing the actual work, plus `order.test.ts` using the mock context. 5. `api/handlers/order.ts` — `ORDER_HANDLERS` attaching the endpoint to the service. Spread into `api/handlers/index.ts`. 6. *(If the server needs its own config)* `api/config/order/`, keeping the `satisfies` completeness check in its barrel. 7. `app/order/OrderPage.tsx` (+ feature-local components/styles/tests in `app/order/`). 8. Register the route in `app/App.tsx`. 9. Run the full test gate locally, then open a PR — CI runs the same gate and deploys a preview environment at the deterministic per-PR URLs. Verify the feature against the live preview (smoke tests run against it automatically) before merging. If every file in that list is in the right place, the client and server share one schema, one endpoint contract, and zero duplicated types. ## Upgrade notes ### From `CacheDBProvider` to `HydratingAPIProvider` `CacheDBProvider` and the paging in `QueryStore` are removed. To upgrade an app: 1. Replace the client database chain (e.g. `new CacheDBProvider(new RESTDBProvider(api))`) with a plain `MemoryDBProvider`, and wrap the API client in `HydratingAPIProvider` with that database (see `app/util/api.ts` above). 2. Change result schemas to the collection's own schemas: `ARRAY(X.item)` → `X.items`, and `{ query, items }` results → `X.queryItems`. 3. Add a shared query function for each `queryItems` result, and use it in both the handler and `useQuery()`. 4. In each component, call `useAPI()` for the endpoint that loads the data, and keep `useQuery()` / `useItem()` to read it. 5. Replace `QueryStore.fetchAfter()` / `fetchBefore()` / `hasAfter` with an API call and `getQueryItemsAfter()`. To poll for new rows, call the endpoint again on a timer. 6. Rename `getAfterQuery()` / `getBeforeQuery()` to `getQueryAfter()` / `getQueryBefore()`. 7. A row deleted on the server now stays in the local database until the page reloads, unless the app deletes it locally. Check that optimistic deletes run. --- # Code styleguide The canonical code style for the `shelving` codebase and for any project built in its style. `AGENTS.md` signposts this file — **read it fully before writing any code**. Rules are stated as requirements — deviate only with a stated reason. The rules here are portable: they describe the style itself, not this repository. Repo-specific mechanics (commands, release flow, docs-site conventions, the UI tint ladder) stay in `AGENTS.md`. ## Philosophy - **Reuse before writing.** Before writing a new function, component, or type, scan the codebase for something that already does the job — exactly, or closely enough that a parameter or variant covers the difference. Compose existing primitives rather than reinvent. - **Immutability by default.** Data in, new data out. Helpers that "change" a value return an updated copy — and intentionally return the *original reference* when nothing changed, so equality checks stay cheap. - **Contracts in names.** A function's prefix (`get*`, `require*`, `assert*`, `create*`, …) is a promise about its behaviour. Callers depend on it, so pick the prefix that matches the contract exactly. - **Terse inside, descriptive outside.** Exported names are clear and descriptive; inside a small function body, very short names are preferred. ## Tooling - **Biome** for linting and formatting; **TypeScript** in strict mode; **Bun** as the test runner. - Formatting: **tabs** for indentation, **line width 140**, `arrowParentheses: "asNeeded"`. - Notable lint rules (treat as law even without Biome): - No non-null assertions (`!`) in source code (allowed in tests). - No namespace imports (`import * as`), no default exports, no unused imports. - No floating promises — every promise is awaited, returned, or explicitly `.catch`-ed. - `console.log` is banned; `console.debug` / `console.warn` / `console.error` are allowed. - No empty blocks; no skipped tests. - TypeScript compiler options to enable: `strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `noImplicitOverride`, `verbatimModuleSyntax`, `isolatedModules`, `module: "nodenext"`, `target: "esnext"`. ## Project structure - One source root (e.g. `modules/`) containing one folder per module; generated output (`dist/`) is never edited by hand. - Tests are **colocated** with source as `*.test.ts`. - File naming: utility files are lowercase (`util/object.ts`); class and schema files are PascalCase and named after their main export (`schema/StringSchema.ts`). React files use `.tsx` only when they contain JSX. - Each module has a barrel `index.ts` that re-exports with `export * from "./X.js"`, alphabetically sorted. Keep barrels in sync when adding or moving files. ## Naming ### General - Prefer single-word names; plurals take an `s` suffix: `item` / `items`, `key` / `keys`. - Module-private (non-exported) functions and constants are prefixed with `_`: `function _helper(...)`, `const _CACHE = ...`. - Prefix unused function arguments with `_`: `_event`, `_value`. - Inside small scopes, very short names are preferred: `n`, `nn`, `k`, `v`, `i`, `r`, `e`, `el`. Exported / outer-scope names stay descriptive (`hostname`, `cookieData`). - Event-handler parameters are `e` — never `event` or `evt`. - Pre-validation values are prefixed `unsafe` or `raw` (`unsafeData`, `rawName`); the validated counterpart drops the prefix: `const data = validateData(unsafeData)`. - `UPPER_SNAKE_CASE` only for truly constant values (config, schemas, fixed data); otherwise `camelCase`. ### Function prefixes Pick the prefix that matches the contract — they are not interchangeable. | Prefix | Contract | Examples | |---|---|---| | `is*` | Boolean type guard returning `value is T` | `isArray`, `isString` | | `has*` | Boolean predicate (possession / state, not type) | `hasItems` | | `assert*` | Throws if invalid; uses `asserts value is T` | `assertArray` | | `validate*` | Throws on invalid; returns the validated value (also strips/coerces) | `validateData` | | `get*` | Returns value or `undefined` — never throws for "missing"; may return input unchanged when already valid | `getString` | | `require*` | Returns value or throws | `requireFirst` | | `create*` | Always returns a freshly constructed instance — never returns the input, never `undefined` | `createDeferred` | | `with*` | Immutable updated copy with a value set | `withProp` | | `omit*` | Immutable updated copy with keys removed | `omitProp` | | `add*` | Immutable updated copy with an item added | `addArrayItem` | | `update*` | Immutable updated copy with changes applied | `updateData` | | `toggle*` | Immutable updated copy with a value toggled | `toggleArrayItem` | | `merge*` | New value combining multiple inputs | `mergeObject` | | `to*` | Pure conversion to another shape | `toStringValue` | | `format*` | Value → display string | `formatDate` | | `parse*` | String → structured value (inverse of `format*`) | `parseRequest` | | `match*` | Result of matching / filtering | `matchQuery` | | `await*` | Async counterpart of an otherwise-sync helper | `awaitArray` | | `run*` | Executes a callback or sequence | `runSequence` | - **Pairing rule:** `get*` / `require*` and `is*` / `assert*` come as pairs over the same target. If you add a new `require*`, consider whether a sibling `get*` belongs alongside it; same for `assert*` / `is*`. - **`get*` vs `create*`:** a helper that unconditionally constructs a brand-new instance is `create*`. Reserve `get*` for "look up, coerce, or normalise" — helpers that may return the input unchanged or return `undefined`. Most construction is written inline as `new X(...)`; `create*` is for the few helpers that wrap construction. ### UI / event-layer prefixes Reserved for the UI layer — don't repurpose them for unrelated helpers: | Prefix | Contract | |---|---| | `notify*` | Dispatches a notice / custom event | | `call*` | Runs a callback, dispatching notices on return / throw | | `handle*` | Top-level request / event entry point | | `on*` | Event-handler **prop** name on a component (not a function name) | ## Imports & exports - Always use the `.js` extension in import paths: `import { x } from "./x.js"`. - **Named exports only** — no default exports. (CSS modules are the one exception.) - In source files, import from the declaration file directly (`../util/array.js`), never from a barrel. - In test files, import from the package's public barrel (`mypackage/util/array`), never a relative source path — this verifies the barrel actually re-exports the token. Wire package-name → source resolution via `tsconfig.json` `paths`, and enforce with a lint rule (Biome `noRestrictedImports`) if possible. - `import type { ... }` is mandatory for type-only imports (`verbatimModuleSyntax`). Inline `type` in mixed imports: `import { type Foo, bar }`. ## Types - `readonly` on properties and arrays by default (`readonly x: string`, `readonly T[]`). - Type guards return `value is T`; assertion functions return `asserts value is T`. - With `exactOptionalPropertyTypes` on, optional properties are written `readonly x?: string | undefined`. - Prefer `type` aliases for simple shapes; use `interface` when extending or when representing a props / options object. - Discriminated unions over class hierarchies for variant data. - Props interfaces use a `*Props` suffix; options interfaces use `*Options`; no `I` prefix. - Prefer named domain types (`Data`, `Arguments`, `ImmutableArray`) in generic constraints over raw `Record` / `unknown[]`. ## Functions - Regular `function` declarations for public API exports; arrow functions for short utilities, callbacks, and one-liners. - Default parameter values are used heavily, especially in destructured options/props: `{ required = false, disabled = false }`. - Prefer early returns and guard clauses over nested conditionals. - Immutable helpers intentionally return the original reference when nothing changed — preserve that behaviour instead of always cloning. ### `caller` for attributable errors Throw-capable helpers accept a `caller` as their **last** parameter, defaulting to the function itself: ```ts export function assertArray(value: unknown, min?: number, max?: number, caller: AnyCaller = assertArray): void { if (!isArray(value, min, max)) throw new RequiredError("Must be array", { received: value, caller }); } ``` When forwarding to another `caller`-aware helper, pass `caller` through so the thrown error attributes to the user's call site, not internal plumbing. ## Classes - Every `_`-prefixed class member is also marked `private` or `protected` — and vice versa. Never `private foo` without an underscore, never bare `_foo` without an access modifier. The two travel together as one "this is internal" signal. - (Module-scope private functions/constants use just the `_` prefix — no modifier exists there.) - Classes that ship convenience instances export them as `ALL_CAPS` constants next to the class: `StringSchema` plus `STRING`, `TITLE`. ## Variables ### Destructuring - Always destructure when reading 2+ properties from the same object: `const { method, url } = request`. - Destructure even single properties when the source name is long. - Rename on destructure to disambiguate or mark validation state: `const { data: unsafeData } = ...`. - Function parameters destructure inline in the signature, including renames. - Methods destructure `this`: `const { state, props } = this`. ### Coercion - `.toString()` on values, not `String(value)`. - `Number.parseInt` / `Number.parseFloat` over global `Number(...)`; only use `Number(...)` when no method form exists. - `!!x` for boolean coercion. - `Number.POSITIVE_INFINITY` not `Infinity`; `Number.isFinite()` not global `isFinite()`. ### Truthy and nullish - Prefer truthy checks over explicit comparisons: `if (!value) return ...`, not `if (value === undefined || value === "")`. - Use `||` (not `??`) when empty string and zero should also fall through to the default — this is the common case. - Reserve `??` for narrow null/undefined defaults, e.g. `process.env.X ?? ""`. - Use optional chaining + truthiness liberally: `if (items?.length) ...`. ### Object spreads - Merge defaults with spread, defaults first: `{ ...DEFAULTS, ...options }`. - Spread order matters and is intentional — don't reorder. - Forward props with rest spread, preserving order. ## Control flow ### Guard clauses One-line `if (...) return ...;` without braces, stacked at the top of the function: ```ts if (!n.length) return ""; if (n.startsWith("44")) return formatUK(n.slice(2)); ``` - Module-scope throw guards use the same form: `if (!process.env.API_URL) throw new ReferenceError("...");` - Combine a `for` loop with a one-line `if` body when filtering: `for (const [k, v] of Object.entries(data)) if (v) ...` ### Loops - `for...of` over `.forEach()`. - `Array.from(iter)` over `[...iter]` when converting iterables. - `function*` generators when assembling a sequence with conditional yields. ### Comments - Short single-line `//` comments label sections of a function. Sentence case, ending in a period: `// Stop the page reloading.` - Group several file-level constants under a `// Constants.` label. - Comment the *why* or the section purpose — never restate what the code says. - The trailing-empty-comment trick — `Foo, //` — deliberately forces the formatter to keep an argument list multi-line: ```ts return getClass( BUTTON_CLASS, // getModuleClass(CSS, variants), ); ``` Preserve these when editing nearby code; don't strip them. ## Async patterns Export a named `BLACKHOLE` no-op (from a shared `util/function` module) and use `.catch(BLACKHOLE)` to mark a promise rejection as deliberately ignored: - `_promise.catch(BLACKHOLE)` — a promise not awaited here whose error resurfaces when something else awaits it; the catch stops the unhandled-rejection warning. - `await flakyOp().catch(BLACKHOLE)` — a known-flaky operation whose rejection is expected and harmless. Don't replace with an inline `() => {}` or omit the catch — the named symbol documents intent. ## Error handling - User-input validation errors throw a plain `string` (human-readable, e.g. `"name: Must be 5-50 characters"`), which UI layers consume directly. Let them propagate as-is. - System / transport problems throw typed error classes (`ResponseError`, `ValueError`, `RequiredError`, …) — e.g. a malformed API response body is a server error, not a user error. - Aggregated validation failures are `"key: message"` lines joined by `\n`; nested keys chain with `.`. - The pattern `const message = getMessage(thrown); if (!message) throw thrown;` handles string messages gracefully while re-throwing real errors. ## React components ### Files and naming - Component files are named after the component they export: `Button.tsx` exports `Button`. CSS modules match: `Button.module.css`. - `*Layout` suffix for structural layout components; `*Page` suffix for top-level router entries. Page components compose existing library components and should not need custom-styled elements. - Reusable components live in a shared component library and expose styling options as **variants**. ### Component patterns - Components are plain named `function` declarations returning `ReactElement` (not `JSX.Element`), never arrow functions. - Props destructure in the signature with defaults; `{ children, ...variants }` separates content from styling variants. - Conditional rendering uses `&&` and ternaries inline in JSX — not separate variables or early returns for fragments. - Inline arrow handlers in JSX are the default, even multi-line ones. Hoist a handler to a module-level `function _name(...)` only when it's pure, doesn't close over state, and is reused. - Titles, headings, and button labels use **sentence case**: "Change details", not "Change Details". ### CSS modules - Never use inline `style` props — all styling lives in a colocated `.module.css`. - Enumerated props for mutually-exclusive scales (`color="red"`, `size="large"`); boolean props for on/off options (`small`, `plain`). Both map to class names via shared `getXxxClass(props)` helpers. - CSS custom properties provide theming with fallback chains: `var(--button-background, var(--tint-90))`. Every custom property a component's CSS file owns starts with the file's kebab-case name (`Card.module.css` owns `--card-*`), so the originating file is obvious from any `var()` reference. - Use CSS nesting for variants (`&.small`), pseudo-classes (`&:hover`), and child selectors; use `:where()` to keep specificity low for default child styles. - CSS modules import as default: `import styles from "./Foo.module.css"` — the one exception to named-exports-only. ## Testing - Use the runtime's built-in test runner (`bun:test`); tests live next to the module they cover as `*.test.ts`. - When changing runtime behaviour, update or add the closest colocated test in the same change. - When changing TypeScript inference or public generic behaviour, include compile-time assignment checks alongside runtime assertions: ```ts test("TypeScript", () => { const s1: Schema = BOOLEAN; const r1: boolean = s1.validate(true); }); ``` - Test files import from the public package barrel, never relative source paths (see Imports & exports). - Test descriptions are lowercase sentence fragments: `test("formats from leading 0", ...)`. `describe` blocks group by method or option name: `describe("validate()", ...)`, `describe("options.value", ...)`. - Use `expect.unreachable()` to assert a code path is never reached (e.g. in catch blocks testing thrown errors). - Shared fixtures live in a dedicated test-utilities module; reuse them rather than redeclaring. ## Documentation - Every public class, function, and type has a JSDoc docblock. Keep it updated when behaviour changes — a stale comment is worse than none. - **Strong first line:** one short declarative sentence stating the token's purpose; it must stand alone (tooling lifts it as the description). Further detail goes in bullets below. - Functions: summary, 0–2 behaviour bullets for anything surprising, `@param` / `@returns` / `@throws`, and one `@example` showing the shortest call site that conveys real usage. Skip the example when the token is micro / self-evident. - Classes: one-sentence summary, bullet points for notable behaviour and caveats. - Options-bag members are documented on the interface itself, with `/** description */` and `@default` on each member — not repeated at every constructor. - Overridden methods and constructors keep minimal docblocks; document behaviour once on the base method. - When referencing another token in docs, write it as a backtick-quoted name in display style: functions with parens (`formatDate()`), methods qualified (`Store.get()`), components in angle brackets (`
`), classes/constants bare (`StringSchema`, `STRING`). - Each module has a `README.md` covering purpose, key concepts, and integration examples. Trust source and tests over a README when they conflict — but fix the README rather than leaving it wrong. - Favour clarity over completeness: a docblock that genuinely helps a reader beats one that mechanically hits every tag. When in doubt, write less. Example shape: ```ts /** * Get the first item of an array, or `undefined` if it's empty. * * @param arr The array to read from. * @returns The first item, or `undefined` when the array is empty. * @example getFirst(["a", "b"]) // "a" */ ``` ## Commits - Use [Conventional Commits](https://www.conventionalcommits.org/): `feat: add cache provider`, `fix: handle undefined schema value`. - Commit types feed semantic-release: `feat:` → minor, `fix:` → patch. - While a project is on `v0`, avoid `BREAKING CHANGE:` footers and `!` markers; describe breaking changes in the commit body instead. ## Workflow - After making any code change, run the auto-fixer (`bun run fix` — Biome fix + format) before committing. - Verify with the full check suite: lint, type check, and unit tests (run in parallel where possible). - When adding or removing a public export, update the nearest barrel `index.ts`; when adding or removing a package subpath, update `package.json` `exports` too. --- # shelving/api - Kind: module - Import: `import { … } from "shelving/api";` Typed, provider-based framework for HTTP API access. Define your routes as `Endpoint` definitions, then call them through a composable provider stack — the same pattern the `shelving/db` module uses for databases. ## Concepts ### Endpoint An `Endpoint` is a declarative, typed description of a single API route. It captures the HTTP method, the URL path (with optional `{placeholder}` segments), a `shelving/schema` for the request payload, and a schema for the response. Think of it the way `Collection` describes a database table — a shared definition both client and server reference. Factory functions (`GET()`, `POST()`, `PUT()`, `PATCH()`, `DELETE()`, `HEAD()`) create endpoints concisely: ```ts import { GET, POST } from "shelving/api" import { STRING, NUMBER } from "shelving/schema" const getUser = GET("/users/{id}", { id: STRING }, { name: STRING, age: NUMBER }) const createPost = POST("/posts", { title: STRING, body: STRING }, { id: STRING }) ``` For `GET` and `HEAD` requests, payload fields that don't fill a `{placeholder}` are appended as `?query` params. All other methods send a JSON body. ### Providers An `APIProvider` is the abstract interface for executing calls against endpoints. `ClientAPIProvider` is the concrete implementation that uses the global `fetch` API. Wrap it with `ThroughAPIProvider` subclasses to layer in behaviour without rewriting transport logic: | Provider | Purpose | |---|---| | `ClientAPIProvider` | Concrete base — sends requests over the network with `fetch()` | | `ThroughAPIProvider` | Pass-through base for wrapping another provider | | `ValidationAPIProvider` | Validates payload and result against endpoint schemas | | `DebugAPIProvider` | Logs fetch attempts, results, and errors to the console | | `JSONAPIProvider` | Forces JSON request bodies and JSON response parsing | | `MockAPIProvider` | Records calls without sending network requests | | `MockEndpointAPIProvider` | Dispatches calls through handler objects (useful for unit tests) | | `XMLAPIProvider` | Forces XML request bodies and returns raw text responses | Extend `ThroughAPIProvider` to add custom behaviour such as auth headers: ```ts import { ThroughAPIProvider } from "shelving/api" class AuthAPIProvider extends ThroughAPIProvider { constructor(source: APIProvider, readonly token: string) { super(source) } override fetch(request: Request): Promise { return super.fetch(new Request(request, { headers: { ...Object.fromEntries(request.headers), Authorization: `Bearer ${this.token}` }, })) } } ``` ### Caching `APICache` manages `EndpointCache` objects, one per endpoint. Each `EndpointCache` manages `EndpointStore` objects, one per unique payload — keyed by the rendered URL. For `GET`/`HEAD` requests, query params are part of the key so `?role=admin` and `?role=editor` are stored separately. `EndpointStore` fetches automatically on first read and de-duplicates in-flight requests. The cache is primarily useful as the backbone of the [React integration](#react-integration). Use it directly only when you need a reactive layer outside React. ## Usage ### Define endpoints and fetch directly ```ts import { GET, POST, ClientAPIProvider, ValidationAPIProvider } from "shelving/api" import { DATA, STRING } from "shelving/schema" const UserSchema = DATA({ id: STRING, name: STRING, email: STRING }) const getUser = GET("/users/{id}", DATA({ id: STRING }), UserSchema) const createUser = POST("/users", DATA({ name: STRING, email: STRING }), UserSchema) const provider = new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" })) const user = await provider.call(getUser, { id: "u_123" }) const created = await provider.call(createUser, { name: "Alice", email: "alice@example.com" }) ``` ### Server-side routing ```ts import { handleEndpoints } from "shelving/api" const handlers = [ getUser.handler(({ id }) => db.users.get(id)), createUser.handler(({ name, email }) => db.users.create({ name, email })), ] // In a Cloudflare Worker or similar: export default { fetch(request: Request) { return handleEndpoints("https://api.example.com", handlers, request) }, } ``` ### Testing with `MockEndpointAPIProvider` Wires your handler array directly into a mock transport so you can test client and server code together without a real network: ```ts import { MockEndpointAPIProvider } from "shelving/api" const api = new MockEndpointAPIProvider(handlers, undefined) const user = await api.call(getUser, { id: "u_123" }) ``` ## React integration The `shelving/react` module's `createAPIContext()` is the primary way to use a provider in a React app. It creates a context backed by an `APICache` and exposes a typed `APIContext.useAPI()` hook that returns reactive `EndpointStore` instances and suspends automatically while loading. ```ts import { createAPIContext } from "shelving/react" import { ClientAPIProvider, ValidationAPIProvider } from "shelving/api" const provider = new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" })) export const { APIContext, useAPI } = createAPIContext(provider) ``` See the `shelving/react` module for full usage. ## Functions - [HEAD()](api/HEAD.md): Define a HEAD endpoint at a path, with validated payload and result types. - [GET()](api/GET.md): Define a GET endpoint at a path, with validated payload and result types. - [POST()](api/POST.md): Define a POST endpoint at a path, with validated payload and result types. - [PUT()](api/PUT.md): Define a PUT endpoint at a path, with validated payload and result types. - [PATCH()](api/PATCH.md): Define a PATCH endpoint at a path, with validated payload and result types. - [DELETE()](api/DELETE.md): Define a DELETE endpoint at a path, with validated payload and result types. - [handleEndpoints()](api/handleEndpoints.md): Handle a Request with the first matching endpoint handler after stripping any base-path prefix from the request pathname. ## Classes - [APICache](api/APICache.md): Cache of EndpointCache objects keyed by Endpoint, providing memoised API results across many endpoints. - [EndpointCache](api/EndpointCache.md): Cache of EndpointStore objects for a single endpoint, keyed by the rendered request URL of each payload. - [Endpoint](api/Endpoint.md): A typed API resource definition pairing a method and path with payload and result schemas. - [APIProvider](api/APIProvider.md): Abstract base for API providers that send requests to a set of Endpoint definitions rooted at a common base URL. - [CachedAPIProvider](api/CachedAPIProvider.md): API provider wrapper that serves requests through an APICache so repeated calls reuse cached results. - [ClientAPIProvider](api/ClientAPIProvider.md): A client-side API provider that sends requests over the network using fetch(). - [DebugAPIProvider](api/DebugAPIProvider.md): Provider that logs every request, response, and error to the console in detail to help diagnose issues in development. - [HydratingAPIProvider](api/HydratingAPIProvider.md): API provider that validates every result, then writes the collection items in it into a local database. - [JSONAPIProvider](api/JSONAPIProvider.md): Client API provider that always sends request bodies as JSON and parses responses as JSON. - [LoggingAPIProvider](api/LoggingAPIProvider.md): Provider that logs fetches to the console to keep useful request/response logs in production. - [MockAPIProvider](api/MockAPIProvider.md): Provider that records API calls and serves them from a handler without sending network requests. - [MockEndpointAPIProvider](api/MockEndpointAPIProvider.md): Provider that mocks an API that calls and matches an array of EndpointHandler objects returned from Endpoint.handler() - [ThroughAPIProvider](api/ThroughAPIProvider.md): Provider wrapper that delegates every API operation to a wrapped source provider. - [ValidationAPIProvider](api/ValidationAPIProvider.md): Provider that validates payloads and results against the endpoint's schemas, so a source of any type is made type-safe. - [XMLAPIProvider](api/XMLAPIProvider.md): Client API provider that always sends request bodies as XML and parses responses as plain text. - [EndpointStore](api/EndpointStore.md): Store that loads and tracks the result of calling a single API endpoint with a fixed payload, through an APIProvider. ## Interfaces - [EndpointHandler](api/EndpointHandler.md): A typed endpoint definition paired with its implementation callback. - [ClientAPIProviderOptions](api/ClientAPIProviderOptions.md): Options for constructing a ClientAPIProvider. ## Types - [AnyEndpoint](api/AnyEndpoint.md): An Endpoint with any payload and result type, for use where the specific types don't matter. - [Endpoints](api/Endpoints.md): An immutable list of endpoints. - [PayloadType](api/PayloadType.md): Extract the payload type from an Endpoint. - [EndpointType](api/EndpointType.md): Extract the result type from an Endpoint. - [EndpointCallback](api/EndpointCallback.md): A function that handles an endpoint request, receiving a validated payload and returning a result. - [AnyEndpointHandler](api/AnyEndpointHandler.md): An EndpointHandler with any payload and result type, for use where the specific types don't matter. - [EndpointHandlers](api/EndpointHandlers.md): A collection of endpoint handlers that can be matched and invoked by handleEndpoints(). - [MockAPIFetchCall](api/MockAPIFetchCall.md): Record of a single mocked fetch, pairing the request with the response the handler returned. - [MockAPIRequestCall](api/MockAPIRequestCall.md): Record of a single request build, capturing the endpoint, payload, options, and built request. - [MockAPIResponseCall](api/MockAPIResponseCall.md): Record of a single response parse, capturing the endpoint, response, and parsed result. --- # APICache - Kind: class - Import: `import { APICache } from "shelving/api";` - Implements: `AsyncDisposable` Cache of `EndpointCache` objects keyed by `Endpoint`, providing memoised API results across many endpoints. - Use `get(endpoint)` to retrieve or create the `EndpointCache` for a given endpoint, then `get(payload)` on that to get a specific `EndpointStore`. - Disposing the cache disposes every nested `EndpointCache` and clears the map. ## Signatures ```ts new APICache(provider: APIProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `APIProvider` | | ## Returns | Type | Description | | --- | --- | | `APICache` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `APIProvider` | The underlying `APIProvider` that backs every cached endpoint. Read only. | ## Examples ```ts const cache = new APICache(provider); const user = await cache.call(getUser, { id: "abc" }); ``` ## Methods - [APICache.get()](api/APICache/get.md): Get (or create) the EndpointCache for the given endpoint. - [APICache.call()](api/APICache/call.md): Fetch (or return a cached result) for the given endpoint and payload. - [APICache.invalidate()](api/APICache/invalidate.md): Invalidate a specific store for an endpoint so the next read refetches. - [APICache.invalidateAll()](api/APICache/invalidateAll.md): Invalidate all stores for an endpoint so the next read of any payload refetches. - [APICache.refresh()](api/APICache/refresh.md): Trigger a refetch on a specific store for an endpoint. - [APICache.refreshAll()](api/APICache/refreshAll.md): Trigger a refetch on all stores for an endpoint. --- # APICache.get() - Kind: method - Member of: `APICache` Get (or create) the `EndpointCache` for the given endpoint. ## Signatures ```ts get(endpoint: Endpoint): EndpointCache get(endpoint: Endpoint): EndpointCache ``` --- # APICache.call() - Kind: method - Member of: `APICache` Fetch (or return a cached result) for the given endpoint and payload. - Returns the cached value immediately if one exists. - Waits for the in-flight fetch if the store is loading. - Throws if the fetch fails, matching `APIProvider.call` behaviour. ## Signatures ```ts call(endpoint: Endpoint, payload: PP, maxAge: number = AVOID_REFRESH, caller: AnyCaller = this.call): Promise ``` --- # APICache.invalidate() - Kind: method - Member of: `APICache` Invalidate a specific store for an endpoint so the next read refetches. ## Signatures ```ts invalidate(endpoint: Endpoint, payload: PP): void ``` --- # APICache.invalidateAll() - Kind: method - Member of: `APICache` Invalidate all stores for an endpoint so the next read of any payload refetches. ## Signatures ```ts invalidateAll(endpoint: Endpoint): void ``` --- # APICache.refresh() - Kind: method - Member of: `APICache` Trigger a refetch on a specific store for an endpoint. ## Signatures ```ts refresh(endpoint: Endpoint, payload: PP, maxAge?: number): Promise ``` --- # APICache.refreshAll() - Kind: method - Member of: `APICache` Trigger a refetch on all stores for an endpoint. ## Signatures ```ts refreshAll(endpoint: Endpoint, maxAge?: number): Promise ``` --- # EndpointCache - Kind: class - Import: `import { EndpointCache } from "shelving/api";` - Implements: `AsyncDisposable` Cache of `EndpointStore` objects for a single endpoint, keyed by the rendered request URL of each payload. - Use `get(payload)` to retrieve or create the `EndpointStore` for a given payload. - Disposing the cache disposes every nested `EndpointStore` and clears the map. ## Signatures ```ts new EndpointCache(endpoint: Endpoint, provider: APIProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `endpoint` | `Endpoint` | | | `provider` | `APIProvider` | | ## Returns | Type | Description | | --- | --- | | `EndpointCache` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `endpoint` | `Endpoint` | The endpoint that every store in this cache fetches from. Read only. | | `provider` | `APIProvider` | The `APIProvider` used to render URLs and fetch results for this endpoint. Read only. | ## Examples ```ts const cache = new EndpointCache(getUser, provider); const user = await cache.call({ id: "abc" }); ``` ## Methods - [EndpointCache.get()](api/EndpointCache/get.md): Get (or create) the EndpointStore for the given payload. - [EndpointCache.call()](api/EndpointCache/call.md): Fetch (or return a cached result) for the given payload. - [EndpointCache.invalidate()](api/EndpointCache/invalidate.md): Invalidate a specific store so the next read refetches. - [EndpointCache.invalidateAll()](api/EndpointCache/invalidateAll.md): Invalidate all stores so the next read of any payload refetches. - [EndpointCache.refresh()](api/EndpointCache/refresh.md): Trigger a refetch on a specific store. - [EndpointCache.refreshAll()](api/EndpointCache/refreshAll.md): Trigger a refetch on all stores. --- # EndpointCache.get() - Kind: method - Member of: `EndpointCache` Get (or create) the `EndpointStore` for the given payload. - Stores are keyed by the rendered request URL, so equivalent payloads share a store. ## Signatures ```ts get(payload: P, caller: AnyCaller = this.get): EndpointStore ``` --- # EndpointCache.call() - Kind: method - Member of: `EndpointCache` Fetch (or return a cached result) for the given payload. - Returns the cached value immediately if one exists. - Waits for the in-flight fetch if the store is loading. - Throws if the fetch fails, matching `APIProvider.call` behaviour. ## Signatures ```ts call(payload: P, maxAge: number = AVOID_REFRESH, caller: AnyCaller = this.call): Promise ``` --- # EndpointCache.invalidate() - Kind: method - Member of: `EndpointCache` Invalidate a specific store so the next read refetches. ## Signatures ```ts invalidate(payload: P, caller: AnyCaller = this.invalidate): void ``` --- # EndpointCache.invalidateAll() - Kind: method - Member of: `EndpointCache` Invalidate all stores so the next read of any payload refetches. ## Signatures ```ts invalidateAll(): void ``` --- # EndpointCache.refresh() - Kind: method - Member of: `EndpointCache` Trigger a refetch on a specific store. ## Signatures ```ts refresh(payload: P, maxAge?: number, caller: AnyCaller = this.refresh): Promise ``` --- # EndpointCache.refreshAll() - Kind: method - Member of: `EndpointCache` Trigger a refetch on all stores. ## Signatures ```ts refreshAll(maxAge?: number): Promise ``` --- # Endpoint - Kind: class - Import: `import { Endpoint } from "shelving/api";` A typed API resource definition pairing a method and path with payload and result schemas. - Acts as the contract both ends of an API agree on: providers render requests from it, handlers validate against it. - Path may contain `{placeholder}` segments that are filled from the payload at request time. ## Signatures ```ts new Endpoint(method: RequestMethod, path: AbsolutePath, payload: Schema

, result: Schema) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `method` | `RequestMethod` | | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `method` | `RequestMethod` | The HTTP method this endpoint responds to, e.g. `GET` Read only. | | `path` | `AbsolutePath` | Endpoint path, possibly including placeholders e.g. `/users/{id}` Read only. | | `placeholders` | `TemplatePlaceholders` | The `{placeholder}` segments extracted from `path`, used to render and match URLs. Read only. | | `payload` | `Schema

` | The `Schema` that request payloads are validated against. Read only. | | `result` | `Schema` | The `Schema` that response results are validated against. Read only. | ## Examples ```ts const getUser = new Endpoint("GET", "/users/{id}", USER_PAYLOAD, USER_RESULT); ``` ## Methods - [Endpoint.renderPath()](api/Endpoint/renderPath.md): Render the path for this endpoint with the given payload. - [Endpoint.match()](api/Endpoint/match.md): Match a method/path pair against this endpoint and return any matched {placeholder} params. - [Endpoint.handler()](api/Endpoint/handler.md): Create an endpoint handler pairing for this endpoint. - [Endpoint.toString()](api/Endpoint/toString.md): Convert this endpoint to a string in METHOD /path form, e.g. GET /user/{id} --- # Endpoint.renderPath() - Kind: method - Member of: `Endpoint` Render the path for this endpoint with the given payload. - Path might contain `{placeholder}` values that are replaced with values from `payload`. ## Signatures ```ts renderPath(payload: P, caller: AnyCaller = this.renderPath): AbsolutePath ``` --- # Endpoint.match() - Kind: method - Member of: `Endpoint` Match a method/path pair against this endpoint and return any matched `{placeholder}` params. ## Signatures ```ts match(method: RequestMethod, path: AbsolutePath, caller: AnyCaller = this.match): RequestParams | undefined ``` --- # Endpoint.handler() - Kind: method - Member of: `Endpoint` Create an endpoint handler pairing for this endpoint. ## Signatures ```ts handler(callback: EndpointCallback): EndpointHandler ``` --- # Endpoint.toString() - Kind: method - Member of: `Endpoint` Convert this endpoint to a string in `METHOD /path` form, e.g. `GET /user/{id}` ## Signatures ```ts toString(): string ``` --- # AnyEndpoint - Kind: type - Import: `import { AnyEndpoint } from "shelving/api";` An `Endpoint` with any payload and result type, for use where the specific types don't matter. ## Signatures ```ts Endpoint ``` ## Types - [Endpoint](api/Endpoint.md): A typed API resource definition pairing a method and path with payload and result schemas. --- # Endpoints - Kind: type - Import: `import { Endpoints } from "shelving/api";` An immutable list of endpoints. ## Signatures ```ts ImmutableArray ``` ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. - [AnyEndpoint](api/AnyEndpoint.md): An Endpoint with any payload and result type, for use where the specific types don't matter. --- # PayloadType - Kind: type - Import: `import { PayloadType } from "shelving/api";` Extract the payload type from an `Endpoint`. ## Signatures ```ts X extends Endpoint ? Y : never ``` ## Types - [Endpoint](api/Endpoint.md): A typed API resource definition pairing a method and path with payload and result schemas. - `Y` --- # EndpointType - Kind: type - Import: `import { EndpointType } from "shelving/api";` Extract the result type from an `Endpoint`. ## Signatures ```ts X extends Endpoint ? Y : never ``` ## Types - [Endpoint](api/Endpoint.md): A typed API resource definition pairing a method and path with payload and result schemas. - `Y` --- # HEAD() - Kind: function - Import: `import { HEAD } from "shelving/api";` Define a `HEAD` endpoint at a path, with validated payload and result types. - "The HEAD method requests a representation of the specified resource. Requests using HEAD should only retrieve data and should not contain a request content." *Factory for `Endpoint`.* ## Signatures ```ts HEAD(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint HEAD(path: AbsolutePath, payload: Schema

): Endpoint HEAD(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `HEAD` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts HEAD("/users/{id}") ``` --- # GET() - Kind: function - Import: `import { GET } from "shelving/api";` Define a `GET` endpoint at a path, with validated payload and result types. - "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content." *Factory for `Endpoint`.* ## Signatures ```ts GET(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint GET(path: AbsolutePath, payload: Schema

): Endpoint GET(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `GET` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts GET("/users/{id}", undefined, USER) ``` --- # POST() - Kind: function - Import: `import { POST } from "shelving/api";` Define a `POST` endpoint at a path, with validated payload and result types. - "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server." *Factory for `Endpoint`.* ## Signatures ```ts POST(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint POST(path: AbsolutePath, payload: Schema

): Endpoint POST(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `POST` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts POST("/users", USER, USER_RESULT) ``` --- # PUT() - Kind: function - Import: `import { PUT } from "shelving/api";` Define a `PUT` endpoint at a path, with validated payload and result types. - "The PUT method replaces all current representations of the target resource with the request content." *Factory for `Endpoint`.* ## Signatures ```ts PUT(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint PUT(path: AbsolutePath, payload: Schema

): Endpoint PUT(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `PUT` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts PUT("/users/{id}", USER) ``` --- # PATCH() - Kind: function - Import: `import { PATCH } from "shelving/api";` Define a `PATCH` endpoint at a path, with validated payload and result types. - "The PATCH method applies partial modifications to a resource." *Factory for `Endpoint`.* ## Signatures ```ts PATCH(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint PATCH(path: AbsolutePath, payload: Schema

): Endpoint PATCH(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `PATCH` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts PATCH("/users/{id}", PARTIAL_USER) ``` --- # DELETE() - Kind: function - Import: `import { DELETE } from "shelving/api";` Define a `DELETE` endpoint at a path, with validated payload and result types. - "The DELETE method deletes the specified resource." *Factory for `Endpoint`.* ## Signatures ```ts DELETE(path: AbsolutePath, payload?: Schema

, result?: Schema): Endpoint DELETE(path: AbsolutePath, payload: Schema

): Endpoint DELETE(path: AbsolutePath, payload: undefined, result: Schema): Endpoint ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `path` | `AbsolutePath` | The endpoint path, possibly including `{placeholders}` | | `payload` | `Schema

` | An optional `Schema` validating the request payload. Optional. | | `result` | `Schema` | An optional `Schema` validating the response result. Optional. | | `path` | `AbsolutePath` | | | `payload` | `Schema

` | | | `payload` | `undefined` | | | `result` | `Schema` | | ## Returns | Type | Description | | --- | --- | | `Endpoint` | An `Endpoint` configured for the `DELETE` method. | | `Endpoint` | | | `Endpoint` | | ## Examples ```ts DELETE("/users/{id}") ``` --- # EndpointCallback - Kind: type - Import: `import { EndpointCallback } from "shelving/api";` A function that handles an endpoint request, receiving a validated payload and returning a result. ## Signatures ```ts (payload: P, request: Request, context: C) => R | Response | Promise ``` ## Returns | Type | Description | | --- | --- | | `Response` | Returning a `Response` object (this will pass back to the client without validation). | | `R` | Returning the return type of the handler (this will be validated against the Endpoint's result schema). | ## Types - `Request` - `Response` - `Promise` --- # EndpointHandler - Kind: interface - Import: `import { EndpointHandler } from "shelving/api";` A typed endpoint definition paired with its implementation callback. - Created with `Endpoint.handler()`; matched and invoked by `handleEndpoints()`. ## Signatures ```ts { readonly endpoint: Endpoint; readonly callback: EndpointCallback; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `endpoint` | `Endpoint` | Read only. | | `callback` | `EndpointCallback` | Read only. | --- # AnyEndpointHandler - Kind: type - Import: `import { AnyEndpointHandler } from "shelving/api";` An `EndpointHandler` with any payload and result type, for use where the specific types don't matter. ## Signatures ```ts EndpointHandler ``` ## Types - [EndpointHandler](api/EndpointHandler.md): A typed endpoint definition paired with its implementation callback. --- # EndpointHandlers - Kind: type - Import: `import { EndpointHandlers } from "shelving/api";` A collection of endpoint handlers that can be matched and invoked by `handleEndpoints()`. ## Signatures ```ts Iterable> ``` ## Types - `Iterable` - [AnyEndpointHandler](api/AnyEndpointHandler.md): An EndpointHandler with any payload and result type, for use where the specific types don't matter. --- # handleEndpoints() - Kind: function - Import: `import { handleEndpoints } from "shelving/api";` Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname. - The original `Request` object is passed through to the callback unchanged. - Path params and query params are merged before payload validation. ## Signatures ```ts handleEndpoints(base: PossibleURL, handlers: EndpointHandlers, request: Request, context: C, caller?: AnyCaller): Promise handleEndpoints(base: PossibleURL, handlers: EndpointHandlers, request: Request, context?: undefined, caller?: AnyCaller): Promise ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `base` | `PossibleURL` | The base URL for the API, e.g. `https://myapi.com/a/b` - `pathname` of this URL gets trimmed from `request.path` to form the target path when matching against endpoints, e.g. `/a/b/c/d` will produce `/c/d` for matching. | | `handlers` | `EndpointHandlers` | The iterable of `EndpointHandler` objects to match the request against, in order. | | `request` | `Request` | The input request to handle. | | `context` | `C` | The additional context value passed through to the matched handler's callback. | | `caller` | `AnyCaller` | The function to attribute thrown errors to (defaults to this function). Optional. | | `base` | `PossibleURL` | | | `handlers` | `EndpointHandlers` | | | `request` | `Request` | | | `context` | `undefined` | Optional. | | `caller` | `AnyCaller` | Optional. | ## Returns | Type | Description | | --- | --- | | `Promise` | A promise resolving to the `Response` produced by the matching handler. | | `Promise` | | ## Throws | Type | Description | | --- | --- | | `MethodNotAllowedError` | if the request method is not a supported HTTP method. | | `NotFoundError` | if no base path matches, or no endpoint matches the target path. | ## Examples ```ts await handleEndpoints("https://myapi.com", [getUser.handler(loadUser)], request, undefined) ``` --- # APIProvider - Kind: class - Import: `import { APIProvider } from "shelving/api";` - Implements: `AsyncDisposable` Abstract base for API providers that send requests to a set of `Endpoint` definitions rooted at a common base URL. - Concrete subclasses implement `renderURL()`, `createRequest()`, `fetch()`, and `parseResponse()`; `call()` orchestrates them. - Implements `AsyncDisposable` so providers can be wrapped and disposed in a chain. The abstract base class every API provider implements. `APIProvider` defines the surface that endpoint calls go through: `call(endpoint, payload)` to execute a typed endpoint, and `fetch(request)` as the lower-level transport step that wrapper providers intercept. You never instantiate `APIProvider` directly — use the concrete `ClientAPIProvider` (or a mock), optionally wrapped in `ThroughAPIProvider` subclasses. `APIProvider` implements `AsyncDisposable`. ## Usage Code that accepts "any provider" should type against `APIProvider` so a bare client, a validated chain, or a mock are all interchangeable: ```ts import type { APIProvider } from "shelving/api" async function loadUser(provider: APIProvider, id: string) { return provider.call(getUser, { id }); // works with any provider in the chain } ``` ## Signatures ```ts new APIProvider() ``` ## Returns | Type | Description | | --- | --- | | `APIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `url` | `URL` | The common base URL that every endpoint request is resolved against. Read only. | ## Methods - [APIProvider.renderURL()](api/APIProvider/renderURL.md): Render the full final URL for an API request to a given endpoint with a given payload. - [APIProvider.createRequest()](api/APIProvider/createRequest.md): Create a Request that targets this endpoint with a given base URL. - [APIProvider.fetch()](api/APIProvider/fetch.md): Send a Request and return its Response (defaults to the JavaScript fetch() API). - [APIProvider.parseResponse()](api/APIProvider/parseResponse.md): Parse an HTTP Response for this endpoint into a result value. - [APIProvider.call()](api/APIProvider/call.md): Send a payload to an Endpoint and retrieve the parsed result. --- # APIProvider.renderURL() - Kind: method - Member of: `APIProvider` Render the full final URL for an API request to a given endpoint with a given payload. - Includes `?query` params if this is a `HEAD` or `GET` request. ## Signatures ```ts renderURL(endpoint: Endpoint, payload: PP, caller?: AnyCaller): URL ``` --- # APIProvider.createRequest() - Kind: method - Member of: `APIProvider` Create a `Request` that targets this endpoint with a given base URL. ## Signatures ```ts createRequest(endpoint: Endpoint, payload: PP, options?: RequestOptions, caller?: AnyCaller): Request ``` --- # APIProvider.fetch() - Kind: method - Member of: `APIProvider` Send a `Request` and return its `Response` (defaults to the JavaScript `fetch()` API). ## Signatures ```ts fetch(request: Request): Promise ``` --- # APIProvider.parseResponse() - Kind: method - Member of: `APIProvider` Parse an HTTP `Response` for this endpoint into a result value. - Non-2xx responses become `ResponseError`. - Does not validate the result against the endpoint schema — use `ValidationAPIProvider` for that. ## Signatures ```ts parseResponse(_endpoint: Endpoint, response: Response, caller?: AnyCaller): Promise ``` --- # APIProvider.call() - Kind: method - Member of: `APIProvider` Send a payload to an `Endpoint` and retrieve the parsed result. - Composes `createRequest()`, `fetch()`, and `parseResponse()` into a single round trip. ## Signatures ```ts call(endpoint: Endpoint, payload: PP, options?: RequestOptions, caller?: AnyCaller): Promise ``` --- # CachedAPIProvider - Kind: class - Import: `import { CachedAPIProvider } from "shelving/api";` - Extends: `ThroughAPIProvider` - Implements: `AsyncDisposable` API provider wrapper that serves requests through an `APICache` so repeated calls reuse cached results. - On `call(...)`, triggers `cache.refresh(maxAge)` for the endpoint+payload before awaiting `cache.call(...)`. - `invalidate`, `invalidateAll`, `refresh`, and `refreshAll` pass through to the underlying cache and use `this.maxAge` as the default refresh timing. A wrapping provider that serves calls through an `APICache`. Repeated calls for the same endpoint and payload skip the network and return the cached value, and it exposes `invalidate` / `refresh` helpers for cache control. The constructor takes the `source` provider and an optional default `maxAge` (defaults to `AVOID_REFRESH` — only refetch when a value is missing or invalidated). ## Usage ```ts import { CachedAPIProvider, ValidationAPIProvider, ClientAPIProvider } from "shelving/api" const provider = new CachedAPIProvider( new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" })) ) await provider.call(getUser, { id: "u_1" }) // fetches await provider.call(getUser, { id: "u_1" }) // returns cached provider.invalidate(getUser, { id: "u_1" }) // mark stale provider.refresh(getUser, { id: "u_1" }) // re-fetch eagerly ``` `invalidateAll(endpoint)` and `refreshAll(endpoint)` act on every cached payload for an endpoint at once. ## Signatures ```ts new CachedAPIProvider(source: APIProvider, maxAge: number = AVOID_REFRESH) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `APIProvider` | | | `maxAge` | `number` | Defaults to `AVOID_REFRESH`. | ## Returns | Type | Description | | --- | --- | | `CachedAPIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `maxAge` | `number \| undefined` | The maximum age used when calling `call()`, defaulting to `AVOID_REFRESH` (only refresh if invalidated or still loading). - Not used for `refresh()` calls, which always refetch immediately. Read only. | ## Methods - [CachedAPIProvider.invalidate()](api/CachedAPIProvider/invalidate.md): Invalidate the cached result for a specific endpoint and payload. - [CachedAPIProvider.invalidateAll()](api/CachedAPIProvider/invalidateAll.md): Invalidate every cached result for an endpoint, across all payloads. - [CachedAPIProvider.refresh()](api/CachedAPIProvider/refresh.md): Refresh the cached result for a specific endpoint and payload. - [CachedAPIProvider.refreshAll()](api/CachedAPIProvider/refreshAll.md): Refresh every cached result for an endpoint, across all payloads. --- # CachedAPIProvider.invalidate() - Kind: method - Member of: `CachedAPIProvider` Invalidate the cached result for a specific endpoint and payload. ## Signatures ```ts invalidate(endpoint: Endpoint, payload: PP): void ``` --- # CachedAPIProvider.invalidateAll() - Kind: method - Member of: `CachedAPIProvider` Invalidate every cached result for an endpoint, across all payloads. ## Signatures ```ts invalidateAll(endpoint: Endpoint): void ``` --- # CachedAPIProvider.refresh() - Kind: method - Member of: `CachedAPIProvider` Refresh the cached result for a specific endpoint and payload. ## Signatures ```ts refresh(endpoint: Endpoint, payload: PP): Promise ``` --- # CachedAPIProvider.refreshAll() - Kind: method - Member of: `CachedAPIProvider` Refresh every cached result for an endpoint, across all payloads. ## Signatures ```ts refreshAll(endpoint: Endpoint): Promise ``` --- # ClientAPIProviderOptions - Kind: interface - Import: `import { ClientAPIProviderOptions } from "shelving/api";` Options for constructing a `ClientAPIProvider`. ## Signatures ```ts { readonly url: PossibleURL; readonly options?: Omit; readonly timeout?: number | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `url` | `PossibleURL` | The common base URL for all rendered endpoint requests. Note: When resolving URLs for endpoints this is treated as if it ends in a slash. - e.g. in `http://p.com/a/b/c` the path will be relative to `c` as if a `/` trailing slash was present. - This is different to the default behaviour of `new URL()`, but is the more natural expected result - This is consistent with our e.g. `getURL()` utilities. Read only. | | `options` | `Omit` | Options used for HTTP requests created with `this.createRequest()` and `this.fetch()` - Omits `signal` because it's not relevant at the provider level. Read only. Optional. | | `timeout` | `number \| undefined` | Timeout in milliseconds before the request is aborted with `TimeoutError`. - Defaults to `20_000` (20 seconds) — chosen to fire before common platform wall-clock caps (Cloudflare Workers ~30s, Vercel/AWS API Gateway ~29s) so the abort propagates as a clean rejection instead of an opaque runtime termination. - Pass `0` to disable the timeout (e.g. for streaming or long-poll endpoints). Raise it for specifically slow endpoints. Read only. Optional. | --- # ClientAPIProvider - Kind: class - Import: `import { ClientAPIProvider } from "shelving/api";` - Extends: `APIProvider` A client-side API provider that sends requests over the network using `fetch()`. - Can be used on a server environment to make outgoing API calls, or in a browser environment to call a server API. - Renders endpoint paths and query params into the URL and sends body payloads as JSON. - Parses JSON responses and throws `ResponseError` for non-2xx responses. - Extendable with custom request-building and response-parsing logic by overriding `createRequest()` and `parseResponse()`. - Wrap in `ValidationAPIProvider` to add automatic validation of request payloads and response results against endpoint schemas. The concrete network provider. `ClientAPIProvider` sends requests over the network with the global `fetch()` API and is the innermost provider in almost every chain. The constructor takes `{ url, options?, timeout? }` — a base URL that endpoint paths are resolved against, optional default `RequestInit` options, and a request timeout in milliseconds (default `20000`). ## Usage ```ts import { ClientAPIProvider } from "shelving/api" const provider = new ClientAPIProvider({ url: "https://api.example.com", timeout: 10_000, }) const user = await provider.call(getUser, { id: "u_123" }) ``` In practice you wrap it — e.g. `new ValidationAPIProvider(new ClientAPIProvider({ url }))` — so payloads and results are validated. See `shelving/api` for building a provider chain. ## Signatures ```ts new ClientAPIProvider({ url, options = {}, timeout = 20_000 }: ClientAPIProviderOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `ClientAPIProviderOptions` | | | `.url` | `PossibleURL` | The common base URL for all rendered endpoint requests. Note: When resolving URLs for endpoints this is treated as if it ends in a slash. - e.g. in `http://p.com/a/b/c` the path will be relative to `c` as if a `/` trailing slash was present. - This is different to the default behaviour of `new URL()`, but is the more natural expected result - This is consistent with our e.g. `getURL()` utilities. Read only. | | `.options` | `Omit` | Options used for HTTP requests created with `this.createRequest()` and `this.fetch()` - Omits `signal` because it's not relevant at the provider level. Read only. Optional. | | `.timeout` | `number \| undefined` | Timeout in milliseconds before the request is aborted with `TimeoutError`. - Defaults to `20_000` (20 seconds) — chosen to fire before common platform wall-clock caps (Cloudflare Workers ~30s, Vercel/AWS API Gateway ~29s) so the abort propagates as a clean rejection instead of an opaque runtime termination. - Pass `0` to disable the timeout (e.g. for streaming or long-poll endpoints). Raise it for specifically slow endpoints. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `ClientAPIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `options` | `RequestOptions` | Default options used for HTTP requests created with `this.createRequest()` and `this.fetch()` Read only. | | `timeout` | `number` | Timeout in milliseconds before the request is aborted, or `0` for no timeout. Read only. | --- # DebugAPIProvider - Kind: class - Import: `import { DebugAPIProvider } from "shelving/api";` - Extends: `ThroughAPIProvider` Provider that logs every request, response, and error to the console in detail to help diagnose issues in development. A wrapping provider for development. `DebugAPIProvider` writes verbose console output for every call — including full request and response bodies — so you can see exactly what is going over the wire. It is the noisy counterpart to `LoggingAPIProvider`: use `DebugAPIProvider` while developing, and `LoggingAPIProvider` (or nothing) in production. ## Usage ```ts import { ClientAPIProvider, DebugAPIProvider } from "shelving/api" const provider = new DebugAPIProvider( new ClientAPIProvider({ url: "https://api.example.com" }) ) await provider.call(getUser, { id: "u_123" }) // Console shows the full request and response, including bodies. ``` ## Signatures ```ts new DebugAPIProvider() ``` ## Returns | Type | Description | | --- | --- | | `DebugAPIProvider` | | --- # HydratingAPIProvider - Kind: class - Import: `import { HydratingAPIProvider } from "shelving/api";` - Extends: `ValidationAPIProvider` API provider that validates every result, then writes the collection items in it into a local database. - Only results that use a collection's own schemas hydrate: `Collection.item`, `Collection.items` and `Collection.queryItems`, or a `DataSchema` whose props use them (one level deep). Every other result passes through with no change. - `Collection.item` sets the item. A `null` or `undefined` result (e.g. `NULLABLE(USERS.item)`) deletes the item with the `id` from the payload (the payload itself, or its `id` prop). - `Collection.items` sets each item. It never deletes items. - `Collection.queryItems` sets each item, then marks the query as loaded (see `MemoryTable.setQueryTime()`). - Writes happen synchronously before `call()` resolves, so no store sees half of a result. - Extends `ValidationAPIProvider`, so it only ever writes validated items. A wrapping provider that validates every result, then writes the collection items in it into a local `MemoryDBProvider`. One API response can then fill the stores for several collections, and `useItem()` / `useQuery()` show the items at once. ## Things to know - Only results that use a collection's own schemas hydrate: `Collection.item`, `Collection.items` and `Collection.queryItems`, or a `DataSchema` whose props use them (one level deep). Every other result passes through with no change. - `ARRAY(USERS.item)` does **not** hydrate, because it does not point at the collection. Use `USERS.items`. - A `null` or `undefined` result for `NULLABLE(USERS.item)` deletes the item with the `id` from the payload. The table then knows the item is missing, so an `ItemStore` shows it as missing and not as loading. - `Collection.items` only adds and replaces items. It never deletes them, so an item the server dropped stays until something deletes it. - `Collection.queryItems` also marks its query as loaded (see `MemoryTable.setQueryTime()`), so a `QueryStore` for exactly that query shows its items, or shows that it has none, without loading. The query matches by `JSON.stringify()`, so build it in one shared function for the server and the client. - **Do not trust the `query` in a `Collection.queryItems` result.** It is only checked to be an object. - Writes happen synchronously before `call()` resolves, so no store sees half of a result. - It extends `ValidationAPIProvider`, so it only writes validated items. ## Usage ```ts import { ClientAPIProvider, GET, HydratingAPIProvider } from "shelving/api"; import { COLLECTION, MemoryDBProvider } from "shelving/db"; import { DATA, NULLABLE, STRING } from "shelving/schema"; const SQUADS = COLLECTION("squads", STRING, { title: STRING }); const MEMBERS = COLLECTION("members", STRING, { squad: STRING, user: STRING }); // One response returns two collections. const MY_SQUADS = GET("/squad/me", DATA({}), DATA({ squads: SQUADS.items, members: MEMBERS.queryItems })); const GET_SQUAD = GET("/squad/{id}", DATA({ id: STRING }), NULLABLE(SQUADS.item)); const DB = new MemoryDBProvider(); const API = new HydratingAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" }), DB); await API.call(MY_SQUADS, {}); // Writes the squads and members into `DB`, and marks the members query as loaded. await API.call(GET_SQUAD, { id: "reds" }); // Sets the squad, or deletes it if the result is `null`. ``` The server sends the query with the items, so the client knows which query the items answer: ```ts // shared/query.ts — one function builds the query on the server and the client. export const getMyMembersQuery = (user: string): Query => ({ user, $order: "squad" }); // Server handler. MY_SQUADS.handler(async (_payload, _request, { user }) => ({ squads: await getMySquads(user), members: { query: getMyMembersQuery(user), items: await getMyMembers(user) }, })); ``` ## Paging Page an endpoint with `getQueryItemsAfter()`. It takes the cursor from the last page, never from the cache: ```ts const page = await API.call(LIST_MESSAGES, { squad, $order: "!when", $limit: 30 }); const next = getQueryItemsAfter(page.messages); // `undefined` when there are no more. if (next) await API.call(LIST_MESSAGES, next); ``` ## Signatures ```ts new HydratingAPIProvider(source: APIProvider, db: MemoryDBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `APIProvider` | | | `db` | `MemoryDBProvider` | | ## Returns | Type | Description | | --- | --- | | `HydratingAPIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `db` | `MemoryDBProvider` | The local database the items are written into. Read only. | --- # JSONAPIProvider - Kind: class - Import: `import { JSONAPIProvider } from "shelving/api";` - Extends: `ClientAPIProvider` Client API provider that always sends request bodies as JSON and parses responses as JSON. - Forces JSON encoding regardless of the `Content-Type` the server returns. A `ClientAPIProvider` that forces JSON. It sends request bodies as JSON and always parses responses as JSON, regardless of content-type headers — useful against APIs that are inconsistent about declaring `application/json`. Because it extends `ClientAPIProvider`, it is a concrete network provider: construct it with the same `{ url, options?, timeout? }` options. ## Usage ```ts import { JSONAPIProvider, ValidationAPIProvider } from "shelving/api" const provider = new ValidationAPIProvider( new JSONAPIProvider({ url: "https://api.example.com" }) ) const user = await provider.call(getUser, { id: "u_123" }) ``` ## Signatures ```ts new JSONAPIProvider() ``` ## Returns | Type | Description | | --- | --- | | `JSONAPIProvider` | | --- # LoggingAPIProvider - Kind: class - Import: `import { LoggingAPIProvider } from "shelving/api";` - Extends: `ThroughAPIProvider` Provider that logs fetches to the console to keep useful request/response logs in production. - Defaults to logging requests, responses, and errors via the `log` utilities; each can be overridden. A wrapping provider that logs requests, responses, and errors. `LoggingAPIProvider` is production-safe — it logs concise lines rather than full bodies (use `DebugAPIProvider` for verbose development output). The constructor takes the `source` provider plus three optional callbacks — `onRequest`, `onResponse`, `onError` — each defaulting to a `console`-based logger. ## Usage ```ts import { ClientAPIProvider, LoggingAPIProvider } from "shelving/api" // Default console logging. const provider = new LoggingAPIProvider( new ClientAPIProvider({ url: "https://api.example.com" }) ) // Or route logs into your own logger. const custom = new LoggingAPIProvider( new ClientAPIProvider({ url: "https://api.example.com" }), (request) => logger.info("api request", request.url), (response, request) => logger.info("api response", request.url, response.status), (reason, request) => logger.error("api error", request.url, reason), ) ``` ## Signatures ```ts new LoggingAPIProvider(source: APIProvider, onRequest: Callback<[Request]> = logRequest, onResponse: Callback<[Response, Request]> = logRequestResponse, onError: Callback<[reason: unknown, Request]> = logRequestError) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `APIProvider` | | | `onRequest` | `Callback<[Request]>` | Defaults to `logRequest`. | | `onResponse` | `Callback<[Response, Request]>` | Defaults to `logRequestResponse`. | | `onError` | `Callback<[reason: unknown, Request]>` | Defaults to `logRequestError`. | ## Returns | Type | Description | | --- | --- | | `LoggingAPIProvider` | | --- # MockAPIFetchCall - Kind: type - Import: `import { MockAPIFetchCall } from "shelving/api";` Record of a single mocked fetch, pairing the request with the response the handler returned. ## Signatures ```ts { readonly request: Request; readonly response: Response; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `request` | `Request` | Read only. | | `response` | `Response` | Read only. | ## Types - `Request` - `Response` --- # MockAPIRequestCall - Kind: type - Import: `import { MockAPIRequestCall } from "shelving/api";` Record of a single request build, capturing the endpoint, payload, options, and built request. ## Signatures ```ts { readonly endpoint: AnyEndpoint; readonly payload: unknown; readonly options: RequestOptions | undefined; readonly request: Request; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `endpoint` | `AnyEndpoint` | Read only. | | `payload` | `unknown` | Read only. | | `options` | `RequestOptions \| undefined` | Read only. | | `request` | `Request` | Read only. | ## Types - [AnyEndpoint](api/AnyEndpoint.md): An Endpoint with any payload and result type, for use where the specific types don't matter. - [RequestOptions](util/http/RequestOptions.md): Configurable options for endpoint requests. - `Request` --- # MockAPIResponseCall - Kind: type - Import: `import { MockAPIResponseCall } from "shelving/api";` Record of a single response parse, capturing the endpoint, response, and parsed result. ## Signatures ```ts { readonly endpoint: AnyEndpoint; readonly response: Response; readonly result: unknown; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `endpoint` | `AnyEndpoint` | Read only. | | `response` | `Response` | Read only. | | `result` | `unknown` | Read only. | ## Types - [AnyEndpoint](api/AnyEndpoint.md): An Endpoint with any payload and result type, for use where the specific types don't matter. - `Response` --- # MockAPIProvider - Kind: class - Import: `import { MockAPIProvider } from "shelving/api";` - Extends: `ThroughAPIProvider` Provider that records API calls and serves them from a handler without sending network requests. - Extends `ThroughAPIProvider` to delegate request building and response parsing to a source `APIProvider`. - The source provider's `fetch()` is never called — this provider intercepts all fetches and routes them through a `RequestHandler`. - Records `requestCalls`, `fetchCalls`, and `responseCalls` so tests can assert on what happened. A wrapping provider for tests. `MockAPIProvider` intercepts fetches through a `RequestHandler` instead of hitting the network, and records every call it makes so a test can assert on them. The constructor takes an optional `RequestHandler` and an optional `source` provider — both have defaults, so `new MockAPIProvider()` is enough for a provider that records calls and returns canned responses. ## Usage ```ts import { MockAPIProvider } from "shelving/api" const api = new MockAPIProvider((request) => new Response(JSON.stringify({ id: "u_1", name: "Alice" }))) const user = await api.call(getUser, { id: "u_1" }) console.log(api.requestCalls) // inspect every recorded request ``` For tests that exercise client and server logic together, use `MockEndpointAPIProvider`, which routes the mock transport through a real handler array. ## Signatures ```ts new MockAPIProvider(handler: RequestHandler = _mockHandler, source: APIProvider = new ClientAPIProvider({ url: "https://api.mock.com" })) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `handler` | `RequestHandler` | Defaults to `_mockHandler`. | | `source` | `APIProvider` | Defaults to `new ClientAPIProvider({ url: "https://api.mock.com" })`. | ## Returns | Type | Description | | --- | --- | | `MockAPIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `requestCalls` | `MockAPIRequestCall[]` | Records of every request built by this provider. Read only. Defaults to `[]`. | | `fetchCalls` | `MockAPIFetchCall[]` | Records of every fetch handled by this provider. Read only. Defaults to `[]`. | | `responseCalls` | `MockAPIResponseCall[]` | Records of every response parsed by this provider. Read only. Defaults to `[]`. | | `handler` | `RequestHandler` | The request handler that serves fetches in place of the network. Read only. | --- # MockEndpointAPIProvider - Kind: class - Import: `import { MockEndpointAPIProvider } from "shelving/api";` - Extends: `MockAPIProvider` Provider that mocks an API that calls and matches an array of `EndpointHandler` objects returned from `Endpoint.handler()` - Used to test server-side API code, calls against an API made up of multiple `Endpoint` instances. A `MockAPIProvider` that wires the mock transport directly to a real `EndpointHandler` array. Client code calls endpoints exactly as in production, but each call is dispatched to its handler in the same process — so you can test client and server logic together without a network. The constructor takes the `handlers` array, a `context` value passed to every handler, and an optional `source` provider. ## Usage ```ts import { MockEndpointAPIProvider } from "shelving/api" const handlers = [ getUser.handler(async ({ id }) => ({ id, name: "Alice", email: "alice@example.com" })), ] const api = new MockEndpointAPIProvider(handlers, undefined) const user = await api.call(getUser, { id: "u_1" }) // user == { id: "u_1", name: "Alice", email: "alice@example.com" } console.log(api.requestCalls) // inspect recorded calls ``` ## Signatures ```ts new MockEndpointAPIProvider(handlers: EndpointHandlers, context: C, source?: ClientAPIProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `handlers` | `EndpointHandlers` | | | `context` | `C` | | | `source` | `ClientAPIProvider` | Optional. | ## Returns | Type | Description | | --- | --- | | `MockEndpointAPIProvider` | | --- # ThroughAPIProvider - Kind: class - Import: `import { ThroughAPIProvider } from "shelving/api";` - Extends: `APIProvider` - Implements: `Sourceable>` Provider wrapper that delegates every API operation to a wrapped `source` provider. - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging. - Implements `Sourceable` so wrapped providers are discoverable via `getSource()` / `requireSource()`. The pass-through base class for wrapping providers. `ThroughAPIProvider` takes a `source` provider and delegates every method to it. Extend it to intercept only the methods you care about — adding auth headers, retries, metrics — without reimplementing transport logic. `ValidationAPIProvider`, `LoggingAPIProvider`, `DebugAPIProvider`, `MockAPIProvider`, and `CachedAPIProvider` are all `ThroughAPIProvider` subclasses. ## Usage Override `fetch()` (or `call()`) and call `super` to delegate the rest. For example, a provider that injects an `Authorization` header: ```ts import { ThroughAPIProvider } from "shelving/api" import type { APIProvider } from "shelving/api" class AuthAPIProvider extends ThroughAPIProvider { constructor(source: APIProvider, readonly token: string) { super(source) } override fetch(request: Request): Promise { const authed = new Request(request, { headers: { ...Object.fromEntries(request.headers), Authorization: `Bearer ${this.token}` }, }) return super.fetch(authed) } } ``` ## Signatures ```ts new ThroughAPIProvider(source: APIProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `APIProvider` | | ## Returns | Type | Description | | --- | --- | | `ThroughAPIProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `APIProvider` | The wrapped source provider that operations delegate to. Read only. | --- # ValidationAPIProvider - Kind: class - Import: `import { ValidationAPIProvider } from "shelving/api";` - Extends: `ThroughAPIProvider` Provider that validates payloads and results against the endpoint's schemas, so a source of any type is made type-safe. - Payload validation errors bubble up as user-readable strings; result validation errors are wrapped in `ResponseError`. A wrapping provider that validates data against the endpoint's `shelving/schema`. `ValidationAPIProvider` validates the payload before the request is built and validates the result after the response is parsed — so a malformed payload is caught before it leaves, and a bad response surfaces as a clear error. Place it just inside the network provider so everything above it works with validated, typed data. ## Usage ```ts import { ClientAPIProvider, ValidationAPIProvider } from "shelving/api" const provider = new ValidationAPIProvider( new ClientAPIProvider({ url: "https://api.example.com" }) ) // Payload validated against the endpoint's payload schema before sending; // result validated against its result schema before returning. const user = await provider.call(getUser, { id: "u_123" }) ``` ## Signatures ```ts new ValidationAPIProvider() ``` ## Returns | Type | Description | | --- | --- | | `ValidationAPIProvider` | | --- # XMLAPIProvider - Kind: class - Import: `import { XMLAPIProvider } from "shelving/api";` - Extends: `ClientAPIProvider` Client API provider that always sends request bodies as XML and parses responses as plain text. - Request payloads must be data objects (serialised to XML); results are returned as raw text strings. A `ClientAPIProvider` for XML APIs. It sends request bodies as XML and returns the raw text of the response rather than parsing it as JSON. Because it extends `ClientAPIProvider`, it is a concrete network provider: construct it with the same `{ url, options?, timeout? }` options. ## Usage ```ts import { XMLAPIProvider } from "shelving/api" const provider = new XMLAPIProvider({ url: "https://legacy.example.com" }) // The result is the raw response text. const xml = await provider.call(getFeed, { channel: "news" }) ``` ## Signatures ```ts new XMLAPIProvider() ``` ## Returns | Type | Description | | --- | --- | | `XMLAPIProvider` | | --- # EndpointStore - Kind: class - Import: `import { EndpointStore } from "shelving/api";` - Extends: `PayloadFetchStore` Store that loads and tracks the result of calling a single API endpoint with a fixed payload, through an `APIProvider`. ## Signatures ```ts new EndpointStore(endpoint: Endpoint, payload: P, provider: APIProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `endpoint` | `Endpoint` | | | `payload` | `P` | | | `provider` | `APIProvider` | | ## Returns | Type | Description | | --- | --- | | `EndpointStore` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `APIProvider` | The API provider this store calls the endpoint through. Read only. | | `endpoint` | `Endpoint` | The endpoint this store calls to fetch its result. Read only. | ## Examples ```ts const store = new EndpointStore(endpoint, payload, provider); const result = await store; // R ``` --- # shelving/bun - Kind: module - Import: `import { … } from "shelving/bun";` `DBProvider` implementation for PostgreSQL using Bun's built-in `Bun.sql` API. No external database driver is required. `BunPostgresProvider` extends the shared `PostgresProvider`, which handles SQL generation, filtering, sorting, pagination, JSONB nested key access, and partial updates. The Bun-specific layer provides the tagged-template SQL execution and wraps identifier quoting through Bun's native SQL engine for additional safety. **Bun only.** This module uses `Bun.sql` and `SQL` from `bun`, which are not available in Node.js or other runtimes. There is no realtime support — `DBProvider.getItemSequence()` and `DBProvider.getQuerySequence()` throw `UnsupportedError`. ## PostgreSQL (`BunPostgresProvider`) **Install:** Bun is the runtime — no extra packages are needed. **Usage:** ```ts import { SQL } from "bun"; import { BunPostgresProvider } from "shelving/bun"; import { PostgresMigrator } from "shelving/db"; import { USERS } from "./collections.js"; const sql = new SQL({ hostname: "localhost", port: 5432, database: "mydb", username: "postgres", password: "secret", }); const provider = new BunPostgresProvider(sql); // Create or migrate tables from your collection definitions before first use. const migrator = new PostgresMigrator(provider); await migrator.migrate(USERS); ``` `new SQL(...)` accepts the same connection options as the Bun PostgreSQL client — see the [Bun SQL docs](https://bun.sh/docs/api/sql) for the full list of options including TLS and connection pooling. You can also pass a connection string as the first argument. ## Transactions `BunPostgresProvider` supports `DBProvider.transact()` — the callback runs in a `SERIALIZABLE` Postgres transaction on a reserved connection, so every write commits together or not at all: ```ts await provider.transact(async tx => { const from = await tx.requireItem(ACCOUNTS, "alice"); const to = await tx.requireItem(ACCOUNTS, "bob"); await tx.updateItem(ACCOUNTS, "alice", { balance: from.balance - 100 }); await tx.updateItem(ACCOUNTS, "bob", { balance: to.balance + 100 }); }); ``` - Returning from the callback commits; throwing rolls back and rethrows. - When Postgres aborts the transaction for contention (serialization failure `40001` or deadlock `40P01`), the whole callback is retried — up to 5 attempts with jittered exponential backoff — so the callback must have no side effects other than through its provider. - `SERIALIZABLE` isolation is set explicitly on every transaction, overriding any `default_transaction_isolation` configured on the database, so the `DBProvider.transact()` contract holds regardless of server configuration. - Nested `transact()` calls throw `UnsupportedError`. ## Testing The universal `DBProvider` contract suite runs against a real PostgreSQL via its own command (excluded from `bun run test`; run in CI on every PR and release): ```sh bun run postgres ``` `scripts/postgres.ts` connects to the server at `POSTGRES_URL` (default `postgres://postgres:postgres@127.0.0.1:5432/postgres`), creates a fresh `shelving-test` database and the fixture tables, runs `bun test ./modules/bun`, then drops the database. Tables must exist before the provider can read or write. `PostgresMigrator.migrate()` inspects the live schema and issues the minimum `CREATE TABLE` or `ALTER TABLE` statements needed to match your collection definitions. ## Classes - [BunPostgresProvider](bun/BunPostgresProvider.md): PostgreSQL database provider backed by Bun's built-in Bun.SQL driver. --- # BunPostgresProvider - Kind: class - Import: `import { BunPostgresProvider } from "shelving/bun";` - Extends: `PostgresProvider` PostgreSQL database provider backed by Bun's built-in `Bun.SQL` driver. Implements the `PostgresProvider` SQL abstraction by executing tagged-template queries against a `Bun.SQL` connection. - Identifiers are escaped through `Bun.SQL`'s own `sql()` helper rather than naive string quoting, which is more secure. - Supports transactions via `transact()` — the callback runs in a `SERIALIZABLE` Postgres transaction, and contention aborts are retried automatically. - Requires the `bun` peer dependency and a running Bun environment. ## Signatures ```ts new BunPostgresProvider(sql: SQL) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `sql` | `SQL` | | ## Returns | Type | Description | | --- | --- | | `BunPostgresProvider` | | --- # shelving/cloudflare - Kind: module - Import: `import { … } from "shelving/cloudflare";` `DBProvider` implementations for Cloudflare Workers. Two backends are available: Workers KV for simple key-value storage and D1 for relational SQL. Both providers accept a binding object injected by the Cloudflare Workers runtime. Declare the binding in `wrangler.toml` and access it through the `env` parameter of your Worker's `fetch` handler. ## KV (`CloudflareKVProvider`) Workers KV is a globally distributed key-value store. It is the right choice when you need fast single-item reads and writes by ID across Cloudflare's edge network, and do not need to query or filter across items. Items are stored as JSON under keys formatted as `collection:id`. `DBProvider.addItem()` generates a UUID v4 identifier automatically. **Limitations:** - No queries — `DBProvider.getQuery()`, `DBProvider.setQuery()`, `DBProvider.deleteQuery()`, and `DBProvider.countQuery()` throw `UnsupportedError`. - No partial updates — `DBProvider.updateItem()` and `DBProvider.updateQuery()` throw `UnsupportedError`. - No realtime — `DBProvider.getItemSequence()` and `DBProvider.getQuerySequence()` throw `UnsupportedError`. - Eventual consistency — reads may briefly return stale data after a write. **Install:** ```sh npm install shelving ``` ```wrangler.toml [[kv_namespaces]] binding = "MY_KV" id = "your-kv-namespace-id" ``` ```worker.ts import { CloudflareKVProvider } from "shelving/cloudflare"; export default { async fetch(request: Request, env: Env) { const provider = new CloudflareKVProvider(env.MY_KV); // use provider with a Collection }, }; ``` ## D1 (`CloudflareD1Provider`) D1 is Cloudflare's edge SQLite database. It supports collection queries with filtering, sorting, and pagination. Use it when you need structured data and the ability to query across items. `CloudflareD1Provider` extends the shared `SQLiteProvider`, so it inherits standard SQL-based CRUD and query behaviour. There is no realtime support — `DBProvider.getItemSequence()` and `DBProvider.getQuerySequence()` throw `UnsupportedError`. Tables must exist before the provider can read or write. Use `SQLiteMigrator` from `shelving/db` to create and migrate tables from your collection definitions. ```wrangler.toml [[d1_databases]] binding = "MY_DB" database_name = "my-database" database_id = "your-d1-database-id" ``` ```worker.ts import { CloudflareD1Provider } from "shelving/cloudflare"; import { SQLiteMigrator } from "shelving/db"; import { USERS } from "./collections.js"; export default { async fetch(request: Request, env: Env) { const provider = new CloudflareD1Provider(env.MY_DB); // Create or migrate tables on first run (or in a setup script). const migrator = new SQLiteMigrator(provider); await migrator.migrate(USERS); // use provider with a Collection }, }; ``` ## Classes - [CloudflareD1Provider](cloudflare/CloudflareD1Provider.md): SQLite database provider backed by Cloudflare D1. - [CloudflareKVProvider](cloudflare/CloudflareKVProvider.md): Cloudflare Workers KV database provider. ## Interfaces - [KVNamespace](cloudflare/KVNamespace.md): Minimal interface matching the Cloudflare Workers KV namespace runtime object. - [D1Meta](cloudflare/D1Meta.md): Metadata returned by the D1 Worker API alongside a query result. - [D1Result](cloudflare/D1Result.md): Result object returned by D1PreparedStatement.run(). - [D1ExecResult](cloudflare/D1ExecResult.md): Result object returned by D1Database.exec(). - [D1PreparedStatement](cloudflare/D1PreparedStatement.md): Minimal prepared statement interface for D1 databases and sessions. - [D1Database](cloudflare/D1Database.md): Minimal D1 binding/session interface used by CloudflareD1Provider. ## Types - [D1Value](cloudflare/D1Value.md): Value that can be passed through the D1 Worker API as a bound parameter. --- # CloudflareD1Provider - Kind: class - Import: `import { CloudflareD1Provider } from "shelving/cloudflare";` - Extends: `SQLiteProvider` SQLite database provider backed by Cloudflare D1. Implements the `SQLiteProvider` SQL abstraction by binding tagged-template queries and running them through the D1 Worker API. - Tagged-template values are flattened into positional `?` bindings; nested `SQLFragment` values are inlined recursively. - Array and plain-object values are JSON-encoded before binding; other non-primitive values throw `ValueError`. - The `D1Database` binding is provided by the Cloudflare Workers runtime environment. ## Signatures ```ts new CloudflareD1Provider(db: D1Database) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `db` | `D1Database` | | ## Returns | Type | Description | | --- | --- | | `CloudflareD1Provider` | | --- # CloudflareKVProvider - Kind: class - Import: `import { CloudflareKVProvider } from "shelving/cloudflare";` - Extends: `DBProvider` Cloudflare Workers KV database provider. Items are stored as JSON values under keys formatted as `collection:id`. The `KVNamespace` object is provided by the Cloudflare Workers runtime environment. ### Supported - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`. - ID generation: `addItem()` generates a UUID v4 identifier automatically. ### Not supported - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError`. KV has no change feed or push notification mechanism. - **Updates:** `updateItem()` and `updateQuery()` throw `UnsupportedError`. - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported. KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids the old "read everything and filter in memory" behavior. ### Performance limitations - **Single-key store only:** This provider is intentionally limited to direct key reads and writes. If you need collection queries, filtering, sorting, or bulk mutations, use a different backend. - **Eventual consistency:** KV is eventually consistent, so reads may briefly return stale values shortly after writes. ## Signatures ```ts new CloudflareKVProvider(kv: KVNamespace) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `kv` | `KVNamespace` | | ## Returns | Type | Description | | --- | --- | | `CloudflareKVProvider` | | --- # KVNamespace - Kind: interface - Import: `import { KVNamespace } from "shelving/cloudflare";` Minimal interface matching the Cloudflare Workers KV namespace runtime object. Declares only the subset of methods `CloudflareKVProvider` uses, so the `@cloudflare/workers-types` package is not required as a dependency. ## Signatures ```ts { get(key: string, options: { type: "json" }): Promise; put(key: string, value: string): Promise; delete(key: string): Promise; } ``` ## Methods - [KVNamespace.get()](cloudflare/KVNamespace/get.md) - [KVNamespace.put()](cloudflare/KVNamespace/put.md) - [KVNamespace.delete()](cloudflare/KVNamespace/delete.md) --- # KVNamespace.get() - Kind: method - Member of: `KVNamespace` ## Signatures ```ts get(key: string, options: { type: "json" }): Promise ``` --- # KVNamespace.put() - Kind: method - Member of: `KVNamespace` ## Signatures ```ts put(key: string, value: string): Promise ``` --- # KVNamespace.delete() - Kind: method - Member of: `KVNamespace` ## Signatures ```ts delete(key: string): Promise ``` --- # D1Value - Kind: type - Import: `import { D1Value } from "shelving/cloudflare";` Value that can be passed through the D1 Worker API as a bound parameter. ## Signatures ```ts boolean | null | number | string ``` --- # D1Meta - Kind: interface - Import: `import { D1Meta } from "shelving/cloudflare";` Metadata returned by the D1 Worker API alongside a query result. ## Signatures ```ts { readonly changed_db?: boolean | undefined; readonly duration?: number | undefined; readonly last_row_id?: number | undefined; readonly rows_read?: number | undefined; readonly rows_written?: number | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `changed_db` | `boolean \| undefined` | Read only. Optional. | | `duration` | `number \| undefined` | Read only. Optional. | | `last_row_id` | `number \| undefined` | Read only. Optional. | | `rows_read` | `number \| undefined` | Read only. Optional. | | `rows_written` | `number \| undefined` | Read only. Optional. | --- # D1Result - Kind: interface - Import: `import { D1Result } from "shelving/cloudflare";` Result object returned by `D1PreparedStatement.run()`. ## Signatures ```ts { readonly success: boolean; readonly meta?: D1Meta | undefined; readonly results?: readonly T[] | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `success` | `boolean` | Read only. | | `meta` | `D1Meta \| undefined` | Read only. Optional. | | `results` | `readonly T[] \| undefined` | Read only. Optional. | --- # D1ExecResult - Kind: interface - Import: `import { D1ExecResult } from "shelving/cloudflare";` Result object returned by `D1Database.exec()`. ## Signatures ```ts { readonly count: number; readonly duration: number; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `count` | `number` | Read only. | | `duration` | `number` | Read only. | --- # D1PreparedStatement - Kind: interface - Import: `import { D1PreparedStatement } from "shelving/cloudflare";` Minimal prepared statement interface for D1 databases and sessions. Declares only the subset of the D1 prepared-statement API used by `CloudflareD1Provider`. ## Signatures ```ts { bind(...values: D1Value[]): D1PreparedStatement; first>(column?: string): Promise; raw(options?: { columnNames?: boolean }): Promise; run = Record>(): Promise>; } ``` ## Methods - [D1PreparedStatement.bind()](cloudflare/D1PreparedStatement/bind.md) - [D1PreparedStatement.first()](cloudflare/D1PreparedStatement/first.md) - [D1PreparedStatement.raw()](cloudflare/D1PreparedStatement/raw.md) - [D1PreparedStatement.run()](cloudflare/D1PreparedStatement/run.md) --- # D1PreparedStatement.bind() - Kind: method - Member of: `D1PreparedStatement` ## Signatures ```ts bind(...values: D1Value[]): D1PreparedStatement ``` --- # D1PreparedStatement.first() - Kind: method - Member of: `D1PreparedStatement` ## Signatures ```ts first(column?: string): Promise ``` --- # D1PreparedStatement.raw() - Kind: method - Member of: `D1PreparedStatement` ## Signatures ```ts raw(options?: { columnNames?: boolean }): Promise ``` --- # D1PreparedStatement.run() - Kind: method - Member of: `D1PreparedStatement` ## Signatures ```ts run(): Promise> ``` --- # D1Database - Kind: interface - Import: `import { D1Database } from "shelving/cloudflare";` Minimal D1 binding/session interface used by `CloudflareD1Provider`. Declares only the subset of the D1 binding API the provider needs, so the `@cloudflare/workers-types` package is not required as a dependency. ## Signatures ```ts { batch = Record>( statements: readonly D1PreparedStatement[], ): Promise[]>; exec?(query: string): Promise; prepare(query: string): D1PreparedStatement; } ``` ## Methods - [D1Database.batch()](cloudflare/D1Database/batch.md) - [D1Database.exec()](cloudflare/D1Database/exec.md) - [D1Database.prepare()](cloudflare/D1Database/prepare.md) --- # D1Database.batch() - Kind: method - Member of: `D1Database` ## Signatures ```ts batch(statements: readonly D1PreparedStatement[]): Promise[]> ``` --- # D1Database.exec() - Kind: method - Member of: `D1Database` ## Signatures ```ts exec(query: string): Promise ``` --- # D1Database.prepare() - Kind: method - Member of: `D1Database` ## Signatures ```ts prepare(query: string): D1PreparedStatement ``` --- # shelving/db - Kind: module - Import: `import { … } from "shelving/db";` Typed database provider abstraction. Define your schema once as a `Collection`, then swap providers (in-memory, SQLite, Firestore, Cloudflare D1, …) without changing any call sites. Providers are composable wrappers — add validation, logging, or caching by stacking them in a chain. ## Concepts ### Collection A `Collection` is a declarative description of a database table or collection. It extends `DataSchema` and carries three things: a string `name`, an `id` schema (the identifier type), and a data schema (the shape of each record). ```ts import { COLLECTION } from "shelving/db" import { STRING, BOOLEAN } from "shelving/schema" const POSTS = COLLECTION("posts", STRING, { title: STRING, body: STRING, published: BOOLEAN, }) ``` `Collection` is the single source of truth for both runtime validation and TypeScript types. All provider methods are generic over the collection so the compiler tracks `id` and `data` types automatically. ### DBProvider `DBProvider` is the abstract base class every backend implements. Its surface covers: | Category | Methods | |---|---| | Single item | `DBProvider.getItem()`, `DBProvider.requireItem()`, `DBProvider.addItem()`, `DBProvider.setItem()`, `DBProvider.updateItem()`, `DBProvider.deleteItem()` | | Single item (realtime) | `DBProvider.getItemSequence()` | | Queries | `DBProvider.getQuery()`, `DBProvider.countQuery()`, `DBProvider.setQuery()`, `DBProvider.updateQuery()`, `DBProvider.deleteQuery()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()` | | Query (realtime) | `DBProvider.getQuerySequence()` | `.getItemSequence()` and `.getQuerySequence()` return `AsyncIterable` — iterate them with `for await...of` to receive realtime updates as data changes. ### Query syntax Query objects use encoded key names alongside plain field names: ```ts { status: "active" } // equality { "!status": "banned" } // not-equal (!key) { "count>": 10 } // greater-than (key>) { "tags[]": "typescript" } // array-contains (key[]) { $order: "name" } // order by field { $limit: 20 } // page size ``` ### Provider composition Providers are layered wrappers. Each takes a `source` and delegates to it, intercepting only what it needs. - **`MemoryDBProvider`** — fully in-memory, ideal for testing and as a lightweight standalone store. - **`ValidationDBProvider`** — validates data in and out using the collection's schema. Throws `ValueError` on bad data from the backend. - **`ThroughDBProvider`** — identity wrapper; extend this to intercept only specific methods (e.g. `DebugDBProvider`). - **`SQLiteProvider`** / **`PostgresProvider`** — SQL-backed abstract providers. Concrete subclasses bind them to a specific driver. Cloud providers live in the `shelving/cloudflare` and `shelving/firebase` sibling modules. ### Migrations `DBMigrator` is an abstract base for schema migrations. SQL-backed providers ship `SQLiteMigrator` and `PostgresMigrator`, which implement `DBMigrator.migrate()` to create or alter tables to match the current collection schemas. ## Usage ### Define a collection and build a provider chain ```ts import { COLLECTION, MemoryDBProvider, ValidationDBProvider } from "shelving/db" import { STRING, BOOLEAN } from "shelving/schema" const POSTS = COLLECTION("posts", STRING, { title: STRING, body: STRING, published: BOOLEAN, }) // In production replace MemoryDBProvider with a real backend (Firestore, D1, …). const provider = new ValidationDBProvider(new MemoryDBProvider()) ``` ### Read and write items ```ts // Add a new item — returns the generated id. const id = await provider.addItem(POSTS, { title: "Hello world", body: "First post.", published: false }) // Read it back. const post = await provider.getItem(POSTS, id) // Partial update. await provider.updateItem(POSTS, id, { published: true }) // Delete. await provider.deleteItem(POSTS, id) ``` ### Query items ```ts const posts = await provider.getQuery(POSTS, { published: true, $order: "title", $limit: 10, }) ``` ### Realtime subscriptions ```ts for await (const post of provider.getItemSequence(POSTS, id)) { console.log(post) // emits whenever this item changes } ``` ## React integration The `shelving/react` module's `createDBContext()` is the primary way to use a provider in a React app. It creates a context that wraps a provider (in a client app, typically a `MemoryDBProvider` that `HydratingAPIProvider` fills) and exposes typed hooks — `DBContext.useItem()` and `DBContext.useQuery()` — that return reactive `Store` instances and suspend automatically while loading. ```ts import { createDBContext } from "shelving/react" import { MemoryDBProvider } from "shelving/db" export const DB = new MemoryDBProvider() export const { DBContext, useItem, useQuery } = createDBContext(DB) ``` See the `shelving/react` module for full usage. ## Functions - [COLLECTION()](db/COLLECTION.md): Create a Collection from a name, an identifier schema, and a data schema. - [replayOperations()](db/replayOperations.md): Replay a list of database operations onto a provider, re-issuing each one in order. ## Classes - [CollectionCache](db/CollectionCache.md): Cache of ItemStore and QueryStore objects for a single collection. - [DBCache](db/DBCache.md): Cache of CollectionCache objects for multiple collections. - [Collection](db/Collection.md): Declarative definition of a database collection/table. - [CollectionItemSchema](db/CollectionItemSchema.md): Schema for one complete item (id + data) of a Collection. - [CollectionItemsSchema](db/CollectionItemsSchema.md): Schema for a list of complete items (id + data) of a Collection. - [CollectionQueryItemsSchema](db/CollectionQueryItemsSchema.md): Schema for QueryItems of a Collection, i.e. a query and the complete list of items that answer it. - [CollectionQuerySchema](db/CollectionQuerySchema.md): Schema for a Query on the items of a Collection. - [DBMigrator](db/DBMigrator.md): Base class for database schema migrators that bring a provider's storage into line with a set of collection schemas. - [PostgresMigrator](db/PostgresMigrator.md): PostgreSQL migrator that inspects the live schema via pg_catalog tables to diff and migrate columns. - [SQLiteMigrator](db/SQLiteMigrator.md): SQLite (and Cloudflare D1) migrator that uses sqlite_master as the schema source of truth. - [SQLMigrator](db/SQLMigrator.md): Shared SQL migration logic that diffs collection schemas against live tables to produce migration statements. - [DBProvider](db/DBProvider.md): Provider with a fully asynchronous interface for database access. - [DebugDBProvider](db/DebugDBProvider.md): Database provider that logs every operation it performs to the console for debugging. - [MemoryDBProvider](db/MemoryDBProvider.md): Synchronous in-memory database provider, storing each collection in a MemoryTable. - [MemoryTable](db/MemoryTable.md): In-memory table holding the items of a single collection for a MemoryDBProvider. - [MockDBProvider](db/MockDBProvider.md): In-memory database provider that records every operation to its calls log, for testing. - [PostgresProvider](db/PostgresProvider.md): Abstract PostgreSQL provider with JSONB function support for nested keys, array containment, and array mutations. - [RecordingDBProvider](db/RecordingDBProvider.md): Database provider that records every operation it performs to its operations log. - [SQLiteProvider](db/SQLiteProvider.md): Abstract SQLite provider with JSON1 function support for nested keys, array containment, and array mutations. - [SQLFragment](db/SQLFragment.md): SQL fragment made from template strings plus embedded expressions, ready to be composed into a query. - [SQLProvider](db/SQLProvider.md): Abstract database provider that implements CRUD and query operations by generating and executing SQL. - [StorageDBProvider](db/StorageDBProvider.md): In-memory database provider that persists every collection to a Storage — localStorage, sessionStorage, or anything else with the same interface. - [StorageTable](db/StorageTable.md): MemoryTable that persists its items to a Storage instance — self-contained, so it can also be used independently of StorageDBProvider. - [ThroughDBProvider](db/ThroughDBProvider.md): Database provider that passes every core operation straight through to a wrapped source provider. - [UndoDBProvider](db/UndoDBProvider.md): Database provider that records every operation and can undo its own writes. - [ValidationDBProvider](db/ValidationDBProvider.md): Database provider that validates data flowing to and from an asynchronous source provider. - [ItemStore](db/ItemStore.md): Store that fetches and tracks a single item by ID in a collection from a database provider. - [QueryStore](db/QueryStore.md): Store that runs a query against a collection from a database provider and tracks its matching items. ## Types - [CollectionName](db/CollectionName.md): Extract the string name from a Collection instance. - [CollectionIdentifier](db/CollectionIdentifier.md): Extract the Identifier type from a Collection instance. - [CollectionData](db/CollectionData.md): Extract the Data type from a Collection instance. - [CollectionItem](db/CollectionItem.md): Extract the Item type from a Collection instance. - [OptionalCollectionItem](db/OptionalCollectionItem.md): Extract the optional (possibly undefined) Item type from a Collection instance. - [CollectionItems](db/CollectionItems.md): Extract the array of Item types from a Collection instance. - [Collections](db/Collections.md): A readonly array of Collection instances, optionally narrowed to a standardised Identifier and Data type. - [CollectionNames](db/CollectionNames.md): Extract the union of string collection names from a Collections type. - [CollectionsDatabase](db/CollectionsDatabase.md): Convert a Collections array type to a database-style object mapping in { name: data } format. - [SQLTableColumn](db/SQLTableColumn.md): Column definition in a live SQL table, pairing a column name with its raw definition statement. - [SQLTable](db/SQLTable.md): Existing SQL table schema keyed by column name, as read back from the database. - [SQLColumn](db/SQLColumn.md): Generated SQL column mapped from a collection schema path, holding its column name, key, and JSON path. - [MockDBCall](db/MockDBCall.md): Structured log entry recording one operation performed through a MockDBProvider. - [DBOperation](db/DBOperation.md): Structured log entry recording a single database operation performed through a RecordingDBProvider. - [DBOperations](db/DBOperations.md): Readonly array of DBOperation entries, e.g. the log recorded by a RecordingDBProvider. ## Constants - [ID](db/ID.md): Default identifier schema (integer) used when a collection doesn't supply its own. --- # CollectionCache - Kind: class - Import: `import { CollectionCache } from "shelving/db";` - Implements: `AsyncDisposable` Cache of `ItemStore` and `QueryStore` objects for a single collection. - Use `getItem(id)` to retrieve or create the `ItemStore` for a given id. - Use `getQuery(query)` to retrieve or create the `QueryStore` for a given query. ## Signatures ```ts new CollectionCache(collection: Collection, provider: DBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | | `provider` | `DBProvider` | | ## Returns | Type | Description | | --- | --- | | `CollectionCache` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `collection` | `Collection` | The collection these cached stores belong to. Read only. | | `provider` | `DBProvider` | The database provider the cached stores fetch from. Read only. | ## Examples ```ts const cache = new CollectionCache(collection, provider); const store = cache.getItem("abc"); ``` ## Methods - [CollectionCache.getItem()](db/CollectionCache/getItem.md): Get (or create) the ItemStore for the given id. - [CollectionCache.getQuery()](db/CollectionCache/getQuery.md): Get (or create) the QueryStore for the given query. - [CollectionCache.refreshItem()](db/CollectionCache/refreshItem.md): Refresh a specific item store. - [CollectionCache.refreshItems()](db/CollectionCache/refreshItems.md): Refresh every cached item store. - [CollectionCache.refreshQuery()](db/CollectionCache/refreshQuery.md): Refresh a specific query store. - [CollectionCache.refreshQueries()](db/CollectionCache/refreshQueries.md): Refresh every cached query store. - [CollectionCache.refreshAll()](db/CollectionCache/refreshAll.md): Refresh every cached store (items and queries). --- # CollectionCache.getItem() - Kind: method - Member of: `CollectionCache` Get (or create) the `ItemStore` for the given id. ## Signatures ```ts getItem(id: I): ItemStore ``` --- # CollectionCache.getQuery() - Kind: method - Member of: `CollectionCache` Get (or create) the `QueryStore` for the given query. ## Signatures ```ts getQuery(query: Query>): QueryStore ``` --- # CollectionCache.refreshItem() - Kind: method - Member of: `CollectionCache` Refresh a specific item store. ## Signatures ```ts refreshItem(id: I, maxAge?: number): Promise ``` --- # CollectionCache.refreshItems() - Kind: method - Member of: `CollectionCache` Refresh every cached item store. ## Signatures ```ts refreshItems(maxAge?: number): Promise ``` --- # CollectionCache.refreshQuery() - Kind: method - Member of: `CollectionCache` Refresh a specific query store. ## Signatures ```ts refreshQuery(query: Query>, maxAge?: number): Promise ``` --- # CollectionCache.refreshQueries() - Kind: method - Member of: `CollectionCache` Refresh every cached query store. ## Signatures ```ts refreshQueries(maxAge?: number): Promise ``` --- # CollectionCache.refreshAll() - Kind: method - Member of: `CollectionCache` Refresh every cached store (items and queries). ## Signatures ```ts refreshAll(maxAge?: number): Promise ``` --- # DBCache - Kind: class - Import: `import { DBCache } from "shelving/db";` - Implements: `AsyncDisposable` Cache of `CollectionCache` objects for multiple collections. - Use `get(collection)` to retrieve or create the `CollectionCache` for a given collection, then `getItem(id)` / `getQuery(query)` on that to get a specific store. - If the provider is (or wraps) a `MemoryDBProvider`, the stores seed from it synchronously. ## Signatures ```ts new DBCache(provider: DBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | | ## Returns | Type | Description | | --- | --- | | `DBCache` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | The database provider the cached stores fetch from. Read only. | ## Examples ```ts const cache = new DBCache(provider); const store = cache.getItem(collection, "abc"); ``` ## Methods - [DBCache.get()](db/DBCache/get.md): Get (or create) the CollectionCache for the given collection. - [DBCache.getItem()](db/DBCache/getItem.md): Get (or create) an ItemStore for a collection/id in one hop. - [DBCache.getQuery()](db/DBCache/getQuery.md): Get (or create) a QueryStore for a collection/query in one hop. - [DBCache.refreshItem()](db/DBCache/refreshItem.md): Refresh a specific item store for a collection. - [DBCache.refreshItems()](db/DBCache/refreshItems.md): Refresh every cached item store for a collection. - [DBCache.refreshQuery()](db/DBCache/refreshQuery.md): Refresh a specific query store for a collection. - [DBCache.refreshQueries()](db/DBCache/refreshQueries.md): Refresh every cached query store for a collection. - [DBCache.refreshAll()](db/DBCache/refreshAll.md): Refresh every cached store (items and queries) for a collection. --- # DBCache.get() - Kind: method - Member of: `DBCache` Get (or create) the `CollectionCache` for the given collection. ## Signatures ```ts get(collection: Collection): CollectionCache get(collection: Collection): CollectionCache ``` --- # DBCache.getItem() - Kind: method - Member of: `DBCache` Get (or create) an `ItemStore` for a collection/id in one hop. ## Signatures ```ts getItem(collection: Collection, id: II): ItemStore ``` --- # DBCache.getQuery() - Kind: method - Member of: `DBCache` Get (or create) a `QueryStore` for a collection/query in one hop. ## Signatures ```ts getQuery(collection: Collection, query: Query>): QueryStore ``` --- # DBCache.refreshItem() - Kind: method - Member of: `DBCache` Refresh a specific item store for a collection. ## Signatures ```ts refreshItem(collection: Collection, id: II, maxAge?: number): Promise ``` --- # DBCache.refreshItems() - Kind: method - Member of: `DBCache` Refresh every cached item store for a collection. ## Signatures ```ts refreshItems(collection: Collection, maxAge?: number): Promise ``` --- # DBCache.refreshQuery() - Kind: method - Member of: `DBCache` Refresh a specific query store for a collection. ## Signatures ```ts refreshQuery(collection: Collection, query: Query>, maxAge?: number): Promise ``` --- # DBCache.refreshQueries() - Kind: method - Member of: `DBCache` Refresh every cached query store for a collection. ## Signatures ```ts refreshQueries(collection: Collection, maxAge?: number): Promise ``` --- # DBCache.refreshAll() - Kind: method - Member of: `DBCache` Refresh every cached store (items and queries) for a collection. ## Signatures ```ts refreshAll(collection: Collection, maxAge?: number): Promise ``` --- # ID - Kind: constant - Import: `import { ID } from "shelving/db";` Default identifier schema (integer) used when a collection doesn't supply its own. --- # Collection - Kind: class - Import: `import { Collection } from "shelving/db";` - Extends: `DataSchema` Declarative definition of a database collection/table. - Pairs a collection name with an identifier schema and a data schema, so providers can validate and address items. - Extends `DataSchema`, so the collection itself validates an item's data, while `.item` validates the complete `id + data` item. - `.updates` validates an `Updates` object for the item data, and `.query` validates a `Query` on the items. - `.item`, `.items`, `.query` and `.queryItems` point back at the collection with `source`, so an endpoint result that uses them can hydrate a database (see `HydratingAPIProvider`). ## Signatures ```ts new Collection(name: N, id: Schema, data: Schemas | DataSchema) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `name` | `N` | | | `id` | `Schema` | | | `data` | `Schemas \| DataSchema` | | ## Returns | Type | Description | | --- | --- | | `Collection` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `name` | `N` | Collection name (used as the table/collection key). Read only. | | `id` | `Schema` | Schema for the identifier type. Read only. | | `item` | `CollectionItemSchema` | Schema for a complete item (id + data). Read only. | | `items` | `CollectionItemsSchema` | Schema for a list of complete items (id + data). Read only. | | `updates` | `UpdatesSchema` | Schema for a set of updates to the data of an item. Read only. | | `query` | `CollectionQuerySchema` | Schema for a query on the items. Read only. | | `queryItems` | `CollectionQueryItemsSchema` | Schema for a query and the complete list of items that answer it. Read only. | ## Examples ```ts const users = new Collection("users", ID, { name: STRING, age: NUMBER }); users.validate({ name: "Dave", age: 40 }); // Validates item data. ``` --- # COLLECTION() - Kind: function - Import: `import { COLLECTION } from "shelving/db";` Create a `Collection` from a name, an identifier schema, and a data schema. *Factory for `Collection`.* ## Signatures ```ts COLLECTION(name: K, id: Schema, data: Schemas | DataSchema): Collection ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `name` | `K` | Collection name used as the table/collection key. | | `id` | `Schema` | Schema for the identifier type. | | `data` | `Schemas \| DataSchema` | Data schema, or the `Schemas` props used to construct one. | ## Returns | Type | Description | | --- | --- | | `Collection` | | ## Examples ```ts COLLECTION("users", ID, { name: STRING, age: NUMBER }) // Collection<"users", number, { name: string; age: number }> ``` --- # CollectionName - Kind: type - Import: `import { CollectionName } from "shelving/db";` Extract the string name from a `Collection` instance. ## Signatures ```ts C extends Collection ? N : never ``` ## Types - [Collection](db/Collection.md): Declarative definition of a database collection/table. - `N` --- # CollectionIdentifier - Kind: type - Import: `import { CollectionIdentifier } from "shelving/db";` Extract the `Identifier` type from a `Collection` instance. ## Signatures ```ts C extends Collection ? I : never ``` ## Types - [Collection](db/Collection.md): Declarative definition of a database collection/table. - `I` --- # CollectionData - Kind: type - Import: `import { CollectionData } from "shelving/db";` Extract the `Data` type from a `Collection` instance. ## Signatures ```ts C extends Collection ? T : never ``` ## Types - [Collection](db/Collection.md): Declarative definition of a database collection/table. - `T` --- # CollectionItem - Kind: type - Import: `import { CollectionItem } from "shelving/db";` Extract the `Item` type from a `Collection` instance. ## Signatures ```ts Item, CollectionData> ``` ## Types - [Item](util/item/Item.md): An item object is a data object that includes an "id" identifier property that is either a string or number. - [CollectionIdentifier](db/CollectionIdentifier.md): Extract the Identifier type from a Collection instance. - [CollectionData](db/CollectionData.md): Extract the Data type from a Collection instance. --- # OptionalCollectionItem - Kind: type - Import: `import { OptionalCollectionItem } from "shelving/db";` Extract the optional (possibly undefined) `Item` type from a `Collection` instance. ## Signatures ```ts OptionalItem, CollectionData> ``` ## Types - [OptionalItem](util/item/OptionalItem.md): Item object, or undefined to indicate the item doesn't exist. - [CollectionIdentifier](db/CollectionIdentifier.md): Extract the Identifier type from a Collection instance. - [CollectionData](db/CollectionData.md): Extract the Data type from a Collection instance. --- # CollectionItems - Kind: type - Import: `import { CollectionItems } from "shelving/db";` Extract the array of `Item` types from a `Collection` instance. ## Signatures ```ts Items, CollectionData> ``` ## Types - [Items](util/item/Items.md): An array of item objects. - [CollectionIdentifier](db/CollectionIdentifier.md): Extract the Identifier type from a Collection instance. - [CollectionData](db/CollectionData.md): Extract the Data type from a Collection instance. --- # Collections - Kind: type - Import: `import { Collections } from "shelving/db";` A readonly array of `Collection` instances, optionally narrowed to a standardised `Identifier` and `Data` type. ## Signatures ```ts ImmutableArray> ``` ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. - [Collection](db/Collection.md): Declarative definition of a database collection/table. --- # CollectionNames - Kind: type - Import: `import { CollectionNames } from "shelving/db";` Extract the union of string collection names from a `Collections` type. ## Signatures ```ts C[number]["name"] ``` --- # CollectionsDatabase - Kind: type - Import: `import { CollectionsDatabase } from "shelving/db";` Convert a `Collections` array type to a database-style object mapping in `{ name: data }` format. ## Signatures ```ts { [E in C[number] as E extends Collection ? N : never]: E extends Collection ? T : never; } ``` ## Types - `E` - [Collection](db/Collection.md): Declarative definition of a database collection/table. - [Identifier](util/item/Identifier.md): Allowed types for the "id" property (identifier) for an item. - [Data](util/data/Data.md): Data object — a plain object with string keys and unknown values. - `N` - `T` --- # CollectionItemSchema - Kind: class - Import: `import { CollectionItemSchema } from "shelving/db";` - Extends: `DataSchema>` - Implements: `Sourceable>` Schema for one complete item (`id` + data) of a `Collection`. - Built by `Collection` as `Collection.item`. Use that instance, and do not make your own. - Its `source` is the `Collection`, so `getSource()` can find the collection from the schema (e.g. through `NULLABLE(USERS.item)`). - `HydratingAPIProvider` writes an endpoint result that uses this schema into its database. A `null` or `undefined` result deletes the item with the `id` from the payload. ## Signatures ```ts new CollectionItemSchema(collection: Collection) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | ## Returns | Type | Description | | --- | --- | | `CollectionItemSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `Collection` | The collection this schema validates items of. Read only. | ## Examples ```ts const GET_USER = GET("/user/{id}", DATA({ id: USERS.id }), NULLABLE(USERS.item)); ``` --- # CollectionItemsSchema - Kind: class - Import: `import { CollectionItemsSchema } from "shelving/db";` - Extends: `ArraySchema>` - Implements: `Sourceable>` Schema for a list of complete items (`id` + data) of a `Collection`. - Built by `Collection` as `Collection.items`. Use that instance, and do not make your own. - Its `source` is the `Collection`, so `getSource()` can find the collection from the schema. - `HydratingAPIProvider` writes an endpoint result that uses this schema into its database. It only adds and replaces items, and never deletes them. - `ARRAY(USERS.item)` validates the same value, but it does not point at the collection, so it does not hydrate. Use `USERS.items`. ## Signatures ```ts new CollectionItemsSchema(collection: Collection) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | ## Returns | Type | Description | | --- | --- | | `CollectionItemsSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `Collection` | The collection this schema validates items of. Read only. | ## Examples ```ts const LIST_USERS = GET("/user", undefined, USERS.items); ``` --- # CollectionQueryItemsSchema - Kind: class - Import: `import { CollectionQueryItemsSchema } from "shelving/db";` - Extends: `DataSchema>` - Implements: `Sourceable>` Schema for `QueryItems` of a `Collection`, i.e. a query and the complete list of items that answer it. - Built by `Collection` as `Collection.queryItems`, from `Collection.query` and `Collection.items`. Use that instance, and do not make your own. - The `query` is validated in full by `Collection.query`, and the `items` by `Collection.items`. - Its `source` is the `Collection`, so `getSource()` can find the collection from the schema. - `HydratingAPIProvider` writes an endpoint result that uses this schema into its database, and marks the query as loaded (see `MemoryTable.getQueryTime()`), so a store for the same query shows the items without loading. - Only use it when `items` is the complete answer to `query` (with its `$limit`). ## Signatures ```ts new CollectionQueryItemsSchema(collection: Collection) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | ## Returns | Type | Description | | --- | --- | | `CollectionQueryItemsSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `Collection` | The collection this schema validates items of. Read only. | ## Examples ```ts const MY_CHALLENGES = GET("/challenge/me", undefined, DATA({ challenges: CHALLENGES.queryItems })); ``` --- # CollectionQuerySchema - Kind: class - Import: `import { CollectionQuerySchema } from "shelving/db";` - Extends: `QuerySchema>` - Implements: `Sourceable>` Schema for a `Query` on the items of a `Collection`. - Built by `Collection` as `Collection.query`. Use that instance, and do not make your own. - Validates the query in full against `Collection.item` (see `QuerySchema`), so a query from outside (e.g. an API request or response) can be trusted. Filter and `$order` keys can use `id` and any prop path of the collection. - Its `source` is the `Collection`, so `getSource()` can find the collection from the schema. ## Signatures ```ts new CollectionQuerySchema(collection: Collection) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | ## Returns | Type | Description | | --- | --- | | `CollectionQuerySchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `Collection` | The collection this schema validates queries for. Read only. | --- # DBMigrator - Kind: class - Import: `import { DBMigrator } from "shelving/db";` Base class for database schema migrators that bring a provider's storage into line with a set of collection schemas. - Subclasses implement `migrate()` for a specific backend (e.g. SQL via `SQLMigrator`). ## Signatures ```ts new DBMigrator(provider: T) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `T` | | ## Returns | Type | Description | | --- | --- | | `DBMigrator` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `T` | The database provider whose storage this migrator updates. Read only. | ## Methods - [DBMigrator.migrate()](db/DBMigrator/migrate.md): Bring the provider's storage into line with the given collection schemas. --- # DBMigrator.migrate() - Kind: method - Member of: `DBMigrator` Bring the provider's storage into line with the given collection schemas. ## Signatures ```ts migrate(...collections: Collections): Promise ``` --- # PostgresMigrator - Kind: class - Import: `import { PostgresMigrator } from "shelving/db";` - Extends: `SQLMigrator` PostgreSQL migrator that inspects the live schema via `pg_catalog` tables to diff and migrate columns. ## Signatures ```ts new PostgresMigrator() ``` ## Returns | Type | Description | | --- | --- | | `PostgresMigrator` | | --- # SQLiteMigrator - Kind: class - Import: `import { SQLiteMigrator } from "shelving/db";` - Extends: `SQLMigrator` SQLite (and Cloudflare D1) migrator that uses `sqlite_master` as the schema source of truth. ## Signatures ```ts new SQLiteMigrator() ``` ## Returns | Type | Description | | --- | --- | | `SQLiteMigrator` | | --- # SQLTableColumn - Kind: type - Import: `import { SQLTableColumn } from "shelving/db";` Column definition in a live SQL table, pairing a column name with its raw definition statement. ## Signatures ```ts { readonly name: string; readonly statement: string; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `name` | `string` | Read only. | | `statement` | `string` | Read only. | --- # SQLTable - Kind: type - Import: `import { SQLTable } from "shelving/db";` Existing SQL table schema keyed by column name, as read back from the database. ## Signatures ```ts { readonly columns: Readonly>; readonly name: string; readonly sql?: string | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `columns` | `Readonly>` | Read only. | | `name` | `string` | Read only. | | `sql` | `string \| undefined` | Read only. Optional. | ## Types - `Readonly` - `Record` - [SQLTableColumn](db/SQLTableColumn.md): Column definition in a live SQL table, pairing a column name with its raw definition statement. --- # SQLColumn - Kind: type - Import: `import { SQLColumn } from "shelving/db";` Generated SQL column mapped from a collection schema path, holding its column name, key, and JSON path. ## Signatures ```ts { readonly column: string; readonly key: string; readonly path: string; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `column` | `string` | Read only. | | `key` | `string` | Read only. | | `path` | `string` | Read only. | --- # SQLMigrator - Kind: class - Import: `import { SQLMigrator } from "shelving/db";` - Extends: `DBMigrator` Shared SQL migration logic that diffs collection schemas against live tables to produce migration statements. - Subclasses implement backend-specific schema inspection and column definitions (e.g. SQLite, PostgreSQL). ## Signatures ```ts new SQLMigrator() ``` ## Returns | Type | Description | | --- | --- | | `SQLMigrator` | | ## Methods - [SQLMigrator.migrate()](db/SQLMigrator/migrate.md): Run each statement from getMigrations() against the provider in order. - [SQLMigrator.getMigrations()](db/SQLMigrator/getMigrations.md): Compute the SQL statements needed to bring the tables into line with the given collection schemas, without executing them. - [SQLMigrator.getCreateTableQuery()](db/SQLMigrator/getCreateTableQuery.md): Build the CREATE TABLE statement for a collection, including its ID, data, and generated columns. - [SQLMigrator.getCreateTableColumns()](db/SQLMigrator/getCreateTableColumns.md): Get the full set of columns for a collection's table: the ID column, data column, and generated columns. - [SQLMigrator.getGeneratedTableColumns()](db/SQLMigrator/getGeneratedTableColumns.md): Get the generated (computed) columns for a collection's table, derived from its schema's scalar properties. --- # SQLMigrator.migrate() - Kind: method - Member of: `SQLMigrator` Run each statement from `getMigrations()` against the provider in order. ## Signatures ```ts migrate(...collections: Collections): Promise ``` --- # SQLMigrator.getMigrations() - Kind: method - Member of: `SQLMigrator` Compute the SQL statements needed to bring the tables into line with the given collection schemas, without executing them. ## Signatures ```ts getMigrations(...collections: Collections): Promise ``` --- # SQLMigrator.getCreateTableQuery() - Kind: method - Member of: `SQLMigrator` Build the `CREATE TABLE` statement for a collection, including its ID, data, and generated columns. ## Signatures ```ts getCreateTableQuery(collection: Collection): string ``` --- # SQLMigrator.getCreateTableColumns() - Kind: method - Member of: `SQLMigrator` Get the full set of columns for a collection's table: the ID column, data column, and generated columns. ## Signatures ```ts getCreateTableColumns(collection: Collection): readonly SQLTableColumn[] ``` --- # SQLMigrator.getGeneratedTableColumns() - Kind: method - Member of: `SQLMigrator` Get the generated (computed) columns for a collection's table, derived from its schema's scalar properties. ## Signatures ```ts getGeneratedTableColumns(collection: Collection): readonly SQLTableColumn[] ``` --- # DBProvider - Kind: class - Import: `import { DBProvider } from "shelving/db";` - Implements: `AsyncDisposable` Provider with a fully asynchronous interface for database access. - Abstract base for every database provider; subclasses implement the storage backend (memory, SQL, remote, etc.). - All operations are keyed by a `Collection`, which carries the name plus identifier and data schemas. - Layered behaviour (caching, validation, logging) is added by wrapping a provider in a `Through*Provider`. The abstract base class every database backend implements. `DBProvider` defines the typed surface that all call sites use — item reads and writes, queries, and realtime sequences — generic over a `Collection` so the compiler tracks `id` and `data` types automatically. Concrete backends implement the abstract methods; the base class derives `requireItem`, `countQuery`, `getFirst`, and `requireFirst` from them. `DBProvider` implements `AsyncDisposable`. ## Usage Type code that accepts "any database" against `DBProvider` so an in-memory store, a validated chain, or a SQL backend are all interchangeable: ```ts import type { DBProvider } from "shelving/db" async function publishPost(provider: DBProvider, id: string) { await provider.updateItem(POSTS, id, { published: true }); } ``` The method surface covers single items (`getItem`, `requireItem`, `addItem`, `setItem`, `updateItem`, `deleteItem`), queries (`getQuery`, `countQuery`, `setQuery`, `updateQuery`, `deleteQuery`, `getFirst`, `requireFirst`), realtime (`getItemSequence`, `getQuerySequence` — iterate with `for await...of`), and transactions (`transact`). Query writes (`setQuery`, `updateQuery`, `deleteQuery`) have a deliberately weak portable contract: `DBProvider` itself implements them "two-step" — resolve the matching items with `getQuery()`, then write each one concurrently through the item methods — and engines override them with a native single call where one exists (SQL's `UPDATE … WHERE`). Either way they are only guaranteed atomic inside `transact()`. The derived reads (`requireItem`, `getFirst`, `requireFirst`) and the default `countQuery` are sugar built on `getItem()` / `getQuery()`, so a provider only ever has to implement the core operations — and behaviour added by a wrapping provider (see `ThroughDBProvider`) automatically applies to them too. ## Transactions `transact()` runs a callback as a single atomic transaction — every write made through the provider the callback receives is committed together, or not at all: ```ts await provider.transact(async db => { const from = await db.requireItem(ACCOUNTS, "alice"); const to = await db.requireItem(ACCOUNTS, "bob"); await db.updateItem(ACCOUNTS, "alice", { balance: from.balance - 100 }); await db.updateItem(ACCOUNTS, "bob", { balance: to.balance + 100 }); }); ``` The contract is the weakest guarantee shared by all backends, so transactional code stays portable: - Reads see a consistent snapshot of the data from before the transaction, and do **not** see the transaction's own uncommitted writes. - If the callback throws, nothing is committed and the error is rethrown. - The callback may run more than once if the backend retries on contention, so it must have no side effects other than through its provider. - Portable code must not use realtime sequences or nested `transact()` calls inside a transaction — most backends throw `UnsupportedError`, though some (e.g. `MemoryDBProvider`) support them scoped to the transaction. - Providers that cannot support transactions (e.g. `CloudflareKVProvider`) throw `UnsupportedError` from `transact()` itself. Transactions are opt-in per backend — `FirestoreProvider` (`shelving/firebase`) implements them, `BunPostgresProvider` (`shelving/bun`) runs the callback in a `SERIALIZABLE` Postgres transaction (retrying contention aborts automatically), and `MemoryDBProvider` runs the callback against a snapshot clone and replays its recorded writes on success. Wrapping providers built on `ThroughDBProvider` (`ValidationDBProvider`, `RecordingDBProvider`, `DebugDBProvider`) support them whenever their `source` does, and keep their own behaviour inside the transaction — e.g. reads and writes in a `ValidationDBProvider` transaction are still validated. ## Signatures ```ts new DBProvider() ``` ## Returns | Type | Description | | --- | --- | | `DBProvider` | | ## Examples ```ts const provider = new MemoryDBProvider(); const id = await provider.addItem(users, { name: "Dave" }); ``` ## Methods - [DBProvider.getItem()](db/DBProvider/getItem.md): Get an item from a collection by its id, or undefined if it doesn't exist. - [DBProvider.requireItem()](db/DBProvider/requireItem.md): Get an item from a collection by its id, or throw if it doesn't exist. - [DBProvider.getItemSequence()](db/DBProvider/getItemSequence.md): Subscribe to live changes for a single item by its id. - [DBProvider.addItem()](db/DBProvider/addItem.md): Add a new item to a collection and return its generated id. - [DBProvider.setItem()](db/DBProvider/setItem.md): Set (insert or overwrite) the data for an item by its id. - [DBProvider.updateItem()](db/DBProvider/updateItem.md): Apply partial updates to an existing item by its id. - [DBProvider.deleteItem()](db/DBProvider/deleteItem.md): Delete an item from a collection by its id. - [DBProvider.countQuery()](db/DBProvider/countQuery.md): Count the items in a collection matching an optional query. - [DBProvider.getQuery()](db/DBProvider/getQuery.md): Get the items in a collection matching an optional query. - [DBProvider.getQuerySequence()](db/DBProvider/getQuerySequence.md): Subscribe to live changes for the result of a query. - [DBProvider.setQuery()](db/DBProvider/setQuery.md): Set (overwrite) the data for every item matching a query. - [DBProvider.updateQuery()](db/DBProvider/updateQuery.md): Apply partial updates to every item matching a query. - [DBProvider.deleteQuery()](db/DBProvider/deleteQuery.md): Delete every item matching a query. - [DBProvider.getFirst()](db/DBProvider/getFirst.md): Get the first item matching a query, or undefined if there are none. - [DBProvider.requireFirst()](db/DBProvider/requireFirst.md): Get the first item matching a query, or throw if there are none. - [DBProvider.transact()](db/DBProvider/transact.md): Run a callback as a single atomic transaction — every write made through the callback's provider is committed together, or not at all. --- # DBProvider.getItem() - Kind: method - Member of: `DBProvider` Get an item from a collection by its id, or `undefined` if it doesn't exist. ## Signatures ```ts getItem(collection: Collection, id: II): Promise> ``` --- # DBProvider.requireItem() - Kind: method - Member of: `DBProvider` Get an item from a collection by its id, or throw if it doesn't exist. ## Signatures ```ts requireItem(collection: Collection, id: II): Promise> ``` --- # DBProvider.getItemSequence() - Kind: method - Member of: `DBProvider` Subscribe to live changes for a single item by its id. ## Signatures ```ts getItemSequence(collection: Collection, id: II): OptionalItemSequence ``` --- # DBProvider.addItem() - Kind: method - Member of: `DBProvider` Add a new item to a collection and return its generated id. ## Signatures ```ts addItem(collection: Collection, data: TT): Promise ``` --- # DBProvider.setItem() - Kind: method - Member of: `DBProvider` Set (insert or overwrite) the data for an item by its id. ## Signatures ```ts setItem(collection: Collection, id: II, data: TT): Promise ``` --- # DBProvider.updateItem() - Kind: method - Member of: `DBProvider` Apply partial updates to an existing item by its id. ## Signatures ```ts updateItem(collection: Collection, id: II, updates: Updates>): Promise ``` --- # DBProvider.deleteItem() - Kind: method - Member of: `DBProvider` Delete an item from a collection by its id. ## Signatures ```ts deleteItem(collection: Collection, id: II): Promise ``` --- # DBProvider.countQuery() - Kind: method - Member of: `DBProvider` Count the items in a collection matching an optional query. ## Signatures ```ts countQuery(collection: Collection, query?: Query>): Promise ``` --- # DBProvider.getQuery() - Kind: method - Member of: `DBProvider` Get the items in a collection matching an optional query. ## Signatures ```ts getQuery(collection: Collection, query?: Query>): Promise> ``` --- # DBProvider.getQuerySequence() - Kind: method - Member of: `DBProvider` Subscribe to live changes for the result of a query. ## Signatures ```ts getQuerySequence(collection: Collection, query?: Query>): ItemsSequence ``` --- # DBProvider.setQuery() - Kind: method - Member of: `DBProvider` Set (overwrite) the data for every item matching a query. - Two-step by default: resolves the query to its matching items with `getQuery()`, then sets each one concurrently with `setItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`). - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters. ## Signatures ```ts setQuery(collection: Collection, query: Query>, data: TT): Promise ``` --- # DBProvider.updateQuery() - Kind: method - Member of: `DBProvider` Apply partial updates to every item matching a query. - Two-step by default: resolves the query to its matching items with `getQuery()`, then updates each one concurrently with `updateItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`). - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters. ## Signatures ```ts updateQuery(collection: Collection, query: Query>, updates: Updates): Promise ``` --- # DBProvider.deleteQuery() - Kind: method - Member of: `DBProvider` Delete every item matching a query. - Two-step by default: resolves the query to its matching items with `getQuery()`, then deletes each one concurrently with `deleteItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `DELETE … WHERE`). - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters. ## Signatures ```ts deleteQuery(collection: Collection, query: Query>): Promise ``` --- # DBProvider.getFirst() - Kind: method - Member of: `DBProvider` Get the first item matching a query, or `undefined` if there are none. ## Signatures ```ts getFirst(collection: Collection, query: Query>): Promise> ``` --- # DBProvider.requireFirst() - Kind: method - Member of: `DBProvider` Get the first item matching a query, or throw if there are none. ## Signatures ```ts requireFirst(collection: Collection, query: Query>): Promise> ``` --- # DBProvider.transact() - Kind: method - Member of: `DBProvider` Run a callback as a single atomic transaction — every write made through the callback's provider is committed together, or not at all. - The callback receives a transaction-scoped `DBProvider`; reads and writes made through it belong to the transaction. - Reads see a consistent snapshot of the data from before the transaction, and do not see the transaction's own uncommitted writes. - If the callback throws, nothing is committed and the error is rethrown. - The callback may run more than once if the backend retries on contention, so it must have no side effects other than through its provider. - Portable code must not use realtime sequences or nested `transact()` calls inside a transaction — most backends throw `UnsupportedError`, though some (e.g. `MemoryDBProvider`) support them scoped to the transaction. - Not every provider supports transactions — the base implementation throws `UnsupportedError`. ## Signatures ```ts transact(callback: (provider: DBProvider) => Promise): Promise ``` --- # DebugDBProvider - Kind: class - Import: `import { DebugDBProvider } from "shelving/db";` - Extends: `ThroughDBProvider` Database provider that logs every operation it performs to the console for debugging. - Wraps a `source` provider and writes `console.debug` lines (with ANSI direction markers) before and after each call, and `console.error` on failure. - Drop it into a provider chain while developing to trace database reads and writes; it changes no data. ## Signatures ```ts new DebugDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `DebugDBProvider` | | ## Examples ```ts const provider = new DebugDBProvider(new MemoryDBProvider()); await provider.getItem(users, 123); // Logs the call and its result. ``` --- # MemoryDBProvider - Kind: class - Import: `import { MemoryDBProvider } from "shelving/db";` - Extends: `DBProvider` Synchronous in-memory database provider, storing each collection in a `MemoryTable`. - Extremely fast (ideal as the client database that `HydratingAPIProvider` fills), but does not persist data after the process or browser window closes. - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`. - Supports live subscriptions, so it can back `ItemStore` / `QueryStore` reads. - Supports transactions: `transact()` runs the callback against a snapshot clone, records its operations with `RecordingDBProvider`, and replays the recorded writes onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction. A fully in-memory `DBProvider`. `MemoryDBProvider` stores every collection in plain memory — fast, with no persistence. It is ideal for tests and prototypes, and it is the client database that `HydratingAPIProvider` fills from API results. Unlike the SQL providers, `MemoryDBProvider` supports realtime sequences (`getItemSequence`, `getQuerySequence`). ## Usage ```ts import { MemoryDBProvider } from "shelving/db"; const provider = new MemoryDBProvider(); const id = await provider.addItem(POSTS, { title: "Hello", body: "First post.", published: false }); const post = await provider.getItem(POSTS, id); // Realtime — emits whenever the item changes. for await (const next of provider.getItemSequence(POSTS, id)) { console.log(next); } ``` ## Item and query times Each `MemoryTable` records when an item was last set or deleted (`MemoryTable.getItemTime()`), and when a query was marked as loaded (`MemoryTable.getQueryTime()`, set with `MemoryTable.setQueryTime()`). Stores use the times to tell "not loaded yet" from "empty": - An item with no item and no time is loading. Deleting an item that does not exist still sets its time, so the table knows it is missing. - A query with no items and no time is loading. A query matches by `JSON.stringify(query)`, including `$order` and `$limit`. ```ts const table = memory.getTable(MEMBERS); table.setItems(items); table.setQueryTime({ user: "dave" }); // `items` is the complete answer to this query. ``` ## Transactions `MemoryDBProvider.transact()` hands the callback a shallow snapshot clone of the provider (see `MemoryDBProvider.clone()`) wrapped in a `RecordingDBProvider` to record its operations, then replays the recorded writes onto the real provider with `RecordingDBProvider.replayWrites()` when the callback resolves — so live subscriptions fire naturally on commit, and a thrown callback commits nothing: ```ts await provider.transact(async db => { const post = await db.requireItem(POSTS, id); await db.updateItem(POSTS, id, { title: `${post.title}!` }); }); ``` Things to know: - The clone is a full provider, so everything works inside the callback: reads see a snapshot from when the transaction began plus the transaction's own writes, realtime sequences observe the transaction's state, and nested `transact()` commits into the outer transaction. Portable code must not rely on any of this — see `DBProvider.transact()` for the weakest shared contract. - The clone is disposed when the transaction completes or fails, ending any sequences opened inside the callback. - Writes made to the provider while the callback is running are kept — the recorded writes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection. Overlapping transactions replay in completion order, last write wins per item. - Query writes (`setQuery`, `updateQuery`, `deleteQuery`) resolve two-step against the clone, so they commit to exactly the items they matched inside the transaction, even if concurrent writes changed which items match. ## Signatures ```ts new MemoryDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `MemoryDBProvider` | | ## Methods - [MemoryDBProvider.getTable()](db/MemoryDBProvider/getTable.md): Get (or lazily create) the MemoryTable backing a collection. - [MemoryDBProvider.createTable()](db/MemoryDBProvider/createTable.md): Create a new MemoryTable for a collection (without registering it — use getTable() for that). - [MemoryDBProvider.setItems()](db/MemoryDBProvider/setItems.md): Set (insert or overwrite) several whole items in a collection at once. - [MemoryDBProvider.clone()](db/MemoryDBProvider/clone.md): Clone this provider into a new plain MemoryDBProvider containing the same items. --- # MemoryDBProvider.getTable() - Kind: method - Member of: `MemoryDBProvider` Get (or lazily create) the `MemoryTable` backing a collection. ## Signatures ```ts getTable(collection: Collection): MemoryTable ``` --- # MemoryDBProvider.createTable() - Kind: method - Member of: `MemoryDBProvider` Create a new `MemoryTable` for a collection (without registering it — use `getTable()` for that). - Override point for subclasses that back collections with a specialised table, e.g. `StorageDBProvider`. ## Signatures ```ts createTable(collection: Collection): MemoryTable ``` --- # MemoryDBProvider.setItems() - Kind: method - Member of: `MemoryDBProvider` Set (insert or overwrite) several whole items in a collection at once. ## Signatures ```ts setItems(collection: Collection, items: Items): void ``` --- # MemoryDBProvider.clone() - Kind: method - Member of: `MemoryDBProvider` Clone this provider into a new plain `MemoryDBProvider` containing the same items. - Shallow: new tables and maps sharing the same (immutable) item instances, so cloning is cheap and unchanged items keep their identity. - The clone is always a plain `MemoryDBProvider` with plain `MemoryTable`s, even when called on a subclass — writes to the clone only touch its own memory, never a subclass's backing store (e.g. `StorageDBProvider` persistence). ## Signatures ```ts clone(): MemoryDBProvider ``` --- # MemoryTable - Kind: class - Import: `import { MemoryTable } from "shelving/db";` - Implements: `AsyncDisposable` In-memory table holding the items of a single collection for a `MemoryDBProvider`. - Keys items by id in a `Map`, preserving the exact object instance passed in. - An internal `DeferredSequence` resolves on every change, powering the live `*Sequence` subscriptions. ## Signatures ```ts new MemoryTable(collection: Collection, items?: Iterable]>) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | | `items` | `Iterable]>` | Optional initial `[id, item]` entries to seed the table with. Optional. | ## Returns | Type | Description | | --- | --- | | `MemoryTable` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `collection` | `Collection` | Collection this table stores the items of. Read only. | ## Examples ```ts const table = provider.getTable(users); table.setItem(123, { name: "Dave" }); ``` ## Methods - [MemoryTable.getItem()](db/MemoryTable/getItem.md): Get an item by its id, or undefined if it doesn't exist. - [MemoryTable.getItemSequence()](db/MemoryTable/getItemSequence.md): Subscribe to all changes for this item key. - [MemoryTable.getItemTime()](db/MemoryTable/getItemTime.md): Get the time an item was last set or deleted in this table, or undefined if it never was. - [MemoryTable.generateUniqueID()](db/MemoryTable/generateUniqueID.md): Generate a unique id for a new item in this table. - [MemoryTable.addItem()](db/MemoryTable/addItem.md): Add a new item with a freshly generated unique id. - [MemoryTable.setItem()](db/MemoryTable/setItem.md): Set (insert or overwrite) an item by its id. - [MemoryTable.setItemSequence()](db/MemoryTable/setItemSequence.md): Mirror an async sequence of item values into this table, passing each value through. - [MemoryTable.updateItem()](db/MemoryTable/updateItem.md): Apply partial updates to an existing item by its id. - [MemoryTable.deleteItem()](db/MemoryTable/deleteItem.md): Delete an item by its id. - [MemoryTable.countQuery()](db/MemoryTable/countQuery.md): Count the items in this table matching an optional query. - [MemoryTable.getQuery()](db/MemoryTable/getQuery.md): Get the items in this table matching an optional query. - [MemoryTable.getQuerySequence()](db/MemoryTable/getQuerySequence.md): Subscribe to the live result of a query. - [MemoryTable.getQueryTime()](db/MemoryTable/getQueryTime.md): Get the time a query was last marked as loaded in this table, or undefined if it never was. - [MemoryTable.setQueryTime()](db/MemoryTable/setQueryTime.md): Mark a query as loaded, i.e. the items in this table are the complete answer to the query. - [MemoryTable.setQuery()](db/MemoryTable/setQuery.md): Set (overwrite) the data for every item matching a query. - [MemoryTable.updateQuery()](db/MemoryTable/updateQuery.md): Apply partial updates to every item matching a query. - [MemoryTable.deleteQuery()](db/MemoryTable/deleteQuery.md) - [MemoryTable.setItems()](db/MemoryTable/setItems.md) - [MemoryTable.setItemsSequence()](db/MemoryTable/setItemsSequence.md) - [MemoryTable.clone()](db/MemoryTable/clone.md): Clone this table into a new plain MemoryTable containing the same items. --- # MemoryTable.getItem() - Kind: method - Member of: `MemoryTable` Get an item by its id, or `undefined` if it doesn't exist. ## Signatures ```ts getItem(id: I): OptionalItem ``` --- # MemoryTable.getItemSequence() - Kind: method - Member of: `MemoryTable` Subscribe to all changes for this item key. - Emits the current item immediately, including `undefined` when absent. - Wakes on every table change, but only yields when this item's value actually changed. ## Signatures ```ts getItemSequence(id: I): AsyncIterable> ``` --- # MemoryTable.getItemTime() - Kind: method - Member of: `MemoryTable` Get the time an item was last set or deleted in this table, or `undefined` if it never was. - Use it to tell an item that does not exist (a time and no item) from an item that has not loaded yet (no time). ## Signatures ```ts getItemTime(id: I): number | undefined ``` --- # MemoryTable.generateUniqueID() - Kind: method - Member of: `MemoryTable` Generate a unique id for a new item in this table. - Uses a random string for `StringSchema` ids, otherwise a random number, regenerating until it doesn't collide. ## Signatures ```ts generateUniqueID(): I ``` --- # MemoryTable.addItem() - Kind: method - Member of: `MemoryTable` Add a new item with a freshly generated unique id. ## Signatures ```ts addItem(data: T): I ``` --- # MemoryTable.setItem() - Kind: method - Member of: `MemoryTable` Set (insert or overwrite) an item by its id. - Sets the item's time (see `getItemTime()`). - Only resolves `next` when the stored instance actually changes, or the item had no time. ## Signatures ```ts setItem(id: I, data: Item | T): void ``` --- # MemoryTable.setItemSequence() - Kind: method - Member of: `MemoryTable` Mirror an async sequence of item values into this table, passing each value through. - Sets the item when a value arrives, or deletes it when the value is `undefined`. ## Signatures ```ts setItemSequence(id: I, sequence: AsyncIterable>): AsyncIterable> ``` --- # MemoryTable.updateItem() - Kind: method - Member of: `MemoryTable` Apply partial updates to an existing item by its id. - Does nothing if no item exists with that id. ## Signatures ```ts updateItem(id: I, updates: Updates>): void ``` --- # MemoryTable.deleteItem() - Kind: method - Member of: `MemoryTable` Delete an item by its id. - Sets the item's time (see `getItemTime()`), even when the item does not exist, so the table knows the item is missing. - Only resolves `next` when an item was actually removed, or the item had no time. ## Signatures ```ts deleteItem(id: I): void ``` --- # MemoryTable.countQuery() - Kind: method - Member of: `MemoryTable` Count the items in this table matching an optional query. ## Signatures ```ts countQuery(query?: Query>): number ``` --- # MemoryTable.getQuery() - Kind: method - Member of: `MemoryTable` Get the items in this table matching an optional query. ## Signatures ```ts getQuery(query?: Query>): Items ``` --- # MemoryTable.getQuerySequence() - Kind: method - Member of: `MemoryTable` Subscribe to the live result of a query. - Emits the current query result immediately, even if empty. - Wakes on every table change, but only yields when the computed query result changed, or the query was first marked as loaded (see `getQueryTime()`). ## Signatures ```ts getQuerySequence(query?: Query>): AsyncIterable> ``` --- # MemoryTable.getQueryTime() - Kind: method - Member of: `MemoryTable` Get the time a query was last marked as loaded in this table, or `undefined` if it never was. - Use it to tell a query with no items (a time and no items) from a query that has not loaded yet (no time). - Queries match by `JSON.stringify(query)`, so the same filters in a different key order are a different query. ## Signatures ```ts getQueryTime(query?: Query>): number | undefined ``` --- # MemoryTable.setQueryTime() - Kind: method - Member of: `MemoryTable` Mark a query as loaded, i.e. the items in this table are the complete answer to the query. - Set the items first (e.g. with `setItems()`), then mark the query. - Resolves `next` when the query was not marked before, so live query sequences see the change. ## Signatures ```ts setQueryTime(query?: Query>, time: number = Date.now()): void ``` --- # MemoryTable.setQuery() - Kind: method - Member of: `MemoryTable` Set (overwrite) the data for every item matching a query. ## Signatures ```ts setQuery(query: Query>, data: T): void ``` --- # MemoryTable.updateQuery() - Kind: method - Member of: `MemoryTable` Apply partial updates to every item matching a query. ## Signatures ```ts updateQuery(query: Query>, updates: Updates): void ``` --- # MemoryTable.deleteQuery() - Kind: method - Member of: `MemoryTable` ## Signatures ```ts deleteQuery(query: Query>): void ``` --- # MemoryTable.setItems() - Kind: method - Member of: `MemoryTable` ## Signatures ```ts setItems(items: Items): void ``` --- # MemoryTable.setItemsSequence() - Kind: method - Member of: `MemoryTable` ## Signatures ```ts setItemsSequence(sequence: AsyncIterable>): AsyncIterable> ``` --- # MemoryTable.clone() - Kind: method - Member of: `MemoryTable` Clone this table into a new plain `MemoryTable` containing the same items. - Shallow: a new map sharing the same (immutable) item instances, so cloning is cheap. - The clone is always a plain `MemoryTable`, even when called on a subclass — writes to the clone have no side effects (e.g. `StorageTable` persistence). ## Signatures ```ts clone(): MemoryTable ``` --- # MockDBCall - Kind: type - Import: `import { MockDBCall } from "shelving/db";` Structured log entry recording one operation performed through a `MockDBProvider`. - `type` is the operation name; `collection` is the collection name; `id`, `query`, `data`, `updates`, and `result` carry whichever fields apply to that operation. ## Signatures ```ts { readonly type: | "getItem" | "addItem" | "setItem" | "updateItem" | "deleteItem" | "countQuery" | "getQuery" | "setQuery" | "updateQuery" | "deleteQuery"; readonly collection: string; readonly id?: Identifier | undefined; readonly query?: unknown; readonly data?: Data; readonly updates?: unknown; readonly result?: unknown; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `type` | `\| "getItem" \| "addItem" \| "setItem" \| "updateItem" \| "deleteItem" \| "countQuery" \| "getQuery" \| "setQuery" \| "updateQuery" \| "deleteQuery"` | Read only. | | `collection` | `string` | Read only. | | `id` | `Identifier \| undefined` | Read only. Optional. | | `query` | `unknown` | Read only. Optional. | | `data` | `Data` | Read only. Optional. | | `updates` | `unknown` | Read only. Optional. | | `result` | `unknown` | Read only. Optional. | ## Types - [Identifier](util/item/Identifier.md): Allowed types for the "id" property (identifier) for an item. - [Data](util/data/Data.md): Data object — a plain object with string keys and unknown values. --- # MockDBProvider - Kind: class - Import: `import { MockDBProvider } from "shelving/db";` - Extends: `MemoryDBProvider` In-memory database provider that records every operation to its `calls` log, for testing. - Extends `MemoryDBProvider`, so it stores data normally, then appends a `MockDBCall` entry (including the result) for each call. - Assert against `calls` in tests to check which operations ran and with what arguments. ## Signatures ```ts new MockDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `MockDBProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `calls` | `MockDBCall[]` | The log of operations performed through this provider, in the order they happened. Read only. Defaults to `[]`. | ## Examples ```ts const provider = new MockDBProvider(); await provider.addItem(users, { name: "Dave" }); provider.calls; // [{ type: "addItem", collection: "users", data: { name: "Dave" }, result: 123 }] ``` --- # PostgresProvider - Kind: class - Import: `import { PostgresProvider } from "shelving/db";` - Extends: `SQLProvider` Abstract PostgreSQL provider with JSONB function support for nested keys, array containment, and array mutations. The abstract SQL provider for PostgreSQL. `PostgresProvider` extends `SQLProvider` with PostgreSQL-specific behaviour: the `data` column is `jsonb NOT NULL`, generated columns use the `#>>` operator with a cast, and compatible numeric/string column type changes are applied in place with `ALTER COLUMN TYPE`. It is abstract — a concrete subclass implements `exec()` against a specific PostgreSQL driver. ## Usage ```ts import { PostgresProvider } from "shelving/db"; class BunPostgresProvider extends PostgresProvider { constructor(private sql: Bun.SQL) { super(); } async exec(strings, ...values) { return this.sql(strings, ...values); } } ``` Pair it with `PostgresMigrator` to create and alter tables to match your collection schemas. A ready-made provider is available in the `shelving/bun` module. ## Signatures ```ts new PostgresProvider() ``` ## Returns | Type | Description | | --- | --- | | `PostgresProvider` | | --- # DBOperation - Kind: type - Import: `import { DBOperation } from "shelving/db";` Structured log entry recording a single database operation performed through a `RecordingDBProvider`. - `action` is the kind of operation; `collection` is the `Collection` it applies to; `id` is the item involved; `data` and `updates` carry whichever fields apply to that operation. - A `"get"` operation records a read — `data` is the item that was observed, or `undefined` if the read confirmed the item absent. ## Signatures ```ts { readonly action: "get" | "add" | "set" | "update" | "delete"; readonly collection: Collection; readonly id: I; readonly data?: unknown; readonly updates?: unknown; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `action` | `"get" \| "add" \| "set" \| "update" \| "delete"` | Read only. | | `collection` | `Collection` | Read only. | | `id` | `I` | Read only. | | `data` | `unknown` | Read only. Optional. | | `updates` | `unknown` | Read only. Optional. | ## Types - [Collection](db/Collection.md): Declarative definition of a database collection/table. --- # DBOperations - Kind: type - Import: `import { DBOperations } from "shelving/db";` Readonly array of `DBOperation` entries, e.g. the log recorded by a `RecordingDBProvider`. ## Signatures ```ts ImmutableArray> ``` ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. - [DBOperation](db/DBOperation.md): Structured log entry recording a single database operation performed through a RecordingDBProvider. --- # replayOperations() - Kind: function - Import: `import { replayOperations } from "shelving/db";` Replay a list of database operations onto a provider, re-issuing each one in order. - Writes re-issue as the corresponding item write — `"add"` replays as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids. - `"get"` reads apply what they observed — the item is set, or deleted when the read confirmed it absent. That's right for refreshing a mirror or cache, and wrong for an authoritative target, where an observed snapshot would overwrite newer data — replay only the writes there (see `RecordingDBProvider.replayWrites()`). - Operations re-issue as a sequence of awaited writes — the replay itself is not atomic. ## Signatures ```ts replayOperations(provider: DBProvider, operations: DBOperations): Promise ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | Provider to replay the operations onto. | | `operations` | `DBOperations` | Operations to replay, in order. | ## Returns | Type | Description | | --- | --- | | `Promise` | | ## Examples ```ts await replayOperations(mirror, recording.operations); ``` --- # RecordingDBProvider - Kind: class - Import: `import { RecordingDBProvider } from "shelving/db";` - Extends: `ThroughDBProvider` Database provider that records every operation it performs to its `operations` log. - Wraps a `source` provider, delegates each operation, then appends a `DBOperation` entry describing what happened. - Records reads as well as writes: `getItem()` logs a `"get"` with the item it observed (or its confirmed absence), and `getQuery()` logs a `"get"` per item returned. Derived reads and two-step query writes are inherited, so everything they do is recorded per item too. Realtime sequences are not recorded. - Replay the log onto another provider with `replay()`, `replayWrites()`, or `replayReads()`. - Useful for building audit logging, change feeds, optimistic updates (see `UndoDBProvider`), or assertions in tests. A wrapping provider that records every operation. `RecordingDBProvider` extends `ThroughDBProvider` and accumulates an `operations` log of every read and write that passes through it — useful for audit trails, change feeds, optimistic updates, and assertions in tests. ## Usage ```ts import { RecordingDBProvider, MemoryDBProvider } from "shelving/db"; const db = new RecordingDBProvider(new MemoryDBProvider()); await db.getItem(POSTS, "abc"); await db.setItem(POSTS, "abc", { title: "Hi", body: "", published: true }); console.log(db.operations); // [{ action: "get", collection: POSTS, id: "abc", data: undefined }, // { action: "set", collection: POSTS, id: "abc", data: { … } }] ``` Every operation is an explicit per-item `DBOperation` carrying the `Collection` object, the `id`, and the `data` or `updates` involved. Reads record what they observed — `getItem()` logs a `"get"` with the item (or `undefined` for confirmed absence), and `getQuery()` logs a `"get"` per item it returned. Query writes are inherited two-step from `ThroughDBProvider`, so they arrive in the log as their resolve reads plus the individual item writes. Realtime sequences are not recorded. Filter the log with the `RecordingDBProvider.writes` and `RecordingDBProvider.reads` getters. ## Replaying `replayOperations()` re-issues a list of operations onto a provider in order, and three methods delegate to it: - `RecordingDBProvider.replayWrites()` — writes only. The right call for **authoritative** targets that hold their own truth (re-applying a log to a real database, audit replay, syncing a second source of truth): updates compose onto current state as deltas, and observed reads are never allowed to overwrite newer data. - `RecordingDBProvider.replay()` — everything, in order. The right call for **mirrors**: reads apply what they observed (setting the item, or deleting it when the read confirmed absence), refreshing stale copies and giving update deltas their correct base. - `RecordingDBProvider.replayReads()` — reads only, e.g. warming a cache from a recorded session. `MemoryDBProvider.transact()` is built on this: it records the callback's operations against a snapshot clone, then commits with `RecordingDBProvider.replayWrites()`. ## Undo `UndoDBProvider` extends this provider to read each item before the first write that touches it, so the log always contains every touched item's original state — which `UndoDBProvider.undo()` restores, enabling optimistic local updates that roll back when a server call fails. ## Signatures ```ts new RecordingDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `RecordingDBProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `operations` | `DBOperations` | The log of operations performed through this provider, in the order they happened. Read only. | | `writes` | `DBOperations` | The write operations from the `operations` log, in the order they happened. Read only. | | `reads` | `DBOperations` | The `"get"` read operations from the `operations` log, in the order they happened. Read only. | ## Methods - [RecordingDBProvider.replay()](db/RecordingDBProvider/replay.md): Replay every recorded operation onto another provider, in order. - [RecordingDBProvider.replayWrites()](db/RecordingDBProvider/replayWrites.md): Replay only the recorded write operations onto another provider, in order. - [RecordingDBProvider.replayReads()](db/RecordingDBProvider/replayReads.md): Replay only the recorded "get" read operations onto another provider, in order. --- # RecordingDBProvider.replay() - Kind: method - Member of: `RecordingDBProvider` Replay every recorded operation onto another provider, in order. - Reads apply what they observed and writes re-issue, so this refreshes a mirror or cache exactly — see `replayOperations()`. - Use `replayWrites()` for an authoritative target, where applying observed reads would overwrite newer data. ## Signatures ```ts replay(provider: DBProvider): Promise ``` --- # RecordingDBProvider.replayWrites() - Kind: method - Member of: `RecordingDBProvider` Replay only the recorded write operations onto another provider, in order. - The right call for authoritative targets — applying a log to a real database, audit replay, syncing a second source of truth — where updates should compose onto current state and observed reads must never overwrite newer data. ## Signatures ```ts replayWrites(provider: DBProvider): Promise ``` --- # RecordingDBProvider.replayReads() - Kind: method - Member of: `RecordingDBProvider` Replay only the recorded `"get"` read operations onto another provider, in order. - Applies what each read observed (setting the item, or deleting it when the read confirmed absence) — e.g. warming a cache from a recorded session. ## Signatures ```ts replayReads(provider: DBProvider): Promise ``` --- # SQLiteProvider - Kind: class - Import: `import { SQLiteProvider } from "shelving/db";` - Extends: `SQLProvider` Abstract SQLite provider with JSON1 function support for nested keys, array containment, and array mutations. Note the following compatibility caveats: - For `with` and `omit` updates this does not preserve ordering of the original array. - For `with` and `omit` updates this does not guarantee equality for de-duplication when working with nested objects or arrays. The abstract SQL provider for SQLite and Cloudflare D1. `SQLiteProvider` extends `SQLProvider` with SQLite-specific behaviour: tables are created in `STRICT` mode, the `data` column is `TEXT NOT NULL CHECK (json_valid(data))`, and generated columns use `json_extract`. It is abstract — a concrete subclass implements `exec()` against a specific SQLite driver. ## Usage ```ts import { SQLiteProvider } from "shelving/db"; class D1Provider extends SQLiteProvider { constructor(private d1: D1Database) { super(); } async exec(strings, ...values) { return this.d1.prepare(strings.join("?")).bind(...values).all(); } } ``` Pair it with `SQLiteMigrator` to create and alter tables to match your collection schemas. A ready-made D1 provider is available in the `shelving/cloudflare` module. ## Signatures ```ts new SQLiteProvider() ``` ## Returns | Type | Description | | --- | --- | | `SQLiteProvider` | | --- # SQLFragment - Kind: class - Import: `import { SQLFragment } from "shelving/db";` SQL fragment made from template strings plus embedded expressions, ready to be composed into a query. - Flattens eagerly: `SQLFragment` values are spliced inline at construction, so `values` only ever contains bindable parameters (or driver-specific identifier tokens) — never other fragments. - Concrete providers can therefore pass `strings` / `values` straight to their driver, and detect fragments with a plain `instanceof` check. ## Signatures ```ts new SQLFragment(strings: ImmutableArray, values: ImmutableArray) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `strings` | `ImmutableArray` | | | `values` | `ImmutableArray` | | ## Returns | Type | Description | | --- | --- | | `SQLFragment` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `strings` | `ImmutableArray` | Read only. | | `values` | `ImmutableArray` | Read only. | --- # SQLProvider - Kind: class - Import: `import { SQLProvider } from "shelving/db";` - Extends: `DBProvider` Abstract database provider that implements CRUD and query operations by generating and executing SQL. - Subclasses implement `exec()` to run a query against a concrete database (e.g. SQLite, PostgreSQL). - The `sql*` helpers build composable `SQLFragment` objects so dialect differences can be overridden. - Realtime subscriptions are unsupported and throw `UnsupportedError`. The abstract SQL base provider. `SQLProvider` implements the `DBProvider` surface in terms of SQL, leaving one method for concrete subclasses to provide: `exec(strings, ...values)`, which runs a parameterised query and returns rows as plain objects. Each scalar field in a collection schema maps to a **generated column** extracted from the `data` JSON blob, so queries use indexed column comparisons. `SQLProvider` supports integer (`number` with `step: 1`) and string identifiers. Realtime sequences are not supported — `getItemSequence` / `getQuerySequence` throw `UnsupportedError`. ## Usage `SQLProvider` is abstract — bind it to a driver by implementing `exec()`. The built-in `SQLiteProvider` and `PostgresProvider` extend it with dialect-specific JSON path syntax; concrete drivers live in the `shelving/cloudflare` and `shelving/bun` modules. ```ts import { SQLiteProvider } from "shelving/db"; // A concrete subclass implements exec() against a specific driver. class D1Provider extends SQLiteProvider { async exec(strings, ...values) { return this.db.prepare(strings.join("?")).bind(...values).all(); } } ``` ## Signatures ```ts new SQLProvider() ``` ## Returns | Type | Description | | --- | --- | | `SQLProvider` | | ## Methods - [SQLProvider.exec()](db/SQLProvider/exec.md): Execute an SQL query built from a template literal and return the resulting rows. - [SQLProvider.sql()](db/SQLProvider/sql.md): Define an SQL fragment using Javascript template literal format. - [SQLProvider.sqlIdentifier()](db/SQLProvider/sqlIdentifier.md): Define an SQL fragment for an escaped identifier, e.g. "myTable". - [SQLProvider.sqlExtract()](db/SQLProvider/sqlExtract.md): Define an SQL fragment that extracts a value at a key for comparison, e.g. "a" #>> {"b","c"} in Postgres. - [SQLProvider.sqlConcat()](db/SQLProvider/sqlConcat.md): Define an SQL fragment that joins a series of fragments with a separator, e.g. "a" = 1 AND "b" = 2. - [SQLProvider.sqlSetters()](db/SQLProvider/sqlSetters.md): Define an SQL fragment for setting a list of values, e.g. "a" = 1, "b" = 2. - [SQLProvider.sqlUpdates()](db/SQLProvider/sqlUpdates.md): Define an SQL fragment for a set of updates, e.g. "a" = 1, "b" = "b" + 5. - [SQLProvider.sqlUpdate()](db/SQLProvider/sqlUpdate.md): Define an SQL fragment for a single update action. - [SQLProvider.sqlValues()](db/SQLProvider/sqlValues.md): Define an SQL fragment for VALUES syntax, e.g. ("a", "b") VALUES (1, 2). - [SQLProvider.sqlClauses()](db/SQLProvider/sqlClauses.md): Define an SQL fragment for the WHERE, ORDER BY and LIMIT clauses of a query, e.g. WHERE x = 1 ORDER BY "name" LIMIT 0, 50. - [SQLProvider.sqlWhere()](db/SQLProvider/sqlWhere.md): Define an SQL fragment for a WHERE clause, e.g. WHERE x = 1 AND y <= 100. - [SQLProvider.sqlFilter()](db/SQLProvider/sqlFilter.md): Define an SQL fragment for a single filter clause on a column, e.g. x = 1 or x IN (1, 2). - [SQLProvider.sqlOrder()](db/SQLProvider/sqlOrder.md): Define an SQL fragment for an ORDER BY clause, e.g. ORDER BY "a" ASC, "b" DESC. - [SQLProvider.sqlSort()](db/SQLProvider/sqlSort.md): Define an SQL fragment for an individual column in an ORDER BY, e.g. "a" ASC. - [SQLProvider.sqlLimit()](db/SQLProvider/sqlLimit.md): Define an SQL fragment for a LIMIT clause, e.g. LIMIT 50. --- # SQLProvider.exec() - Kind: method - Member of: `SQLProvider` Execute an SQL query built from a template literal and return the resulting rows. ## Signatures ```ts exec(strings: TemplateStringsArray, ...values: ImmutableArray): Promise> ``` --- # SQLProvider.sql() - Kind: method - Member of: `SQLProvider` Define an SQL fragment using Javascript template literal format. ## Signatures ```ts sql(strings: TemplateStringsArray, ...values: ImmutableArray): SQLFragment ``` --- # SQLProvider.sqlIdentifier() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for an escaped identifier, e.g. `"myTable"`. ## Signatures ```ts sqlIdentifier(name: string): SQLFragment ``` --- # SQLProvider.sqlExtract() - Kind: method - Member of: `SQLProvider` Define an SQL fragment that extracts a value at a key for comparison, e.g. `"a" #>> {"b","c"}` in Postgres. - Base implementation only supports flat (single-segment) keys; subclasses override for nested JSON paths. ## Signatures ```ts sqlExtract(key: Segments): SQLFragment ``` --- # SQLProvider.sqlConcat() - Kind: method - Member of: `SQLProvider` Define an SQL fragment that joins a series of fragments with a separator, e.g. `"a" = 1 AND "b" = 2`. ## Signatures ```ts sqlConcat(values: ImmutableArray, separator = ", ", before = "", after = ""): SQLFragment ``` --- # SQLProvider.sqlSetters() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for setting a list of values, e.g. `"a" = 1, "b" = 2`. ## Signatures ```ts sqlSetters(data: TT): SQLFragment ``` --- # SQLProvider.sqlUpdates() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for a set of updates, e.g. `"a" = 1, "b" = "b" + 5`. ## Signatures ```ts sqlUpdates(updates: Updates): SQLFragment ``` --- # SQLProvider.sqlUpdate() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for a single update action. - Handles flat `set` and `sum` only (single-segment key). - Nested keys (multi-segment) and `with`/`omit` actions throw `UnsupportedError`. - Subclasses should override to support nested keys and array mutation actions. ## Signatures ```ts sqlUpdate({ action, key, value }: Update): SQLFragment ``` --- # SQLProvider.sqlValues() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for `VALUES` syntax, e.g. `("a", "b") VALUES (1, 2)`. ## Signatures ```ts sqlValues(data: Data): SQLFragment ``` --- # SQLProvider.sqlClauses() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for the `WHERE`, `ORDER BY` and `LIMIT` clauses of a query, e.g. ` WHERE x = 1 ORDER BY "name" LIMIT 0, 50`. ## Signatures ```ts sqlClauses(query: Query): void ``` --- # SQLProvider.sqlWhere() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for a `WHERE` clause, e.g. ` WHERE x = 1 AND y <= 100`. - Returns an empty fragment when the query has no filters. ## Signatures ```ts sqlWhere(query: Query): void ``` --- # SQLProvider.sqlFilter() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for a single filter clause on a column, e.g. `x = 1` or `x IN (1, 2)`. ## Signatures ```ts sqlFilter({ key, operator, value }: QueryFilter): SQLFragment ``` --- # SQLProvider.sqlOrder() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for an `ORDER BY` clause, e.g. ` ORDER BY "a" ASC, "b" DESC`. - Returns an empty fragment when the query has no orders. - Nested keys (multi-segment) throw `UnsupportedError`. ## Signatures ```ts sqlOrder(query: Query): void ``` --- # SQLProvider.sqlSort() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for an individual column in an `ORDER BY`, e.g. `"a" ASC`. ## Signatures ```ts sqlSort({ key, direction }: QueryOrder): SQLFragment ``` --- # SQLProvider.sqlLimit() - Kind: method - Member of: `SQLProvider` Define an SQL fragment for a `LIMIT` clause, e.g. ` LIMIT 50`. - Returns an empty fragment when the query has no limit. ## Signatures ```ts sqlLimit(query: Query): void ``` --- # StorageDBProvider - Kind: class - Import: `import { StorageDBProvider } from "shelving/db";` - Extends: `MemoryDBProvider` In-memory database provider that persists every collection to a `Storage` — `localStorage`, `sessionStorage`, or anything else with the same interface. - Extends `MemoryDBProvider`, so all reads, queries, and realtime sequences are served synchronously from memory, and it can seed `ItemStore` / `QueryStore` and be filled by `HydratingAPIProvider`. The only difference is that collections are backed by `StorageTable` instead of `MemoryTable`. - The storage is a required argument (there is no default), so server code that constructs this provider must reference `localStorage` / `sessionStorage` itself — surfacing the mistake at the callsite instead of deep inside this class. - Treat the persisted data as best-effort: quota is shared across the origin (typically ~5MB) and users can clear it at any time. Data read back from storage is unvalidated — wrap this provider in `ValidationDBProvider` if it may have been written by an older version of your app. - If the storage is unusable (writes blocked by browser settings, or private browsing with zero quota), the provider degrades to memory-only operation — check the `persistent` flag to warn users their changes won't be saved. A `MemoryDBProvider` that persists every collection to a `Storage` — `localStorage`, `sessionStorage`, or anything else with the same interface. Each collection is hydrated from storage once, lazily, on first access — after that all reads, queries, and realtime sequences are served synchronously from memory, and every write persists to storage before it touches memory. Because it *is* a `MemoryDBProvider`, it plugs into everything that expects one: it seeds `ItemStore` / `QueryStore` synchronously (including via `DBCache` and the React hooks from `createDBContext()`), and `HydratingAPIProvider` can fill it, to give API results a persistent local copy. `storage` events from other tabs/windows are applied to memory and notify realtime sequences, so `getItemSequence()` / `getQuerySequence()` update live across tabs. All the persistence logic lives in `StorageTable`, a self-contained `MemoryTable` subclass that hydrates itself, persists its own writes, and manages its own `storage` event listener — the provider is just a `MemoryDBProvider` whose `createTable()` returns `StorageTable`. `StorageTable` can also be used independently. Dispose the provider (or table) to remove the event listeners. ## Usage The storage to persist to is a required argument — there is no default, so server code that constructs this provider must reference `localStorage` / `sessionStorage` itself, surfacing the mistake at the callsite (at compile time, in a project without DOM types) rather than deep inside the class: ```ts import { StorageDBProvider } from "shelving/db"; const provider = new StorageDBProvider(localStorage); // Persists under "shelving:*" keys. if (!provider.persistent) console.warn("Changes won't be saved on this device."); const id = await provider.addItem(POSTS, { title: "Hello", body: "First post.", published: false }); const post = await provider.getItem(POSTS, id); // Still there after a reload. ``` Pass `sessionStorage` for per-tab persistence, and a prefix to namespace keys: ```ts const provider = new StorageDBProvider(sessionStorage, "myapp:"); ``` As a persistent client database that API results fill: ```ts import { HydratingAPIProvider } from "shelving/api"; import { StorageDBProvider } from "shelving/db"; const DB = new StorageDBProvider(localStorage); const API = new HydratingAPIProvider(new MyClientAPIProvider(), DB); ``` Items persisted from an earlier visit show at once, but times are not persisted. A query with no persisted items loads again after a reload. ## Reliability Treat the persisted data as best-effort, not guaranteed: - **Quota** is shared across the whole origin (typically ~5MB) and a write can fail at any time with `QuotaExceededError`. The provider persists *before* updating memory, so a failed write throws and changes nothing — catch it at the app level to warn the user (e.g. "we can't save data locally right now"). - **Users can clear storage** at any time, and browsers may partition or restrict it. - **Unusable storage** (writes blocked by browser settings, or private browsing with zero quota) makes the provider degrade to memory-only operation without throwing. Check `StorageDBProvider.persistent` to detect this and warn users their changes won't be saved. - **Stored data is unvalidated** — it may have been written by an older version of your app. Wrap the provider in `ValidationDBProvider`, and use `shelving/db` migrations for upgrading old shapes. Passing nothing at runtime (a plain-JS caller with no type checking) throws `UnsupportedError`. ## Signatures ```ts new StorageDBProvider(storage: Storage, prefix = "shelving:") ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `storage` | `Storage` | `Storage` instance to persist to, e.g. `localStorage` or `sessionStorage` (required so environments without one, e.g. the server, fail at the callsite). | | `prefix` | `unknown` | Prefix for every storage key written by this provider, so it can share an origin's storage with other code (defaults to `"shelving:"`). Defaults to `"shelving:"`. | ## Returns | Type | Description | | --- | --- | | `StorageDBProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `prefix` | `string` | Prefix for every storage key written by this provider. Read only. | | `persistent` | `boolean` | Whether this provider is actually persisting to storage. - `false` when storage exists but is unusable (e.g. blocked by browser settings, or private browsing with zero quota) — the provider still works, but data only lives in memory and is lost when the page closes. Read only. | ## Examples ```ts const provider = new StorageDBProvider(localStorage); if (!provider.persistent) console.warn("Changes won't be saved on this device."); const id = await provider.addItem(users, { name: "Dave" }); ``` --- # StorageTable - Kind: class - Import: `import { StorageTable } from "shelving/db";` - Extends: `MemoryTable` `MemoryTable` that persists its items to a `Storage` instance — self-contained, so it can also be used independently of `StorageDBProvider`. - Hydrates itself from storage on construction, then persists each write *before* applying it to memory, so a failed write (e.g. `QuotaExceededError` when the origin's quota is full) throws and leaves memory and storage consistent. - Items are stored as JSON under keys formatted as `{prefix}{collection}:{id}`, so values must be JSON-serializable. The id in the key is authoritative — an `id` stored inside the JSON is overridden. - Listens for `storage` events, so changes made in other tabs/windows update memory and notify realtime sequences. Dispose the table (or its provider) to remove the listener. ## Signatures ```ts new StorageTable(collection: Collection, storage: Storage, prefix = "shelving:") ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | | `storage` | `Storage` | | | `prefix` | `unknown` | Defaults to `"shelving:"`. | ## Returns | Type | Description | | --- | --- | | `StorageTable` | | --- # ThroughDBProvider - Kind: class - Import: `import { ThroughDBProvider } from "shelving/db";` - Extends: `DBProvider` - Implements: `Sourceable>` Database provider that passes every core operation straight through to a wrapped `source` provider. - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate. - Only the core operations delegate to `source` — derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) and two-step query writes are inherited from `DBProvider`, so they route through this provider's own overridden methods and wrapper behaviour applies to everything they do. Wrappers that don't need per-item behaviour override the query writes to pass through to `source` directly. - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`. The identity pass-through base for wrapping providers. `ThroughDBProvider` takes a `source` provider and delegates every method to it. Extend it to intercept only the methods you care about — timing, metrics, access control — without reimplementing the rest. `ValidationDBProvider`, `DebugDBProvider`, and `RecordingDBProvider` are all `ThroughDBProvider` subclasses. ## Usage Override only the methods you need and call `super` to delegate the rest: ```ts import { ThroughDBProvider } from "shelving/db"; class TimingDBProvider extends ThroughDBProvider { override async getItem(collection, id) { const t = performance.now(); const result = await super.getItem(collection, id); console.log(`getItem took ${performance.now() - t}ms`); return result; } } ``` ## Query writes: passthrough vs two-step A query write (`setQuery`, `updateQuery`, `deleteQuery`) can be implemented with one of two strategies: - **Passthrough** — forward the query write to `source` in a single call, so the underlying engine executes it natively (e.g. one SQL `UPDATE … WHERE`). Wrapper behaviour applies to the operation, but not to the individual items it changes. - **Two-step** — resolve the query to its matching items with `getQuery()`, then write each item through the wrapper's own `setItem()` / `updateItem()` / `deleteItem()`, so every implied write flows through the wrapper's overrides. Two-step is the `DBProvider` default, and `ThroughDBProvider` deliberately inherits it rather than passing the query through — the same theory as `transact()` below: a wrapper's behaviour should apply to everything that happens through it. `RecordingDBProvider` relies on this to record exactly which items a query write changed. Wrappers that don't need per-item behaviour override the query writes back to passthrough (calling `source` directly) to keep the engine's native efficiency and atomicity — `ValidationDBProvider` (validates the query write's inputs up front) and `DebugDBProvider` (logs the operation the caller made) both do this. The derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) are inherited the same way, so they route through the wrapper's own `getItem()` / `getQuery()` — e.g. `requireItem()` through a `ValidationDBProvider` returns validated data. Only the core operations delegate directly to `source`. The per-item writes run concurrently (via `awaitValues()`, which settles every write before rejecting), so a two-step batch over a remote source costs one round-trip of latency rather than one per item — though the order the individual writes complete in (and appear in a `RecordingDBProvider` log) is not guaranteed within the batch. The resolve and the writes are separate steps, so a two-step query write is only atomic when it runs inside `transact()`. ## Transactions `ThroughDBProvider.transact()` keeps the wrapper's behaviour inside the transaction: the callback receives a copy of the wrapping provider whose `source` is swapped for the source's transaction provider, so overridden methods (validation, timing, logging, etc.) still apply to every read and write in the callback. Subclasses get this for free — `TimingDBProvider` above times operations inside transactions without any extra code. The copy is made by the `cloneWith()` method and shares the wrapper's prototype and state. Override `cloneWith()` if a subclass holds per-instance mutable state that must not be shared with transaction copies — `RecordingDBProvider` does this to give each transaction its own log, so only a committed transaction's operations are merged into the main `operations` log. ## Signatures ```ts new ThroughDBProvider(source: DBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `DBProvider` | | ## Returns | Type | Description | | --- | --- | | `ThroughDBProvider` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `DBProvider` | The wrapped source provider that every operation is delegated to. Read only. | ## Methods - [ThroughDBProvider.cloneWith()](db/ThroughDBProvider/cloneWith.md): Clone this provider with different source. --- # ThroughDBProvider.cloneWith() - Kind: method - Member of: `ThroughDBProvider` Clone this provider with different `source`. ## Signatures ```ts cloneWith(source: DBProvider): this ``` --- # UndoDBProvider - Kind: class - Import: `import { UndoDBProvider } from "shelving/db";` - Extends: `RecordingDBProvider` Database provider that records every operation and can undo its own writes. - Extends `RecordingDBProvider`, additionally reading each item before the first write that touches it — so the log always contains every touched item's original state. - Call `undo()` to restore the wrapped provider to the state the log first observed, e.g. rolling back optimistic local updates after a failed server call. - The extra read is skipped when the log already establishes the item's state (an earlier read observed it, or an earlier add created it) — over a local `MemoryDBProvider` the reads are effectively free anyway. A recording provider whose writes can be undone. `UndoDBProvider` extends `RecordingDBProvider` and reads each item before the first write that touches it, so the `operations` log always contains every touched item's original state — which `UndoDBProvider.undo()` restores by applying the log's earliest observations in reverse. ## Usage — optimistic updates Apply changes to a local copy immediately so the UI updates instantly, push to the server, and roll back if the push fails: ```ts import { UndoDBProvider } from "shelving/db"; const local = new UndoDBProvider(memory); await runServiceLogic(local); // Applies locally right away. try { await api.push(local.writes); // Send the writes to the server. } catch { await local.undo(); // Server failed — restore the local copy. } ``` Things to know: - The extra read happens only for the first write that touches each item — skipped when the log already establishes the item's state (an earlier read observed it, a query returned it, or an earlier add created it). Over a local `MemoryDBProvider` the reads are effectively free anyway. - `UndoDBProvider.undo()` restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten. - `UndoDBProvider.undo()` writes directly to `source` without recording, so the log still describes the original operations afterwards. ## Signatures ```ts new UndoDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `UndoDBProvider` | | ## Examples ```ts const local = new UndoDBProvider(memory); await runServiceLogic(local); // Applies locally right away. try { await api.push(local.writes); // Send the writes to the server. } catch { await local.undo(); // Server failed — restore the local copy. } ``` ## Methods - [UndoDBProvider.undo()](db/UndoDBProvider/undo.md): Restore the wrapped provider to the state the log first observed, undoing this provider's writes. --- # UndoDBProvider.undo() - Kind: method - Member of: `UndoDBProvider` Restore the wrapped provider to the state the log first observed, undoing this provider's writes. - Applies the state-establishing operations in reverse order, so the earliest observed state of each item wins: reads restore the item that was observed (or delete it when the read confirmed it absent), and adds delete (the item did not exist before). - Restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten. - Writes directly to `source` without recording, so the log still describes the original operations afterwards. ## Signatures ```ts undo(): Promise ``` --- # ValidationDBProvider - Kind: class - Import: `import { ValidationDBProvider } from "shelving/db";` - Extends: `ThroughDBProvider` Database provider that validates data flowing to and from an asynchronous source provider. - Wraps a `source` provider (which may have any type, because validation guarantees the type) and runs every value through the relevant `Collection` schema before writing and after reading. - Written data is validated against the collection's data schema; read data is validated against the item schema, so trusted, correctly-typed values reach the rest of the app. - Updates are validated with `Collection.updates`, and every query with `Collection.query`, so an unknown key or a bad value never reaches the source. - Validation failures here are program-state errors, so they throw a typed `ValueError` rather than a raw validation `string`. - Applies inside `transact()` too — the transaction provider the callback receives validates its reads and writes the same way. A wrapping provider that validates data against the collection schema. `ValidationDBProvider` validates data on the way **in** (writes) and on the way **out** (reads) — so bad data never reaches the backend, and corrupt data from the backend surfaces as a `ValueError` rather than propagating silently. Place it between the cache and the backend so cached data is always known-good. ## Usage ```ts import { ValidationDBProvider, MemoryDBProvider } from "shelving/db"; const provider = new ValidationDBProvider(new MemoryDBProvider()); // Validated against the POSTS schema before writing. await provider.addItem(POSTS, { title: "Hello", body: "…", published: false }); // If the backend ever returns data that fails the schema, this throws ValueError. const post = await provider.getItem(POSTS, "abc"); ``` ## Signatures ```ts new ValidationDBProvider() ``` ## Returns | Type | Description | | --- | --- | | `ValidationDBProvider` | | --- # ItemStore - Kind: class - Import: `import { ItemStore } from "shelving/db";` - Extends: `FetchStore>` Store that fetches and tracks a single item by ID in a collection from a database provider. - Holds an `OptionalItem` value: the item if it exists, or `undefined` if it doesn't. - When the provider is (or wraps) a `MemoryDBProvider`, the store seeds from the memory at once and follows its changes. A missing item only counts as loaded when the memory has a time for it (see `MemoryTable.getItemTime()`). Until then the store is loading, and a fetch waits until the memory loads the item (e.g. from `HydratingAPIProvider`). - Any other provider is fetched, and its `DBProvider.getItemSequence()` is followed while the store has subscribers. A provider that throws `UnsupportedError` for subscriptions is only fetched. ## Signatures ```ts new ItemStore(collection: Collection, id: I, provider: DBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | | `id` | `I` | | | `provider` | `DBProvider` | | ## Returns | Type | Description | | --- | --- | | `ItemStore` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | The database provider this store fetches its item from. Read only. | | `collection` | `Collection` | The collection the item lives in. Read only. | | `id` | `I` | The ID of the item this store tracks. Read only. | | `item` | `Item` | The required item data of this store. - Use this when the item is known to exist; use `value` when it may be `undefined`. | ## Examples ```ts const store = new ItemStore(collection, "abc", provider); const item = await store; // OptionalItem ``` --- # QueryStore - Kind: class - Import: `import { QueryStore } from "shelving/db";` - Extends: `FetchStore>` - Implements: `Iterable>` Store that runs a query against a collection from a database provider and tracks its matching items. - Holds an `Items` value (the array of matching items) and is iterable over those items. - When the provider is (or wraps) a `MemoryDBProvider`, the store seeds from the memory at once and follows the live query result. An empty result only counts as loaded when the memory has a time for the query (see `MemoryTable.getQueryTime()`). Until then the store is loading, and a fetch waits until the memory loads the query (e.g. from `HydratingAPIProvider`). - Any other provider is fetched, and its `DBProvider.getQuerySequence()` is followed while the store has subscribers. A provider that throws `UnsupportedError` for subscriptions is only fetched. - The store does not page. To load more items into a memory, call the API with `getQueryItemsAfter()`, and the store shows the new items. ## Signatures ```ts new QueryStore(collection: Collection, query: Query>, provider: DBProvider) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `collection` | `Collection` | | | `query` | `Query>` | | | `provider` | `DBProvider` | | ## Returns | Type | Description | | --- | --- | | `QueryStore` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | The database provider this store fetches its items from. Read only. | | `collection` | `Collection` | The collection the query runs against. Read only. | | `query` | `Query>` | The query that selects the items tracked by this store. Read only. | | `optionalFirst` | `Item \| undefined` | The first item in this store, or `undefined` if the query has no items. Read only. | | `optionalLast` | `Item \| undefined` | The last item in this store, or `undefined` if the query has no items. Read only. | | `first` | `Item` | The first item in this store. Read only. | | `last` | `Item` | The last item in this store. Read only. | ## Examples ```ts const store = new QueryStore(collection, query, provider); for (const item of store) console.log(item); ``` --- # shelving/error - Kind: module - Import: `import { … } from "shelving/error";` Typed error classes for system and transport failures. ## Concepts Schema validation in shelving throws plain `string` values, not `Error` instances — form handlers consume those strings directly. The classes in this module are for a different layer: errors that represent infrastructure failures, bad requests, unexpected states, and programmer mistakes. All classes extend `BaseError`, which itself extends `Error` with two additions: - **Extra context fields.** Any key/value pairs passed in the options object (other than `cause` and `caller`) are attached directly to the error instance. Use `received` and `expected` as conventional field names when showing what went wrong. - **`caller` trimming.** `Error.captureStackTrace` is called with the public-facing function or class as the `caller` argument, so the stack trace starts at the callsite rather than inside an internal helper. Each subclass sets its own `caller` default, so you only need to supply `caller` explicitly when you are wrapping one of these classes inside your own public function. ```ts function requirePositive(n: number): number { if (n <= 0) throw new ValueError("Value must be positive", { caller: requirePositive, // stack starts here, not inside ValueError received: n, expected: "> 0", }); return n; } ``` ## Choosing the right class | Class | When to throw | |---|---| | `RequiredError` | Something required was absent (also thrown by `require*()` helpers) | | `ValueError` | A value was present but invalid in context (e.g. data from a database failed validation) | | `NetworkError` | Network-level failure — connection refused, server unreachable | | `RequestError` | The request was malformed or unacceptable (HTTP 4xx range; `RequestError.code` defaults to `400`) | | `ResponseError` | The received response indicated an error (HTTP 4xx/5xx; `ResponseError.code` defaults to `400`) | | `UnexpectedError` | Something that should never happen did — an invariant was violated | | `UnsupportedError` | A method or feature is not supported — by a backend, an environment, or an implementation | `RequestError` has named subclasses for common HTTP status codes: `UnauthorizedError` (401), `ForbiddenError` (403), `NotFoundError` (404), `MethodNotAllowedError` (405), and `UnprocessableError` (422). See each class's own page for focused usage examples. ## Usage ### Attaching context Any extra fields in the options bag land on the error instance and will appear in structured logs — this works the same way for every class in the module: ```ts throw new ValueError("Unexpected status", { received: status, expected: ["active", "inactive"], entity: "subscription", cause: originalError, }); ``` ### Catching by type Every class participates in `instanceof` checks, and subclasses match their base class — so a `catch` block can branch from specific to general: ```ts import { NotFoundError, ResponseError } from "shelving/error"; try { await loadDocument(id); } catch (err) { if (err instanceof NotFoundError) return null; if (err instanceof ResponseError) reportHttpError(err.code, err.message); throw err; } ``` ## Functions - [setBaseErrorOptions()](error/setBaseErrorOptions.md): Apply BaseErrorOptions to an error instance by copying contextual fields onto it and trimming its stack. ## Classes - [Errors](error/Errors.md): Aggregate an array of errors into a single combined error. - [NetworkError](error/NetworkError.md): Thrown in the event of network issues e.g. the user's internet connection is down, or the server is down. - [RequestError](error/RequestError.md): Throw when a request isn't well-formed or is unacceptable in some way. - [UnauthorizedError](error/UnauthorizedError.md): Throw if an operation failed because the user is not logged in, or the login information is not well-formed. - [NotFoundError](error/NotFoundError.md): Throw if the requested content is not found. - [UnprocessableError](error/UnprocessableError.md): Throw when a request is valid and well-formed, but its actual data is not. - [ForbiddenError](error/ForbiddenError.md): Throw if an operation failed because the user is logged in, but does not have sufficient privileges to access this content. - [MethodNotAllowedError](error/MethodNotAllowedError.md): Throw if a request uses an HTTP method that is not supported. - [RequiredError](error/RequiredError.md): Thrown when something is required but not supplied. - [ResponseError](error/ResponseError.md): Error thrown when a received HTTP response isn't OK. - [UnexpectedError](error/UnexpectedError.md): Thrown when something is wrong or unexpected. - [UnsupportedError](error/UnsupportedError.md): Error thrown when functionality is called but is not supported — by a backend, an environment, or an implementation. - [ValueError](error/ValueError.md): Thrown when a value is wrong or unexpected. ## Interfaces - [BaseErrorOptions](error/BaseErrorOptions.md): Options for BaseError that provide additional helpful error functionality. - [BaseError](error/BaseError.md): An Error that provides additional helpful functionality. --- # BaseErrorOptions - Kind: interface - Import: `import { BaseErrorOptions } from "shelving/error";` - Extends: `globalThis.ErrorOptions` Options for `BaseError` that provide additional helpful error functionality. - Extends the built-in `ErrorOptions` (so `cause` is supported) and adds a `caller` plus arbitrary contextual fields. ## Signatures ```ts { [key: string]: unknown; caller?: AnyCaller | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | --- # BaseError - Kind: interface - Import: `import { BaseError } from "shelving/error";` - Extends: `Error` An `Error` that provides additional helpful functionality. - Carries arbitrary named contextual data alongside the standard `Error` fields. - All concrete error classes in this module (`ValueError`, `RequiredError`, etc.) implement this shape. ## Signatures ```ts { readonly [key: string]: unknown; } ``` --- # setBaseErrorOptions() - Kind: function - Import: `import { setBaseErrorOptions } from "shelving/error";` Apply `BaseErrorOptions` to an error instance by copying contextual fields onto it and trimming its stack. - Copies every option except `cause` and `caller` onto the error as a named property. - Uses `Error.captureStackTrace()` with the resolved caller so the stack points at the user's call site, not the error plumbing. ## Signatures ```ts setBaseErrorOptions(defaultCaller: AnyCaller, error: BaseError, options: BaseErrorOptions): void ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `defaultCaller` | `AnyCaller` | Function to attribute the stack to when `options.caller` is not supplied. | | `error` | `BaseError` | The error instance to mutate with contextual data and a trimmed stack. | | `options` | `BaseErrorOptions` | | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Examples ```ts const error = new ValueError("Wrong"); setBaseErrorOptions(ValueError, error, { received: 123, caller: myFunction }); ``` --- # Errors - Kind: class - Import: `import { Errors } from "shelving/error";` - Extends: `AggregateError` - Implements: `BaseError` Aggregate an array of errors into a single combined error. - Extends (and shadows) the built-in `AggregateError` but with additional allowed options like `caller`. ## Signatures ```ts new Errors(errors: Iterable, message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `errors` | `Iterable` | Iterable of the individual errors being aggregated. | | `message` | `string` | Optional human-readable message describing the aggregate failure. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `Errors` | | ## Examples ```ts throw new Errors([new ValueError("Bad name"), new ValueError("Bad age")], "Validation failed"); ``` --- # NetworkError - Kind: class - Import: `import { NetworkError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Thrown in the event of network issues e.g. the user's internet connection is down, or the server is down. Thrown on a network-level failure — a connection refused, a server unreachable, a request that never completed. Use it for transport problems, as distinct from a response that arrived but indicated an error (`ResponseError`). ## Usage ```ts import { NetworkError } from "shelving/error"; async function fetchData(url: string): Promise { try { return await fetch(url); } catch { throw new NetworkError("Could not reach server"); } } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new NetworkError(message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the network problem. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `NetworkError` | | --- # RequestError - Kind: class - Import: `import { RequestError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Throw when a request isn't well-formed or is unacceptable in some way. - Carries an HTTP-style `code` in the `400-499` client-error range (defaults to `400`). - Base class for the specific request errors in this file (`UnauthorizedError`, `NotFoundError`, etc.). Thrown when an incoming request is malformed or unacceptable — the HTTP 4xx range. Its `.code` property defaults to `400`. `RequestError` has named subclasses for common HTTP status codes, each setting its own `.code`: `UnauthorizedError` (401), `ForbiddenError` (403), `NotFoundError` (404), `MethodNotAllowedError` (405), and `UnprocessableError` (422). Throw the most specific class that fits. ## Usage ```ts import { RequestError, NotFoundError } from "shelving/error"; function loadDocument(id: string): Document { if (!isValidId(id)) throw new RequestError("Malformed document ID", { received: id }); const doc = documents.get(id); if (!doc) throw new NotFoundError("Document not found", { received: id }); // .code === 404 return doc; } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new RequestError(message?: string, { code = 400, ...options }: RequestErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of why the request was rejected. Optional. | | `options` | `RequestErrorOptions` | Defaults to `{}`. | ## Returns | Type | Description | | --- | --- | | `RequestError` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `code` | `number` | HTTP status code for this error in the range `400-499`, defaults to `400`. Read only. | --- # UnauthorizedError - Kind: class - Import: `import { UnauthorizedError } from "shelving/error";` - Extends: `RequestError` Throw if an operation failed because the user is not logged in, or the login information is not well-formed. - Sets HTTP status `code` to `401`. ## Signatures ```ts new UnauthorizedError(message?: string, options?: RequestErrorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the authorization failure. Optional. | | `options` | `RequestErrorOptions` | Optional. | ## Returns | Type | Description | | --- | --- | | `UnauthorizedError` | | ## Examples ```ts throw new UnauthorizedError("Please log in"); ``` --- # NotFoundError - Kind: class - Import: `import { NotFoundError } from "shelving/error";` - Extends: `RequestError` Throw if the requested content is not found. - Sets HTTP status `code` to `404`. ## Signatures ```ts new NotFoundError(message?: string, options?: RequestErrorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of what was not found. Optional. | | `options` | `RequestErrorOptions` | Optional. | ## Returns | Type | Description | | --- | --- | | `NotFoundError` | | ## Examples ```ts throw new NotFoundError("User does not exist"); ``` --- # UnprocessableError - Kind: class - Import: `import { UnprocessableError } from "shelving/error";` - Extends: `RequestError` Throw when a request is valid and well-formed, but its actual data is not. - Sets HTTP status `code` to `422`. ## Signatures ```ts new UnprocessableError(message?: string, options?: RequestErrorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of why the data could not be processed. Optional. | | `options` | `RequestErrorOptions` | Optional. | ## Returns | Type | Description | | --- | --- | | `UnprocessableError` | | ## Examples ```ts throw new UnprocessableError("Email address is invalid"); ``` --- # ForbiddenError - Kind: class - Import: `import { ForbiddenError } from "shelving/error";` - Extends: `RequestError` Throw if an operation failed because the user is logged in, but does not have sufficient privileges to access this content. - Sets HTTP status `code` to `403`. ## Signatures ```ts new ForbiddenError(message?: string, options?: RequestErrorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the privilege failure. Optional. | | `options` | `RequestErrorOptions` | Optional. | ## Returns | Type | Description | | --- | --- | | `ForbiddenError` | | ## Examples ```ts throw new ForbiddenError("Admins only"); ``` --- # MethodNotAllowedError - Kind: class - Import: `import { MethodNotAllowedError } from "shelving/error";` - Extends: `RequestError` Throw if a request uses an HTTP method that is not supported. - Sets HTTP status `code` to `405`. ## Signatures ```ts new MethodNotAllowedError(message?: string, options?: RequestErrorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the unsupported method. Optional. | | `options` | `RequestErrorOptions` | Optional. | ## Returns | Type | Description | | --- | --- | | `MethodNotAllowedError` | | ## Examples ```ts throw new MethodNotAllowedError("Use POST not GET"); ``` --- # RequiredError - Kind: class - Import: `import { RequiredError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Thrown when something is required but not supplied. - Usually thrown from `requireX()` functions, e.g. `requireString()` and `requireNumber()`. Thrown when something required was absent — a missing argument, an empty lookup result, or an unset value. The `require*()` helpers — e.g. `requireString()` — throw this class. ## Usage ```ts import { RequiredError } from "shelving/error"; function getUser(id: string | undefined): User { if (!id) throw new RequiredError("User ID is required"); // ... } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new RequiredError(message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of what was required but missing. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `RequiredError` | | --- # ResponseError - Kind: class - Import: `import { ResponseError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Error thrown when a received HTTP response isn't OK. - Carries the response's HTTP status `code` (defaults to `400`). Thrown when a received response indicates an error — the HTTP 4xx/5xx range. Its `.code` property defaults to `400`. Use it for a response that arrived but was unsuccessful, as distinct from a transport failure (`NetworkError`). ## Usage ```ts import { ResponseError } from "shelving/error"; function checkResponse(res: Response): void { if (!res.ok) throw new ResponseError(res.statusText, { code: res.status }); } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new ResponseError(message?: string, { code = 400, ...options }: ResponseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of why the response was rejected. Optional. | | `options` | `ResponseErrorOptions` | Defaults to `{}`. | ## Returns | Type | Description | | --- | --- | | `ResponseError` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `code` | `number` | HTTP status code for the response. Read only. | --- # UnexpectedError - Kind: class - Import: `import { UnexpectedError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Thrown when something is wrong or unexpected. - Signals a logic or invariant failure that shouldn't normally happen, rather than a value or input problem. Thrown when something that should never happen did — an invariant was violated or an unreachable code path was reached. Reaching an `UnexpectedError` means there is a bug, not a bad input. ## Usage ```ts import { UnexpectedError } from "shelving/error"; function assertNever(x: never): never { throw new UnexpectedError("Unhandled case", { received: x }); } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new UnexpectedError(message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the unexpected condition. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `UnexpectedError` | | --- # UnsupportedError - Kind: class - Import: `import { UnsupportedError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Error thrown when functionality is called but is not supported — by a backend, an environment, or an implementation. Thrown when functionality is called but is not supported — a backend that can't run a type of operation (e.g. a key-value store asked to run a query), an environment missing a required capability (e.g. `localStorage` on the server), or an abstract method left for a subclass to override. ## Usage ```ts import { UnsupportedError } from "shelving/error"; class KVProvider { getQuery(): Promise { throw new UnsupportedError("KVProvider does not support querying items"); } } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new UnsupportedError(message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of the unsupported functionality. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `UnsupportedError` | | --- # ValueError - Kind: class - Import: `import { ValueError } from "shelving/error";` - Extends: `Error` - Implements: `BaseError` Thrown when a value is wrong or unexpected. - Use for invalid runtime values; pass `received` / `expected` via `options` for context. Thrown when a value was present but invalid in its context — for example data read from a database or an external source that failed validation. Pass the offending value as `received` so it appears in structured logs. ## Usage ```ts import { ValueError } from "shelving/error"; function parseConfig(raw: unknown): Config { if (!isConfig(raw)) throw new ValueError("Invalid config from server", { received: raw }); return raw; } ``` See `shelving/error` for shared behaviour — attaching context fields, `caller` trimming, and catching by type. ## Signatures ```ts new ValueError(message?: string, options: BaseErrorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `message` | `string` | Optional human-readable description of why the value is wrong. Optional. | | `options` | `BaseErrorOptions` | Defaults to `{}`. | | `.caller` | `AnyCaller \| undefined` | Modify the stack to trim off lines after a certain calling function. Optional. | ## Returns | Type | Description | | --- | --- | | `ValueError` | | --- # shelving/extract - Kind: module - Import: `import { … } from "shelving/extract";` Turn a folder of source files into a navigable tree of documentation. `extract` is the engine behind the Shelving documentation site itself. API references and hand-written guides usually live in separate tools. `extract` unifies them: it reads files and directories from disk and produces a single `TreeElement` tree describing every directory, file, and exported code symbol. A directory's `README.md` becomes that directory's intro page. A TypeScript file's exports become documented symbols. A `name.md` file sitting next to `name.ts` is merged onto the same page. One tree, one site, prose and source together. Pair the tree with the `shelving/ui` and you have a complete static documentation site — exactly how this site is built. ## Concepts ### Extractors An `Extractor` converts an input into a `TreeElement`. Extractors are composable — an outer extractor delegates to inner ones. | Extractor | Input | Output | |---|---|---| | `DirectoryExtractor` | a directory path | a `tree-element` node, recursing into subdirectories | | `FileExtractor` | a file | a `tree-element` node holding the raw text | | `MarkupExtractor` | a `.md` file | a `tree-element` node with `title` taken from the first `# heading` | | `TypescriptExtractor` | a `.ts` / `.tsx` file | a `tree-element` node whose children are the exported symbols | `DirectoryExtractor` is the entry point. It walks a directory, dispatches each file to a `FileExtractor` by extension, and recurses into subdirectories. ### The tree Every extractor produces a `TreeElement` (see `shelving/util/tree`). There are two element types: - `tree-element` — a directory or a file. A directory's content is absorbed from an index file; a file's children are its exported symbols (for TypeScript). Its `source` records the absolute path it came from. - `tree-documentation` — one documented symbol (function, class, type, constant), carrying the `signatures`, `params`, `returns`, `throws`, and `examples` parsed from its JSDoc. The tree is plain, JSON-serialisable data. The docs build writes it to `tree.json` so the browser can fetch it and hydrate. ### Index files A directory's index file — `README.md` by default — is *absorbed* into the directory element. Its title, description, and content become the directory's own. This is why writing a `README.md` is all it takes to give a directory an intro page. ### Same-key merging Files whose names slugify to the same key are merged into one element. `template.md` and `template.ts` both have the key `template`, so they become a single page: the Markdown supplies the prose, the TypeScript supplies the documented symbols. Markdown has the higher `priority`, so it wins on `title`. This is the pattern behind every `util/*.ts` file paired with a `name.md` guide — the hand-written page and the extracted API land together. ## Building a documentation site The pipeline, end to end. `docs/build.tsx` and `docs/render.tsx` in this repository are the working reference; `renderApp` in `docs/render.tsx` is the best-annotated example. ### 1. Extract the tree ```ts import { DirectoryExtractor } from "shelving/extract"; const root = await new DirectoryExtractor().extract("/path/to/modules"); ``` `root` is a `tree-element` node — the whole project as one tree. ### 2. Render with `` `` is the shell. Give it the tree and it produces a complete site — a sidebar menu, routing, and one page per element. ```tsx import { TreeApp } from "shelving/ui"; ``` Internally `` wires together: - `` — owns URL state and intercepts link clicks. - `` — `/` renders the root; `/{...path}` catches every deeper path. - `` — resolves the URL path to a tree element and renders it. `` dispatches on element type: `tree-element` renders as `` and `tree-documentation` as ``. ### 3. Build static pages `docs/build.tsx` shows the production build: extract the tree, write `tree.json`, bundle the browser and server scripts, then render every page to static HTML. `docs/render.tsx`'s `renderApp` enumerates every page from the canonical path keys of `flattenTree()` and writes one `index.html` per page. The browser later fetches `tree.json` and hydrates the same React tree the server rendered. ## Customising ### Controlling what gets indexed `DirectoryExtractor` accepts a `DirectoryExtractorOptions` object: ```ts new DirectoryExtractor({ extractors: { md: new MarkupExtractor() }, // file extension → extractor ignore: [/\.test\.tsx?$/i], // entries to skip base: "/abs/path", // base for resolving relative paths }); ``` (Index-file absorption is configured separately on `IndexExtractor` via its `index` option.) By default, test files, hidden files, underscore-prefixed files, and `node_modules` are skipped, and `.md`, `.ts`, `.tsx`, and `.txt` files are extracted. ### Overriding page renderers The tree components render through *mappings* — wrap a subtree to swap a renderer for one element type: - `` — the full-page component for an element type. - `` — the sidebar menu renderer. - `` — the card renderer used in directory listings. ```tsx ``` ## Functions - [mergeTreeElements()](extract/mergeTreeElements.md): Merge two tree elements — primary keeps its identity (type, key, source); secondary contributes any metadata primary does not already have. - [extractMarkdownProps()](extract/extractMarkdownProps.md): Parse a markdown source string once and derive its title and description in a single pass. ## Classes - [DirectoryExtractor](extract/DirectoryExtractor.md): Extractor that walks a directory on disk and produces a tree of tree-element nodes. - [Extractor](extract/Extractor.md): Base class for an extractor that converts an input of type I into a TreeElement output. - [FileExtractor](extract/FileExtractor.md): Base extractor for a file in a tree. - [IndexExtractor](extract/IndexExtractor.md): Through extractor that absorbs each element's index child into the element itself. - [MarkupExtractor](extract/MarkupExtractor.md): File extractor for Markdown files. - [MergingExtractor](extract/MergingExtractor.md): Through extractor that walks a tree of tree-element nodes and merges sibling tree elements whose keys match a merges template pair. - [ModuleExtractor](extract/ModuleExtractor.md): Extractor that builds a kind: "module" DocumentationElement from a source file or directory. - [PackageExtractor](extract/PackageExtractor.md): Extractor that reads a package.json and produces a flat tree of modules — one kind: "module" DocumentationElement per export entry, in declaration order. - [ThroughExtractor](extract/ThroughExtractor.md): Base class for an extractor that wraps another extractor. - [TypescriptExtractor](extract/TypescriptExtractor.md): File extractor that parses a TypeScript source file into a tree element. ## Interfaces - [DirectoryExtractorOptions](extract/DirectoryExtractorOptions.md): Options for a directory extractor. - [IndexExtractorOptions](extract/IndexExtractorOptions.md): Options for an IndexExtractor. - [MergingExtractorOptions](extract/MergingExtractorOptions.md): Options for a MergingExtractor. - [ModuleExtractorInput](extract/ModuleExtractorInput.md): Input for a ModuleExtractor. - [PackageExtractorOptions](extract/PackageExtractorOptions.md): Options for a PackageExtractor. --- # DirectoryExtractorOptions - Kind: interface - Import: `import { DirectoryExtractorOptions } from "shelving/extract";` Options for a directory extractor. ## Signatures ```ts { readonly extractors?: ImmutableDictionary; readonly base?: AbsolutePath; readonly ignore?: Matchables; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `extractors` | `ImmutableDictionary` | Extractor dispatch table keyed by file extension (without the leading dot, e.g. `"md"`). - Files with no matching extractor are silently skipped. - Defaults to `.md` (markdown), `.ts` / `.tsx` (TypeScript), and `.txt` (plain text). Read only. Optional. | | `base` | `AbsolutePath` | Absolute base path used to resolve relative paths passed to `extract()`. Read only. Optional. | | `ignore` | `Matchables` | Glob patterns for entries to skip — applied to both files and directories. - Defaults to test and spec files: `["*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx"]`. - Hidden entries (`.`-prefixed), underscore-prefixed entries, and `node_modules` are always skipped on top of these patterns. Read only. Optional. | --- # DirectoryExtractor - Kind: class - Import: `import { DirectoryExtractor } from "shelving/extract";` - Extends: `Extractor` Extractor that walks a directory on disk and produces a tree of `tree-element` nodes. - Recursively descends into subdirectories. - Sorts entries by name in natural order (`2-intro.md` comes before `10-advanced.md`), so the output is the same on every file system. - Dispatches non-ignored files to a matching `FileExtractor` based on extension; files with no matching extractor are silently skipped. - Keys on the produced elements are the verbatim filenames (e.g. `"string.ts"`, `"README.md"`) and directory names (e.g. `"util"`). - This is a pure walker: same-key merging and README absorption are intentionally *not* applied here — wrap with `MergingExtractor` and/or `IndexExtractor` to opt in to those behaviours. ## Signatures ```ts new DirectoryExtractor({ extractors = DEFAULT_EXTRACTORS, base, ignore = DEFAULT_IGNORE }: DirectoryExtractorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `DirectoryExtractorOptions` | Defaults to `{}`. | | `.extractors` | `ImmutableDictionary` | Extractor dispatch table keyed by file extension (without the leading dot, e.g. `"md"`). - Files with no matching extractor are silently skipped. - Defaults to `.md` (markdown), `.ts` / `.tsx` (TypeScript), and `.txt` (plain text). Read only. Optional. | | `.base` | `AbsolutePath` | Absolute base path used to resolve relative paths passed to `extract()`. Read only. Optional. | | `.ignore` | `Matchables` | Glob patterns for entries to skip — applied to both files and directories. - Defaults to test and spec files: `["*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx"]`. - Hidden entries (`.`-prefixed), underscore-prefixed entries, and `node_modules` are always skipped on top of these patterns. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `DirectoryExtractor` | | ## Examples ```ts const tree = await new DirectoryExtractor().extract("modules/util"); ``` --- # Extractor - Kind: class - Import: `import { Extractor } from "shelving/extract";` Base class for an extractor that converts an input of type `I` into a `TreeElement` output. - Extractors are composable: outer extractors delegate to inner extractors. - The output type is always a `TreeElement` (or a more specific subtype). ## Signatures ```ts new Extractor() ``` ## Returns | Type | Description | | --- | --- | | `Extractor` | | ## Examples ```ts class JSONExtractor extends Extractor { extract(input: string): TreeElement { return JSON.parse(input); } } ``` ## Methods - [Extractor.extract()](extract/Extractor/extract.md): Extract a tree element from the given input. --- # Extractor.extract() - Kind: method - Member of: `Extractor` Extract a tree element from the given input. ## Signatures ```ts extract(input: I): O | Promise ``` --- # mergeTreeElements() - Kind: function - Import: `import { mergeTreeElements } from "shelving/extract";` Merge two tree elements — `primary` keeps its identity (`type`, `key`, `source`); `secondary` contributes any metadata `primary` does not already have. - `title` and `description` are taken from `primary` when set, otherwise from `secondary` — primary stays canonical but a missing field falls back rather than disappearing. - `content` and `children` from both are concatenated (primary first). ## Signatures ```ts mergeTreeElements(primary: T, secondary: TreeElement): T ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `primary` | `T` | The element whose identity is preserved and whose set fields win. | | `secondary` | `TreeElement` | The element whose metadata fills any gaps in `primary`. | | `.key` | `string` | Read only. | | `.type` | `` `tree-${string}` `` | Read only. | ## Returns | Type | Description | | --- | --- | | `T` | A new `TreeElement` with `primary`'s identity and the merged metadata of both. | ## Examples ```ts mergeTreeElements(tsElement, mdElement) // ts identity + md prose ``` --- # FileExtractor - Kind: class - Import: `import { FileExtractor } from "shelving/extract";` - Extends: `Extractor` Base extractor for a file in a tree. - Reads the file's content as text and stores it in `content`. - Sets `source` to the file's absolute path (`BunFile.name`); throws `RequiredError` if missing or non-absolute. - Sets `name` to the basename without extension, preserving case (e.g. `"OptionalSchema"` from `"OptionalSchema.ts"`); URL paths use `name`. - Sets `key` to the verbatim filename including extension (e.g. `"OptionalSchema.ts"`). Keys are unique within a directory and used by `MergingExtractor` to pair siblings (`{base}.md` + `{base}.ts`) and by `PackageExtractor` to look up sources. - Does NOT set `title` — `title` is only set by subclasses that have a confident source for one (e.g. `MarkupExtractor` uses the first `

`). Renderers fall back to `name` when missing. - Subclasses (e.g. `MarkupExtractor`, `TypescriptExtractor`) override `extractProps()` to parse the content into richer elements. ## Signatures ```ts new FileExtractor() ``` ## Returns | Type | Description | | --- | --- | | `FileExtractor` | | ## Examples ```ts new FileExtractor().extract(Bun.file("/abs/path/notes.txt")) ``` ## Methods - [FileExtractor.extract()](extract/FileExtractor/extract.md): Read a file and build a tree-element from its text content. - [FileExtractor.extractProps()](extract/FileExtractor/extractProps.md): Build the file element props from the extracted content. --- # FileExtractor.extract() - Kind: method - Member of: `FileExtractor` Read a file and build a `tree-element` from its text content. ## Signatures ```ts extract(file: BunFile): Promise ``` --- # FileExtractor.extractProps() - Kind: method - Member of: `FileExtractor` Build the file element props from the extracted content. - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus, cards, and URL paths. - Override to parse `text` into richer elements (content/children/description) and to set `title` if a confident title is available. Overrides may be async (e.g. `TypescriptExtractor` parses via the native compiler server) — `extract()` awaits the result either way. ## Signatures ```ts extractProps(name: string, content: string): (Partial & { name: string }) | Promise & { name: string }> ``` --- # IndexExtractorOptions - Kind: interface - Import: `import { IndexExtractorOptions } from "shelving/extract";` Options for an `IndexExtractor`. ## Signatures ```ts { readonly index?: Matchables; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `index` | `Matchables` | Filename patterns treated as a parent's index. Matched case-insensitively against each child element's `key`. - Defaults to README and index files: `["readme.txt", "readme.md", "index.md", "index.ts", "index.tsx"]`. Read only. Optional. | --- # IndexExtractor - Kind: class - Import: `import { IndexExtractor } from "shelving/extract";` - Extends: `ThroughExtractor` Through extractor that absorbs each element's index child into the element itself. - For every element with children: finds the first child whose `key` matches any `index` pattern. - The matched child's `title`, `description`, `content`, and `children` are folded into the parent. - The matched child is removed from the parent's children list. - Purely name-based: it doesn't care whether an element is a directory or a file — any element with children is processed, deepest level first. ## Signatures ```ts new IndexExtractor(source: Extractor, { index = DEFAULT_INDEX }: IndexExtractorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `Extractor` | | | `options` | `IndexExtractorOptions` | Defaults to `{}`. | | `.index` | `Matchables` | Filename patterns treated as a parent's index. Matched case-insensitively against each child element's `key`. - Defaults to README and index files: `["readme.txt", "readme.md", "index.md", "index.ts", "index.tsx"]`. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `IndexExtractor` | | ## Examples ```ts const extractor = new IndexExtractor(new DirectoryExtractor()); ``` --- # MarkupExtractor - Kind: class - Import: `import { MarkupExtractor } from "shelving/extract";` - Extends: `FileExtractor` File extractor for Markdown files. - Stores the markdown text as `content`; rendering happens at output time via ``. - Sets `title` from the first `# h1` heading if one is present — otherwise leaves it undefined (a confident title only). - When a `title` is found, strips the leading `# h1` from `content` so renderers (which show `title` separately) don't display the heading twice. - Sets `description` to the first prose paragraph as a plain-text summary (used for card listings and ``). ## Signatures ```ts new MarkupExtractor() ``` ## Returns | Type | Description | | --- | --- | | `MarkupExtractor` | | ## Examples ```ts new MarkupExtractor().extract(Bun.file("/abs/path/README.md")) ``` --- # extractMarkdownProps() - Kind: function - Import: `import { extractMarkdownProps } from "shelving/extract";` Parse a markdown source string once and derive its `title` and `description` in a single pass. - `title` — plain text of the first `# h1` heading, or `undefined` if there is none. - `description` — plain-text summary of the first prose paragraph, or `undefined` if there is none. - Both query the parsed markup tree, so inline syntax (`` `code` ``, `*emphasis*`, links) resolves to clean plain text. ## Signatures ```ts extractMarkdownProps(text: string): { title: string | undefined; description: string | undefined } ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `text` | `string` | The markdown source string (a markdown file's text, or a JSDoc description). | ## Returns | Type | Description | | --- | --- | | `{ title: string \| undefined; description: string \| undefined }` | An object whose `title` and `description` are each a plain string, or `undefined` when absent. | ## Examples ```ts extractMarkdownProps("# Title\n\nSome intro.") // { title: "Title", description: "Some intro." } ``` --- # MergingExtractorOptions - Kind: interface - Import: `import { MergingExtractorOptions } from "shelving/extract";` Options for a `MergingExtractor`. ## Signatures ```ts { readonly merges?: ImmutableDictionary; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `merges` | `ImmutableDictionary` | Templated key pairs that should merge. Each key is a `{base}` template matched against the secondary element's key; each value is an ordered list of `{base}` templates for the primary candidates to merge into. - The first primary candidate that exists in the same directory wins; remaining candidates are ignored. - If no candidate exists the secondary is left in place as its own tree element. - Defaults to `{ "{base}.md": ["{base}.ts", "{base}.tsx", "{base}.js", "{base}.jsx"] }`. Read only. Optional. | --- # MergingExtractor - Kind: class - Import: `import { MergingExtractor } from "shelving/extract";` - Extends: `ThroughExtractor` Through extractor that walks a tree of `tree-element` nodes and merges sibling tree elements whose keys match a `merges` template pair. - Purely key-based: it doesn't care whether siblings are directories or files — any element with children is processed, at every level. - **Token-first:** a secondary (e.g. `Card.md`) is preferentially folded into the same-named documentation token declared by a sibling file (the `Card` symbol inside `Card.tsx`), not the file element. Exported names are unique across the package, so this is unambiguous — and it lands the prose on the published symbol page, which survives the module flatten (file-element content does not). - **File fallback:** when no same-named token exists, the secondary folds into a sibling *file* whose key matches a `merges` candidate (whole-file prose like `template.md` → `template.ts`). - The primary (winning) element keeps its `key`, `source`, and `type`; the secondary's `title`, `description`, `content`, and `children` are folded in via `mergeTreeElements()`. - A secondary with no matching token or file is left in place — pure prose files (e.g. `concepts.md` with no `concepts.ts`) stand alone. ## Signatures ```ts new MergingExtractor(source: Extractor, { merges = DEFAULT_MERGES }: MergingExtractorOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `Extractor` | | | `options` | `MergingExtractorOptions` | Defaults to `{}`. | | `.merges` | `ImmutableDictionary` | Templated key pairs that should merge. Each key is a `{base}` template matched against the secondary element's key; each value is an ordered list of `{base}` templates for the primary candidates to merge into. - The first primary candidate that exists in the same directory wins; remaining candidates are ignored. - If no candidate exists the secondary is left in place as its own tree element. - Defaults to `{ "{base}.md": ["{base}.ts", "{base}.tsx", "{base}.js", "{base}.jsx"] }`. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `MergingExtractor` | | ## Examples ```ts const extractor = new MergingExtractor(new DirectoryExtractor()); ``` --- # ModuleExtractorInput - Kind: interface - Import: `import { ModuleExtractorInput } from "shelving/extract";` Input for a `ModuleExtractor`. ## Signatures ```ts { readonly name: string; readonly source: TreeElement; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `name` | `string` | Display name for the module, derived from the package.json export key (e.g. `"util/string"`, `"firebase"`). Read only. | | `source` | `TreeElement` | The source element this module is built from. - A file-backed `tree-element` — the module is backed by a single source file (with its `.md` sibling already merged in by `MergingExtractor`). - A directory-backed `tree-element` — the module is backed by a directory; its absorbed index file provides the content. Read only. | --- # ModuleExtractor - Kind: class - Import: `import { ModuleExtractor } from "shelving/extract";` - Extends: `Extractor` Extractor that builds a `kind: "module"` `DocumentationElement` from a source file or directory. - The module's `content`, `description`, and `title` are taken from the source element (`MergingExtractor` and `IndexExtractor` are expected to have run upstream so `.md` siblings and `README.md` are already folded in). - The module's `children` are every `tree-documentation` element found by deep-walking the source — flattened across files and subdirectories, but never descending into a `tree-documentation`'s own members. ## Signatures ```ts new ModuleExtractor() ``` ## Returns | Type | Description | | --- | --- | | `ModuleExtractor` | | ## Examples ```ts const module = new ModuleExtractor().extract({ name: "util/string", source }); ``` --- # PackageExtractorOptions - Kind: interface - Import: `import { PackageExtractorOptions } from "shelving/extract";` Options for a `PackageExtractor`. ## Signatures ```ts { readonly tree: TreeElement; readonly extensions?: ImmutableDictionary; readonly module?: ModuleExtractor; readonly base?: AbsolutePath; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `tree` | `TreeElement` | Pre-extracted source tree the `package.json` exports resolve against — typically the result of `IndexExtractor(MergingExtractor(DirectoryExtractor()))` over the source root. Read only. | | `extensions` | `ImmutableDictionary` | Maps an export *target* file extension (the right-hand side of an `exports` entry) to the *source* extensions to look for, in order. - Bridges built output and source: `package.json` targets a `.js` file but the tree holds the `.ts` source. - The first source extension that exists in the tree wins; a target extension with no mapping falls back to itself. - Defaults to `{ js: ["ts", "tsx", "js", "jsx"] }`. Read only. Optional. | | `module` | `ModuleExtractor` | `ModuleExtractor` used to build each module element. Defaults to a fresh `new ModuleExtractor()`. Read only. Optional. | | `base` | `AbsolutePath` | Absolute base path used to resolve a relative `package.json` path passed to `extract()`. Read only. Optional. | --- # PackageExtractor - Kind: class - Import: `import { PackageExtractor } from "shelving/extract";` - Extends: `Extractor` Extractor that reads a `package.json` and produces a flat tree of modules — one `kind: "module"` `DocumentationElement` per export entry, in declaration order. - Static export keys (e.g. `"./api"`, `"./firebase"`) become one module each. - Wildcard export keys (e.g. `"./util/*"`) expand against the source tree — one module per matching child file or subdirectory. - Each export's *target* extension (e.g. the `.js` in `"./util/*.js"`) is mapped to source extensions via `extensions`, so built `.js` paths resolve to their `.ts` sources. - Each module's `title` is prefixed with the package `name` (e.g. `ui` → `shelving/ui`) so listings read as package subpaths. - The `"."` root export is skipped — its content is the root tree element itself. - Throws if a static export key has no matching source element in the tree. ## Signatures ```ts new PackageExtractor({ tree, extensions = DEFAULT_EXTENSIONS, module = new ModuleExtractor(), base }: PackageExtractorOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `PackageExtractorOptions` | | | `.tree` | `TreeElement` | Pre-extracted source tree the `package.json` exports resolve against — typically the result of `IndexExtractor(MergingExtractor(DirectoryExtractor()))` over the source root. Read only. | | `.extensions` | `ImmutableDictionary` | Maps an export *target* file extension (the right-hand side of an `exports` entry) to the *source* extensions to look for, in order. - Bridges built output and source: `package.json` targets a `.js` file but the tree holds the `.ts` source. - The first source extension that exists in the tree wins; a target extension with no mapping falls back to itself. - Defaults to `{ js: ["ts", "tsx", "js", "jsx"] }`. Read only. Optional. | | `.module` | `ModuleExtractor` | `ModuleExtractor` used to build each module element. Defaults to a fresh `new ModuleExtractor()`. Read only. Optional. | | `.base` | `AbsolutePath` | Absolute base path used to resolve a relative `package.json` path passed to `extract()`. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `PackageExtractor` | | ## Examples ```ts const tree = await new PackageExtractor({ tree: sourceTree }).extract("package.json"); ``` --- # ThroughExtractor - Kind: class - Import: `import { ThroughExtractor } from "shelving/extract";` - Extends: `Extractor` Base class for an extractor that wraps another extractor. - Subclasses delegate to `source` and (optionally) transform the input before or the output after. - Composes the same way `Through*Provider` chains do — chain multiple `ThroughExtractor`s to build up behaviour. ## Signatures ```ts new ThroughExtractor(source: Extractor) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `Extractor` | | ## Returns | Type | Description | | --- | --- | | `ThroughExtractor` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `source` | `Extractor` | The wrapped source extractor this through extractor delegates to. Read only. | ## Examples ```ts new MergingExtractor(new DirectoryExtractor()) // a ThroughExtractor wrapping a source extractor ``` ## Methods - [ThroughExtractor.extract()](extract/ThroughExtractor/extract.md): Extract from the input by forwarding to source.extract(). --- # ThroughExtractor.extract() - Kind: method - Member of: `ThroughExtractor` Extract from the input by forwarding to `source.extract()`. - Default implementation is a pass-through; subclasses override to do work before or after. ## Signatures ```ts extract(input: I): O | Promise ``` --- # TypescriptExtractor - Kind: class - Import: `import { TypescriptExtractor } from "shelving/extract";` - Extends: `FileExtractor` File extractor that parses a TypeScript source file into a tree element. - Uses the native TypeScript compiler's API (`typescript/unstable/async` — TypeScript 7+) to parse the AST. - Parsing runs on a shared native compiler server, spawned on demand into a virtual filesystem and closed again after a short idle — callers never manage its lifecycle. - Extracts exported, public, non-`_`-prefixed declarations as `tree-documentation` children. - Overloaded declarations sharing a name are merged into a single `tree-documentation` with multiple `signatures`. When a function (or constructor) is overloaded, the implementation declaration (the "base definition") is dropped entirely — only the overload signatures are documented. - A `@param name {Type}` (or `@returns {Type}`) whose `{Type}` is given is canonical: it supersedes the inferred type from the base definition and any overloads. Multiple `@param name` tags for one parameter each emit a row, letting a single parameter be documented as several typed variants. - Class declarations synthesise their `signatures`, `params`, and `returns` from the constructor — `new ClassName<…>(…)` including generics, one signature per constructor overload, with `returns` set to the class type. Param descriptions come from the constructor's `@param` first, then the class's `@param`. - A `@kind` tag in a symbol's JSDoc overrides the inferred kind — e.g. `@kind component` relabels a React component (otherwise a `function`) so the docs site groups and colours it as a component. The override also drops the trailing `()` from the title, since a non-function kind reads as a bare name. - Sets `description` (a plain-text summary from the first JSDoc paragraph) on every `tree-documentation` child. - Sets `title` on every `tree-documentation` child — `name()` for functions, `Class.name()` for methods, bare `name` for other kinds. (Data members are not child elements — see `properties` below.) - Records relational metadata as raw strings for render-time linking: `class` (owning class), `readonly`, `extends` / `implements` (full heritage type text including generic arguments, e.g. `AbstractStore` or `Omit`), and `types` (the type names a `type` alias's body references, e.g. `OtherType` in `string | OtherType`). - Records a structured `properties` list for classes, interfaces, and object-literal `type` aliases — each data member's (property/getter/setter) name, type, optionality, default, and description. This is the single source for a type's data members: they render as the type's Properties table and flatten into an options-bag parameter's fields at render time, rather than each becoming its own child element. Methods stay as child elements. - Names a destructured (binding-pattern) parameter for the Param column — which has no name of its own — from an explicit `@param`, else its rest element (`...options`), else a type-derived fallback (`props` / `options`). The signature still shows the full `{ … }`. - Pretty-prints object-literal signatures (interfaces and object-literal `type` aliases) as multi-line `{ … }` blocks, one member per line; other type bodies (`string | null`, mapped types, …) are emitted verbatim. - Members declared with the `override` or `declare` modifier are skipped — the base class already documents overrides, and `declare` members are ambient type-only re-declarations rather than new API. - Keys are the raw declared `name` (case-preserving) so case-distinct exports like `Collection` and `COLLECTION` stay separate. - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`. ## Signatures ```ts new TypescriptExtractor() ``` ## Returns | Type | Description | | --- | --- | | `TypescriptExtractor` | | ## Examples ```ts const element = await new TypescriptExtractor().extractProps("string.ts", sourceText); ``` --- # shelving/firebase - Kind: module - Import: `import { … } from "shelving/firebase";` `FirestoreProvider` — a Cloud Firestore `DBProvider` that talks directly to the [Firestore REST API](https://docs.cloud.google.com/firestore/docs/reference/rest) using `fetch`. - **Zero dependencies.** No `firebase` or `@google-cloud/firestore` SDK, no gRPC — it runs anywhere `fetch` runs: Node.js, Bun, Cloudflare Workers, Deno, and other edge runtimes. - **Transactions.** `DBProvider.transact()` is fully supported: reads (including queries and counts) see one consistent snapshot, writes buffer and commit atomically, and contended commits retry automatically. - **No realtime.** `DBProvider.getItemSequence()` and `DBProvider.getQuerySequence()` throw `UnsupportedError` — the `:listen` endpoint needs a streaming session the plain REST API can't provide. ## Usage ```ts import { FirestoreProvider } from "shelving/firebase"; const provider = new FirestoreProvider({ project: "my-project", token: () => auth.getAccessToken(), // e.g. from `google-auth-library` }); const id = await provider.addItem(POSTS, { title: "Hello", body: "First post.", published: false }); await provider.transact(async db => { const post = await db.requireItem(POSTS, id); await db.updateItem(POSTS, id, { published: true, "+=views": 1 }); }); ``` ### Options - `project` — Google Cloud project id (required). - `database` — Firestore database id (defaults to `"(default)"`). - `token` — callback returning an OAuth2 access token for each request. Use `google-auth-library`, or any code that can mint a token for the `https://www.googleapis.com/auth/datastore` scope. Omit for the emulator. - `host` — base URL of the Firestore API (defaults to `https://firestore.googleapis.com`). Point it at the emulator in tests. - `fetch` — custom fetch implementation (defaults to the global `fetch`). ### With the Firestore emulator ```ts const provider = new FirestoreProvider({ project: "demo-project", host: `http://${process.env.FIRESTORE_EMULATOR_HOST}`, }); ``` ## Behaviour notes - **Ids**: `DBProvider.addItem()` generates a random 20-character id client-side and commits with an `exists: false` precondition, so it fails rather than overwriting on the (vanishingly unlikely) collision. - **Updates**: `DBProvider.updateItem()` uses an update mask plus field transforms — `+=` maps to `increment`, `+[]` to `appendMissingElements`, `-[]` to `removeAllFromArray` — and fails if the item does not exist. - **Query writes**: `setQuery()` / `updateQuery()` / `deleteQuery()` read the matching document names (a `__name__`-only query) then commit writes in batches of 500. - **Values**: safe integers store as Firestore integers, other finite numbers as doubles; integers beyond `Number.MAX_SAFE_INTEGER` lose precision when read back. Foreign types written by other clients (timestamps, references, bytes) read back as their string form. Data read from Firestore is unvalidated — wrap the provider in `ValidationDBProvider` to guarantee types. - **Transactions**: retried up to 5 times with jittered exponential backoff on contention (`ABORTED`), so `transact()` callbacks must have no side effects other than through their provider. Firestore limits a transaction to 270 seconds with a 60-second idle timeout. ## Testing Unit tests (value codec and request protocol) run offline in the normal test suite. The universal `DBProvider` contract suite runs against the real Firestore emulator via its own command (excluded from `bun run test`; run in CI on every PR and release): ```sh bun run firebase ``` This wraps `bun test ./modules/firebase` in `firebase emulators:exec`, which starts the emulator (requires Java 21+), sets `FIRESTORE_EMULATOR_HOST`, and shuts it down afterwards. Without that env var the emulator-backed tests don't register, so the offline suite stays green. `firebase.json` pins the emulator to `127.0.0.1` (not `localhost`, whose IPv6-first resolution depends on client fallback behaviour) and to port `8981` rather than the default `8080`. The port matters on macOS: when Screen Time's "Web content" filtering is enabled, Apple's `webfilterproxyd` transparently proxies loopback connections on well-known HTTP ports (including `8080`). The emulator's REST front-end opens an internal loopback connection back to its own port for every data request; the filter captures that connection and cannot relay it, so every read and write hangs forever (or resets, e.g. the historical `ECONNRESET` from `fetch` on `localhost:8080`). Ports outside the filter's watch list, like `8981`, are untouched. ## Functions - [toFirestoreValue()](firebase/toFirestoreValue.md): Convert a data value to its Firestore REST API JSON representation. - [toFirestoreFields()](firebase/toFirestoreFields.md): Convert a data object to Firestore REST API document fields. - [toDataValue()](firebase/toDataValue.md): Convert a Firestore REST API JSON value back to a data value. - [toData()](firebase/toData.md): Convert Firestore REST API document fields back to a data object. ## Classes - [FirestoreProvider](firebase/FirestoreProvider.md): Cloud Firestore database provider that talks to the Firestore REST API using fetch, implementing the DBProvider abstraction. ## Interfaces - [FirestoreProviderOptions](firebase/FirestoreProviderOptions.md): Options for FirestoreProvider. ## Types - [FirestoreValue](firebase/FirestoreValue.md): JSON representation of a single value in the Firestore REST API. - [FirestoreFields](firebase/FirestoreFields.md): JSON representation of the fields of a Firestore document in the Firestore REST API. --- # FirestoreProviderOptions - Kind: interface - Import: `import { FirestoreProviderOptions } from "shelving/firebase";` Options for `FirestoreProvider`. ## Signatures ```ts { readonly project: string; readonly database?: string | undefined; readonly host?: string | undefined; readonly token?: (() => string | PromiseLike) | undefined; readonly fetch?: ((input: string, init: RequestInit) => Promise) | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `project` | `string` | Google Cloud project id. Read only. | | `database` | `string \| undefined` | Firestore database id. Read only. Defaults to `"(default)"`. | | `host` | `string \| undefined` | Base URL of the Firestore API, e.g. `"http://127.0.0.1:8080"` for the Firestore emulator. Read only. Defaults to `"https://firestore.googleapis.com"`. | | `token` | `(() => string \| PromiseLike) \| undefined` | Return an OAuth2 access token for each request, e.g. from `google-auth-library` or a hand-rolled service-account JWT exchange. - Omit to send no `Authorization` header (correct for the Firestore emulator). Read only. Optional. | | `fetch` | `((input: string, init: RequestInit) => Promise) \| undefined` | Fetch implementation to use for requests (defaults to the global `fetch`). Read only. Optional. | --- # FirestoreProvider - Kind: class - Import: `import { FirestoreProvider } from "shelving/firebase";` - Extends: `DBProvider` Cloud Firestore database provider that talks to the Firestore REST API using `fetch`, implementing the `DBProvider` abstraction. - Zero dependencies, so it runs anywhere `fetch` runs: Node.js, Bun, Cloudflare Workers, Deno, and other edge runtimes. - Supports transactions via `transact()` — reads see a consistent snapshot, writes buffer and commit atomically, and contended commits retry. - Does not support realtime subscriptions: `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError` (the `:listen` endpoint requires a streaming session the plain REST API cannot provide). - Data read from Firestore is unvalidated — wrap in `ValidationDBProvider` to guarantee types. ## Signatures ```ts new FirestoreProvider(options: FirestoreProviderOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `FirestoreProviderOptions` | | | `.project` | `string` | Google Cloud project id. Read only. | | `.database` | `string \| undefined` | Firestore database id. Read only. Defaults to `"(default)"`. | | `.host` | `string \| undefined` | Base URL of the Firestore API, e.g. `"http://127.0.0.1:8080"` for the Firestore emulator. Read only. Defaults to `"https://firestore.googleapis.com"`. | | `.token` | `(() => string \| PromiseLike) \| undefined` | Return an OAuth2 access token for each request, e.g. from `google-auth-library` or a hand-rolled service-account JWT exchange. - Omit to send no `Authorization` header (correct for the Firestore emulator). Read only. Optional. | | `.fetch` | `((input: string, init: RequestInit) => Promise) \| undefined` | Fetch implementation to use for requests (defaults to the global `fetch`). Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `FirestoreProvider` | | ## Examples ```ts const provider = new FirestoreProvider({ project: "my-project", token: () => auth.getAccessToken() }); const id = await provider.addItem(users, { name: "Dave" }); ``` --- # FirestoreValue - Kind: type - Import: `import { FirestoreValue } from "shelving/firebase";` JSON representation of a single value in the Firestore REST API. ## Signatures ```ts { readonly nullValue?: null; readonly booleanValue?: boolean; readonly integerValue?: string | number; readonly doubleValue?: string | number; readonly stringValue?: string; readonly timestampValue?: string; readonly bytesValue?: string; readonly referenceValue?: string; readonly geoPointValue?: { readonly latitude?: number; readonly longitude?: number }; readonly arrayValue?: { readonly values?: ImmutableArray }; readonly mapValue?: { readonly fields?: FirestoreFields }; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `nullValue` | `null` | Read only. Optional. | | `booleanValue` | `boolean` | Read only. Optional. | | `integerValue` | `string \| number` | Read only. Optional. | | `doubleValue` | `string \| number` | Read only. Optional. | | `stringValue` | `string` | Read only. Optional. | | `timestampValue` | `string` | Read only. Optional. | | `bytesValue` | `string` | Read only. Optional. | | `referenceValue` | `string` | Read only. Optional. | | `geoPointValue` | `{ readonly latitude?: number; readonly longitude?: number }` | Read only. Optional. | | `arrayValue` | `{ readonly values?: ImmutableArray }` | Read only. Optional. | | `mapValue` | `{ readonly fields?: FirestoreFields }` | Read only. Optional. | ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. - [FirestoreValue](firebase/FirestoreValue.md): JSON representation of a single value in the Firestore REST API. - [FirestoreFields](firebase/FirestoreFields.md): JSON representation of the fields of a Firestore document in the Firestore REST API. --- # FirestoreFields - Kind: type - Import: `import { FirestoreFields } from "shelving/firebase";` JSON representation of the fields of a Firestore document in the Firestore REST API. ## Signatures ```ts { readonly [key: string]: FirestoreValue; } ``` ## Types - [FirestoreValue](firebase/FirestoreValue.md): JSON representation of a single value in the Firestore REST API. --- # toFirestoreValue() - Kind: function - Import: `import { toFirestoreValue } from "shelving/firebase";` Convert a data value to its Firestore REST API JSON representation. - Safe integers become `integerValue` (which the REST API encodes as a string), other finite numbers become `doubleValue`. - Arrays and plain objects convert recursively. ## Signatures ```ts toFirestoreValue(value: unknown, caller: AnyCaller = toFirestoreValue): FirestoreValue ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `value` | `unknown` | The value to convert (null, boolean, string, finite number, array, or plain object). | | `caller` | `AnyCaller` | Caller function used to attribute thrown errors. Defaults to `toFirestoreValue`. | ## Returns | Type | Description | | --- | --- | | `FirestoreValue` | The Firestore JSON value. | ## Throws | Type | Description | | --- | --- | | `unknown` | `ValueError` if the value cannot be represented (e.g. `undefined`, functions, non-finite numbers, class instances). | ## Examples ```ts toFirestoreValue(123) // { integerValue: "123" } ``` --- # toFirestoreFields() - Kind: function - Import: `import { toFirestoreFields } from "shelving/firebase";` Convert a data object to Firestore REST API document fields. - Props with `undefined` value are skipped (matching JSON serialisation). ## Signatures ```ts toFirestoreFields(data: Data, caller: AnyCaller = toFirestoreFields): FirestoreFields ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `data` | `Data` | The data object to convert. | | `caller` | `AnyCaller` | Caller function used to attribute thrown errors. Defaults to `toFirestoreFields`. | ## Returns | Type | Description | | --- | --- | | `FirestoreFields` | The Firestore fields object. | ## Throws | Type | Description | | --- | --- | | `unknown` | `ValueError` if any prop value cannot be represented. | ## Examples ```ts toFirestoreFields({ num: 123 }) // { num: { integerValue: "123" } } ``` --- # toDataValue() - Kind: function - Import: `import { toDataValue } from "shelving/firebase";` Convert a Firestore REST API JSON value back to a data value. - `integerValue` parses to a `number`, so integers beyond `Number.MAX_SAFE_INTEGER` lose precision. - `timestampValue`, `bytesValue`, and `referenceValue` pass through as their string form; `geoPointValue` becomes a plain `{ latitude, longitude }` object — wrap the provider in `ValidationDBProvider` to reject types your schemas don't allow. ## Signatures ```ts toDataValue(value: FirestoreValue, caller: AnyCaller = toDataValue): unknown ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `value` | `FirestoreValue` | The Firestore JSON value to convert. | | `.nullValue` | `null` | Read only. Optional. | | `.booleanValue` | `boolean` | Read only. Optional. | | `.integerValue` | `string \| number` | Read only. Optional. | | `.doubleValue` | `string \| number` | Read only. Optional. | | `.stringValue` | `string` | Read only. Optional. | | `.timestampValue` | `string` | Read only. Optional. | | `.bytesValue` | `string` | Read only. Optional. | | `.referenceValue` | `string` | Read only. Optional. | | `.geoPointValue` | `{ readonly latitude?: number; readonly longitude?: number }` | Read only. Optional. | | `.arrayValue` | `{ readonly values?: ImmutableArray }` | Read only. Optional. | | `.mapValue` | `{ readonly fields?: FirestoreFields }` | Read only. Optional. | | `caller` | `AnyCaller` | Caller function used to attribute thrown errors. Defaults to `toDataValue`. | ## Returns | Type | Description | | --- | --- | | `unknown` | The plain data value. | ## Throws | Type | Description | | --- | --- | | `unknown` | `ValueError` if the value has no recognised type. | ## Examples ```ts toDataValue({ integerValue: "123" }) // 123 ``` --- # toData() - Kind: function - Import: `import { toData } from "shelving/firebase";` Convert Firestore REST API document fields back to a data object. ## Signatures ```ts toData(fields: FirestoreFields | undefined, caller: AnyCaller = toData): Data ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `fields` | `FirestoreFields \| undefined` | The Firestore fields object to convert (missing means an empty document). | | `caller` | `AnyCaller` | Caller function used to attribute thrown errors. Defaults to `toData`. | ## Returns | Type | Description | | --- | --- | | `Data` | The plain data object. | ## Throws | Type | Description | | --- | --- | | `unknown` | `ValueError` if any field value has no recognised type. | ## Examples ```ts toData({ num: { integerValue: "123" } }) // { num: 123 } ``` --- # shelving/icon/AddIcon - Kind: module - Import: `import { … } from "shelving/icon/AddIcon";` ## Constants - [AddIcon](icon/AddIcon/AddIcon.md): Icon to add an item, e.g. the add button on and . --- # AddIcon - Kind: constant - Import: `import { AddIcon } from "shelving/icon/AddIcon";` Icon to add an item, e.g. the add button on `` and ``. - Default is the Heroicons `PlusIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/AddIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/CloseIcon - Kind: module - Import: `import { … } from "shelving/icon/CloseIcon";` ## Constants - [CloseIcon](icon/CloseIcon/CloseIcon.md): Icon to close something, e.g. the close button on and . --- # CloseIcon - Kind: constant - Import: `import { CloseIcon } from "shelving/icon/CloseIcon";` Icon to close something, e.g. the close button on `` and ``. - Default is the Heroicons `XMarkIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/CloseIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/DownIcon - Kind: module - Import: `import { … } from "shelving/icon/DownIcon";` ## Constants - [DownIcon](icon/DownIcon/DownIcon.md): Chevron that points down. --- # DownIcon - Kind: constant - Import: `import { DownIcon } from "shelving/icon/DownIcon";` Chevron that points down. - Default is the Heroicons `ChevronDownIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/DownIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/ErrorIcon - Kind: module - Import: `import { … } from "shelving/icon/ErrorIcon";` ## Constants - [ErrorIcon](icon/ErrorIcon/ErrorIcon.md): Icon for status="error" in . --- # ErrorIcon - Kind: constant - Import: `import { ErrorIcon } from "shelving/icon/ErrorIcon";` Icon for `status="error"` in ``. - Default is the Heroicons `XCircleIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/ErrorIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/ExpandIcon - Kind: module - Import: `import { … } from "shelving/icon/ExpandIcon";` ## Constants - [ExpandIcon](icon/ExpandIcon/ExpandIcon.md): Icon to expand, e.g. to enter fullscreen, used in . --- # ExpandIcon - Kind: constant - Import: `import { ExpandIcon } from "shelving/icon/ExpandIcon";` Icon to expand, e.g. to enter fullscreen, used in ``. - Default is the Heroicons `ArrowsPointingOutIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/ExpandIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/InfoIcon - Kind: module - Import: `import { … } from "shelving/icon/InfoIcon";` ## Constants - [InfoIcon](icon/InfoIcon/InfoIcon.md): Icon for status="info" (the default) in . --- # InfoIcon - Kind: constant - Import: `import { InfoIcon } from "shelving/icon/InfoIcon";` Icon for `status="info"` (the default) in ``. - Default is the Heroicons `InformationCircleIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/InfoIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/LeftIcon - Kind: module - Import: `import { … } from "shelving/icon/LeftIcon";` ## Constants - [LeftIcon](icon/LeftIcon/LeftIcon.md): Chevron that points left. --- # LeftIcon - Kind: constant - Import: `import { LeftIcon } from "shelving/icon/LeftIcon";` Chevron that points left. - Default is the Heroicons `ChevronLeftIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/LeftIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/LoadingIcon - Kind: module - Import: `import { … } from "shelving/icon/LoadingIcon";` ## Constants - [LoadingIcon](icon/LoadingIcon/LoadingIcon.md): Animated icon that shows something is loading, e.g. and LOADING. --- # LoadingIcon - Kind: constant - Import: `import { LoadingIcon } from "shelving/icon/LoadingIcon";` Animated icon that shows something is loading, e.g. `` and `LOADING`. - Default is shelving's own `` spinner. - Swap it for your own icon with a `paths` entry for `shelving/icon/LoadingIcon` in your `tsconfig.json`. See `README.md` in this folder. - An icon has no CSS of its own, so animate your icon inside the SVG, e.g. with SMIL ``. --- # shelving/icon/MenuIcon - Kind: module - Import: `import { … } from "shelving/icon/MenuIcon";` ## Constants - [MenuIcon](icon/MenuIcon/MenuIcon.md): Icon to open a menu, e.g. the menu button on . --- # MenuIcon - Kind: constant - Import: `import { MenuIcon } from "shelving/icon/MenuIcon";` Icon to open a menu, e.g. the menu button on ``. - Default is the Heroicons `Bars3Icon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/MenuIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/RemoveIcon - Kind: module - Import: `import { … } from "shelving/icon/RemoveIcon";` ## Constants - [RemoveIcon](icon/RemoveIcon/RemoveIcon.md): Icon to remove an item, e.g. the remove buttons on , and . The pair of AddIcon. --- # RemoveIcon - Kind: constant - Import: `import { RemoveIcon } from "shelving/icon/RemoveIcon";` Icon to remove an item, e.g. the remove buttons on ``, `` and ``. The pair of `AddIcon`. - Default is the Heroicons `XMarkIcon`, the same as `CloseIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/RemoveIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/RetryIcon - Kind: module - Import: `import { … } from "shelving/icon/RetryIcon";` ## Constants - [RetryIcon](icon/RetryIcon/RetryIcon.md): Icon for . --- # RetryIcon - Kind: constant - Import: `import { RetryIcon } from "shelving/icon/RetryIcon";` Icon for ``. - Default is the Heroicons `ArrowPathIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/RetryIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/RightIcon - Kind: module - Import: `import { … } from "shelving/icon/RightIcon";` ## Constants - [RightIcon](icon/RightIcon/RightIcon.md): Chevron that points right, e.g. the separator in . --- # RightIcon - Kind: constant - Import: `import { RightIcon } from "shelving/icon/RightIcon";` Chevron that points right, e.g. the separator in ``. - Default is the Heroicons `ChevronRightIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/RightIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/ShrinkIcon - Kind: module - Import: `import { … } from "shelving/icon/ShrinkIcon";` ## Constants - [ShrinkIcon](icon/ShrinkIcon/ShrinkIcon.md): Icon to shrink, e.g. to exit fullscreen, used in . --- # ShrinkIcon - Kind: constant - Import: `import { ShrinkIcon } from "shelving/icon/ShrinkIcon";` Icon to shrink, e.g. to exit fullscreen, used in ``. - Default is the Heroicons `ArrowsPointingInIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/ShrinkIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/SuccessIcon - Kind: module - Import: `import { … } from "shelving/icon/SuccessIcon";` ## Constants - [SuccessIcon](icon/SuccessIcon/SuccessIcon.md): Icon for status="success" in . --- # SuccessIcon - Kind: constant - Import: `import { SuccessIcon } from "shelving/icon/SuccessIcon";` Icon for `status="success"` in ``. - Default is the Heroicons `CheckCircleIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/SuccessIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/UpIcon - Kind: module - Import: `import { … } from "shelving/icon/UpIcon";` ## Constants - [UpIcon](icon/UpIcon/UpIcon.md): Chevron that points up, e.g. the open and close arrow on
. --- # UpIcon - Kind: constant - Import: `import { UpIcon } from "shelving/icon/UpIcon";` Chevron that points up, e.g. the open and close arrow on `
`. - Default is the Heroicons `ChevronUpIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/UpIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/icon/WarningIcon - Kind: module - Import: `import { … } from "shelving/icon/WarningIcon";` ## Constants - [WarningIcon](icon/WarningIcon/WarningIcon.md): Icon for status="warning" and status="danger" in . --- # WarningIcon - Kind: constant - Import: `import { WarningIcon } from "shelving/icon/WarningIcon";` Icon for `status="warning"` and `status="danger"` in ``. - Default is the Heroicons `ExclamationTriangleIcon`. - Swap it for your own icon with a `paths` entry for `shelving/icon/WarningIcon` in your `tsconfig.json`. See `README.md` in this folder. --- # shelving/markup - Kind: module - Import: `import { … } from "shelving/markup";` A rule-based Markdown renderer that turns user-facing text into React nodes. ## Concepts This module converts Markdown-ish text into React nodes — suitable for rendering blog post bodies, user-written descriptions, or any rich text that originates outside your code. Rendering is done by a `MarkupParser` instance. Each block element type — heading, paragraph, blockquote, fenced code, ordered list, unordered list (including `[ ]` / `[x]` todo items), table, separator — is handled by an independent `MarkupRule`. Inline elements — bold, italic, inserted/deleted/highlighted text, inline code, links, autolinks, line breaks — are a separate rule set applied within block content. The engine groups rules into priority tiers and resolves the highest tier first. Once a tier claims a region of the text that region is "masked" so lower tiers cannot match into or across it. Each rule renders its match to a React element and recurses into its own children, optionally in a different context. The default ruleset intentionally diverges from CommonMark: - `*asterisk*` is always ``, `_underscore_` is always `` — no ambiguity between the two. - A single `\n` newline is always a `
` — no trailing double-space needed. - Literal HTML tags and `&` character entities are not supported — they render as plain text. - All bare URLs are autolinked. - Whitespace is not fussy: there is no four-space indented code block, and nesting (e.g. a sub-list) uses a single tab per level — see [Input normalisation](#input-normalisation). ## Usage ### Rendering markup Create a `MarkupParser` and call `MarkupParser.parse()`: ```ts import { MarkupParser } from "shelving/markup"; const parser = new MarkupParser(); const node = parser.parse("# Hello\n\nThis is *bold* and _italic_."); // `node` is a `ReactNode` — render it directly in JSX. ``` `parse()` returns a `ReactNode`: a single element, a string, an array of those, or `null`. For the common case the shared `MARKUP_PARSER` sentinel — a `MarkupParser` with default options — saves constructing one: ```ts import { MARKUP_PARSER } from "shelving/markup"; const node = MARKUP_PARSER.parse(content); ``` ### Input length (untrusted content) `parse()` is linear in the length of normal content, but a few rules can still degrade on deliberately adversarial input — notably long unbroken runs of backtick code fences, and deeply-nested `>` blockquotes (each leading `>` recurses one level, so a pathological line can overflow the stack). When the source comes from users, **cap its length before parsing**. A sane maximum for your use case — for typical user-generated content, tens of kilobytes — keeps worst-case work bounded: ```ts const MAX_MARKUP_LENGTH = 100_000; if (source.length > MAX_MARKUP_LENGTH) throw new Error("Content too long"); const node = MARKUP_PARSER.parse(source); ``` ### Options `MarkupParser` is constructed with `MarkupOptions`: | Option | Type | Description | |---|---|---| | `rules` | `MarkupRules` | Rules to apply. Defaults to `MARKUP_RULES` (all block + inline rules). | | `rel` | `string` | `rel` attribute applied to every rendered link, e.g. `"nofollow ugc"`. | | `url` | `ImmutableURL` | Current page URL — base for resolving relative refs (`./foo`, `#x`). | | `root` | `ImmutableURL` | Site root URL — base for resolving site-absolute paths (`/foo`). | | `schemes` | `URISchemes` | Allowed URI schemes for links. Defaults to `["http:", "https:"]`. | | `context` | `string` | Default starting context. Defaults to `"block"`. | ```ts const parser = new MarkupParser({ rel: "nofollow ugc", url: requireURL("https://example.com/page/"), root: requireURL("https://example.com/"), schemes: ["http:", "https:", "mailto:"], }); ``` Link href resolution goes through `getLink()` — site-absolute paths resolve against `root`, relative refs against `url`, scheme-prefixed URIs (`mailto:`, `tel:`, …) pass through, `URL` instances are emitted as-is. ### Block-only or inline-only rendering `MarkupParser.parse()` takes an optional second argument — the starting context. Rules declare which contexts they apply in, so `"inline"` skips every block-level rule: ```ts // Inline only — no block wrappers like

or

. const inline = MARKUP_PARSER.parse("Some *bold* text", "inline"); ``` `MARKUP_RULES_BLOCK` and `MARKUP_RULES_INLINE` expose the block and inline rule sets separately if you want a parser with only one. ### Custom rules Build custom rules with `createMarkupRule()` and combine them with the defaults: ```tsx import { createMarkupRule, MARKUP_RULES, MarkupParser } from "shelving/markup"; const HIGHLIGHT_RULE = createMarkupRule<{ text: string }>( /==(?[^=]+)==/, (key, { text }) => {text}, ["inline"], ); const parser = new MarkupParser({ rules: [...MARKUP_RULES, HIGHLIGHT_RULE] }); ``` `createMarkupRule()` takes a regexp (named captures are typed), a render function `(key, data, parser) => ReactElement`, the `contexts` the rule applies in, and an optional `priority` (default `0`; higher priorities form earlier-resolved tiers). See `shelving/markup` for the full built-in rule set. ## Input normalisation The parser expects **normalised** input and deliberately does not try to absorb every whitespace variation. Normalised text means: - Indentation is **tabs** — one tab per nesting level. Leading spaces are not significant. - No trailing whitespace at the end of a line. - No runs of more than two `\n` newlines; `\r\n` / `\r` line endings are normalised to `\n`. - Control characters are removed. Keeping this guarantee out of the rules is what keeps them simple: nesting — a sub-list inside a list item, a quote inside a quote — is always exactly one extra tab, never an ambiguous run of spaces. Normalisation is **not** performed by the parser — give it text that is already clean. `sanitizeMultilineText()` from `shelving/util/string` produces exactly this form: it converts four-space indents to tabs, strips sub-tab leading spaces, trims trailing whitespace, and collapses three-or-more newlines to two. `StringSchema` runs `sanitizeMultilineText()` automatically when validating any multi-line field (`rows > 1`), so text stored through a schema is already normalised. ```ts import { sanitizeMultilineText } from "shelving/util"; // Raw / untrusted text — normalise before parsing. const node = MARKUP_PARSER.parse(sanitizeMultilineText(untrustedInput)); ``` If the text reaches you straight from a `StringSchema`-validated field it is already normalised, and you can parse it directly. ## Functions - [createMarkupRule()](markup/createMarkupRule.md): Create a typed MarkupRule from a NamedRegExp, a renderer, its contexts, and an optional priority. - [createBlockRegExp()](markup/createBlockRegExp.md): Create a RegExp that matches a block of content, wrapped between block-start and block-end boundaries. - [createLineRegExp()](markup/createLineRegExp.md): Create a RegExp that matches a single line of content, wrapped between line-start and line-end boundaries. - [createWordRegExp()](markup/createWordRegExp.md): Create a RegExp that matches a word of content, wrapped between word-start and word-end boundaries. ## Classes - [MarkupParser](markup/MarkupParser.md): Parses a Markdownish markup string and renders it as a React node using a tiered, masking rule engine. - [Parser](markup/Parser.md): Base class for a parser that converts an input of type I into an output of type O. ## Interfaces - [MarkupRule](markup/MarkupRule.md): A single markup rule: a regular expression that matches a span of input plus a renderer that turns the match into an element. ## Types - [MarkupOptions](markup/MarkupOptions.md): Options configuring a MarkupParser (represents the current state of the parsing). - [MarkupContexts](markup/MarkupContexts.md): One or more named contexts a markup rule renders in (e.g. ["block"], ["inline", "list", "link"]). - [MarkupRules](markup/MarkupRules.md): An immutable list of MarkupRule instances applied by a MarkupParser. ## Constants - [MARKUP_PARSER](markup/MARKUP_PARSER.md): Shared MarkupParser instance configured with the default markup rules and behaviour. - [BLOCKQUOTE_RULE](markup/BLOCKQUOTE_RULE.md): Blockquote block. - [CODE_RULE](markup/CODE_RULE.md): Inline code. - [FENCED_RULE](markup/FENCED_RULE.md): Fenced code blocks - [HEADING_RULE](markup/HEADING_RULE.md): Headings are single line only (don't allow multiline). - [MARKUP_RULES_BLOCK](markup/MARKUP_RULES_BLOCK.md): Default markup rules that render in a block context — fenced code, headings, separators, lists, blockquotes, tables, and paragraphs. - [MARKUP_RULES_INLINE](markup/MARKUP_RULES_INLINE.md): Default markup rules that render in an inline context — inline code, links, autolinks, emphasis, and hard linebreaks. - [MARKUP_RULES](markup/MARKUP_RULES.md): Default markup rules — the combined block and inline rules MarkupParser uses when none are supplied. - [INLINE_RULE](markup/INLINE_RULE.md): Inline strong, emphasis, insert, delete, highlight. - [LINEBREAK_RULE](markup/LINEBREAK_RULE.md): Hard linebreak (
tag). - [LINK_RULE](markup/LINK_RULE.md): Markdown-style link. - [AUTOLINK_RULE](markup/AUTOLINK_RULE.md): Autolinked URL starts with scheme: (any scheme in MarkupOptions.schemes) and matches an unlimited number of non-space characters. - [ORDERED_RULE](markup/ORDERED_RULE.md): Ordered list. - [PARAGRAPH_RULE](markup/PARAGRAPH_RULE.md): Paragraph. - [SEPARATOR_RULE](markup/SEPARATOR_RULE.md): Separator (horizontal rule / thematic break). - [TABLE_RULE](markup/TABLE_RULE.md): Table. - [UNORDERED_RULE](markup/UNORDERED_RULE.md): Unordered list. - [BLOCK_CONTENT_REGEXP](markup/BLOCK_CONTENT_REGEXP.md): Regular expression source matching block content — the shortest run of any character. - [BLOCK_SPACE_REGEXP](markup/BLOCK_SPACE_REGEXP.md): Regular expression source matching block whitespace — any single whitespace character. - [BLOCK_START_REGEXP](markup/BLOCK_START_REGEXP.md): Regular expression source matching the start of a block — the start of the string, or one linebreak. - [BLOCK_END_REGEXP](markup/BLOCK_END_REGEXP.md): Regular expression source matching the end of a block — the end of the string, or two linebreaks, with trailing whitespace trimmed. - [LINE_CONTENT_REGEXP](markup/LINE_CONTENT_REGEXP.md): Regular expression source matching line content — the shortest run of any character except newline. - [LINE_SPACE_REGEXP](markup/LINE_SPACE_REGEXP.md): Regular expression source matching line whitespace — any single whitespace character except newline. - [LINE_START_REGEXP](markup/LINE_START_REGEXP.md): Regular expression source matching the start of a line — the start of the string, or one linebreak. - [LINE_END_REGEXP](markup/LINE_END_REGEXP.md): Regular expression source matching the end of a line — the end of the string, or one linebreak, with trailing whitespace trimmed. - [WORD_CONTENT_REGEXP](markup/WORD_CONTENT_REGEXP.md): Regular expression source matching word content — at least one letter or number character. - [WORD_START_REGEXP](markup/WORD_START_REGEXP.md): Regular expression source matching the start of a word — a zero-width assertion that the previous character is not a letter or number. - [WORD_END_REGEXP](markup/WORD_END_REGEXP.md): Regular expression source matching the end of a word — a zero-width assertion that the next character is not a letter or number. --- # MarkupOptions - Kind: type - Import: `import { MarkupOptions } from "shelving/markup";` Options configuring a `MarkupParser` (represents the current state of the parsing). - Every field is optional — an empty object yields a parser with the default rules and behaviour. - Link resolution honours `url`, `root`, and `schemes`; link safety hinges on `schemes`. ## Signatures ```ts { readonly rules?: MarkupRules | undefined; readonly rel?: string | undefined; readonly url?: ImmutableURL | undefined; readonly root?: ImmutableURL | undefined; readonly schemes?: URISchemes | undefined; readonly context?: string; readonly depth?: number | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `rules` | `MarkupRules \| undefined` | The active list of parsing rules. Read only. Defaults to `MARKUP_RULES`. | | `rel` | `string \| undefined` | Set the `rel=""` property used for any links (e.g. `rel="nofollow ugc"`). Read only. Optional. | | `url` | `ImmutableURL \| undefined` | Current page URL — used as the base for resolving relative refs (`./foo`, `#x`, bare segments) in link hrefs. Read only. Optional. | | `root` | `ImmutableURL \| undefined` | Site root URL — used as the base for resolving site-absolute path hrefs (`/foo`), honoring its subfolder. Read only. Optional. | | `schemes` | `URISchemes \| undefined` | Valid URI schemes/protocols for URLs and URIs. Read only. Defaults to `["http:", "https:"]`. | | `context` | `string` | Default context to use if one isn't set. Defaults to `"block"` Read only. Optional. | | `depth` | `number \| undefined` | Current nesting depth — used to cap self-recursive rules (e.g. blockquotes) so pathological input can't overflow the stack. Internal: set automatically as rules recurse via `MarkupParser.nested()`; callers normally leave it at the default `0`. Read only. Defaults to `0`. | ## Types - [MarkupRules](markup/MarkupRules.md): An immutable list of MarkupRule instances applied by a MarkupParser. - [ImmutableURL](util/url/ImmutableURL.md): Object that describes a valid URL, e.g. http://example.com/path/to/resource - [URISchemes](util/uri/URISchemes.md): List of allowed URI schemes. --- # MarkupParser - Kind: class - Import: `import { MarkupParser } from "shelving/markup";` - Implements: `Parser` Parses a Markdownish markup string and renders it as a React node using a tiered, masking rule engine. - The syntax isn't hardcoded — it's defined entirely by the `rules` supplied (defaults to `MARKUP_RULES`). - Rules are grouped into priority tiers and resolved highest tier first; a claimed region is masked so lower-priority rules can't match into or across it. - Rules own the recursion into their own children by calling `parse()` again, optionally with a different context. ## Signatures ```ts new MarkupParser({ rules = MARKUP_RULES, rel, url, root, schemes = HTTP_SCHEMES, context = "block", depth = 0 }: MarkupOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `MarkupOptions` | Defaults to `{}`. | | `.rules` | `MarkupRules \| undefined` | The active list of parsing rules. Read only. Defaults to `MARKUP_RULES`. | | `.rel` | `string \| undefined` | Set the `rel=""` property used for any links (e.g. `rel="nofollow ugc"`). Read only. Optional. | | `.url` | `ImmutableURL \| undefined` | Current page URL — used as the base for resolving relative refs (`./foo`, `#x`, bare segments) in link hrefs. Read only. Optional. | | `.root` | `ImmutableURL \| undefined` | Site root URL — used as the base for resolving site-absolute path hrefs (`/foo`), honoring its subfolder. Read only. Optional. | | `.schemes` | `URISchemes \| undefined` | Valid URI schemes/protocols for URLs and URIs. Read only. Defaults to `["http:", "https:"]`. | | `.context` | `string` | Default context to use if one isn't set. Defaults to `"block"` Read only. Optional. | | `.depth` | `number \| undefined` | Current nesting depth — used to cap self-recursive rules (e.g. blockquotes) so pathological input can't overflow the stack. Internal: set automatically as rules recurse via `MarkupParser.nested()`; callers normally leave it at the default `0`. Read only. Defaults to `0`. | ## Returns | Type | Description | | --- | --- | | `MarkupParser` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `rules` | `MarkupRules` | The list of parsing rules this parser applies. Read only. | | `priorities` | `ImmutableArray` | Calculated list of priorities to iterate over (extracted from the rules), e.g. [10, 0, -10] Read only. | | `rel` | `string \| undefined` | Set the `rel=""` property used for any links (e.g. `rel="nofollow ugc"`). Read only. | | `url` | `ImmutableURL \| undefined` | Current page URL — used as the base for resolving relative refs (`./foo`, `#x`, bare segments) in link hrefs. Falls back to `root` if not set. Read only. | | `root` | `ImmutableURL \| undefined` | Site root URL — used as the base for resolving site-absolute path hrefs (`/foo`), honoring its subfolder. Falls back to `url` if not set. Read only. | | `schemes` | `URISchemes` | Valid URI schemes/protocols for URLs and URIs. Read only. Defaults to `["http:", "https:"]`. | | `context` | `string` | Default context to use if one isn't set. Defaults to `"block"` Read only. | | `depth` | `number` | Current nesting depth, incremented by `nested()` as self-recursive rules descend. Used to cap recursion so pathological input (e.g. a long `>>>>…` blockquote) can't overflow the stack. Read only. | ## Methods - [MarkupParser.nested()](markup/MarkupParser/nested.md): Return a copy of this parser with depth incremented by one, sharing all other options. - [MarkupParser.parse()](markup/MarkupParser/parse.md): Parse a text string as Markdownish markup syntax and render it as a React node. - [MarkupParser.getRules()](markup/MarkupParser/getRules.md): Yield the rules active in context that sit in the given priority tier. - [MarkupParser.getLink()](markup/MarkupParser/getLink.md): Resolve a link href against this parser's url and root, returning it only if its scheme is allowed. --- # MarkupParser.nested() - Kind: method - Member of: `MarkupParser` Return a copy of this parser with `depth` incremented by one, sharing all other options. - Self-recursive rules (e.g. blockquotes) parse their nested content through `nested()` so `depth` tracks how deep the recursion has gone, letting them stop before the call stack overflows. ## Signatures ```ts nested(): MarkupParser ``` --- # MarkupParser.parse() - Kind: method - Member of: `MarkupParser` Parse a text string as Markdownish markup syntax and render it as a React node. - Syntax is not defined by this code, but by the rules supplied to it. - **Untrusted input:** parsing is linear for normal content, but a few rules can still degrade on adversarial input — long unbroken runs of backtick code fences, and deeply-nested `>` blockquotes (each leading `>` recurses one level, so a pathological line can overflow the stack). When `input` is user-generated, cap its length first; a sane maximum for your use case (for typical user content, tens of kilobytes) bounds worst-case work. ## Signatures ```ts parse(input: string, context = this.context): ReactNode ``` --- # MarkupParser.getRules() - Kind: method - Member of: `MarkupParser` Yield the rules active in `context` that sit in the given priority tier. ## Signatures ```ts getRules(context: string, priority: number): Iterable ``` --- # MarkupParser.getLink() - Kind: method - Member of: `MarkupParser` Resolve a link href against this parser's `url` and `root`, returning it only if its scheme is allowed. ## Signatures ```ts getLink(href: Nullish): ImmutableURI | undefined ``` --- # MARKUP_PARSER - Kind: constant - Import: `import { MARKUP_PARSER } from "shelving/markup";` Shared `MarkupParser` instance configured with the default markup rules and behaviour. - Use this singleton when no custom rules, link resolution, or default context are needed. ## Examples ```ts MARKUP_PARSER.parse("This is a *bold* string.") ``` --- # MarkupContexts - Kind: type - Import: `import { MarkupContexts } from "shelving/markup";` One or more named contexts a markup rule renders in (e.g. `["block"]`, `["inline", "list", "link"]`). ## Signatures ```ts [string, ...string[]] ``` --- # MarkupRule - Kind: interface - Import: `import { MarkupRule } from "shelving/markup";` A single markup rule: a regular expression that matches a span of input plus a renderer that turns the match into an element. - Rules are grouped into priority tiers and resolved highest tier first by `MarkupParser`. - A rule renders only in the contexts it lists, letting the same syntax behave differently in block vs inline vs list context. ## Signatures ```ts { regexp: RegExp; render(key: string, data: NamedRegExpData | undefined, parser: MarkupParser): ReactElement; contexts: MarkupContexts; priority: number; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `regexp` | `RegExp` | Regular expression used for matching the rule. | | `contexts` | `MarkupContexts` | One or more contexts this rule should render in. | | `priority` | `number` | Priority for this rule (higher priority rules override lower priority rules). | ## Methods - [MarkupRule.render()](markup/MarkupRule/render.md): Use the matched data to render an element. --- # MarkupRule.render() - Kind: method - Member of: `MarkupRule` Use the matched data to render an element. ## Signatures ```ts render(key: string, data: NamedRegExpData | undefined, parser: MarkupParser): ReactElement ``` --- # MarkupRules - Kind: type - Import: `import { MarkupRules } from "shelving/markup";` An immutable list of `MarkupRule` instances applied by a `MarkupParser`. ## Signatures ```ts readonly MarkupRule[] ``` ## Types - [MarkupRule](markup/MarkupRule.md): A single markup rule: a regular expression that matches a span of input plus a renderer that turns the match into an element. --- # createMarkupRule() - Kind: function - Import: `import { createMarkupRule } from "shelving/markup";` Create a typed `MarkupRule` from a `NamedRegExp`, a renderer, its contexts, and an optional priority. *Factory for `MarkupRule`.* - The overload taking a `NamedRegExp` infers the named capture groups handed to `render`. - Higher `priority` rules are resolved (and masked) before lower-priority ones. ## Signatures ```ts createMarkupRule(regexp: NamedRegExp, render: (key: string, data: T, parser: MarkupParser) => ReactElement, contexts: MarkupContexts, priority?: number): MarkupRule createMarkupRule(regexp: RegExp, render: (key: string, data: EmptyDictionary, parser: MarkupParser) => ReactElement, contexts: MarkupContexts, priority?: number): MarkupRule ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `regexp` | `NamedRegExp` | Regular expression used to match the rule (a `NamedRegExp` to type the render data). | | `render` | `(key: string, data: T, parser: MarkupParser) => ReactElement` | Renderer turning a `key`, the matched `data`, and the active `parser` into a `ReactElement`. | | `contexts` | `MarkupContexts` | One or more contexts this rule renders in (e.g. `["block"]`). | | `priority` | `number` | Tier priority — higher rules override lower ones (defaults to `0`). Optional. | | `regexp` | `RegExp` | | | `render` | `(key: string, data: EmptyDictionary, parser: MarkupParser) => ReactElement` | | | `contexts` | `MarkupContexts` | | | `priority` | `number` | Optional. | ## Returns | Type | Description | | --- | --- | | `MarkupRule` | A `MarkupRule` ready to add to a `MarkupRules` list. | | `MarkupRule` | | ## Examples ```ts const HR_RULE = createMarkupRule(/^---$/m, key =>
, ["block"]); ``` --- # Parser - Kind: class - Import: `import { Parser } from "shelving/markup";` Base class for a parser that converts an input of type `I` into an output of type `O`. - Subclasses implement `parse()` to define the actual transformation. - `MarkupParser` is the canonical implementation — it parses a markup string into a React node. ## Signatures ```ts new Parser() ``` ## Returns | Type | Description | | --- | --- | | `Parser` | | ## Examples ```ts class UppercaseParser extends Parser { parse(input: string): string { return input.toUpperCase(); } } ``` ## Methods - [Parser.parse()](markup/Parser/parse.md): Parse an input value into an output value. --- # Parser.parse() - Kind: method - Member of: `Parser` Parse an input value into an output value. ## Signatures ```ts parse(input: I): O ``` --- # BLOCKQUOTE_RULE - Kind: constant - Import: `import { BLOCKQUOTE_RULE } from "shelving/markup";` Blockquote block. - `>` quote character followed by zero or more spaces. - No spaces can appear before the `>` quote character. - Quote block is only broken by `\n\n` two newline characters. - Nesting is capped at `MAX_DEPTH` (10) levels; deeper `>` prefixes are rendered as plain text rather than recursed, so pathological input can't overflow the stack. ## Examples ```ts new MarkupParser({ rules: [BLOCKQUOTE_RULE] }).parse("> Quoted text") ``` --- # CODE_RULE - Kind: constant - Import: `import { CODE_RULE } from "shelving/markup";` Inline code. - Text surrounded by one or more "`" backtick tilde characters. - Unlike strong/emphasis first or last character of the element can be space, (e.g. `- abc -` will not work). - Closing characters must exactly match opening characters. - Works inside link text too, e.g. `` [`code`](url) ``. - Same as Markdown syntax. ## Examples ```ts new MarkupParser({ rules: [CODE_RULE] }).parse("Some `inline code` here") ``` --- # FENCED_RULE - Kind: constant - Import: `import { FENCED_RULE } from "shelving/markup";` Fenced code blocks - Same as Markdown syntax. - Start when we reach an opening fence on a new line. - Stop when we reach a matching closing fence on a new line, or the end of the string. - Closing fence must be exactly the same as the opening fence, and can be made of three or more backticks, or three or more `~~~` tildes. - If there's no closing fence the code block will run to the end of the current string. - Markdown-style four-space indent syntax is not supported (only fenced code since it's less confusing and more common). ## Examples ```ts new MarkupParser({ rules: [FENCED_RULE] }).parse("```\ncode block\n```") ``` --- # HEADING_RULE - Kind: constant - Import: `import { HEADING_RULE } from "shelving/markup";` Headings are single line only (don't allow multiline). - `#` 1-6 hashes, then one or more spaces, then the title. - `#` must be the first character on the line. - Markdown's underline syntax is not supported (for simplification). - Each heading gets a `getSlug()` `id` derived from its text, so same-page `#anchor` links resolve to it (e.g. `## React integration` becomes `id="react-integration"`). An all-punctuation heading that slugs to nothing gets no `id`. ## Examples ```ts new MarkupParser({ rules: [HEADING_RULE] }).parse("# Title") ``` --- # MARKUP_RULES_BLOCK - Kind: constant - Import: `import { MARKUP_RULES_BLOCK } from "shelving/markup";` Default markup rules that render in a block context — fenced code, headings, separators, lists, blockquotes, tables, and paragraphs. ## Signatures ```ts MARKUP_RULES_BLOCK: MarkupRules ``` ## Examples ```ts new MarkupParser({ rules: MARKUP_RULES_BLOCK }) ``` --- # MARKUP_RULES_INLINE - Kind: constant - Import: `import { MARKUP_RULES_INLINE } from "shelving/markup";` Default markup rules that render in an inline context — inline code, links, autolinks, emphasis, and hard linebreaks. ## Signatures ```ts MARKUP_RULES_INLINE: MarkupRules ``` ## Examples ```ts new MarkupParser({ rules: MARKUP_RULES_INLINE }) ``` --- # MARKUP_RULES - Kind: constant - Import: `import { MARKUP_RULES } from "shelving/markup";` Default markup rules — the combined block and inline rules `MarkupParser` uses when none are supplied. These rules define a syntax similar to Markdown but with improvements: 1. Syntax is more intuitive (e.g. `*strong*` always uses `*` asterisk and `_em_` always uses `_` underscore, and URLs are always autolinked). 2. More compatible with textboxes that wrap lines by default (e.g. single `\n` linebreaks don't need the trailing double space to, they're always treated as `
`). 3. Don't support fussy fragile syntax that lets users make mistakes (e.g. literal HTML tags or `&` character entities). ## Signatures ```ts MARKUP_RULES: MarkupRules ``` ## Examples ```ts new MarkupParser({ rules: MARKUP_RULES }).parse("This is a *bold* string.") ``` --- # INLINE_RULE - Kind: constant - Import: `import { INLINE_RULE } from "shelving/markup";` Inline strong, emphasis, insert, delete, highlight. - Inline strong text wrapped in one or more `*` asterisks. - Inline emphasis text wrapped in one or more `_` underscores. - Inline inserted text wrapped in one or more `+` pluses. - Inline deleted text wrapped in one or more `~` tildes. - Inline highlighted text wrapped in one or more `=` equals or `:` colons. - Whitespace cannot be the first or last character of the element (e.g. `* abc *` will not work). - Closing chars must match opening characters. - Cannot occur in the middle of a word (e.g. `this*that*this` will not work). - Closing characters must exactly match opening characters. - Different to Markdown: strong is always surrounded by `*asterisks*` and emphasis is always surrounded by `_underscores_` (strong isn't 'double emphasis'). ## Examples ```ts new MarkupParser({ rules: [INLINE_RULE] }).parse("This is *bold* and _italic_") ``` --- # LINEBREAK_RULE - Kind: constant - Import: `import { LINEBREAK_RULE } from "shelving/markup";` Hard linebreak (`
` tag). - Any `\n` linebreak in a paragraph will become a hard `
` tag. - Leading and trailing whitespace before/after the linebreak is trimmed. - Different to Markdown: - Markdown needs two spaces at the end of a line to create a linebreak. - We just directly convert any `\n` linebreak into a `
` tag (lines without two spaces are not joined together). - This is more intuitive (a linebreak becomes a linebreak is isn't silently ignored). - This works better with textareas that wrap text (since manually breaking up long lines is no longer necessary). ## Examples ```ts new MarkupParser({ rules: [LINEBREAK_RULE] }).parse("Line one\nLine two") ``` --- # LINK_RULE - Kind: constant - Import: `import { LINK_RULE } from "shelving/markup";` Markdown-style link. - Link in standard Markdown format, e.g. `[Google Maps](http://google.com/maps)` - If no title is specified a cleaned up version of the URL will be used, e.g. `google.com/maps` - Does not need space before/after the link. - If link is not valid (using `new URL(url)` then unparsed text will be returned. - For security only schemes that appear in `MarkupOptions.schemes` will match (defaults to `http:` and `https:`). ## Examples ```ts new MarkupParser({ rules: [LINK_RULE] }).parse("[Google Maps](http://google.com/maps)") ``` --- # AUTOLINK_RULE - Kind: constant - Import: `import { AUTOLINK_RULE } from "shelving/markup";` Autolinked URL starts with `scheme:` (any scheme in `MarkupOptions.schemes`) and matches an unlimited number of non-space characters. - If followed by space and then text in `()` round or `[]` square brackets that will be used as the title, e.g. `http://google.com/maps (Google Maps)` or `http://google.com/maps [Google Maps]` (this syntax is from Todoist and maybe other things too). - If no title is specified a cleaned up version of the URL will be used, e.g. `google.com/maps` - If link is not valid (using `new URL(url)` then unparsed text will be returned. - For security only schemes that appear in `MarkupOptions.schemes` will match (defaults to `http:` and `https:`). ## Examples ```ts new MarkupParser({ rules: [AUTOLINK_RULE] }).parse("http://google.com/maps (Google Maps)") ``` --- # ORDERED_RULE - Kind: constant - Import: `import { ORDERED_RULE } from "shelving/markup";` Ordered list. - Line starting with number, followed by `.`, `)` or `:` character, then one or more space characters. - No spaces can appear before the number character. - Second-level list can be created by indenting with `\t` one tab. - List block runs until a blank line that is not followed by another item or an indented continuation line. - A list with blank lines between its items (or before a continuation paragraph) is "loose": its items are wrapped in `

` tags. - Sparse lists are not supported. ## Examples ```ts new MarkupParser({ rules: [ORDERED_RULE] }).parse("1. First\n2. Second") ``` --- # PARAGRAPH_RULE - Kind: constant - Import: `import { PARAGRAPH_RULE } from "shelving/markup";` Paragraph. - Captures almost anything in a block context. - Any run of non-whitespace. - Leading and trailing whitespace is trimmed. ## Examples ```ts new MarkupParser({ rules: [PARAGRAPH_RULE] }).parse("Some plain text") ``` --- # SEPARATOR_RULE - Kind: constant - Import: `import { SEPARATOR_RULE } from "shelving/markup";` Separator (horizontal rule / thematic break). - Same as Markdown syntax but also allows `•` bullet character (in addition to `-` dash, `+` plus, `*` asterisk, `_` underscore). - Character must be repeated three (or more) times. - Character must be the same every time (can't mix) - Might have infinite number of spaces between the characters. ## Examples ```ts new MarkupParser({ rules: [SEPARATOR_RULE] }).parse("---") ``` --- # TABLE_RULE - Kind: constant - Import: `import { TABLE_RULE } from "shelving/markup";` Table. - Markdown-style pipe table: a header row, a `|---|` delimiter row, then body rows. - Cells are pipe-separated; outer pipes are optional and whitespace around cells is trimmed. - Extra `|---|` delimiter rows split the table into sections: the first section becomes ``, the last becomes `` (only when there are three or more sections), and every section in between becomes its own ``. - Column count and per-column alignment (`:--` left, `--:` right, `:-:` centered) come from the first delimiter row; ragged rows are padded or truncated to that count. - Cell content is rendered as inline markup; write `\|` for a literal pipe inside a cell. ## Examples ```ts new MarkupParser({ rules: [TABLE_RULE] }).parse("| A | B |\n|---|---|\n| 1 | 2 |") ``` --- # UNORDERED_RULE - Kind: constant - Import: `import { UNORDERED_RULE } from "shelving/markup";` Unordered list. - Line starting with `-`, `*`, `•`, or `+` character followed by one or more space characters. - No spaces can appear before the bullet character. - Second-level list can be created by indenting with `\t` one tab. - List block runs until a blank line that is not followed by another item or an indented continuation line. - A list with blank lines between its items (or before a continuation paragraph) is "loose": its items are wrapped in `

` tags. - An item starting with `[ ]` or `[x]` (case-insensitive) is a todo item: a checkbox `` plus the content wrapped in a `

  • {user.name}
  • ; } function UserList() { const users = useQuery(Users, { $order: "name" }).value; return
      {users.map(u => )}
    ; } function App() { return ( Something went wrong.

    }> Loading…

    }>
    ); } ``` ### Building a store inside a component A store that depends on props can't live at module scope. `useInstance()` constructs it once and keeps it stable across renders, while `useStore()` subscribes the component to its updates. ```tsx import { useInstance, useStore } from "shelving/react"; import { BooleanStore } from "shelving/store"; function Toggle() { const open = useInstance(BooleanStore); // Created once, stable across renders. useStore(open); // Re-render whenever the store changes. return ; } ``` ## Functions - [createAPIContext()](react/createAPIContext.md): Create an API context. - [createDBContext()](react/createDBContext.md): Create a data context - [useInstance()](react/useInstance.md): Use a memoised class instance. - [useLazy()](react/useLazy.md): Use a memoised value with lazy initialisation. - [useMap()](react/useMap.md): Create a mutable Map that persists for the lifetime of the component. - [useReduce()](react/useReduce.md): Use memoised value with reduction logic. - [useSequence()](react/useSequence.md): Subscribe to an async iterable for the lifetime of the component. - [useStore()](react/useStore.md): Subscribe to a Shelving Store instance to refresh this component when its value changes. ## Interfaces - [APIContext](react/APIContext.md): Bundle of hooks and a provider component returned by createAPIContext() - [DBContext](react/DBContext.md): Bundle of hooks and a provider component returned by createDBContext(). --- # APIContext - Kind: interface - Import: `import { APIContext } from "shelving/react";` Bundle of hooks and a provider component returned by `createAPIContext()` ## Signatures ```ts { useAPI(this: void, endpoint: Endpoint, payload: PP): EndpointStore; useAPI(this: void, endpoint: Nullish>, payload: PP): EndpointStore | undefined; readonly APIContext: (props: ChildProps) => ReactElement; requireAPICache(): APICache; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `APIContext` | `(props: ChildProps) => ReactElement` | Create a new `APICache` and provide it as context to child nodes, to allow them to use `useAPI()` Read only. | ## Methods - [APIContext.useAPI()](react/APIContext/useAPI.md): React hook to return an EndpointStore for the specified endpoint/payload in the current APIProvider context. - [APIContext.requireAPICache()](react/APIContext/requireAPICache.md): Return the underlying APICache for --- # APIContext.useAPI() - Kind: method - Member of: `APIContext` React hook to return an `EndpointStore` for the specified endpoint/payload in the current `APIProvider` context. ## Signatures ```ts useAPI(this: void, endpoint: Endpoint, payload: PP): EndpointStore useAPI(this: void, endpoint: Nullish>, payload: PP): EndpointStore | undefined ``` --- # APIContext.requireAPICache() - Kind: method - Member of: `APIContext` Return the underlying `APICache` for `` ## Signatures ```ts requireAPICache(): APICache ``` --- # createAPIContext() - Kind: function - Import: `import { createAPIContext } from "shelving/react";` Create an API context. - Allows React elements to call `useAPI()` to access endpoint stores in an API provider. - Each mounted `APIContext` gets its own in-memory store cache. Create a React context for a Shelving `shelving/api` provider. Call `createAPIContext(provider)` once, outside any component, to produce an `` wrapper component and a `useAPI` hook. Each mounted `` gets its own in-memory `APICache`, so independent subtrees can use independent providers. ## Usage Wrap your tree in the context component and call `useAPI(endpoint, payload)` wherever you need data. `useAPI` returns an `EndpointStore`; read `.value` to get the response — the component suspends while loading and surfaces errors to the nearest error boundary. ```tsx import { Suspense } from "react"; import { ClientAPIProvider, ValidationAPIProvider, GET } from "shelving/api"; import { createAPIContext } from "shelving/react"; import { STRING, DATA } from "shelving/schema"; const UserSchema = DATA({ id: STRING, name: STRING }); const getUser = GET("/users/{id}", { id: STRING }, UserSchema); const provider = new ValidationAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" })); const { APIContext, useAPI } = createAPIContext(provider); function UserCard({ id }: { id: string }) { const store = useAPI(getUser, { id }); // Returns an EndpointStore; component suspends while loading. const user = store.value; return
    {user.name}
    ; } function App() { return ( Error

    }> Loading...

    }>
    ); } ``` `useAPI` accepts a nullish endpoint — passing `undefined` returns `undefined` instead of a store, which keeps the hook call unconditional. Calling `useAPI` outside an `` throws a `RequiredError`. ## Signatures ```ts createAPIContext(provider: APIProvider): APIContext ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `APIProvider` | `APIProvider` the created context resolves endpoint stores against. | ## Returns | Type | Description | | --- | --- | | `APIContext` | | --- # DBContext - Kind: interface - Import: `import { DBContext } from "shelving/react";` Bundle of hooks and a provider component returned by `createDBContext()`. ## Signatures ```ts { useItem( collection: Nullish>, // id: Nullish, ): ItemStore | undefined; useItem( collection: Collection, // id: II, ): ItemStore; useQuery( collection: Nullish>, // query: Nullish>>, ): QueryStore | undefined; useQuery( collection: Collection, // query: Query>, ): QueryStore; readonly DBContext: ({ children }: { children: ReactNode }) => ReactElement; requireDBCache(): DBCache; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `DBContext` | `({ children }: { children: ReactNode }) => ReactElement` | Create a new `DBCache` and provide it as context to child nodes, to allow them to use `useItem()` and `useQuery()` Read only. | ## Methods - [DBContext.useItem()](react/DBContext/useItem.md): Get an ItemStore for the specified collection item in the current DataProvider context and subscribe to any changes in it. - [DBContext.useQuery()](react/DBContext/useQuery.md): Get an QueryStore for the specified collection query in the current DataProvider context and subscribe to any changes in it. - [DBContext.requireDBCache()](react/DBContext/requireDBCache.md): Return the underlying DBCache for --- # DBContext.useItem() - Kind: method - Member of: `DBContext` Get an `ItemStore` for the specified collection item in the current `DataProvider` context and subscribe to any changes in it. ## Signatures ```ts useItem(collection: Nullish>, id: Nullish): ItemStore | undefined useItem(collection: Collection, id: II): ItemStore ``` --- # DBContext.useQuery() - Kind: method - Member of: `DBContext` Get an `QueryStore` for the specified collection query in the current `DataProvider` context and subscribe to any changes in it. ## Signatures ```ts useQuery(collection: Nullish>, query: Nullish>>): QueryStore | undefined useQuery(collection: Collection, query: Query>): QueryStore ``` --- # DBContext.requireDBCache() - Kind: method - Member of: `DBContext` Return the underlying `DBCache` for `` ## Signatures ```ts requireDBCache(): DBCache ``` --- # createDBContext() - Kind: function - Import: `import { createDBContext } from "shelving/react";` Create a data context - Allows React elements to call `useItem()` and `useQuery()` to access items/queries in a database provider. - If the database is (or wraps) a `MemoryDBProvider`, the returned stores seed from it synchronously. Create a React context for a Shelving `shelving/db` provider. Call `createDBContext(provider)` once, outside any component, to produce a `` wrapper component plus `useItem` and `useQuery` hooks. Each mounted `` gets its own in-memory `DBCache`; if the provider is (or wraps) a `MemoryDBProvider`, the returned stores seed from it synchronously. ## Usage Wrap your tree in the context component, then call `useItem` for a single document or `useQuery` for a collection query. Both return a store (`ItemStore` / `QueryStore`); read `.value` to get the data — the component suspends while loading. ```tsx import { Suspense } from "react"; import { createDBContext } from "shelving/react"; const { DBContext, useItem, useQuery } = createDBContext(dbProvider); function UserCard({ id }: { id: string }) { const store = useItem(Users, id); // ItemStore — suspends while loading. const user = store.value; return
    {user.name}
    ; } function UserList() { const store = useQuery(Users, { $order: "name" }); // QueryStore. const users = store.value; return
      {users.map(u =>
    • {u.name}
    • )}
    ; } function App() { return ( Error

    }> Loading...

    }>
    ); } ``` Both hooks accept nullish arguments — passing `undefined` for the collection, id, or query returns `undefined` instead of a store, keeping the hook call unconditional. Calling either hook outside a `` throws a `RequiredError`. ## Signatures ```ts createDBContext(provider: DBProvider): DBContext ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `provider` | `DBProvider` | `DBProvider` the created context resolves item and query stores against. | ## Returns | Type | Description | | --- | --- | | `DBContext` | | --- # useInstance() - Kind: function - Import: `import { useInstance } from "shelving/react";` Use a memoised class instance. - Creates a new instance of `Constructor` using `args` - Returns same instance for as long as `args` is equal to previous `args`. Create a stable class instance inside a component. A new instance is constructed only when the constructor arguments change (by deep equality of the argument list), so the instance is safe to depend on across renders. This is useful for creating stores, caches, or other objects that should live for the lifetime of a component without being hoisted to module scope — for example when the object depends on props. ## Usage ```tsx import { useInstance } from "shelving/react"; import { APICache } from "shelving/api"; function MyComponent({ provider }: { provider: APIProvider }) { const cache = useInstance(APICache, provider); // Stable across renders. // ...rebuilt only when `provider` changes. } ``` Arguments are compared by deep equality, so passing equal-but-not-identical values (a fresh object literal each render) does not trigger reconstruction. ## Signatures ```ts useInstance(Constructor: new (...a: A) => T, ...args: A): T ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `Constructor` | `new (...a: A) => T` | Class to instantiate with `args`. | | `args` | `A` | Constructor arguments — a change in `args` constructs a fresh instance. | ## Returns | Type | Description | | --- | --- | | `T` | | --- # useLazy() - Kind: function - Import: `import { useLazy } from "shelving/react";` Use a memoised value with lazy initialisation. - Returns `value` (if not a function) or the result of calling `value(...args)` (if a function). - Returns same `value` for as long as `args` is equal to previous `args`. Compute a value once and recompute it only when its arguments change (by deep equality of the argument list). If the first argument is a function it is called with the remaining arguments; otherwise the value is returned as-is. `useLazy` is the factory-call counterpart to `useInstance()` — use it when you have a plain function rather than a class constructor. ## Usage ```tsx import { useLazy } from "shelving/react"; const sorted = useLazy((items) => [...items].sort(), items); ``` The result is recomputed only when `items` changes. Because arguments are compared by deep equality, a value that is equal-but-not-identical across renders does not trigger recomputation. ## Signatures ```ts useLazy(value: (...args: A) => T, ...args: A): T useLazy(value: T, ...args: Arguments): T useLazy(value: Lazy, ...args: A): T ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `value` | `(...args: A) => T` | Value to return, or a function called with `args` to lazily produce it. | | `args` | `A` | Arguments passed to `value()` when it is a function — a change in `args` recomputes the value. | | `value` | `T` | | | `args` | `Arguments` | | | `value` | `Lazy` | | | `args` | `A` | | ## Returns | Type | Description | | --- | --- | | `T` | | --- # useMap() - Kind: function - Import: `import { useMap } from "shelving/react";` Create a mutable Map that persists for the lifetime of the component. - The same `Map` instance is returned on every render; mutations are not reactive (won't trigger a re-render). Create a mutable `Map` that persists for the lifetime of the component. The same `Map` instance is returned on every render, so it is safe to read from and write to across renders without triggering reconstruction. `useMap` does **not** re-render the component when the map is mutated — it is a stable scratch container, not reactive state. Use it for caches, lookup tables, or per-key bookkeeping that lives alongside a component. ## Usage ```tsx import { useMap } from "shelving/react"; function Gallery({ ids }: { ids: string[] }) { const seen = useMap(); // Same Map on every render. return <>{ids.map(id => { const isNew = !seen.has(id); seen.set(id, true); return ; })}; } ``` ## Signatures ```ts useMap(): Map ``` ## Returns | Type | Description | | --- | --- | | `Map` | | --- # useReduce() - Kind: function - Import: `import { useReduce } from "shelving/react";` Use memoised value with reduction logic. - Calls `reduce()` on each render. - First argument `previous` is either `undefined` (on first render) or the value returned by `reduce()` on the previous render (on subsequent renders). - `reduce()` can implement custom logic to decide whether to return the previous value or a new one. - Returns whatever `reduce()` returns on this render. Run a reducer on every render, passing it the value returned on the previous render. This lets you implement custom memoisation — deciding render by render whether to keep the previous reference or adopt a new one. On the first render the reducer's `previous` argument is `undefined`; on every later render it is whatever the reducer returned last time. ## Usage `useReduce` runs a reducer on every render, receiving the previous return value so you can implement custom equality logic: ```tsx import { useReduce } from "shelving/react"; import { isDeepEqual } from "shelving/util"; const stable = useReduce((prev, next) => { if (prev && isDeepEqual(prev, next)) return prev; // Preserve reference if equal. return next; }, incoming); ``` Here `stable` keeps the same reference for as long as the incoming value is deeply equal, even though `incoming` is a fresh object each render. ## Signatures ```ts useReduce(reduce: (previous: T | undefined, ...a: A) => T, ...args: A): T useReduce(reduce: (previous: T | undefined, ...a: A) => T | undefined, ...args: A): T | undefined ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `reduce` | `(previous: T \| undefined, ...a: A) => T` | Reducer called on every render with the previous result and `args`; returns this render's value. | | `args` | `A` | Additional arguments forwarded to `reduce()` after `previous`. | | `reduce` | `(previous: T \| undefined, ...a: A) => T \| undefined` | | | `args` | `A` | | ## Returns | Type | Description | | --- | --- | | `T` | | | `T \| undefined` | | --- # useSequence() - Kind: function - Import: `import { useSequence } from "shelving/react";` Subscribe to an async iterable for the lifetime of the component. - Re-renders the component with each value yielded by the sequence. Subscribe to an `AsyncIterable` for the lifetime of the component and return its most recent value. The subscription is recreated whenever the iterable reference changes, so memoise the iterable to keep the subscription stable. The return value is `undefined` until the first emission. If the sequence throws, the error is re-thrown during render so the nearest error boundary catches it. ## Usage ```tsx import { useSequence } from "shelving/react"; function LiveCounter({ counter }: { counter: AsyncIterable }) { const count = useSequence(counter); // undefined until first emission. return {count ?? "—"}; } ``` Because the subscription resets when the iterable reference changes, pass a stable reference — hoist it, or memoise it with `useLazy()` / `useInstance()` — to avoid resubscribing on every render. ## Signatures ```ts useSequence(sequence?: AsyncIterable): T | undefined ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `sequence` | `AsyncIterable` | An object implementing the `AsyncIterable` interface. - Subscription is recreated every time this value changes. - Memoise this value to persist the subscription for the lifetime of the component. Optional. | ## Returns | Type | Description | | --- | --- | | `T \| undefined` | | ## Throws | Type | Description | | --- | --- | | `unknown` | unknown Re-throws (during render) any error thrown by the sequence, for an error boundary to catch. | --- # useStore() - Kind: function - Import: `import { useStore } from "shelving/react";` Subscribe to a Shelving `Store` instance to refresh this component when its value changes. - Re-renders the component whenever the store's value changes for as long as it is mounted. - Pass `undefined` to subscribe to nothing (e.g. for conditional subscriptions). Subscribe a component to a Shelving `shelving/store` so it re-renders whenever the store emits a new value. The store is wired into React's `useSyncExternalStore`, and `useStore` returns the same store it was given. `useStore` only handles the subscription. To read the value, use `store.value` — which throws a `Promise` while the store is loading (caught by ``) and throws the error reason if the store has failed (caught by an error boundary). ## Usage `useStore` subscribes to a store and returns it. The component suspends while the store is loading and surfaces errors to the nearest error boundary. ```tsx import { Suspense } from "react"; import { useStore } from "shelving/react"; function UserCard({ store }: { store: Store }) { useStore(store); // Subscribe — re-renders when the store updates. const user = store.value; // Throws Promise (loading) or reason (error). return
    {user.name}
    ; } function App() { return ( Something went wrong.

    }> Loading...

    }>
    ); } ``` The argument is optional — passing `undefined` is allowed (the hook is still called unconditionally, satisfying the rules of hooks) and returns `undefined`. ## Signatures ```ts useStore(store: T): T useStore(store?: T | undefined): T | undefined ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `store` | `T` | `Store` instance to subscribe to, or `undefined` to subscribe to nothing. | | `store` | `T \| undefined` | Optional. | ## Returns | Type | Description | | --- | --- | | `T` | | | `T \| undefined` | | --- # shelving/schema - Kind: module - Import: `import { … } from "shelving/schema";` Schema validation for TypeScript. A `Schema` describes how to coerce and validate an unknown value into a typed `T`. Schemas are the foundation of shelving — database collections, API endpoints, and form handlers all build on them. By convention, instantiated schemas are constants and use `UPPERCASE`. ## Concepts ### The `Schema` interface Every schema has a single `Schema.validate()` method — `validate(value: unknown): T`. If the value is valid (after any coercion), it returns `T`. If it is not, it throws a plain **string** error message — for example `"Must be 5–50 characters"` or `"Required"`. This is intentional: form handlers and API layers can consume these strings directly without any unwrapping. Multi-field errors from `DataSchema` are joined by `\n`, one line per failed field, with the field name prepended: `"name: Required\nemail: Invalid format"`. ### Default value `validate()` is called with `undefined` when no value is provided; the schema falls back to its `Schema.value` default. ### Coercion Schema functionality in Shelving follows the **robustness principle**: "be conservative in what you do, be liberal in what you accept from others". Values that are obvious in their intent are coerced to appropriate valid values. `StringSchema` converts numbers to strings, `BooleanSchema` converts the strings `"no"` and `"false"` to `false` and tests other values for truthiness, `ArraySchema` splits comma-separated strings into arrays, `NumberSchema` parses strings into numbers. ### Metadata Every schema carries optional display metadata set at construction time: | Property | Default | Purpose | | ------------- | --------------- | -------------------------------------------------- | | `Schema.one` | `"value"` | Singular noun, e.g. `"product"` | | `Schema.many` | `one + "s"` | Plural noun, e.g. `"products"` | | `Schema.title` | `""` | Human-readable label for a field | | `Schema.description` | `""` | Longer description for a field | | `Schema.placeholder` | `""` | Placeholder text for an input | | `Schema.value` | schema-specific | Default used when `validate(undefined)` is called | | `Schema.format()` | schema-specific | Display formatter for downstream form and UI use | ### Sugar instances and factories For convenience the `shelving/schema` module offers sugar constants and factories to improve the readability of code that creates complex schemas. Sugar instances are pre-instantiated copies of `Schema` classes — by convention an instantiated schema is named in `ALL_CAPS`, a convention you will find it convenient to use in your own codebase too. Sugar factories are functions like `OPTIONAL()` and `DATA()` that build a configured schema for you. | Sugar instance / factory | Validated type | | ----------------------- | -------------------- | | `STRING` | `string` | | `NUMBER` | `number` | | `CURRENCY_AMOUNT(code)` | `number` | | `BOOLEAN` | `boolean` | | `DATA(props)` | `T` (plain object) | | `ARRAY(itemSchema)` | `readonly T[]` | | `CHOICE(options)` | union of string keys | | `NULLABLE(schema)` | `T \| null` | | `OPTIONAL(schema)` | `T \| undefined` | | `PARTIAL(schema)` | `Partial` | | `ITEM(idSchema, props)` | `{ id: I } & T` | | `UPDATES(dataSchema)` | `Updates` | | `QUERY(dataSchema)` | `Query` | ## Usage Schemas compose: primitives, wrappers (`NULLABLE()`, `OPTIONAL()`, `ARRAY()`), and `DATA()` nest freely to describe an entire payload in a single validator. See each schema's own page for detailed usage of that class and its constants. ```ts import { ITEM, STRING, REQUIRED_STRING, NUMBER, ARRAY, OPTIONAL, INTEGER } from "shelving/schema"; const PRODUCT = ITEM(INTEGER, { name: REQUIRED_STRING, price: NUMBER, tags: ARRAY(STRING), notes: OPTIONAL(STRING), }); PRODUCT.validate({ id: 1, name: "Widget", price: 9.99, tags: "a,b" }); // { id: 1, name: "Widget", price: 9.99, tags: ["a", "b"], notes: undefined } PRODUCT.validate({ id: 2, name: "", price: 9.99, tags: [] }); // throws "name: Required" ``` ### Deep props, updates, and queries `getDataSchemaProp()` is the schema twin of `getDataProp()`. It walks a dotted path (`"sub.num"`) down nested `DataSchema` props, through wrappers like `NULLABLE()`, and gives the schema at the path, or `undefined`. `UPDATES()` and `QUERY()` use it to validate every key of an `Updates` or `Query` object against the prop at its path. An unknown key throws. ```ts import { ARRAY, DATA, NUMBER, QUERY, STRING, UPDATES } from "shelving/schema"; const PERSON = DATA({ name: STRING, tags: ARRAY(STRING), address: DATA({ city: STRING, floor: NUMBER }) }); UPDATES(PERSON).validate({ "address.city": "Leeds", "+=address.floor": 1, "+[]tags": "new" }); // Valid. UPDATES(PERSON).validate({ "address.floor": "high" }); // throws "address.floor: Must be number" QUERY(PERSON).validate({ "address.floor>": 2, "tags[]": "new", $order: "name", $limit: 10 }); // Valid. QUERY(PERSON).validate({ "address.street": "Main" }); // throws "address.street: Unknown prop" ``` ## Functions - [ARRAY()](schema/ARRAY.md): Create a schema for a valid array with specified items. - [CHOICE()](schema/CHOICE.md): Create a schema for a valid choice from an allowed set of values. - [CURRENCY_AMOUNT()](schema/CURRENCY_AMOUNT.md): Create a CurrencyAmountSchema for a non-negative monetary amount in a currency. - [NULLABLE_CURRENCY_AMOUNT()](schema/NULLABLE_CURRENCY_AMOUNT.md): Create a NullableSchema for an optional monetary amount in a currency, or null. - [DATA()](schema/DATA.md): Create a DataSchema for a set of properties. - [NULLABLE_DATA()](schema/NULLABLE_DATA.md): Create a schema for a valid data object with specified properties, or null. - [PARTIAL()](schema/PARTIAL.md): Create a DataSchema that validates partially, i.e. every property can be its value or undefined. - [ITEM()](schema/ITEM.md): Create a DataSchema that validates a data item, i.e. it has a string or number .id identifier property. - [getDataSchemaProp()](schema/getDataSchemaProp.md): Get the schema for an optional (possibly deep) prop of a DataSchema, or undefined if it doesn't exist. - [DICTIONARY()](schema/DICTIONARY.md): Create a schema for a valid dictionary object with specified entry values. - [NULLABLE()](schema/NULLABLE.md): Create a NullableSchema that wraps a source schema and also allows null. - [OPTIONAL()](schema/OPTIONAL.md): Create an OptionalSchema that wraps a source schema and also allows undefined. - [QUERY()](schema/QUERY.md): Create a QuerySchema for the data objects of a DataSchema. - [REQUIRED()](schema/REQUIRED.md): Create a RequiredSchema that wraps a source schema and rejects a falsy result. - [getSourceSchema()](schema/getSourceSchema.md): Get the innermost source of a wrapper schema, or the schema itself if it is not a ThroughSchema. - [UPDATES()](schema/UPDATES.md): Create an UpdatesSchema for the data object of a DataSchema. ## Classes - [AddressSchema](schema/AddressSchema.md): Schema that validates a postal address. - [ArraySchema](schema/ArraySchema.md): Schema that validates an array and ensures every item matches a specified item schema. - [BooleanSchema](schema/BooleanSchema.md): Schema that defines a valid boolean. - [ChoiceSchema](schema/ChoiceSchema.md): Schema that validates a value against a fixed set of allowed string choices. - [ColorSchema](schema/ColorSchema.md): Schema that defines a valid color hex string, e.g. #00CCFF - [CountrySchema](schema/CountrySchema.md): Schema that defines a valid ISO 3166 country code, e.g. GB. - [CurrencyAmountSchema](schema/CurrencyAmountSchema.md): Schema representing a numeric amount in a specific currency. - [CurrencyCodeSchema](schema/CurrencyCodeSchema.md): Schema that defines a valid ISO 4217 currency code, e.g. GBP. - [DataSchema](schema/DataSchema.md): Schema that validates a data object against a fixed set of named property schemas. - [DateSchema](schema/DateSchema.md): Schema that defines a valid date stored as a YYYY-MM-DD string, e.g. 2005-09-12. - [DateTimeSchema](schema/DateTimeSchema.md): Schema that defines a valid UTC datetime in ISO 8601 format, e.g. 2005-09-12T18:15:00.000Z. - [DictionarySchema](schema/DictionarySchema.md): Schema that validates a dictionary object whose entries all share the same value schema and have string keys. - [EmailSchema](schema/EmailSchema.md): Schema that defines a valid email address. - [EntitySchema](schema/EntitySchema.md): Schema that validates an Entity string combining a type and ID, e.g. challenge:a1b2c3. - [FileSchema](schema/FileSchema.md): Schema that validates a file name matching one or more extensions. - [ImageSchema](schema/ImageSchema.md): Schema that validates the file name of an image. - [KeySchema](schema/KeySchema.md): Schema that validates a database key string, stripping any non-alphanumeric characters. - [MediaSchema](schema/MediaSchema.md): Schema that validates the file name of an image or a video. - [NullableSchema](schema/NullableSchema.md): Schema that wraps a source schema and additionally allows null. - [NumberSchema](schema/NumberSchema.md): Schema that defines a valid number. - [OptionalSchema](schema/OptionalSchema.md): Schema that wraps a source schema and additionally allows undefined. - [PasswordSchema](schema/PasswordSchema.md): Schema that defines a valid password string. - [PhoneSchema](schema/PhoneSchema.md): Schema that defines a valid phone number. - [QuerySchema](schema/QuerySchema.md): Schema that validates a Query against the schema of the data objects it filters and sorts. - [RequiredSchema](schema/RequiredSchema.md): Schema that wraps a source schema but rejects a falsy result. - [Schema](schema/Schema.md): Schema is an object instance with a validate() method that converts unknown input into a known, valid type T. - [SlugSchema](schema/SlugSchema.md): Schema that defines a valid slug, e.g. this-is-a-slug. - [StringSchema](schema/StringSchema.md): Schema that defines a valid string. - [ThroughSchema](schema/ThroughSchema.md): Schema that wraps and passes through to a source schema, used as a base for schemas that augment another schema's behaviour (e.g. NullableSchema, OptionalSchema, RequiredSchema). - [TimeSchema](schema/TimeSchema.md): Schema that defines a valid abstract time in 24h hh:mm:ss.fff format, e.g. 23:59 or 24:00. - [UpdatesSchema](schema/UpdatesSchema.md): Schema that validates an Updates object against the schema of the data object it changes. - [URISchema](schema/URISchema.md): Schema that defines a valid absolute URI string. - [URLSchema](schema/URLSchema.md): Schema that defines a valid absolute or relative URL string. - [UUIDSchema](schema/UUIDSchema.md): Schema that defines a valid UUID string (versions 1-5). Defaults to any-version validation. - [VideoSchema](schema/VideoSchema.md): Schema that validates the file name of a video. ## Interfaces - [AddressSchemaOptions](schema/AddressSchemaOptions.md): Allowed options for AddressSchema. - [ArraySchemaOptions](schema/ArraySchemaOptions.md): Options for ArraySchema. - [BooleanSchemaOptions](schema/BooleanSchemaOptions.md): Allowed options for BooleanSchema. - [ChoiceSchemaOptions](schema/ChoiceSchemaOptions.md): Options for ChoiceSchema. - [ColorSchemaOptions](schema/ColorSchemaOptions.md): Options for a ColorSchema. - [CountrySchemaOptions](schema/CountrySchemaOptions.md): Options for CountrySchema. - [CurrencyAmountSchemaOptions](schema/CurrencyAmountSchemaOptions.md): Options for CurrencyAmountSchema. - [CurrencyCodeSchemaOptions](schema/CurrencyCodeSchemaOptions.md): Options for CurrencyCodeSchema. - [DataSchemaOptions](schema/DataSchemaOptions.md): Options for DataSchema. - [DateSchemaOptions](schema/DateSchemaOptions.md): Options for DateSchema. - [DictionarySchemaOptions](schema/DictionarySchemaOptions.md): Options for DictionarySchema. - [EmailSchemaOptions](schema/EmailSchemaOptions.md): Options for an EmailSchema. - [EntitySchemaOptions](schema/EntitySchemaOptions.md): Options for EntitySchema. - [FileSchemaOptions](schema/FileSchemaOptions.md): Allowed options for FileSchema. - [MediaSchemaOptions](schema/MediaSchemaOptions.md): Allowed options for MediaSchema. - [NullableSchemaOptions](schema/NullableSchemaOptions.md): Allowed options for NullableSchema. - [NumberSchemaOptions](schema/NumberSchemaOptions.md): Allowed options for NumberSchema. - [OptionalSchemaOptions](schema/OptionalSchemaOptions.md): Allowed options for OptionalSchema. - [PhoneSchemaOptions](schema/PhoneSchemaOptions.md): Options for a PhoneSchema. - [QuerySchemaOptions](schema/QuerySchemaOptions.md): Options for QuerySchema. - [SlugSchemaOptions](schema/SlugSchemaOptions.md): Options for a SlugSchema. - [StringSchemaOptions](schema/StringSchemaOptions.md): Options for StringSchema. - [ThroughSchemaOptions](schema/ThroughSchemaOptions.md): Allowed options for ThroughSchema. - [UpdatesSchemaOptions](schema/UpdatesSchemaOptions.md): Options for UpdatesSchema. - [URISchemaOptions](schema/URISchemaOptions.md): Options for URISchema. - [URLSchemaOptions](schema/URLSchemaOptions.md): Options for URLSchema. - [UUIDSchemaOptions](schema/UUIDSchemaOptions.md): Options for a UUIDSchema. ## Types - [ChoiceOptions](schema/ChoiceOptions.md): Dictionary of allowed options for a ChoiceSchema in { key: title } format. - [PossibleChoiceOptions](schema/PossibleChoiceOptions.md): Things that can be converted to a ChoiceOptions dictionary. - [ChoiceGroups](schema/ChoiceGroups.md): Named groups of option keys for a ChoiceSchema in { group: [key] } format, e.g. { Fruit: ["apple", "pear"] }. - [ChoiceInputType](schema/ChoiceInputType.md): Input control hint for a ChoiceSchema: "radio" for radio inputs, or "select" for a tags that are relevant for dates. - [SchemaOptions](schema/SchemaOptions.md): Options allowed by a Schema instance. - [SchemaType](schema/SchemaType.md): Extract the validated value type T from a Schema. - [Schemas](schema/Schemas.md): A set of named schemas in { name: schema } format. - [StringInputType](schema/StringInputType.md): type="" prop for HTML tags that are relevant for strings. ## Constants - [ADDRESS](schema/ADDRESS.md): Sugar instance of AddressSchema for postal address data. Equivalent to new AddressSchema({}). - [NULLABLE_ADDRESS](schema/NULLABLE_ADDRESS.md): Sugar instance allowing an ADDRESS or null. Equivalent to NULLABLE(ADDRESS). - [BOOLEAN](schema/BOOLEAN.md): Sugar instance of BooleanSchema for a coerced boolean. Equivalent to new BooleanSchema({}). - [COLOR](schema/COLOR.md): Sugar instance of ColorSchema for a required hex color string, e.g. #00CCFF. Equivalent to new ColorSchema({}). - [NULLABLE_COLOR](schema/NULLABLE_COLOR.md): Sugar instance allowing a COLOR or null. Equivalent to NULLABLE(COLOR). - [COUNTRY](schema/COUNTRY.md): Sugar instance of CountrySchema for a required ISO 3166 country code, e.g. GB. Equivalent to new CountrySchema({}). - [NULLABLE_COUNTRY](schema/NULLABLE_COUNTRY.md): Sugar instance allowing a COUNTRY or null. Equivalent to NULLABLE(COUNTRY). - [USD_AMOUNT](schema/USD_AMOUNT.md): Sugar instance of CurrencyAmountSchema for a US dollar amount. Equivalent to new CurrencyAmountSchema({ currency: "USD" }). - [GBP_AMOUNT](schema/GBP_AMOUNT.md): Sugar instance of CurrencyAmountSchema for a pound sterling amount. Equivalent to new CurrencyAmountSchema({ currency: "GBP" }). - [EUR_AMOUNT](schema/EUR_AMOUNT.md): Sugar instance of CurrencyAmountSchema for a euro amount. Equivalent to new CurrencyAmountSchema({ currency: "EUR" }). - [NULLABLE_USD_AMOUNT](schema/NULLABLE_USD_AMOUNT.md): Sugar instance allowing a USD_AMOUNT or null. Equivalent to NULLABLE(USD_AMOUNT). - [NULLABLE_GBP_AMOUNT](schema/NULLABLE_GBP_AMOUNT.md): Sugar instance allowing a GBP_AMOUNT or null. Equivalent to NULLABLE(GBP_AMOUNT). - [NULLABLE_EUR_AMOUNT](schema/NULLABLE_EUR_AMOUNT.md): Sugar instance allowing an EUR_AMOUNT or null. Equivalent to NULLABLE(EUR_AMOUNT). - [CURRENCY_CODE](schema/CURRENCY_CODE.md): Sugar instance of CurrencyCodeSchema for a required ISO 4217 currency code, e.g. GBP. Equivalent to new CurrencyCodeSchema({}). - [NULLABLE_CURRENCY_CODE](schema/NULLABLE_CURRENCY_CODE.md): Sugar instance allowing a CURRENCY_CODE or null. Equivalent to NULLABLE(CURRENCY_CODE). - [DATE](schema/DATE.md): Sugar instance of DateSchema for a required date. Equivalent to new DateSchema({}). - [NULLABLE_DATE](schema/NULLABLE_DATE.md): Sugar instance allowing a DATE or null. Equivalent to NULLABLE(DATE). - [DATETIME](schema/DATETIME.md): Sugar instance of DateTimeSchema for a required UTC datetime. Equivalent to new DateTimeSchema({}). - [NULLABLE_DATETIME](schema/NULLABLE_DATETIME.md): Sugar instance allowing a DATETIME or null. Equivalent to NULLABLE(DATETIME). - [EMAIL](schema/EMAIL.md): Sugar instance of EmailSchema for a valid email address. Equivalent to new EmailSchema({}). - [NULLABLE_EMAIL](schema/NULLABLE_EMAIL.md): Sugar instance allowing an EMAIL or null. Equivalent to NULLABLE(EMAIL). - [ENTITY](schema/ENTITY.md): Sugar instance of EntitySchema for an entity string, e.g. challenge:a1b2c3. Equivalent to new EntitySchema({}). - [NULLABLE_ENTITY](schema/NULLABLE_ENTITY.md): Sugar instance allowing an ENTITY or null. Equivalent to NULLABLE(ENTITY). - [FILE](schema/FILE.md): Sugar instance of FileSchema for a file name, e.g. file.txt. Equivalent to new FileSchema({}). - [NULLABLE_FILE](schema/NULLABLE_FILE.md): Sugar instance allowing a FILE or null. Equivalent to NULLABLE(FILE). - [IMAGE](schema/IMAGE.md): Sugar instance of ImageSchema for an image file name, e.g. photo.jpg. Equivalent to new ImageSchema({}). - [NULLABLE_IMAGE](schema/NULLABLE_IMAGE.md): Sugar instance allowing an IMAGE or null. Equivalent to NULLABLE(IMAGE). - [KEY](schema/KEY.md): Sugar instance of KeySchema for a database key. Equivalent to new KeySchema({ title: "ID" }). - [NULLABLE_KEY](schema/NULLABLE_KEY.md): Sugar instance allowing a KEY or null. Equivalent to NULLABLE(KEY). - [MEDIA](schema/MEDIA.md): Sugar instance of MediaSchema for an image or video file name, e.g. photo.jpg. Equivalent to new MediaSchema({}). - [NULLABLE_MEDIA](schema/NULLABLE_MEDIA.md): Sugar instance allowing a MEDIA or null. Equivalent to NULLABLE(MEDIA). - [NUMBER](schema/NUMBER.md): Sugar instance of NumberSchema for an unconstrained number. Equivalent to new NumberSchema({ title: "Number" }). - [NULLABLE_NUMBER](schema/NULLABLE_NUMBER.md): Sugar instance allowing a NUMBER or null. Equivalent to NULLABLE(NUMBER). - [INTEGER](schema/INTEGER.md): Sugar instance of NumberSchema for an integer. Equivalent to new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }). - [POSITIVE_INTEGER](schema/POSITIVE_INTEGER.md): Sugar instance of NumberSchema for a positive integer (excluding zero). Equivalent to new NumberSchema({ step: 1, min: 1, max: Number.MAX_SAFE_INTEGER }). - [NON_NEGATIVE_INTEGER](schema/NON_NEGATIVE_INTEGER.md): Sugar instance of NumberSchema for a non-negative integer (including zero). Equivalent to new NumberSchema({ step: 1, min: 0, max: Number.MAX_SAFE_INTEGER }). - [NEGATIVE_INTEGER](schema/NEGATIVE_INTEGER.md): Sugar instance of NumberSchema for a negative integer (excluding zero). Equivalent to new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: -1 }). - [NON_POSITIVE_INTEGER](schema/NON_POSITIVE_INTEGER.md): Sugar instance of NumberSchema for a non-positive integer (including zero). Equivalent to new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: 0 }). - [NULLABLE_INTEGER](schema/NULLABLE_INTEGER.md): Sugar instance allowing an INTEGER or null. Equivalent to NULLABLE(INTEGER). - [TIMESTAMP](schema/TIMESTAMP.md): Sugar instance of NumberSchema for a Unix timestamp (including milliseconds). Equivalent to new NumberSchema({ title: "Timestamp", step: 1, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }). - [NULLABLE_TIMESTAMP](schema/NULLABLE_TIMESTAMP.md): Sugar instance — alias of NULLABLE_INTEGER. Equivalent to NULLABLE_INTEGER. - [PASSWORD](schema/PASSWORD.md): Sugar instance of PasswordSchema for a password string. Equivalent to new PasswordSchema({}). - [PHONE](schema/PHONE.md): Sugar instance of PhoneSchema for a valid phone number. Equivalent to new PhoneSchema({}). - [NULLABLE_PHONE](schema/NULLABLE_PHONE.md): Sugar instance allowing a PHONE or null. Equivalent to NULLABLE(PHONE). - [UNKNOWN](schema/UNKNOWN.md): Unknown validator that always passes through its input value as unknown. - [UNDEFINED](schema/UNDEFINED.md): Undefined validator that always returns undefined. - [NULL](schema/NULL.md): Null validator that always returns null. - [SLUG](schema/SLUG.md): Sugar instance of SlugSchema for a valid slug. Equivalent to new SlugSchema({}). - [NULLABLE_SLUG](schema/NULLABLE_SLUG.md): Sugar instance allowing a SLUG or null. Equivalent to NULLABLE(SLUG). - [STRING](schema/STRING.md): Sugar instance of StringSchema for an unconstrained string. Equivalent to new StringSchema({}). - [REQUIRED_STRING](schema/REQUIRED_STRING.md): Sugar instance of StringSchema requiring at least one character. Equivalent to new StringSchema({ min: 1 }). - [TITLE](schema/TITLE.md): Sugar instance of StringSchema for a title of 1–100 characters. Equivalent to new StringSchema({ one: "title", title: "Title", min: 1, max: 100 }). - [NULLABLE_TITLE](schema/NULLABLE_TITLE.md): Sugar instance allowing a TITLE or null. Equivalent to NULLABLE(TITLE). - [NAME](schema/NAME.md): Sugar instance of StringSchema for a name of 1–100 characters. Equivalent to new StringSchema({ one: "name", title: "Name", min: 1, max: 100 }). - [NULLABLE_NAME](schema/NULLABLE_NAME.md): Sugar instance allowing a NAME or null. Equivalent to NULLABLE(NAME). - [TIME](schema/TIME.md): Sugar instance of TimeSchema for a required abstract time. Equivalent to new TimeSchema({}). - [NULLABLE_TIME](schema/NULLABLE_TIME.md): Sugar instance allowing a TIME or null. Equivalent to NULLABLE(TIME). - [URI_SCHEMA](schema/URI_SCHEMA.md): Sugar instance of URISchema for an absolute URI string. Equivalent to new URISchema({}). - [NULLABLE_URI_SCHEMA](schema/NULLABLE_URI_SCHEMA.md): Sugar instance allowing a URI_SCHEMA or null. Equivalent to NULLABLE(URI_SCHEMA). - [URL_SCHEMA](schema/URL_SCHEMA.md): Sugar instance of URLSchema for an absolute or relative URL string. Equivalent to new URLSchema({}). - [NULLABLE_URL_SCHEMA](schema/NULLABLE_URL_SCHEMA.md): Sugar instance allowing a URL_SCHEMA or null. Equivalent to NULLABLE(URL_SCHEMA). - [UUID](schema/UUID.md): Sugar instance of UUIDSchema for any valid UUID (versions 1-5). Equivalent to new UUIDSchema({ title: "ID" }). - [NULLABLE_UUID](schema/NULLABLE_UUID.md): Sugar instance allowing a UUID or null. Equivalent to NULLABLE(UUID). - [VIDEO](schema/VIDEO.md): Sugar instance of VideoSchema for a video file name, e.g. clip.mp4. Equivalent to new VideoSchema({}). - [NULLABLE_VIDEO](schema/NULLABLE_VIDEO.md): Sugar instance allowing a VIDEO or null. Equivalent to NULLABLE(VIDEO). --- # AddressSchemaOptions - Kind: interface - Import: `import { AddressSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Allowed options for `AddressSchema`. - The `props` are fixed to the postal-address fields internally, so only the default `value` and base schema options are exposed. ## Signatures ```ts { readonly value?: Partial | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `Partial \| undefined` | Partial default value merged over the per-field defaults. Read only. Optional. | --- # AddressSchema - Kind: class - Import: `import { AddressSchema } from "shelving/schema";` - Extends: `DataSchema` Schema that validates a postal address. - Validates the `address1`, `address2`, `city`, `state`, `postcode`, and `country` fields as a single `AddressData` object. - Formats validated values into a human-readable address string via `formatAddress()`. ## Signatures ```ts new AddressSchema({ one = "address", title = "Address", ...options }: AddressSchemaOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `AddressSchemaOptions` | Defaults to `{}`. | | `.value` | `Partial \| undefined` | Partial default value merged over the per-field defaults. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `AddressSchema` | | ## Examples ```ts ADDRESS.validate({ address1: "1 High St", city: "London", postcode: "SW1A 1AA", country: "GB" }); ``` --- # ADDRESS - Kind: constant - Import: `import { ADDRESS } from "shelving/schema";` Sugar instance of `AddressSchema` for postal address data. Equivalent to `new AddressSchema({})`. ## Examples ```ts ADDRESS.validate({ address1: "1 High St", city: "London", postcode: "SW1A 1AA", country: "GB" }); ``` --- # NULLABLE_ADDRESS - Kind: constant - Import: `import { NULLABLE_ADDRESS } from "shelving/schema";` Sugar instance allowing an `ADDRESS` or `null`. Equivalent to `NULLABLE(ADDRESS)`. ## Examples ```ts NULLABLE_ADDRESS.validate(null); // Returns null ``` --- # ArraySchemaOptions - Kind: interface - Import: `import { ArraySchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `ArraySchema`. ## Signatures ```ts { readonly value?: ImmutableArray; readonly items: Schema; readonly min?: number; readonly max?: number; readonly unique?: boolean; readonly separator?: string | RegExp; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `ImmutableArray` | Default value used when the input is `undefined`. Read only. Defaults to `[]`. | | `items` | `Schema` | Schema every item in the array must conform to. Read only. | | `min` | `number` | Minimum number of items. Read only. Defaults to `0`. | | `max` | `number` | Maximum number of items. Read only. Defaults to `Number.POSITIVE_INFINITY`. | | `unique` | `boolean` | Whether to deduplicate the items. Read only. Defaults to `false`. | | `separator` | `string \| RegExp` | String or `RegExp` used to split a string input into items. Read only. Defaults to `","`. | --- # ArraySchema - Kind: class - Import: `import { ArraySchema } from "shelving/schema";` - Extends: `Schema>` Schema that validates an array and ensures every item matches a specified item schema. - A string input is split into items using `separator`. - Items are optionally deduplicated (`unique`), then the count is checked against `min` and `max`. Validates a value into a `readonly` array, validating each element against an `items` schema. Following the robustness principle, a comma-separated string is split into an array, and a missing value falls back to an empty array. `ARRAY(itemSchema)` is the sugar factory that builds an `ArraySchema` for a given element schema. ## Usage ```ts import { ARRAY, STRING, NUMBER } from "shelving/schema"; const TAGS = ARRAY(STRING); TAGS.validate(["a", "b"]); // ["a", "b"] TAGS.validate("a,b,c"); // ["a", "b", "c"] (split on comma) TAGS.validate(undefined); // [] const NUMBERS = ARRAY(NUMBER); NUMBERS.validate([1, 2]); // [1, 2] NUMBERS.validate("1,2,3"); // [1, 2, 3] (split on comma, each coerced) NUMBERS.validate(undefined); // [] ``` ## Signatures ```ts new ArraySchema({ items, one = items.one, many = items.many, title = "Items", placeholder = `No ${many}`, unique = false, min = 0, max = Number.POSITIVE_INFINITY, separator = ",", value = [], ...options }: ArraySchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `ArraySchemaOptions` | | | `.value` | `ImmutableArray` | Default value used when the input is `undefined`. Read only. Defaults to `[]`. | | `.items` | `Schema` | Schema every item in the array must conform to. Read only. | | `.min` | `number` | Minimum number of items. Read only. Defaults to `0`. | | `.max` | `number` | Maximum number of items. Read only. Defaults to `Number.POSITIVE_INFINITY`. | | `.unique` | `boolean` | Whether to deduplicate the items. Read only. Defaults to `false`. | | `.separator` | `string \| RegExp` | String or `RegExp` used to split a string input into items. Read only. Defaults to `","`. | ## Returns | Type | Description | | --- | --- | | `ArraySchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `items` | `Schema` | Read only. | | `unique` | `boolean` | Read only. | | `min` | `number` | Read only. | | `max` | `number` | Read only. | | `separator` | `string \| RegExp` | Read only. | --- # ARRAY() - Kind: function - Import: `import { ARRAY } from "shelving/schema";` Create a schema for a valid array with specified items. Sugar factory for `ArraySchema`. ## Signatures ```ts ARRAY(items: Schema): ArraySchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `items` | `Schema` | Schema every item in the array must conform to. | ## Returns | Type | Description | | --- | --- | | `ArraySchema` | | ## Examples ```ts ARRAY(NUMBER) // ArraySchema ``` --- # BooleanSchemaOptions - Kind: interface - Import: `import { BooleanSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Allowed options for `BooleanSchema`. ## Signatures ```ts { readonly value?: boolean | undefined; readonly required?: boolean | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `boolean \| undefined` | Default boolean value used when the input is `undefined`. Read only. Defaults to `false`. | | `required` | `boolean \| undefined` | When `true`, a falsy result is rejected as invalid. Read only. Defaults to `false`. | --- # BooleanSchema - Kind: class - Import: `import { BooleanSchema } from "shelving/schema";` - Extends: `Schema` Schema that defines a valid boolean. - Strings are coerced: known negative strings (`""`, `"false"`, `"0"`, `"no"`, `"n"`, `"off"`) become `false`, everything else becomes `true`. - All other values are coerced with standard truthiness. Validates a value into a `boolean`. Following the robustness principle, the strings `"no"` and `"false"` coerce to `false`, and any other value is tested for truthiness. A missing value falls back to the schema's `value` default (`false`). `BOOLEAN` is the ready-made sugar instance for a boolean field. ## Usage ### Sugar instances To save creating a new instance of `BooleanSchema` for trivial uses, you can use the `BOOLEAN` sugar instance instead. ```ts import { BOOLEAN } from "shelving/schema"; BOOLEAN.validate(true); // true BOOLEAN.validate("false"); // false (coerced) BOOLEAN.validate("no"); // false (coerced) BOOLEAN.validate(1); // true (truthiness) BOOLEAN.validate(undefined); // false (default) ``` ## Signatures ```ts new BooleanSchema({ value = false, required = false, ...options }: BooleanSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `BooleanSchemaOptions` | | | `.value` | `boolean \| undefined` | Default boolean value used when the input is `undefined`. Read only. Defaults to `false`. | | `.required` | `boolean \| undefined` | When `true`, a falsy result is rejected as invalid. Read only. Defaults to `false`. | ## Returns | Type | Description | | --- | --- | | `BooleanSchema` | | ## Methods - [BooleanSchema.validate()](schema/BooleanSchema/validate.md): Validate an unknown value and coerce it to a boolean. --- # BooleanSchema.validate() - Kind: method - Member of: `BooleanSchema` Validate an unknown value and coerce it to a boolean. ## Signatures ```ts validate(unsafeValue: unknown = this.value): boolean ``` --- # BOOLEAN - Kind: constant - Import: `import { BOOLEAN } from "shelving/schema";` Sugar instance of `BooleanSchema` for a coerced boolean. Equivalent to `new BooleanSchema({})`. ## Examples ```ts BOOLEAN.validate("yes"); // Returns true ``` --- # ChoiceOptions - Kind: type - Import: `import { ChoiceOptions } from "shelving/schema";` Dictionary of allowed options for a `ChoiceSchema` in `{ key: title }` format. ## Signatures ```ts { readonly [KK in K]: string } ``` --- # PossibleChoiceOptions - Kind: type - Import: `import { PossibleChoiceOptions } from "shelving/schema";` Things that can be converted to a `ChoiceOptions` dictionary. - Dictionary of string options in `{ key: title }` format. - Array of string options in `[key]` format (`key` is used as the `title` too). ## Signatures ```ts ImmutableArray | ChoiceOptions ``` ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. - [ChoiceOptions](schema/ChoiceOptions.md): Dictionary of allowed options for a ChoiceSchema in { key: title } format. --- # ChoiceGroups - Kind: type - Import: `import { ChoiceGroups } from "shelving/schema";` Named groups of option keys for a `ChoiceSchema` in `{ group: [key] }` format, e.g. `{ Fruit: ["apple", "pear"] }`. - Groups are a display hint only: validation and `format()` use `options`, and do not change. - The group order and the key order in each group set the display order. ## Signatures ```ts { readonly [group: string]: ImmutableArray; } ``` ## Types - [ImmutableArray](util/array/ImmutableArray.md): Immutable array: an array that cannot be changed. --- # ChoiceInputType - Kind: type - Import: `import { ChoiceInputType } from "shelving/schema";` Input control hint for a `ChoiceSchema`: `"radio"` for radio inputs, or `"select"` for a `` field in HTML. The validated type is the union of the option keys. `CHOICE` is the sugar factory that builds a `ChoiceSchema` from either an array of strings (keys and labels are the same) or an object (keys are the validated values, object values are display labels). ## Usage ```ts import { CHOICE } from "shelving/schema"; // Array form — keys and labels are the same. const STATUS = CHOICE(["draft", "published", "archived"] as const); STATUS.validate("published"); // "published" STATUS.validate("deleted"); // throws "Unknown value" // Object form — keys are validated values, object values are display labels. const Priority = CHOICE({ low: "Low priority", high: "High priority" }); ``` `ChoiceSchema` is iterable and exposes `.keys()` and `.entries()` for building select menus. It does not implicitly default to the first option; pass `value` if you want a preselected choice. ## Input control `input` tells a form UI which control to show. It defaults from the number of options: `"radio"` for 8 or fewer, `"select"` for more. Set it to always use one control: ```ts import { ChoiceSchema } from "shelving/schema"; const UNIT = new ChoiceSchema({ options: ["kg", "g", "lb"], input: "select" }); // Always a ` `type=""` hint for downstream UIs. Read only. Defaults to `"color"`. | --- # ColorSchema - Kind: class - Import: `import { ColorSchema } from "shelving/schema";` - Extends: `StringSchema` Schema that defines a valid color hex string, e.g. `#00CCFF` - Coerces the value to a six-digit uppercase `#RRGGBB` hex string. - Rejects anything that isn't a valid hex color (use `NULLABLE_COLOR` to also allow `null`). ## Signatures ```ts new ColorSchema({ one = "color", title = "Color", value = "#000000", input = "color", max = 7, ...options }: ColorSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `ColorSchemaOptions` | | | `.value` | `string \| undefined` | Default hex value used when the input is `undefined`. Read only. Defaults to `"#000000"`. | | `.max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `7`. | | `.case` | `"upper" \| "lower" \| undefined` | Force the result to `"upper"` or `"lower"` case. Read only. Optional. | | `.input` | `StringInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"color"`. | ## Returns | Type | Description | | --- | --- | | `ColorSchema` | | ## Examples ```ts COLOR.validate("00ccff"); // Returns "#00CCFF" ``` --- # COLOR - Kind: constant - Import: `import { COLOR } from "shelving/schema";` Sugar instance of `ColorSchema` for a required hex color string, e.g. `#00CCFF`. Equivalent to `new ColorSchema({})`. ## Examples ```ts COLOR.validate("#00CCFF"); // Returns "#00CCFF" ``` --- # NULLABLE_COLOR - Kind: constant - Import: `import { NULLABLE_COLOR } from "shelving/schema";` Sugar instance allowing a `COLOR` or `null`. Equivalent to `NULLABLE(COLOR)`. ## Examples ```ts NULLABLE_COLOR.validate(null); // Returns null ``` --- # CountrySchemaOptions - Kind: interface - Import: `import { CountrySchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `CountrySchema`. ## Signatures ```ts { readonly value?: PossibleCountry; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `PossibleCountry` | Country value, or `"detect"` to resolve from browser language. Read only. Optional. | --- # CountrySchema - Kind: class - Import: `import { CountrySchema } from "shelving/schema";` - Extends: `ChoiceSchema` Schema that defines a valid ISO 3166 country code, e.g. `GB`. - The input is coerced with `getCountry()`, then checked against the known set of `COUNTRIES`. - A `value` of `"detect"` resolves the default country from the browser language. ## Signatures ```ts new CountrySchema({ one = "country", title = "Country", value = "detect", ...options }: CountrySchemaOptions = {}) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `CountrySchemaOptions` | Defaults to `{}`. | | `.value` | `PossibleCountry` | Country value, or `"detect"` to resolve from browser language. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `CountrySchema` | | ## Examples ```ts const schema = new CountrySchema({}); schema.validate("GB"); // "GB" ``` --- # COUNTRY - Kind: constant - Import: `import { COUNTRY } from "shelving/schema";` Sugar instance of `CountrySchema` for a required ISO 3166 country code, e.g. `GB`. Equivalent to `new CountrySchema({})`. ## Examples ```ts COUNTRY.validate("GB") // "GB" ``` --- # NULLABLE_COUNTRY - Kind: constant - Import: `import { NULLABLE_COUNTRY } from "shelving/schema";` Sugar instance allowing a `COUNTRY` or `null`. Equivalent to `NULLABLE(COUNTRY)`. ## Examples ```ts NULLABLE_COUNTRY.validate(null) // null ``` --- # CurrencyAmountSchemaOptions - Kind: interface - Import: `import { CurrencyAmountSchemaOptions } from "shelving/schema";` - Extends: `NumberSchemaOptions` Options for `CurrencyAmountSchema`. ## Signatures ```ts { readonly symbol?: string | undefined; readonly currency: CurrencyCode; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `symbol` | `string \| undefined` | Override the currency symbol used when formatting. Read only. Optional. | | `currency` | `CurrencyCode` | ISO 4217 currency code that determines the step and symbol. Read only. | --- # CurrencyAmountSchema - Kind: class - Import: `import { CurrencyAmountSchema } from "shelving/schema";` - Extends: `NumberSchema` Schema representing a numeric amount in a specific currency. - The validation step is inferred from the currency's minor units. - The default formatter renders amounts using shelving's currency helpers. A `NumberSchema` subclass for monetary amounts. It rounds to the currency's minor units (e.g. two decimal places for `GBP`) and exposes a `format()` method that renders the amount with the correct currency symbol. `USD_AMOUNT`, `GBP_AMOUNT`, and `EUR_AMOUNT` are ready-made sugar instances; the `CURRENCY_AMOUNT(code)` sugar factory builds one for any currency code. ## Usage ```ts import { CurrencyAmountSchema } from "shelving/schema"; const PRICE = new CurrencyAmountSchema({ title: "Price", currency: "GBP", min: 0 }); PRICE.validate("12.345"); // 12.35 (rounded to minor units) PRICE.format(12.3); // "£12.30" ``` ## Signatures ```ts new CurrencyAmountSchema({ currency, one = "amount", title = "Amount", symbol, step, ...options }: CurrencyAmountSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `CurrencyAmountSchemaOptions` | | | `.symbol` | `string \| undefined` | Override the currency symbol used when formatting. Read only. Optional. | | `.currency` | `CurrencyCode` | ISO 4217 currency code that determines the step and symbol. Read only. | ## Returns | Type | Description | | --- | --- | | `CurrencyAmountSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `currency` | `CurrencyCode` | ISO 4217 currency code this schema validates amounts for. Read only. | | `symbol` | `string` | Currency symbol used when formatting amounts. Read only. | --- # CURRENCY_AMOUNT() - Kind: function - Import: `import { CURRENCY_AMOUNT } from "shelving/schema";` Create a `CurrencyAmountSchema` for a non-negative monetary amount in a currency. Sugar factory for `CurrencyAmountSchema`. ## Signatures ```ts CURRENCY_AMOUNT(currency: CurrencyCode): CurrencyAmountSchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `currency` | `CurrencyCode` | ISO 4217 currency code that determines the step and symbol. | ## Returns | Type | Description | | --- | --- | | `CurrencyAmountSchema` | | ## Throws | Type | Description | | --- | --- | | `unknown` | `string` if `currency` is not a valid ISO 4217 currency code. | ## Examples ```ts CURRENCY_AMOUNT("GBP").validate("12.345") // 12.35 ``` --- # USD_AMOUNT - Kind: constant - Import: `import { USD_AMOUNT } from "shelving/schema";` Sugar instance of `CurrencyAmountSchema` for a US dollar amount. Equivalent to `new CurrencyAmountSchema({ currency: "USD" })`. ## Examples ```ts USD_AMOUNT.validate("12.345") // 12.35 ``` --- # GBP_AMOUNT - Kind: constant - Import: `import { GBP_AMOUNT } from "shelving/schema";` Sugar instance of `CurrencyAmountSchema` for a pound sterling amount. Equivalent to `new CurrencyAmountSchema({ currency: "GBP" })`. ## Examples ```ts GBP_AMOUNT.validate("12.345") // 12.35 ``` --- # EUR_AMOUNT - Kind: constant - Import: `import { EUR_AMOUNT } from "shelving/schema";` Sugar instance of `CurrencyAmountSchema` for a euro amount. Equivalent to `new CurrencyAmountSchema({ currency: "EUR" })`. ## Examples ```ts EUR_AMOUNT.validate("12.345") // 12.35 ``` --- # NULLABLE_CURRENCY_AMOUNT() - Kind: function - Import: `import { NULLABLE_CURRENCY_AMOUNT } from "shelving/schema";` Create a `NullableSchema` for an optional monetary amount in a currency, or `null`. Sugar factory for `NullableSchema`. ## Signatures ```ts NULLABLE_CURRENCY_AMOUNT(currency: CurrencyCode): NullableSchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `currency` | `CurrencyCode` | ISO 4217 currency code that determines the step and symbol. | ## Returns | Type | Description | | --- | --- | | `NullableSchema` | | ## Throws | Type | Description | | --- | --- | | `unknown` | `string` if `currency` is not a valid ISO 4217 currency code. | ## Examples ```ts NULLABLE_CURRENCY_AMOUNT("GBP").validate(null) // null ``` --- # NULLABLE_USD_AMOUNT - Kind: constant - Import: `import { NULLABLE_USD_AMOUNT } from "shelving/schema";` Sugar instance allowing a `USD_AMOUNT` or `null`. Equivalent to `NULLABLE(USD_AMOUNT)`. ## Examples ```ts NULLABLE_USD_AMOUNT.validate(null) // null ``` --- # NULLABLE_GBP_AMOUNT - Kind: constant - Import: `import { NULLABLE_GBP_AMOUNT } from "shelving/schema";` Sugar instance allowing a `GBP_AMOUNT` or `null`. Equivalent to `NULLABLE(GBP_AMOUNT)`. ## Examples ```ts NULLABLE_GBP_AMOUNT.validate(null) // null ``` --- # NULLABLE_EUR_AMOUNT - Kind: constant - Import: `import { NULLABLE_EUR_AMOUNT } from "shelving/schema";` Sugar instance allowing an `EUR_AMOUNT` or `null`. Equivalent to `NULLABLE(EUR_AMOUNT)`. ## Examples ```ts NULLABLE_EUR_AMOUNT.validate(null) // null ``` --- # CurrencyCodeSchemaOptions - Kind: interface - Import: `import { CurrencyCodeSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `CurrencyCodeSchema`. - The length, format, and single-line constraints are fixed internally, so only the presentation-level string options plus `currencies` are exposed. ## Signatures ```ts { readonly value?: string | undefined; readonly max?: number | undefined; readonly case?: "upper" | "lower" | undefined; readonly input?: StringInputType | undefined; readonly currencies?: ImmutableArray | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `string \| undefined` | Default string value used when the input is `undefined`. Read only. Optional. | | `max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `3`. | | `case` | `"upper" \| "lower" \| undefined` | Force the result to `"upper"` or `"lower"` case. Read only. Optional. | | `input` | `StringInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Optional. | | `currencies` | `ImmutableArray \| undefined` | Set of allowed ISO 4217 currency codes. Read only. Defaults to `CURRENCY_CODES`. | --- # CurrencyCodeSchema - Kind: class - Import: `import { CurrencyCodeSchema } from "shelving/schema";` - Extends: `StringSchema` Schema that defines a valid ISO 4217 currency code, e.g. `GBP`. - The input is sanitized to three uppercase letters, then checked against the allowed `currencies`. ## Signatures ```ts new CurrencyCodeSchema({ one = "currency", title = "Currency", currencies = CURRENCY_CODES, max = 3, ...options }: CurrencyCodeSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `CurrencyCodeSchemaOptions` | | | `.value` | `string \| undefined` | Default string value used when the input is `undefined`. Read only. Optional. | | `.max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `3`. | | `.case` | `"upper" \| "lower" \| undefined` | Force the result to `"upper"` or `"lower"` case. Read only. Optional. | | `.input` | `StringInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Optional. | | `.currencies` | `ImmutableArray \| undefined` | Set of allowed ISO 4217 currency codes. Read only. Defaults to `CURRENCY_CODES`. | ## Returns | Type | Description | | --- | --- | | `CurrencyCodeSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `currencies` | `ImmutableArray` | Set of allowed ISO 4217 currency codes. Read only. | ## Examples ```ts const schema = new CurrencyCodeSchema({}); schema.validate("gbp"); // "GBP" ``` --- # CURRENCY_CODE - Kind: constant - Import: `import { CURRENCY_CODE } from "shelving/schema";` Sugar instance of `CurrencyCodeSchema` for a required ISO 4217 currency code, e.g. `GBP`. Equivalent to `new CurrencyCodeSchema({})`. ## Examples ```ts CURRENCY_CODE.validate("gbp") // "GBP" ``` --- # NULLABLE_CURRENCY_CODE - Kind: constant - Import: `import { NULLABLE_CURRENCY_CODE } from "shelving/schema";` Sugar instance allowing a `CURRENCY_CODE` or `null`. Equivalent to `NULLABLE(CURRENCY_CODE)`. ## Examples ```ts NULLABLE_CURRENCY_CODE.validate(null) // null ``` --- # DataSchemaOptions - Kind: interface - Import: `import { DataSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `DataSchema`. ## Signatures ```ts { readonly props: Schemas; readonly value?: Partial | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `props` | `Schemas` | A named schema for each property the data object must have. Read only. | | `value` | `Partial \| undefined` | Partial default value merged over the per-prop defaults. Read only. Optional. | --- # DataSchema - Kind: class - Import: `import { DataSchema } from "shelving/schema";` - Extends: `Schema` Schema that validates a data object against a fixed set of named property schemas. - Each property is validated by its own schema in `props`. - Excess keys are stripped and `undefined` outputs removed (via `validateData()`). Validates a plain object whose properties each have their own schema. The term `Data` in Shelving refers to a plain object with known named properties; a `DataSchema` is the validator for one. When several properties fail, the errors are joined by `\n` with each field name prepended — e.g. `"name: Required\nprice: Minimum 0"`. This file also exports the `DATA`, `ITEM`, and `PARTIAL` sugar factories that build `DataSchema` instances. ## Usage ### `DATA` — validate an object ```ts import { DATA, BOOLEAN, StringSchema, NumberSchema } from "shelving/schema"; const PRODUCT = DATA({ name: new StringSchema({ title: "Name", min: 1, max: 100 }), price: new NumberSchema({ title: "Price", min: 0 }), available: BOOLEAN, }); PRODUCT.validate({ name: "Widget", price: 9.99, available: true }); // { name: "Widget", price: 9.99, available: true } PRODUCT.validate({ name: "", price: -1, available: true }); // throws "name: Required\nprice: Minimum 0" ``` Use `.pick()` and `.omit()` to derive subset schemas without redefining props: ```ts const PatchProduct = PRODUCT.omit("available"); const NameOnly = PRODUCT.pick("name"); ``` ### `ITEM` — add a typed `id` field `ITEM` wraps a `DataSchema` to add a typed `id` field, matching the `Item` type in `shelving/util/item`. ```ts import { ITEM, STRING, INTEGER, NUMBER } from "shelving/schema"; const PRODUCT_ITEM = ITEM(INTEGER, { name: STRING, price: NUMBER, }); // Validates: { id: number, name: string, price: number } ``` ### `PARTIAL` — make every field optional `PARTIAL` wraps a `DataSchema` so every field becomes optional — useful for PATCH-style update payloads. ```ts import { PARTIAL } from "shelving/schema"; const PARTIAL_PRODUCT = PARTIAL(PRODUCT); PARTIAL_PRODUCT.validate({ price: 4.99 }); // { price: 4.99 } ``` ## Signatures ```ts new DataSchema({ one = "item", title = "Item", props, value: partialValue, ...options }: DataSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `DataSchemaOptions` | | | `.props` | `Schemas` | A named schema for each property the data object must have. Read only. | | `.value` | `Partial \| undefined` | Partial default value merged over the per-prop defaults. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `DataSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `props` | `Schemas` | Read only. | ## Methods - [DataSchema.pick()](schema/DataSchema/pick.md): Make a new DataSchema that only uses a defined subset of the current props. - [DataSchema.omit()](schema/DataSchema/omit.md): Make a new DataSchema that omits one or more of the current props. --- # DataSchema.pick() - Kind: method - Member of: `DataSchema` Make a new `DataSchema` that only uses a defined subset of the current props. ## Signatures ```ts pick(...keys: K[]): DataSchema> ``` --- # DataSchema.omit() - Kind: method - Member of: `DataSchema` Make a new `DataSchema` that omits one or more of the current props. ## Signatures ```ts omit(...keys: K[]): DataSchema> ``` --- # DATA() - Kind: function - Import: `import { DATA } from "shelving/schema";` Create a `DataSchema` for a set of properties. Sugar factory for `DataSchema`. ## Signatures ```ts DATA(props: Schemas): DataSchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `props` | `Schemas` | A named schema for each property of the data object. | ## Returns | Type | Description | | --- | --- | | `DataSchema` | | ## Examples ```ts DATA({ name: STRING, age: NUMBER }) // DataSchema<{ name: string; age: number }> ``` --- # NULLABLE_DATA() - Kind: function - Import: `import { NULLABLE_DATA } from "shelving/schema";` Create a schema for a valid data object with specified properties, or `null`. Sugar factory for `NullableSchema`. ## Signatures ```ts NULLABLE_DATA(props: Schemas): NullableSchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `props` | `Schemas` | A named schema for each property of the data object. | ## Returns | Type | Description | | --- | --- | | `NullableSchema` | | ## Examples ```ts NULLABLE_DATA({ name: STRING }) // NullableSchema<{ name: string }> ``` --- # PARTIAL() - Kind: function - Import: `import { PARTIAL } from "shelving/schema";` Create a `DataSchema` that validates partially, i.e. every property can be its value or `undefined`. Sugar factory for `DataSchema`. ## Signatures ```ts PARTIAL(source: Schemas | DataSchema): DataSchema> ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `source` | `Schemas \| DataSchema` | The props schemas or an existing `DataSchema` to make partial. | ## Returns | Type | Description | | --- | --- | | `DataSchema>` | | ## Examples ```ts PARTIAL({ name: STRING, age: NUMBER }) // DataSchema<{ name?: string; age?: number }> ``` --- # ITEM() - Kind: function - Import: `import { ITEM } from "shelving/schema";` Create a `DataSchema` that validates a data item, i.e. it has a string or number `.id` identifier property. Sugar factory for `DataSchema`. ## Signatures ```ts ITEM(id: Schema, schemas: Schemas | DataSchema): DataSchema> ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `id` | `Schema` | Schema for the item's `id` identifier property. | | `schemas` | `Schemas \| DataSchema` | The props schemas or an existing `DataSchema` for the rest of the item. | ## Returns | Type | Description | | --- | --- | | `DataSchema>` | | ## Examples ```ts ITEM(NUMBER, { name: STRING }) // DataSchema<{ id: number; name: string }> ``` --- # getDataSchemaProp() - Kind: function - Import: `import { getDataSchemaProp } from "shelving/schema";` Get the schema for an optional (possibly deep) prop of a `DataSchema`, or `undefined` if it doesn't exist. - The schema twin of `getDataProp()`: it walks `DataSchema.props` one path segment at a time. - Goes through wrapper schemas (e.g. `NULLABLE()`, `OPTIONAL()`) with `getSourceSchema()`, and through the `items` of a `DictionarySchema`. - The schema it returns is not unwrapped, so e.g. a nullable prop still allows `null`. ## Signatures ```ts getDataSchemaProp(schema: DataSchema, path: K): Schema[K]> getDataSchemaProp(schema: DataSchema, path: string | Segments): Schema | undefined ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `schema` | `DataSchema` | The `DataSchema` to read from. | | `path` | `K` | The dotted path string (e.g. `"a.b"`) or an array of segments pointing at the prop. | | `schema` | `DataSchema` | | | `path` | `string \| Segments` | | ## Returns | Type | Description | | --- | --- | | `Schema[K]>` | The schema at the path, or `undefined` if any segment along the path doesn't exist. | | `Schema \| undefined` | | ## Examples ```ts getDataSchemaProp(DATA({ sub: DATA({ num: NUMBER }) }), "sub.num") // NUMBER ``` --- # DateInputType - Kind: type - Import: `import { DateInputType } from "shelving/schema";` `type=""` prop for HTML `` tags that are relevant for dates. ## Signatures ```ts "time" | "date" | "datetime-local" ``` --- # DateSchemaOptions - Kind: interface - Import: `import { DateSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `DateSchema`. ## Signatures ```ts { readonly value?: PossibleDate | undefined; readonly min?: Nullish; readonly max?: Nullish; readonly input?: DateInputType | undefined; readonly step?: number | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `PossibleDate \| undefined` | Default date used when the input is `undefined`. Read only. Optional. | | `min` | `Nullish` | Earliest allowed date (`null` for no bound). Read only. Optional. | | `max` | `Nullish` | Latest allowed date (`null` for no bound). Read only. Optional. | | `input` | `DateInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"date"`. | | `step` | `number \| undefined` | Rounding step (in milliseconds, because that's the base unit for time). - E.g. `1000 * 60` will round to the nearest minute. - Note: HTML `` `step` attributes are in _seconds_, so you may need to convert units. Read only. Optional. | --- # DateSchema - Kind: class - Import: `import { DateSchema } from "shelving/schema";` - Extends: `Schema` Schema that defines a valid date stored as a `YYYY-MM-DD` string, e.g. `2005-09-12`. - Validates an abstract date without a timezone; use `DateTimeSchema` for UTC datetimes and `TimeSchema` for times. - The input is coerced to a `Date`, optionally rounded to `step`, range-checked against `min`/`max`, then stringified. ## Signatures ```ts new DateSchema({ one = "date", min, max, value, input = "date", step, ...options }: DateSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `DateSchemaOptions` | | | `.value` | `PossibleDate \| undefined` | Default date used when the input is `undefined`. Read only. Optional. | | `.min` | `Nullish` | Earliest allowed date (`null` for no bound). Read only. Optional. | | `.max` | `Nullish` | Latest allowed date (`null` for no bound). Read only. Optional. | | `.input` | `DateInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"date"`. | | `.step` | `number \| undefined` | Rounding step (in milliseconds, because that's the base unit for time). - E.g. `1000 * 60` will round to the nearest minute. - Note: HTML `` `step` attributes are in _seconds_, so you may need to convert units. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `DateSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `min` | `Date \| undefined` | Earliest allowed date, or `undefined` for no minimum. Read only. | | `max` | `Date \| undefined` | Latest allowed date, or `undefined` for no maximum. Read only. | | `input` | `DateInputType` | HTML `` `type=""` hint for downstream UIs. Read only. | | `step` | `number \| undefined` | Rounding step in milliseconds, or `undefined` for no rounding. Read only. | ## Examples ```ts const schema = new DateSchema({ min: "2000-01-01" }); schema.validate("2005-09-12"); // "2005-09-12" ``` ## Methods - [DateSchema.stringify()](schema/DateSchema/stringify.md): Convert a Date object to the string representation used by this schema. --- # DateSchema.stringify() - Kind: method - Member of: `DateSchema` Convert a `Date` object to the string representation used by this schema. ## Signatures ```ts stringify(value: Date): string ``` --- # DATE - Kind: constant - Import: `import { DATE } from "shelving/schema";` Sugar instance of `DateSchema` for a required date. Equivalent to `new DateSchema({})`. ## Examples ```ts DATE.validate("2005-09-12") // "2005-09-12" ``` --- # NULLABLE_DATE - Kind: constant - Import: `import { NULLABLE_DATE } from "shelving/schema";` Sugar instance allowing a `DATE` or `null`. Equivalent to `NULLABLE(DATE)`. ## Examples ```ts NULLABLE_DATE.validate(null) // null ``` --- # DateTimeSchema - Kind: class - Import: `import { DateTimeSchema } from "shelving/schema";` - Extends: `DateSchema` Schema that defines a valid UTC datetime in ISO 8601 format, e.g. `2005-09-12T18:15:00.000Z`. - The date includes the `Z` suffix to indicate UTC time, this ensures consistent transfer of the date between client and server. - If you wish to define an _abstract_ date without a timezone, e.g. a birthday or anniversary, use `DateSchema` instead. - If you wish to define an _abstract_ time without a timezone, e.g. a daily alarm, use `TimeSchema` instead. ## Signatures ```ts new DateTimeSchema({ one = "time", title = "Time", input = "datetime-local", ...options }: DateSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `DateSchemaOptions` | | | `.value` | `PossibleDate \| undefined` | Default date used when the input is `undefined`. Read only. Optional. | | `.min` | `Nullish` | Earliest allowed date (`null` for no bound). Read only. Optional. | | `.max` | `Nullish` | Latest allowed date (`null` for no bound). Read only. Optional. | | `.input` | `DateInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"date"`. | | `.step` | `number \| undefined` | Rounding step (in milliseconds, because that's the base unit for time). - E.g. `1000 * 60` will round to the nearest minute. - Note: HTML `` `step` attributes are in _seconds_, so you may need to convert units. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `DateTimeSchema` | | ## Examples ```ts const schema = new DateTimeSchema({}); schema.validate("2005-09-12T18:15:00Z"); // "2005-09-12T18:15:00.000Z" ``` --- # DATETIME - Kind: constant - Import: `import { DATETIME } from "shelving/schema";` Sugar instance of `DateTimeSchema` for a required UTC datetime. Equivalent to `new DateTimeSchema({})`. ## Examples ```ts DATETIME.validate("2005-09-12T08:00:00Z") // "2005-09-12T08:00:00.000Z" ``` --- # NULLABLE_DATETIME - Kind: constant - Import: `import { NULLABLE_DATETIME } from "shelving/schema";` Sugar instance allowing a `DATETIME` or `null`. Equivalent to `NULLABLE(DATETIME)`. ## Examples ```ts NULLABLE_DATETIME.validate(null) // null ``` --- # DictionarySchemaOptions - Kind: interface - Import: `import { DictionarySchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for `DictionarySchema`. ## Signatures ```ts { readonly items: Schema; readonly value?: ImmutableDictionary | undefined; readonly min?: number | undefined; readonly max?: number | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `items` | `Schema` | Schema every entry value in the dictionary must conform to. Read only. | | `value` | `ImmutableDictionary \| undefined` | Default dictionary used when the input is `undefined`. Read only. Optional. | | `min` | `number \| undefined` | Minimum number of entries. Read only. Defaults to `0`. | | `max` | `number \| undefined` | Maximum number of entries. Read only. Defaults to `Number.POSITIVE_INFINITY`. | --- # DictionarySchema - Kind: class - Import: `import { DictionarySchema } from "shelving/schema";` - Extends: `Schema>` Schema that validates a dictionary object whose entries all share the same value schema and have string keys. - Every entry value is validated by the `items` schema. - Entry count is checked against `min` and `max`. ## Signatures ```ts new DictionarySchema({ items, one = items.one, many = items.many, placeholder = `No ${many}`, min = 0, max = Number.POSITIVE_INFINITY, title = "Items", value = {}, ...options }: DictionarySchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `DictionarySchemaOptions` | | | `.items` | `Schema` | Schema every entry value in the dictionary must conform to. Read only. | | `.value` | `ImmutableDictionary \| undefined` | Default dictionary used when the input is `undefined`. Read only. Optional. | | `.min` | `number \| undefined` | Minimum number of entries. Read only. Defaults to `0`. | | `.max` | `number \| undefined` | Maximum number of entries. Read only. Defaults to `Number.POSITIVE_INFINITY`. | ## Returns | Type | Description | | --- | --- | | `DictionarySchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `items` | `Schema` | Read only. | | `min` | `number` | Read only. | | `max` | `number` | Read only. | ## Examples ```ts const schema = new DictionarySchema({ items: NUMBER }); schema.validate({ a: 1, b: 2 }); // { a: 1, b: 2 } ``` --- # DICTIONARY() - Kind: function - Import: `import { DICTIONARY } from "shelving/schema";` Create a schema for a valid dictionary object with specified entry values. Sugar factory for `DictionarySchema`. ## Signatures ```ts DICTIONARY(items: Schema): DictionarySchema ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `items` | `Schema` | Schema every entry value in the dictionary must conform to. | ## Returns | Type | Description | | --- | --- | | `DictionarySchema` | | ## Examples ```ts DICTIONARY(NUMBER) // DictionarySchema ``` --- # EmailSchemaOptions - Kind: interface - Import: `import { EmailSchemaOptions } from "shelving/schema";` - Extends: `SchemaOptions` Options for an `EmailSchema`. - The length, format, and single-line constraints are fixed internally, so only the presentation-level string options are exposed. ## Signatures ```ts { readonly value?: string | undefined; readonly max?: number | undefined; readonly case?: "upper" | "lower" | undefined; readonly input?: StringInputType | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `value` | `string \| undefined` | Default string value used when the input is `undefined`. Read only. Optional. | | `max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `254`. | | `case` | `"upper" \| "lower" \| undefined` | Force the result to `"upper"` or `"lower"` case. Read only. Optional. | | `input` | `StringInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"email"`. | --- # EmailSchema - Kind: class - Import: `import { EmailSchema } from "shelving/schema";` - Extends: `StringSchema` Schema that defines a valid email address. - Falsy values are converted to `""` empty string. - Total length must be 254 characters or fewer (in SMTP email is encoded with `<` and `>` to make 256 characters). - No minimum length is enforced because the email's format is enforced instead. - RFC specifies username _should_ be case insensitive to avoid confusion. - We lowercase the entire address as a simple way to enforce this. - This is technically incorrect but practically better, and avoids case insensitive lookups in databases etc. - Username portion must be 64 characters or fewer from the range `[a-z0-9.!#$%&’*+/=?^_`{|}~-]` - We subset this to `[a-z0-9._+-]` with no special characters at start/end (and we lowercase automatically). - If someone uses e.g. `*` asterisk at all or '-' at the start then it's most likely to be a mistake. - Server portion must be a valid domain - Limited to `[a-z0-9]` with mid-word hyphens only. - Up to 10 segments of up to 63 characters each, separated by `.` - TLD is a segment of 2-63 characters, possibly in `xn--` international format. ## Signatures ```ts new EmailSchema({ one = "email address", title = "Email", input = "email", max = 254, ...options }: EmailSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `EmailSchemaOptions` | | | `.value` | `string \| undefined` | Default string value used when the input is `undefined`. Read only. Optional. | | `.max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `254`. | | `.case` | `"upper" \| "lower" \| undefined` | Force the result to `"upper"` or `"lower"` case. Read only. Optional. | | `.input` | `StringInputType \| undefined` | HTML `` `type=""` hint for downstream UIs. Read only. Defaults to `"email"`. | ## Returns | Type | Description | | --- | --- | | `EmailSchema` | | ## Examples ```ts EMAIL.validate("Test@Test.com"); // Returns "test@test.com" ``` --- # EMAIL - Kind: constant - Import: `import { EMAIL } from "shelving/schema";` Sugar instance of `EmailSchema` for a valid email address. Equivalent to `new EmailSchema({})`. ## Examples ```ts EMAIL.validate("test@test.com"); // Returns "test@test.com" ``` --- # NULLABLE_EMAIL - Kind: constant - Import: `import { NULLABLE_EMAIL } from "shelving/schema";` Sugar instance allowing an `EMAIL` or `null`. Equivalent to `NULLABLE(EMAIL)`. ## Examples ```ts NULLABLE_EMAIL.validate(null); // Returns null ``` --- # EntitySchemaOptions - Kind: interface - Import: `import { EntitySchemaOptions } from "shelving/schema";` - Extends: `StringSchemaOptions` Options for `EntitySchema`. ## Signatures ```ts { readonly types?: ImmutableArray | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `types` | `ImmutableArray \| undefined` | Restrict the allowed entity types; any other type is rejected. Read only. Optional. | --- # EntitySchema - Kind: class - Import: `import { EntitySchema } from "shelving/schema";` - Extends: `StringSchema` Schema that validates an `Entity` string combining a type and ID, e.g. `challenge:a1b2c3`. - The input must contain both a type and an ID separated by `:`, otherwise it is rejected. - When `types` is set, the entity's type must be one of the allowed types. ## Signatures ```ts new EntitySchema({ one = "entity", title = "Entity", types, ...options }: EntitySchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `EntitySchemaOptions` | | | `.types` | `ImmutableArray \| undefined` | Restrict the allowed entity types; any other type is rejected. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `EntitySchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `types` | `ImmutableArray \| undefined` | Read only. | ## Examples ```ts const schema = new EntitySchema({ types: ["challenge"] }); schema.validate("challenge:a1b2c3"); // "challenge:a1b2c3" ``` --- # ENTITY - Kind: constant - Import: `import { ENTITY } from "shelving/schema";` Sugar instance of `EntitySchema` for an entity string, e.g. `challenge:a1b2c3`. Equivalent to `new EntitySchema({})`. ## Examples ```ts ENTITY.validate("challenge:a1b2c3") // "challenge:a1b2c3" ``` --- # NULLABLE_ENTITY - Kind: constant - Import: `import { NULLABLE_ENTITY } from "shelving/schema";` Sugar instance allowing an `ENTITY` or `null`. Equivalent to `NULLABLE(ENTITY)`. ## Examples ```ts NULLABLE_ENTITY.validate("") // null ``` --- # FileSchemaOptions - Kind: interface - Import: `import { FileSchemaOptions } from "shelving/schema";` - Extends: `StringSchemaOptions` Allowed options for `FileSchema`. ## Signatures ```ts { readonly types?: FileTypes | undefined; } ``` ## Properties | Property | Type | Description | | --- | --- | --- | | `types` | `FileTypes \| undefined` | Set of allowed file extensions; when set, the file name's extension must be one of these. Read only. Optional. | --- # FileSchema - Kind: class - Import: `import { FileSchema } from "shelving/schema";` - Extends: `StringSchema` Schema that validates a file name matching one or more extensions. - Requires the file name to have an extension. - When `types` is set, the extension must be one of the allowed `FileTypes`. ## Signatures ```ts new FileSchema({ one = "file", title = "File", types, ...options }: FileSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `FileSchemaOptions` | | | `.types` | `FileTypes \| undefined` | Set of allowed file extensions; when set, the file name's extension must be one of these. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `FileSchema` | | ## Properties | Property | Type | Description | | --- | --- | --- | | `types` | `FileTypes \| undefined` | Set of allowed file extensions; when set, the file name's extension must be one of these. Read only. | ## Examples ```ts FILE.validate("photo.jpg"); // Returns "photo.jpg" ``` --- # FILE - Kind: constant - Import: `import { FILE } from "shelving/schema";` Sugar instance of `FileSchema` for a file name, e.g. `file.txt`. Equivalent to `new FileSchema({})`. ## Examples ```ts FILE.validate("file.txt"); // Returns "file.txt" ``` --- # NULLABLE_FILE - Kind: constant - Import: `import { NULLABLE_FILE } from "shelving/schema";` Sugar instance allowing a `FILE` or `null`. Equivalent to `NULLABLE(FILE)`. ## Examples ```ts NULLABLE_FILE.validate(null); // Returns null ``` --- # ImageSchema - Kind: class - Import: `import { ImageSchema } from "shelving/schema";` - Extends: `MediaSchema` Schema that validates the file name of an image. - The extension must be one of `types` (default `IMAGE_TYPES`). ## Signatures ```ts new ImageSchema({ one = "image", title = "Image", types = IMAGE_TYPES, ...options }: MediaSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `MediaSchemaOptions` | | | `.capture` | `FileCapture \| undefined` | Which camera a file picker for this schema opens straight away, or `undefined` for the normal picker. Read only. Optional. | ## Returns | Type | Description | | --- | --- | | `ImageSchema` | | --- # IMAGE - Kind: constant - Import: `import { IMAGE } from "shelving/schema";` Sugar instance of `ImageSchema` for an image file name, e.g. `photo.jpg`. Equivalent to `new ImageSchema({})`. ## Examples ```ts IMAGE.validate("photo.jpg"); // Returns "photo.jpg" ``` --- # NULLABLE_IMAGE - Kind: constant - Import: `import { NULLABLE_IMAGE } from "shelving/schema";` Sugar instance allowing an `IMAGE` or `null`. Equivalent to `NULLABLE(IMAGE)`. ## Examples ```ts NULLABLE_IMAGE.validate(null); // Returns null ``` --- # KeySchema - Kind: class - Import: `import { KeySchema } from "shelving/schema";` - Extends: `StringSchema` Schema that validates a database key string, stripping any non-alphanumeric characters. - Characters that are not a-z, A-Z, 0-9 are removed. - Default minimum key length is 1 character. - Default maximum key length is 32 characters. - 32 characters is enough for UUIDs, as the 4 `-` hyphens are removed. ## Signatures ```ts new KeySchema({ one = "key", title = "Key", min = 1, max = 32, ...options }: StringSchemaOptions) ``` ## Parameters | Param | Type | Description | | --- | --- | --- | | `options` | `StringSchemaOptions` | | | `.value` | `string \| undefined` | Default string value used when the input is `undefined`. Read only. Defaults to `""`. | | `.min` | `number \| undefined` | Minimum allowed character length. Read only. Defaults to `0`. | | `.max` | `number \| undefined` | Maximum allowed character length. Read only. Defaults to `Number.POSITIVE_INFINITY`. | | `.multiline` | `boolean \| undefined` | Whether the string keeps line breaks (multiline sanitization). An input for it is a `