first commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# `route-options`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `route-options` module is the Zod-validated boundary for every per-route
|
||||
configuration block. It widens the minimal `RouteOptions` type from
|
||||
`@suaveplan/api-types` with the policy switches every Stage-1-and-later
|
||||
adapter must honour: runtime Zod validation toggle, OpenAPI inclusion,
|
||||
streaming mode, CSRF, idempotency, and a discriminated rate-limit shape.
|
||||
|
||||
Centralising the schema here lets adapters, plugin-loader, and the host all
|
||||
parse the same shape, so a route option declared in a plugin manifest, a
|
||||
TypeScript builder, or a JSON config file gets the same defaults.
|
||||
|
||||
## Features
|
||||
|
||||
- `routeOptionsSchema` — `z.strictObject` rejecting unknown keys.
|
||||
- Defaults: `runtimeValidation: true`, `openapi: true`, `streaming: false`.
|
||||
- `request.body` / `request.params` / `request.query` accept any Zod schema
|
||||
(structural check — no instanceof, since two `z` instances in the same
|
||||
process may not share constructors).
|
||||
- `responses` is a `Record<status, { body: ZodSchema }>` so adapters can map
|
||||
each documented status to its validator.
|
||||
- `rateLimit` is a discriminated union: either `{ rpm }` or `{ rps }`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseRouteOptions } from "@suaveplan/api-router/route-options";
|
||||
|
||||
const opts = parseRouteOptions({ auth: "users.read", summary: "List users" });
|
||||
// opts.runtimeValidation === true (default applied)
|
||||
// opts.openapi === true
|
||||
// opts.streaming === false
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import z from "zod";
|
||||
import { parseRouteOptions } from "@suaveplan/api-router/route-options";
|
||||
|
||||
const opts = parseRouteOptions({
|
||||
auth: "users.write",
|
||||
summary: "Create user",
|
||||
tags: ["users", "admin"],
|
||||
request: {
|
||||
body: z.object({ name: z.string(), email: z.email() }),
|
||||
query: z.object({ dryRun: z.boolean().optional() }),
|
||||
},
|
||||
responses: {
|
||||
201: { body: z.object({ id: z.string() }) },
|
||||
409: { body: z.object({ code: z.string() }) },
|
||||
},
|
||||
streaming: false,
|
||||
csrf: true,
|
||||
idempotent: true,
|
||||
rateLimit: { rpm: 60 },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `routeOptionsSchema: z.ZodObject<…>` — the validated schema. Use
|
||||
`.parse(...)`, `.safeParse(...)`, or `.shape` for adapter-side
|
||||
introspection.
|
||||
- `parseRouteOptions(input: unknown): RouteOptions` — throw-on-fail.
|
||||
- `safeParseRouteOptions(input: unknown): ZodSafeParseResult<RouteOptions>`
|
||||
— non-throwing variant.
|
||||
- `RouteOptions` — `z.output` of the schema (post-defaults shape).
|
||||
- `RouteOptionsInput` — `z.input` (caller-supplied shape).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema uses `z.custom` with a structural predicate (looking for
|
||||
`safeParse` / `_zod`) rather than `instanceof z.ZodType` so plugins that
|
||||
import zod through their own dependency cone still pass the check. This is
|
||||
deliberate: two copies of zod in the dependency graph would otherwise break
|
||||
`instanceof`-based validation in surprising ways.
|
||||
|
||||
`z.strictObject` rejects unknown keys so typos in a plugin's route options
|
||||
surface at registration time, not at the first failing request. Adapters
|
||||
that want a permissive parse can call `routeOptionsSchema.passthrough()`
|
||||
instead — but the default contract is strict.
|
||||
|
||||
The `rateLimit` union is intentionally narrow: per-minute and per-second
|
||||
are the two real-world axes. Other windows (per-hour, per-day) belong in
|
||||
dedicated rate-limit plugins, not in the route schema.
|
||||
Reference in New Issue
Block a user