100 lines
3.3 KiB
Markdown
100 lines
3.3 KiB
Markdown
# `routes`
|
|
|
|
## Purpose
|
|
|
|
Defines the Zod request/response schemas the five management routes
|
|
mount when the plugin is configured with a `TenantStore`. Every body,
|
|
query, and response shape flows through these schemas so the OpenAPI
|
|
3.1 document published at `/openapi.json` is in lock-step with the
|
|
runtime behaviour — operators and SDKs generated from the spec see the
|
|
same surface the runtime enforces.
|
|
|
|
## Features
|
|
|
|
- `TenantResponseSchema` — wire-shape projection of the domain
|
|
`Tenant` (Dates serialised as ISO strings).
|
|
- `TenantListResponseSchema` — paginated list response (`items` +
|
|
`total`).
|
|
- `CreateTenantBodySchema` / `UpdateTenantBodySchema` — strict
|
|
body schemas; unknown keys are rejected.
|
|
- `TenantIdParamSchema` / `ListTenantsQuerySchema` — path-param and
|
|
query schemas with coercion where appropriate (`limit`, `offset`
|
|
are coerced from query string numerals).
|
|
- Slug validation regex: lowercase alphanumeric + hyphen, starting with
|
|
an alphanumeric. Keeps URLs predictable and prevents subtle
|
|
case-folding bugs in path-prefix resolution.
|
|
|
|
## Usage: Basic Example
|
|
|
|
```ts
|
|
import { CreateTenantBodySchema } from "@suaveplan/api-plugin-tenant";
|
|
|
|
const result = CreateTenantBodySchema.safeParse({
|
|
name: "Acme",
|
|
slug: "acme",
|
|
});
|
|
if (!result.success) throw new Error(result.error.message);
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
```ts
|
|
import {
|
|
CreateTenantBodySchema,
|
|
ListTenantsQuerySchema,
|
|
TenantResponseSchema,
|
|
} from "@suaveplan/api-plugin-tenant";
|
|
|
|
// Query coercion — limit/offset arrive as strings from the URL.
|
|
const query = ListTenantsQuerySchema.parse({
|
|
status: "active",
|
|
limit: "50",
|
|
offset: "0",
|
|
});
|
|
console.log(query.limit); // number, not string
|
|
|
|
// Round-trip a Tenant through the response schema.
|
|
const wire = TenantResponseSchema.parse({
|
|
id: "tenant_1",
|
|
name: "Acme",
|
|
slug: "acme",
|
|
status: "active",
|
|
settings: {},
|
|
enabledPlugins: ["billing"],
|
|
domains: [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
// Reject invalid slug at the boundary.
|
|
const bad = CreateTenantBodySchema.safeParse({
|
|
name: "X",
|
|
slug: "ACME!", // uppercase + punctuation
|
|
});
|
|
console.assert(!bad.success);
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `TenantResponseSchema` — single-tenant wire shape.
|
|
- `TenantListResponseSchema` — `{ items: TenantResponse[]; total: number }`.
|
|
- `CreateTenantBodySchema` — strict object; `name`, `slug` required.
|
|
- `UpdateTenantBodySchema` — strict object; all fields optional.
|
|
- `TenantIdParamSchema` — `{ id: string }`.
|
|
- `ListTenantsQuerySchema` — `{ status?, limit?, offset? }` with
|
|
numeric coercion on `limit` and `offset`.
|
|
- `DeleteTenantResponseSchema` — `{ success: true }`.
|
|
- Type aliases (`CreateTenantBody`, `UpdateTenantBody`, etc.) are
|
|
exported for handler signatures.
|
|
|
|
## Implementation Notes
|
|
|
|
`strict()` is applied to `CreateTenantBodySchema`, `UpdateTenantBodySchema`,
|
|
and the nested settings shape so unknown keys are rejected instead of
|
|
silently dropped. This matters because the same shape round-trips
|
|
through OpenAPI codegen on the client side — unknown keys on a strict
|
|
schema imply the client has drifted, and the right behaviour is to fail
|
|
loudly. The slug regex is intentionally conservative so URLs survive
|
|
percent-encoding, CDN normalisation, and case-insensitive DNS lookups
|
|
without ambiguity.
|