Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# health
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for the admin health check endpoint (`GET /admin/health`). The health endpoint exposes a structured view of the service's operational status — both an aggregate roll-up and per-subsystem detail — so that load balancers, container orchestrators, and monitoring dashboards can make informed routing and alerting decisions without parsing unstructured text responses.
|
||||
|
||||
## Features
|
||||
|
||||
- `HealthStatus` enum schema — `"ok" | "degraded" | "down"` covering the full range of operational states
|
||||
- `HealthCheck` object schema — per-subsystem check with status, optional latency measurement, and optional message
|
||||
- `AdminHealthOutput` schema — aggregate `status` plus a `checks` record keyed by subsystem name (non-empty keys)
|
||||
- `latencyMs` validated as a non-negative number — negative latency is not physically meaningful
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AdminHealthOutput } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
const health = AdminHealthOutput.parse({
|
||||
status: "ok",
|
||||
checks: {
|
||||
database: { status: "ok", latencyMs: 3 },
|
||||
cache: { status: "ok", latencyMs: 1 },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AdminHealthOutput, HealthStatus } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
async function collectHealth(): Promise<AdminHealthOutput> {
|
||||
const dbLatency = await measureDbPing();
|
||||
const cacheLatency = await measureCachePing();
|
||||
|
||||
const checks = {
|
||||
database: { status: dbLatency < 100 ? "ok" : "degraded" as HealthStatus, latencyMs: dbLatency },
|
||||
cache: { status: cacheLatency < 10 ? "ok" : "degraded" as HealthStatus, latencyMs: cacheLatency },
|
||||
};
|
||||
|
||||
const anyDown = Object.values(checks).some((c) => c.status === "down");
|
||||
const anyDegraded = Object.values(checks).some((c) => c.status === "degraded");
|
||||
const status: HealthStatus = anyDown ? "down" : anyDegraded ? "degraded" : "ok";
|
||||
|
||||
return AdminHealthOutput.parse({ status, checks });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `HealthStatus`
|
||||
|
||||
Zod enum: `"ok" | "degraded" | "down"`.
|
||||
|
||||
**Type export:** `type HealthStatus = z.infer<typeof HealthStatus>`
|
||||
|
||||
### `HealthCheck`
|
||||
|
||||
Zod object schema — single subsystem check result.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|----------------------|--------------------------------------------------|
|
||||
| `status` | `HealthStatus` | Status of this individual check |
|
||||
| `latencyMs` | `number` (≥ 0) | Round-trip latency; omit when not measured |
|
||||
| `message` | `string` | Optional human-readable context |
|
||||
|
||||
**Type export:** `type HealthCheck = z.infer<typeof HealthCheck>`
|
||||
|
||||
### `AdminHealthOutput`
|
||||
|
||||
Zod object schema — health endpoint response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------|-------------------------------------|-----------------------------------------------|
|
||||
| `status` | `HealthStatus` | Aggregate service status |
|
||||
| `checks` | `Record<string, HealthCheck>` | Keyed map of subsystem check results |
|
||||
|
||||
**Type export:** `type AdminHealthOutput = z.infer<typeof AdminHealthOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The aggregate `status` should be the worst of all individual check statuses — if any check is `"down"` the aggregate should be `"down"`; if any is `"degraded"` and none are `"down"` the aggregate should be `"degraded"`. This roll-up logic belongs in the route handler, not the schema. The `checks` record uses `z.record(z.string().min(1), ...)` to enforce non-empty subsystem names, preventing accidental empty-string keys that would make the map structure ambiguous.
|
||||
@@ -0,0 +1,63 @@
|
||||
# ping
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for the admin ping endpoint (`GET /admin/ping`). The ping endpoint is the simplest possible health indicator — it verifies that the server is alive, the request pipeline is functional, and the clock is ticking. These schemas are used by server route handlers, monitoring systems, and the OpenAPI generator to define the contract for this sentinel endpoint.
|
||||
|
||||
## Features
|
||||
|
||||
- `AdminPingInput` schema — empty object (no input required); useful as a typed placeholder for framework plugins that require a body schema
|
||||
- `AdminPingOutput` schema — `{pong: true, timestamp}` where `pong` is a typed literal and `timestamp` is an ISO 8601 string with timezone offset
|
||||
- `pong: z.literal(true)` ensures TypeScript narrows the field to `true`, not `boolean`
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AdminPingOutput } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
const response = AdminPingOutput.parse({
|
||||
pong: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// → { pong: true, timestamp: "2024-06-01T12:00:00.000Z" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AdminPingInput, AdminPingOutput } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
// Route handler using both input and output schemas
|
||||
async function handlePing(_rawBody: unknown): Promise<AdminPingOutput> {
|
||||
AdminPingInput.parse(_rawBody); // no-op for empty body, ensures shape check
|
||||
return AdminPingOutput.parse({
|
||||
pong: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AdminPingInput`
|
||||
|
||||
Zod object schema — empty input (no fields).
|
||||
|
||||
**Type export:** `type AdminPingInput = z.infer<typeof AdminPingInput>`
|
||||
|
||||
### `AdminPingOutput`
|
||||
|
||||
Zod object schema — ping response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|----------------------------------|------------------------------------------|
|
||||
| `pong` | `true` (literal) | Confirms endpoint reachability |
|
||||
| `timestamp` | `string` (ISO 8601 with offset) | Server clock time at request processing |
|
||||
|
||||
**Type export:** `type AdminPingOutput = z.infer<typeof AdminPingOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `pong` field uses `z.literal(true)` rather than `z.boolean()` to make the success state an unambiguous type-level fact — TypeScript will infer `pong: true` not `pong: boolean`, enabling exhaustiveness checks. The `timestamp` is included so monitoring tools can detect clock skew between the client and server without an additional round-trip to a time endpoint. The `AdminPingInput` empty-object schema exists to keep route plugin signatures uniform: every endpoint has both an input and an output schema.
|
||||
@@ -0,0 +1,60 @@
|
||||
# status
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the Zod schema for the admin status endpoint (`GET /admin/status`), which exposes static deployment metadata for debugging, support, and operational dashboards. Unlike the health endpoint, the status endpoint does not probe live dependencies — it reports the deployed application version, process uptime, environment name, and Node.js runtime version. These schemas ensure the response is consistently structured across all environments.
|
||||
|
||||
## Features
|
||||
|
||||
- `AdminStatusOutput` schema — `{version, uptime, environment, nodeVersion}` with strict Zod validation
|
||||
- `uptime` validated as a non-negative number representing seconds since process start
|
||||
- All string fields validated as non-empty to prevent blank deployment metadata
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AdminStatusOutput } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
const status = AdminStatusOutput.parse({
|
||||
version: "1.0.0",
|
||||
uptime: 86400,
|
||||
environment: "production",
|
||||
nodeVersion: "v22.0.0",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AdminStatusOutput } from "@suaveplan/api-schemas/admin";
|
||||
|
||||
function buildStatus(startTime: number): AdminStatusOutput {
|
||||
return AdminStatusOutput.parse({
|
||||
version: process.env["APP_VERSION"] ?? "unknown",
|
||||
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||
environment: process.env["NODE_ENV"] ?? "development",
|
||||
nodeVersion: process.version,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AdminStatusOutput`
|
||||
|
||||
Zod object schema — status endpoint response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------------|------------------|-----------------------------------------------------|
|
||||
| `version` | `string` (min 1) | Semantic version of the deployed application |
|
||||
| `uptime` | `number` (≥ 0) | Seconds the process has been running |
|
||||
| `environment` | `string` (min 1) | Deployment environment (production, staging, etc.) |
|
||||
| `nodeVersion` | `string` (min 1) | Node.js runtime version string |
|
||||
|
||||
**Type export:** `type AdminStatusOutput = z.infer<typeof AdminStatusOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `uptime` field uses floating-point seconds rather than milliseconds for human readability — `86400` (one day) is far more legible than `86400000`. The `nodeVersion` field is included so support engineers can quickly confirm whether a production issue is related to a known Node.js version bug without needing shell access. In Bun environments, `process.version` still reports the Node.js compatibility version rather than the Bun version; populate `nodeVersion` accordingly.
|
||||
@@ -0,0 +1,69 @@
|
||||
# context
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `AuthContext` Zod schema representing the authenticated caller's identity as it flows through a request lifecycle. After a server middleware validates the incoming access token, it extracts the user's identifier, email, roles, and optional tenant identifier into an `AuthContext` object. This schema ensures that the shape of that context is validated on construction and typed consistently across every server plugin, middleware, and service function that consumes it.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuthContext` schema — `{userId, email, roles, tenantId?}` with strict Zod validation
|
||||
- `roles` as a typed `string[]` — supports role-based access control checks without custom parsing
|
||||
- Optional `tenantId` for multi-tenant deployments — present only when the tenant scope is known
|
||||
- Non-empty validation on all string fields to prevent blank identity tokens
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AuthContext } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
// Validate the decoded JWT claims after signature verification
|
||||
const ctx = AuthContext.parse({
|
||||
userId: "user_01J8ZGCZ7ECTFAKEUID123456",
|
||||
email: "alice@example.com",
|
||||
roles: ["member"],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { AuthContext } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
// Middleware that builds the AuthContext from a validated JWT
|
||||
function buildAuthContext(claims: JwtClaims): AuthContext {
|
||||
return AuthContext.parse({
|
||||
userId: claims.sub,
|
||||
email: claims.email,
|
||||
roles: claims.roles ?? [],
|
||||
tenantId: claims.tenant_id,
|
||||
});
|
||||
}
|
||||
|
||||
// RBAC helper using the context
|
||||
function requireRole(ctx: AuthContext, role: string): void {
|
||||
if (!ctx.roles.includes(role)) {
|
||||
throw new Error("Forbidden");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AuthContext`
|
||||
|
||||
Zod object schema — authenticated request context.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|--------------------|-----------------------------------------------------|
|
||||
| `userId` | `string` (min 1) | Unique identifier of the authenticated user |
|
||||
| `email` | `string` (email) | Email address of the authenticated user |
|
||||
| `roles` | `string[]` | Role identifiers for RBAC checks |
|
||||
| `tenantId` | `string` (optional)| Tenant scope for multi-tenant deployments |
|
||||
|
||||
**Type export:** `type AuthContext = z.infer<typeof AuthContext>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`AuthContext` does not include the raw token or session identifier intentionally — the context represents decoded, validated identity, not the credential itself. Services that need to revoke or refresh tokens should do so through a separate token store, not through the auth context. The `roles` array is left as a plain `string[]` to remain agnostic about whether roles are stored as flat strings, hierarchical paths, or RBAC/ABAC expressions; the data layer is responsible for populating it correctly.
|
||||
@@ -0,0 +1,87 @@
|
||||
# login
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for the login request and response shapes used by Suaveplan's authentication endpoints. These schemas serve as the single source of truth consumed by the server route handler for `POST /auth/login`, the React login mutation hook, and the OpenAPI document generator — ensuring every layer shares the same field names, validation rules, and TypeScript types without duplication.
|
||||
|
||||
## Features
|
||||
|
||||
- `LoginInput` schema — validates `{email, password}` credentials with RFC 5322 email validation
|
||||
- `LoginUser` embedded object schema — user identifier, email, display name, and roles array returned inside the login response
|
||||
- `LoginOutput` schema — wraps a short-lived access `token`, a long-lived `refreshToken`, and a `LoginUser` object
|
||||
- All fields non-empty with appropriate Zod refinements
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LoginInput, LoginOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
// Validate the parsed request body
|
||||
const input = LoginInput.parse(req.body);
|
||||
// → { email: "user@example.com", password: "secret" }
|
||||
|
||||
// Validate the response before serialising to JSON
|
||||
const output = LoginOutput.parse({
|
||||
token: "eyJhbGciOiJFZERTQSJ9...",
|
||||
refreshToken: "rt_abc123",
|
||||
user: { id: "user_01", email: "user@example.com", name: "Alice", roles: ["member"] },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LoginInput, LoginOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
async function handleLogin(rawBody: unknown): Promise<LoginOutput> {
|
||||
const { email, password } = LoginInput.parse(rawBody);
|
||||
const user = await authService.verifyPassword(email, password);
|
||||
const token = await tokenService.issueAccessToken(user.id);
|
||||
const refreshToken = await tokenService.issueRefreshToken(user.id);
|
||||
return LoginOutput.parse({ token, refreshToken, user });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `LoginInput`
|
||||
|
||||
Zod object schema — login request body.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|---------------------|---------------------------------|
|
||||
| `email` | `string` (email) | RFC 5322 valid email address |
|
||||
| `password` | `string` (min 1) | Plaintext password (TLS-only) |
|
||||
|
||||
**Type export:** `type LoginInput = z.infer<typeof LoginInput>`
|
||||
|
||||
### `LoginUser`
|
||||
|
||||
Zod object schema — user payload embedded in the login response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------|--------------------|------------------------------------------|
|
||||
| `id` | `string` (min 1) | Unique user identifier |
|
||||
| `email` | `string` (email) | User email address |
|
||||
| `name` | `string` (min 1) | Display name |
|
||||
| `roles` | `string[]` | Array of role identifiers for this user |
|
||||
|
||||
**Type export:** `type LoginUser = z.infer<typeof LoginUser>`
|
||||
|
||||
### `LoginOutput`
|
||||
|
||||
Zod object schema — login success response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|-----------------|-------------------------------------------|
|
||||
| `token` | `string` (min 1)| Short-lived access token |
|
||||
| `refreshToken` | `string` (min 1)| Long-lived refresh token |
|
||||
| `user` | `LoginUser` | Authenticated user details |
|
||||
|
||||
**Type export:** `type LoginOutput = z.infer<typeof LoginOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `password` field uses `z.string().min(1)` rather than a minimum-length policy constraint because the schema layer validates wire format, not security policy. Minimum password length requirements belong in the registration flow and are enforced by the auth service. The access token format (JWT, opaque, etc.) is also not validated here — `z.string().min(1)` accepts any non-empty token string, keeping the schema decoupled from the token implementation.
|
||||
@@ -0,0 +1,65 @@
|
||||
# logout
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for the logout request and response shapes used by Suaveplan's `POST /auth/logout` endpoint. Logout is a server-side operation: the client sends its refresh token so the server can revoke it, preventing silent background re-authentication after the user has explicitly signed out. These schemas are shared between the server route handler, React logout mutation hooks, and OpenAPI document generation.
|
||||
|
||||
## Features
|
||||
|
||||
- `LogoutInput` schema — accepts a `refreshToken` string (non-empty) for server-side revocation
|
||||
- `LogoutOutput` schema — returns `{success: true}` as a typed literal, making the success state unambiguous in TypeScript
|
||||
- `success` field uses `z.literal(true)` rather than `z.boolean()` so TypeScript narrows the type to `true` rather than `boolean`
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LogoutInput, LogoutOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
// Validate the request body
|
||||
const input = LogoutInput.parse(req.body);
|
||||
// → { refreshToken: "rt_abc123" }
|
||||
|
||||
// Validate the response
|
||||
const output = LogoutOutput.parse({ success: true });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LogoutInput, LogoutOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
async function handleLogout(rawBody: unknown): Promise<LogoutOutput> {
|
||||
const { refreshToken } = LogoutInput.parse(rawBody);
|
||||
await sessionStore.revoke(refreshToken);
|
||||
// Also clear access token from an allowlist if using stateful tokens
|
||||
return LogoutOutput.parse({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `LogoutInput`
|
||||
|
||||
Zod object schema — logout request body.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|------------------|---------------------------------------------------|
|
||||
| `refreshToken` | `string` (min 1) | Refresh token to revoke on the server |
|
||||
|
||||
**Type export:** `type LogoutInput = z.infer<typeof LogoutInput>`
|
||||
|
||||
### `LogoutOutput`
|
||||
|
||||
Zod object schema — logout success response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|----------------|------------------------------------------------|
|
||||
| `success` | `true` (literal)| Always `true` on a successful logout |
|
||||
|
||||
**Type export:** `type LogoutOutput = z.infer<typeof LogoutOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Using `z.literal(true)` rather than `z.boolean()` means TypeScript will infer `success: true` rather than `success: boolean`, enabling exhaustiveness checks in client code that branches on the success field. Logout endpoints conventionally return 204 No Content with no body, but Suaveplan uses a 200 with a JSON body to accommodate clients (such as React hooks) that expect a typed response. Clients that receive a 401 or 404 should still clear local tokens regardless of the server response.
|
||||
@@ -0,0 +1,67 @@
|
||||
# refresh
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for the token-refresh request and response shapes used by Suaveplan's `POST /auth/refresh` endpoint. The refresh flow implements token rotation — each use of a refresh token returns both a new access token and a new refresh token, invalidating the consumed one. These schemas are shared between the server route handler, the React auth hook that silently re-authenticates expiring sessions, and the OpenAPI generator.
|
||||
|
||||
## Features
|
||||
|
||||
- `RefreshInput` schema — accepts a single `refreshToken` string (non-empty)
|
||||
- `RefreshOutput` schema — returns a new `token` (access) and new `refreshToken` (rotation pattern)
|
||||
- Non-empty validation on both token fields to prevent silent empty-string bypasses
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RefreshInput, RefreshOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
// Validate the request body
|
||||
const input = RefreshInput.parse(req.body);
|
||||
// → { refreshToken: "rt_abc123..." }
|
||||
|
||||
// Validate the response
|
||||
const output = RefreshOutput.parse({ token: "new-access-token", refreshToken: "new-rt_xyz" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RefreshInput, RefreshOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
async function handleRefresh(rawBody: unknown): Promise<RefreshOutput> {
|
||||
const { refreshToken } = RefreshInput.parse(rawBody);
|
||||
const session = await sessionStore.consume(refreshToken); // invalidates old token
|
||||
const newAccess = await tokenService.issueAccessToken(session.userId);
|
||||
const newRefresh = await sessionStore.issue(session.userId);
|
||||
return RefreshOutput.parse({ token: newAccess, refreshToken: newRefresh });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RefreshInput`
|
||||
|
||||
Zod object schema — token refresh request body.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|------------------|---------------------------------------------------|
|
||||
| `refreshToken` | `string` (min 1) | Long-lived refresh token from a prior login/refresh|
|
||||
|
||||
**Type export:** `type RefreshInput = z.infer<typeof RefreshInput>`
|
||||
|
||||
### `RefreshOutput`
|
||||
|
||||
Zod object schema — token refresh success response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|------------------|------------------------------------------------|
|
||||
| `token` | `string` (min 1) | New short-lived access token |
|
||||
| `refreshToken` | `string` (min 1) | New long-lived refresh token (rotation) |
|
||||
|
||||
**Type export:** `type RefreshOutput = z.infer<typeof RefreshOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The output deliberately echoes the same shape as `LoginOutput`'s token fields, allowing React hooks to handle both login and refresh responses through a shared token-update function. Token rotation (issuing a new refresh token each time) is recommended by OAuth 2.0 Security Best Practices to limit the damage from a stolen refresh token. The server should record and compare the previous refresh token before issuing the new pair, rejecting any reuse attempts.
|
||||
@@ -0,0 +1,74 @@
|
||||
# register
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for the user registration request and response shapes used by Suaveplan's `POST /auth/register` endpoint. These schemas are the single source of truth for the server route validator, the React registration mutation hook, and the OpenAPI document generator, ensuring that field names, length constraints, and TypeScript types are consistent across every layer without duplication.
|
||||
|
||||
## Features
|
||||
|
||||
- `RegisterInput` schema — validates email (RFC 5322), password (min 8 chars), and display name (1–200 chars)
|
||||
- `RegisterOutput` schema — returns the new user's `id`, `email`, and ISO 8601 `createdAt` timestamp
|
||||
- Password minimum-length policy applied at schema layer (8 characters)
|
||||
- Name length capped at 200 characters to prevent database overflow
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RegisterInput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
const input = RegisterInput.parse({
|
||||
email: "alice@example.com",
|
||||
password: "securePassword1",
|
||||
name: "Alice Smith",
|
||||
});
|
||||
// → { email: "alice@example.com", password: "securePassword1", name: "Alice Smith" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RegisterInput, RegisterOutput } from "@suaveplan/api-schemas/auth";
|
||||
|
||||
async function handleRegister(rawBody: unknown): Promise<RegisterOutput> {
|
||||
const { email, password, name } = RegisterInput.parse(rawBody);
|
||||
const hashedPassword = await crypto.hashPassword(password);
|
||||
const user = await db.user.create({ data: { email, passwordHash: hashedPassword, name } });
|
||||
return RegisterOutput.parse({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RegisterInput`
|
||||
|
||||
Zod object schema — registration request body.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|---------------------------|----------------------------------------------|
|
||||
| `email` | `string` (email) | RFC 5322 valid email address |
|
||||
| `password` | `string` (min 8) | Plaintext password; minimum 8 characters |
|
||||
| `name` | `string` (min 1, max 200) | Display name for the new user |
|
||||
|
||||
**Type export:** `type RegisterInput = z.infer<typeof RegisterInput>`
|
||||
|
||||
### `RegisterOutput`
|
||||
|
||||
Zod object schema — successful registration response.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|----------------------------------|----------------------------------------|
|
||||
| `id` | `string` (min 1) | Newly created user identifier |
|
||||
| `email` | `string` (email) | Email address of the new user |
|
||||
| `createdAt` | `string` (ISO 8601 with offset) | Account creation timestamp |
|
||||
|
||||
**Type export:** `type RegisterOutput = z.infer<typeof RegisterOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 8-character minimum on `password` is a deliberate schema-level policy choice for Suaveplan, aligning with NIST SP 800-63B guidance. Password complexity (uppercase, digits, special chars) is not enforced here — entropy-based policies are the responsibility of the auth service. The `createdAt` field uses `z.string().datetime({ offset: true })` rather than `z.date()` because JSON serialisation produces strings, not `Date` objects, and the schema layer should validate what actually travels over the wire.
|
||||
@@ -0,0 +1,91 @@
|
||||
# error-envelope
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for the standard error response envelope used across all Suaveplan API endpoints. By sharing these schemas between server route handlers, React error-boundary hooks, and OpenAPI generators, every layer of the stack interprets error responses identically — no per-client parsing logic, no mismatched field names, and consistent TypeScript types end-to-end.
|
||||
|
||||
## Features
|
||||
|
||||
- `ErrorEnvelope` schema — `{code, message, details?}` base shape: stable machine-readable code, human-readable message, and an open-typed optional details field
|
||||
- `FieldError` schema — per-field validation error descriptor with dot-notation field path, message, and optional code; suitable for populating `details` on 422 responses
|
||||
- `ValidationErrorEnvelope` schema — extends `ErrorEnvelope` with `details: FieldError[]` for typed 422 Unprocessable Entity responses
|
||||
- Stable `code` strings enable programmatic error handling without string matching on `message`
|
||||
- `details` typed as `z.unknown()` in the base `ErrorEnvelope` to remain flexible; `ValidationErrorEnvelope` provides the typed narrowing for validation failures
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ErrorEnvelope } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Parse an error response body from a failed API call
|
||||
const error = ErrorEnvelope.parse({
|
||||
code: "AUTH_TOKEN_EXPIRED",
|
||||
message: "The access token has expired.",
|
||||
});
|
||||
// → { code: "AUTH_TOKEN_EXPIRED", message: "The access token has expired." }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ValidationErrorEnvelope } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Parse a 422 response body with per-field errors
|
||||
const validationError = ValidationErrorEnvelope.parse({
|
||||
code: "VALIDATION_FAILED",
|
||||
message: "Request body failed validation.",
|
||||
details: [
|
||||
{ field: "email", message: "Must be a valid email address.", code: "INVALID_EMAIL" },
|
||||
{ field: "password", message: "Must be at least 8 characters.", code: "TOO_SHORT" },
|
||||
],
|
||||
});
|
||||
|
||||
// Display field errors in a React form
|
||||
for (const fieldErr of validationError.details) {
|
||||
console.log(`${fieldErr.field}: ${fieldErr.message}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ErrorEnvelope`
|
||||
|
||||
Zod object schema — base error shape for all API error responses.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|--------------------|--------------------------------------------------------|
|
||||
| `code` | `string` (min 1) | Stable machine-readable error code |
|
||||
| `message` | `string` (min 1) | Human-readable error description |
|
||||
| `details` | `unknown` | Optional structured context (field errors, sub-errors)|
|
||||
|
||||
**Type export:** `type ErrorEnvelope = z.infer<typeof ErrorEnvelope>`
|
||||
|
||||
### `FieldError`
|
||||
|
||||
Zod object schema — per-field validation failure descriptor.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|-------------------|--------------------------------------------------|
|
||||
| `field` | `string` (min 1) | Dot-notation path to the offending field |
|
||||
| `message` | `string` (min 1) | Human-readable validation failure description |
|
||||
| `code` | `string` | Optional machine-readable field-error code |
|
||||
|
||||
**Type export:** `type FieldError = z.infer<typeof FieldError>`
|
||||
|
||||
### `ValidationErrorEnvelope`
|
||||
|
||||
Extends `ErrorEnvelope` — typed 422 response envelope.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|-----------------|------------------------------------------|
|
||||
| `code` | `string` | Inherited from `ErrorEnvelope` |
|
||||
| `message` | `string` | Inherited from `ErrorEnvelope` |
|
||||
| `details` | `FieldError[]` | Array of per-field validation errors |
|
||||
|
||||
**Type export:** `type ValidationErrorEnvelope = z.infer<typeof ValidationErrorEnvelope>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The base `ErrorEnvelope.details` field is typed as `z.unknown()` to support the full range of structured error payloads that Suaveplan endpoints may return — not just field validation errors but also rate-limit metadata, conflict information, or retry-after hints. Callers that know they are dealing with a validation error should narrow to `ValidationErrorEnvelope` rather than relying on `details` being `unknown`.
|
||||
@@ -0,0 +1,81 @@
|
||||
# filter
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for filter predicate inputs used in Suaveplan list and search endpoints. These schemas provide a structured, type-safe way to express query conditions that travel from HTTP clients through server validators to data-access layers, with each layer relying on the same schema to avoid translation errors or mismatched operator enumerations.
|
||||
|
||||
## Features
|
||||
|
||||
- `FilterOperator` enum — covers equality (`eq`, `neq`), ordering (`lt`, `lte`, `gt`, `gte`), string matching (`contains`, `startsWith`, `endsWith`), set membership (`in`, `notIn`), and nullability (`isNull`, `isNotNull`)
|
||||
- `FilterInput` object schema — a `{field, operator, value?}` triple with the value optional for unary operators
|
||||
- `FilterInputList` array schema — zero or more predicates combined with AND semantics; empty array means "no filter"
|
||||
- Dot-notation field paths supported (e.g. `"user.email"`, `"metadata.tags"`)
|
||||
- `value` typed as `z.unknown()` to remain schema-layer agnostic — data adapters perform their own value coercion
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { FilterInput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Filter records where status equals "active"
|
||||
const filter = FilterInput.parse({
|
||||
field: "status",
|
||||
operator: "eq",
|
||||
value: "active",
|
||||
});
|
||||
// → { field: "status", operator: "eq", value: "active" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { FilterInputList } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// AND-joined predicates: active users whose email ends with @example.com
|
||||
const filters = FilterInputList.parse([
|
||||
{ field: "status", operator: "eq", value: "active" },
|
||||
{ field: "email", operator: "endsWith", value: "@example.com" },
|
||||
{ field: "deletedAt", operator: "isNull" },
|
||||
]);
|
||||
|
||||
// Translate to a query builder predicate list
|
||||
function applyFilters(query: SomeQueryBuilder, filters: typeof filters) {
|
||||
for (const { field, operator, value } of filters) {
|
||||
if (operator === "isNull") query.whereNull(field);
|
||||
else if (operator === "eq") query.where(field, "=", value);
|
||||
// … other operators
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `FilterOperator`
|
||||
|
||||
Zod enum with values: `"eq" | "neq" | "lt" | "lte" | "gt" | "gte" | "contains" | "startsWith" | "endsWith" | "in" | "notIn" | "isNull" | "isNotNull"`.
|
||||
|
||||
**Type export:** `type FilterOperator = z.infer<typeof FilterOperator>`
|
||||
|
||||
### `FilterInput`
|
||||
|
||||
Zod object schema.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|------------------|----------------------------------------------------------|
|
||||
| `field` | `string` (min 1) | Dot-notation field path to filter on |
|
||||
| `operator` | `FilterOperator` | Comparison operator |
|
||||
| `value` | `unknown` | Operand; omit for `isNull`/`isNotNull` unary operators |
|
||||
|
||||
**Type export:** `type FilterInput = z.infer<typeof FilterInput>`
|
||||
|
||||
### `FilterInputList`
|
||||
|
||||
Zod array of `FilterInput`. Empty array is valid and means "apply no filter".
|
||||
|
||||
**Type export:** `type FilterInputList = z.infer<typeof FilterInputList>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `value` field uses `z.unknown()` deliberately. Specific data adapters (SQL, SurrealDB, Elasticsearch) each interpret value types differently, and this schema layer should not impose premature coercion. Adapters should validate and coerce `value` according to the target field's type and the operator semantics. For `isNull`/`isNotNull` operators, the value is meaningless and should be ignored by adapters even if accidentally supplied by a client.
|
||||
@@ -0,0 +1,62 @@
|
||||
# id-param
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for URL path parameter objects carrying resource identifiers. Rather than validating raw strings ad-hoc in every route handler, all Suaveplan endpoints that accept an `:id` segment share this schema, ensuring consistent error messages, type inference, and zero-duplication across the server plugin layer and generated OpenAPI documents.
|
||||
|
||||
## Features
|
||||
|
||||
- `IdParam` object schema — validates a single `{id: string}` path parameter (non-empty)
|
||||
- `NestedIdParam` object schema — validates `{parentId, id}` pairs for nested resource routes (e.g. `/orgs/:orgId/members/:id`)
|
||||
- Both schemas enforce non-empty strings, catching blank or missing identifiers before any database query is attempted
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { IdParam } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Validate route params from an HTTP framework
|
||||
const params = IdParam.parse({ id: "user_01J8ZGCZ7ECTFAKEUID123456" });
|
||||
// → { id: "user_01J8ZGCZ7ECTFAKEUID123456" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NestedIdParam } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Validate nested resource route params: /orgs/:parentId/members/:id
|
||||
function getMember(rawParams: unknown) {
|
||||
const { parentId, id } = NestedIdParam.parse(rawParams);
|
||||
return db.member.findUnique({ where: { id, orgId: parentId } });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IdParam`
|
||||
|
||||
Zod object schema for single-resource routes.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------------------|------------------------------------------------|
|
||||
| `id` | `string` (min 1) | Non-empty resource identifier from URL segment |
|
||||
|
||||
**Type export:** `type IdParam = z.infer<typeof IdParam>`
|
||||
|
||||
### `NestedIdParam`
|
||||
|
||||
Zod object schema for nested-resource routes.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|------------------|----------------------------------|
|
||||
| `parentId` | `string` (min 1) | Parent resource identifier |
|
||||
| `id` | `string` (min 1) | Child resource identifier |
|
||||
|
||||
**Type export:** `type NestedIdParam = z.infer<typeof NestedIdParam>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Both schemas use `z.string().min(1)` rather than additional format constraints (UUID, ULID, etc.) to keep them format-agnostic. Suaveplan uses multiple ID formats depending on the domain (ULIDs for user-facing resources, SHA hashes for content-addressable objects, numeric sequences for legacy integrations). Services that require a specific format should compose these schemas with `.refine()` or `.regex()` at the route layer rather than tightening the shared base schema.
|
||||
@@ -0,0 +1,79 @@
|
||||
# pagination
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for pagination input and output shapes used across all Suaveplan list endpoints. These schemas serve as the single source of truth that is consumed by server route handlers, React data-fetching hooks, and OpenAPI document generators alike, ensuring cursor-based and offset-based pagination are expressed identically across every layer of the stack.
|
||||
|
||||
## Features
|
||||
|
||||
- `PaginationInput` — accepts an optional 1-based `page` number, a `limit` (clamped 1–200), and an opaque `cursor` string; all fields optional so callers can omit what they don't need
|
||||
- `PaginationOutput` — carries total count (nullable when unknown), current page, limit, `hasNextPage` flag, and opaque `startCursor`/`endCursor` strings for cursor-based traversal
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Both cursor-based and offset-based strategies represented in one schema set
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PaginationInput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Parse a query-string object coming from an HTTP request
|
||||
const params = PaginationInput.parse({ limit: 20, cursor: "eyJpZCI6IjEifQ==" });
|
||||
// → { limit: 20, cursor: "eyJpZCI6IjEifQ==" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PaginationInput, PaginationOutput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Validate request query params with safe parse
|
||||
const result = PaginationInput.safeParse({ page: 2, limit: 50 });
|
||||
if (!result.success) {
|
||||
console.error(result.error.flatten());
|
||||
} else {
|
||||
const { page, limit } = result.data;
|
||||
// Use page/limit to run a DB query, then build PaginationOutput:
|
||||
const meta = PaginationOutput.parse({
|
||||
total: 120,
|
||||
page: 2,
|
||||
limit: 50,
|
||||
hasNextPage: true,
|
||||
startCursor: "eyJpZCI6IjUxIn0=",
|
||||
endCursor: "eyJpZCI6IjEwMCJ9",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PaginationInput`
|
||||
|
||||
Zod object schema. All fields optional.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------|--------------------------|-------------------------------------------------------|
|
||||
| `page` | `number` (int ≥ 1) | 1-based page index; ignored when `cursor` is present |
|
||||
| `limit` | `number` (int 1–200) | Maximum items per page |
|
||||
| `cursor` | `string` (min length 1) | Opaque cursor from a prior response |
|
||||
|
||||
**Type export:** `type PaginationInput = z.infer<typeof PaginationInput>`
|
||||
|
||||
### `PaginationOutput`
|
||||
|
||||
Zod object schema. Returned as part of list responses.
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------------|----------------------------|--------------------------------------------------|
|
||||
| `total` | `number \| null` | Total matching records; null when count is skipped|
|
||||
| `page` | `number \| null` | Current page (null for pure cursor pagination) |
|
||||
| `limit` | `number` (int ≥ 1) | Items per page |
|
||||
| `hasNextPage` | `boolean` | Whether more items follow this page |
|
||||
| `startCursor` | `string \| null` | Cursor pointing to first item in this page |
|
||||
| `endCursor` | `string \| null` | Cursor pointing to last item in this page |
|
||||
|
||||
**Type export:** `type PaginationOutput = z.infer<typeof PaginationOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All fields in `PaginationInput` are optional to support partial query-string parsing in HTTP frameworks that pass unknown keys through. The `limit` field is hard-clamped to 200 to prevent accidental unbounded queries; services that require a different ceiling should create a refined schema via `.superRefine()`. The cursor is validated as a non-empty string but its internal encoding is opaque to this schema layer — encoding/decoding is the responsibility of the data adapter.
|
||||
@@ -0,0 +1,74 @@
|
||||
# sort
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for sort criteria used in Suaveplan list and search endpoints. By centralising the `SortInput` and `SortDirection` definitions here, every layer of the application — from REST route validators to ORM query builders to OpenAPI generators — derives its types from a single authoritative schema, preventing divergence between what the API accepts and what the database layer interprets.
|
||||
|
||||
## Features
|
||||
|
||||
- `SortDirection` enum schema — enforces only `"asc"` or `"desc"` values
|
||||
- `SortInput` object schema — a single `{field, direction}` pair with `direction` defaulting to `"asc"` for convenience
|
||||
- `SortInputList` array schema — ordered list of `SortInput` for multi-column sorts (first element has highest precedence)
|
||||
- Dot-notation field paths supported (e.g. `"user.createdAt"`)
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SortInput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Parse a single sort criterion
|
||||
const sort = SortInput.parse({ field: "createdAt", direction: "desc" });
|
||||
// → { field: "createdAt", direction: "desc" }
|
||||
|
||||
// Direction defaults to "asc" when omitted
|
||||
const sortAsc = SortInput.parse({ field: "name" });
|
||||
// → { field: "name", direction: "asc" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SortInputList } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Multi-column sort: primary by createdAt desc, secondary by name asc
|
||||
const sorts = SortInputList.parse([
|
||||
{ field: "createdAt", direction: "desc" },
|
||||
{ field: "name" },
|
||||
]);
|
||||
|
||||
// Apply to an ORM query
|
||||
function buildOrderBy(sorts: typeof sorts) {
|
||||
return sorts.map(({ field, direction }) => ({ [field]: direction }));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SortDirection`
|
||||
|
||||
Zod enum: `"asc" | "desc"`.
|
||||
|
||||
**Type export:** `type SortDirection = z.infer<typeof SortDirection>`
|
||||
|
||||
### `SortInput`
|
||||
|
||||
Zod object schema.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|-----------------|------------------------------------------------------------|
|
||||
| `field` | `string` (min 1)| Dot-notation field path to sort on |
|
||||
| `direction` | `SortDirection` | `"asc"` or `"desc"`; defaults to `"asc"` when not provided|
|
||||
|
||||
**Type export:** `type SortInput = z.infer<typeof SortInput>`
|
||||
|
||||
### `SortInputList`
|
||||
|
||||
Zod array of `SortInput` (min length 1). First element has highest sort precedence.
|
||||
|
||||
**Type export:** `type SortInputList = z.infer<typeof SortInputList>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `direction` field uses Zod's `.default("asc")` so that it is always present on parsed output regardless of whether the client omitted it. This removes the need for downstream `?? "asc"` guard expressions. The `SortInputList` requires at least one element to prevent callers from passing an empty array, which would result in a no-op sort that silently returns database-ordered results — an ambiguous state that is better caught at the schema layer.
|
||||
@@ -0,0 +1,71 @@
|
||||
# timestamps
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines Zod schemas for the audit timestamp fields (`createdAt`, `updatedAt`, and optionally `deletedAt`) that appear on virtually every Suaveplan resource response. Centralising these schemas ensures that every endpoint returns timestamps in the same ISO 8601 wire format and that the TypeScript types derived from the schemas are consistent across server handlers, React data hooks, and OpenAPI generators.
|
||||
|
||||
## Features
|
||||
|
||||
- `TimestampsOutput` schema — `{createdAt, updatedAt}` as ISO 8601 strings with timezone offset (`z.string().datetime({ offset: true })`)
|
||||
- `SoftDeleteTimestampsOutput` schema — extends `TimestampsOutput` with a nullable `deletedAt` field for soft-delete patterns
|
||||
- Strict ISO 8601 validation with timezone offset requirement — prevents bare local-time strings
|
||||
- Composable via `z.extend()` so resource schemas can merge timestamps in without boilerplate
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TimestampsOutput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
const ts = TimestampsOutput.parse({
|
||||
createdAt: "2024-01-15T12:00:00Z",
|
||||
updatedAt: "2024-06-01T09:30:00+02:00",
|
||||
});
|
||||
// → { createdAt: "2024-01-15T12:00:00Z", updatedAt: "2024-06-01T09:30:00+02:00" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import z from "zod";
|
||||
import { TimestampsOutput, SoftDeleteTimestampsOutput } from "@suaveplan/api-schemas/common";
|
||||
|
||||
// Merge timestamps into a resource schema
|
||||
const UserResponse = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().email(),
|
||||
}).merge(SoftDeleteTimestampsOutput);
|
||||
|
||||
type UserResponse = z.infer<typeof UserResponse>;
|
||||
// → { id: string; email: string; createdAt: string; updatedAt: string; deletedAt: string | null }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `TimestampsOutput`
|
||||
|
||||
Zod object schema.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|-------------------|-----------------------------------------------------|
|
||||
| `createdAt` | `string` (ISO 8601 with offset) | Resource creation timestamp |
|
||||
| `updatedAt` | `string` (ISO 8601 with offset) | Most recent modification timestamp |
|
||||
|
||||
**Type export:** `type TimestampsOutput = z.infer<typeof TimestampsOutput>`
|
||||
|
||||
### `SoftDeleteTimestampsOutput`
|
||||
|
||||
Extends `TimestampsOutput`.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------------|----------------------------------|----------------------------------------------|
|
||||
| `createdAt` | `string` (ISO 8601 with offset) | Inherited from `TimestampsOutput` |
|
||||
| `updatedAt` | `string` (ISO 8601 with offset) | Inherited from `TimestampsOutput` |
|
||||
| `deletedAt` | `string \| null` (ISO 8601 with offset) | Soft-delete timestamp, null if not deleted |
|
||||
|
||||
**Type export:** `type SoftDeleteTimestampsOutput = z.infer<typeof SoftDeleteTimestampsOutput>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `{ offset: true }` option on `z.string().datetime()` requires that the timestamp include a UTC offset (e.g. `Z` or `+HH:MM`). This prevents bare local-time strings like `"2024-01-15T12:00:00"` from passing validation, which is important because ambiguous timestamps cause subtle bugs when crossing timezone boundaries. All Suaveplan services store and transmit timestamps in UTC or with explicit offset.
|
||||
@@ -0,0 +1,71 @@
|
||||
# domain-event
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `DomainEvent` Zod schema — the canonical wire format for immutable domain events flowing through the Suaveplan event-sourcing and event-driven integration infrastructure. Domain events record facts about things that happened within a bounded context: "UserRegistered", "OrderPlaced", "PaymentFailed". This schema provides the structural envelope; the domain-specific payload schema is defined and validated by each aggregate separately.
|
||||
|
||||
## Features
|
||||
|
||||
- `DomainEvent` schema — `{id, type, aggregateId, aggregateType, payload, occurredAt}` fully typed
|
||||
- `type` as a dot-separated event-type name (e.g. `"user.registered"`) — convention over constraint
|
||||
- `aggregateId` + `aggregateType` together identify which aggregate instance raised the event
|
||||
- `payload` typed as `z.unknown()` — the envelope is payload-agnostic; aggregate event types carry their own Zod schema
|
||||
- `occurredAt` as an ISO 8601 string with timezone offset — the moment in domain time when the event occurred
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DomainEvent } from "@suaveplan/api-schemas/events";
|
||||
|
||||
const event = DomainEvent.parse({
|
||||
id: "evt_01J8ZGCZ7ECTFAKEUID123456",
|
||||
type: "user.registered",
|
||||
aggregateId: "user_abc123",
|
||||
aggregateType: "User",
|
||||
payload: { email: "alice@example.com", name: "Alice" },
|
||||
occurredAt: "2024-06-01T12:00:00Z",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DomainEvent } from "@suaveplan/api-schemas/events";
|
||||
import z from "zod";
|
||||
|
||||
// Narrow the payload for a specific event type
|
||||
const UserRegisteredPayload = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
function handleUserRegistered(raw: unknown) {
|
||||
const event = DomainEvent.parse(raw);
|
||||
if (event.type !== "user.registered") return;
|
||||
const payload = UserRegisteredPayload.parse(event.payload);
|
||||
console.log(`New user: ${payload.name} (${payload.email})`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `DomainEvent`
|
||||
|
||||
Zod object schema — immutable domain event record.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|---------------------------------|------------------------------------------------------|
|
||||
| `id` | `string` (min 1) | Unique event identifier |
|
||||
| `type` | `string` (min 1) | Event type name (e.g. `"user.registered"`) |
|
||||
| `aggregateId` | `string` (min 1) | Identifier of the source aggregate |
|
||||
| `aggregateType` | `string` (min 1) | Type name of the source aggregate |
|
||||
| `payload` | `unknown` | Domain-specific event data |
|
||||
| `occurredAt` | `string` (ISO 8601 with offset) | Domain time when the event occurred |
|
||||
|
||||
**Type export:** `type DomainEvent = z.infer<typeof DomainEvent>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`payload` is `z.unknown()` intentionally — the base schema validates the event envelope, not the payload contents. Each aggregate type defines its own Zod schema for each of its event types; payload validation is performed after routing by event type. This pattern keeps the event bus loosely coupled and avoids a centralised schema registry for domain events, which would become a bottleneck in a large bounded-context landscape.
|
||||
@@ -0,0 +1,69 @@
|
||||
# envelope
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `EventEnvelope` Zod schema — the transport wrapper that carries a domain event across message brokers, event store write operations, and event projector ingestion pipelines. The envelope separates the domain event (what happened) from the routing metadata (why it happened and who caused it) and adds a sequence number for ordering guarantees and gap detection during replay.
|
||||
|
||||
## Features
|
||||
|
||||
- `EventEnvelope` schema — composes `DomainEvent`, `EventMetadata`, and a `sequence` integer
|
||||
- `sequence` validated as a non-negative integer — zero is a valid sequence (first event in a store)
|
||||
- Separation of domain event, metadata, and sequence concerns into distinct fields
|
||||
- Composable from existing schemas — no duplication of `DomainEvent` or `EventMetadata` fields
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EventEnvelope } from "@suaveplan/api-schemas/events";
|
||||
|
||||
const envelope = EventEnvelope.parse({
|
||||
event: {
|
||||
id: "evt_1",
|
||||
type: "user.registered",
|
||||
aggregateId: "user_abc",
|
||||
aggregateType: "User",
|
||||
payload: { email: "alice@example.com" },
|
||||
occurredAt: "2024-06-01T12:00:00Z",
|
||||
},
|
||||
metadata: { correlationId: "corr_1", userId: "user_abc" },
|
||||
sequence: 42,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { EventEnvelope } from "@suaveplan/api-schemas/events";
|
||||
|
||||
// Deserialise a batch of envelopes from the event store
|
||||
function parseEnvelopes(rawRows: unknown[]): EventEnvelope[] {
|
||||
return rawRows.map((row) => EventEnvelope.parse(row));
|
||||
}
|
||||
|
||||
// Detect sequence gaps (missing events)
|
||||
function hasGap(envelopes: EventEnvelope[]): boolean {
|
||||
return envelopes.some(
|
||||
(env, i) => i > 0 && env.sequence !== (envelopes[i - 1]?.sequence ?? -1) + 1
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EventEnvelope`
|
||||
|
||||
Zod object schema — event transport wrapper.
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------|-----------------|-------------------------------------------------------|
|
||||
| `event` | `DomainEvent` | The domain event being transported |
|
||||
| `metadata` | `EventMetadata` | Correlation, causation, and actor context |
|
||||
| `sequence` | `number` (int ≥ 0)| Store/broker sequence number for ordering |
|
||||
|
||||
**Type export:** `type EventEnvelope = z.infer<typeof EventEnvelope>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `sequence` field is assigned by the event store or message broker at write time — not by the aggregate or command handler. Consumers use `sequence` for two purposes: (1) ordered replay during projection rebuilds, and (2) gap detection when a consumer processes events from a partitioned broker and needs to confirm no events were missed. Using `z.number().int().min(0)` allows sequence 0 as the first valid sequence number, which is conventional in zero-based event stores.
|
||||
@@ -0,0 +1,64 @@
|
||||
# metadata
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `EventMetadata` Zod schema for the distributed tracing and actor context attached to every domain event in the Suaveplan event-sourcing infrastructure. Metadata fields enable correlation of all events produced by a single user action, causation tracing between commands and their resulting events, and multi-tenant scoping of event streams — all without embedding these concerns in domain event payloads.
|
||||
|
||||
## Features
|
||||
|
||||
- `EventMetadata` schema — `{correlationId, causationId?, userId?, tenantId?}` with strict non-empty validation on all present string fields
|
||||
- `correlationId` required — enables log correlation and distributed trace aggregation across service boundaries
|
||||
- `causationId` optional — links this event to the specific command or event that caused it (event sourcing causal chain)
|
||||
- `userId` optional — identifies the human or service account that triggered the originating action
|
||||
- `tenantId` optional — scopes the event to a tenant in multi-tenant deployments
|
||||
- Full TypeScript inference via `z.infer<>` — no manual interface duplication
|
||||
- Zod v4 compatible
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EventMetadata } from "@suaveplan/api-schemas/events";
|
||||
|
||||
const meta = EventMetadata.parse({
|
||||
correlationId: "corr_01J8ZGCZ7ECTFAKEUID123456",
|
||||
userId: "user_abc",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { EventMetadata } from "@suaveplan/api-schemas/events";
|
||||
|
||||
// Build metadata for an event triggered by a command
|
||||
function buildEventMetadata(
|
||||
command: { correlationId: string; id: string },
|
||||
ctx: { userId?: string; tenantId?: string }
|
||||
): EventMetadata {
|
||||
return EventMetadata.parse({
|
||||
correlationId: command.correlationId,
|
||||
causationId: command.id,
|
||||
userId: ctx.userId,
|
||||
tenantId: ctx.tenantId,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EventMetadata`
|
||||
|
||||
Zod object schema — event tracing and actor context.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|--------------------|----------------------------------------------------------|
|
||||
| `correlationId` | `string` (min 1) | Ties all events from one user action together |
|
||||
| `causationId` | `string` (optional)| ID of the command/event that caused this event |
|
||||
| `userId` | `string` (optional)| Actor who triggered the originating action |
|
||||
| `tenantId` | `string` (optional)| Tenant scope for multi-tenant deployments |
|
||||
|
||||
**Type export:** `type EventMetadata = z.infer<typeof EventMetadata>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`correlationId` is the only required field because it is the primary key for grouping related events in log aggregation and tracing systems. All other fields are optional because not every event originates from an identifiable user action (e.g. scheduled jobs, system-initiated events) or requires tenant scoping. Optional fields that are present are validated as non-empty strings to prevent accidentally storing blank identifiers that would pollute trace data.
|
||||
@@ -0,0 +1,74 @@
|
||||
# to-suaveplan-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a `toSuaveplanError` helper function that converts a Zod `ZodError` into a plain `ErrorEnvelope`-shaped object with per-field `FieldError` details. This helper bridges the Zod validation layer and the Suaveplan API error response format without introducing a runtime dependency on `@suaveplan/error`, keeping `@suaveplan/api-schemas` a true leaf package that can be imported in any environment.
|
||||
|
||||
## Features
|
||||
|
||||
- Converts `ZodError.issues` into an array of `FieldError` objects with dot-notation field paths
|
||||
- Aggregates all validation failures into a single `ErrorEnvelope` with `details: FieldError[]`
|
||||
- Customisable `code` and top-level `message` overrides via optional parameters
|
||||
- Issues with empty `path` (root-level failures) map to the special `"_root"` field name
|
||||
- Zero dependencies on `@suaveplan/error` — operates entirely within the schema layer
|
||||
- Full TypeScript inference — return type is `ErrorEnvelope & { details: FieldError[] }`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import z from "zod";
|
||||
import { toSuaveplanError } from "@suaveplan/api-schemas/helpers";
|
||||
|
||||
const schema = z.object({ email: z.string().email(), age: z.number().min(18) });
|
||||
const result = schema.safeParse({ email: "bad", age: 5 });
|
||||
if (!result.success) {
|
||||
const envelope = toSuaveplanError(result.error);
|
||||
console.log(envelope.code); // "VALIDATION_FAILED"
|
||||
console.log(envelope.details); // [{ field: "email", ... }, { field: "age", ... }]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import z from "zod";
|
||||
import { toSuaveplanError } from "@suaveplan/api-schemas/helpers";
|
||||
|
||||
// Use a custom error code and message for a specific endpoint
|
||||
const LoginInput = z.object({ email: z.string().email(), password: z.string().min(1) });
|
||||
|
||||
function validateLoginBody(body: unknown) {
|
||||
const result = LoginInput.safeParse(body);
|
||||
if (!result.success) {
|
||||
throw toSuaveplanError(
|
||||
result.error,
|
||||
"LOGIN_VALIDATION_FAILED",
|
||||
"Login credentials failed validation."
|
||||
);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `toSuaveplanError(error, code?, message?)`
|
||||
|
||||
Converts a Zod `ZodError` to an `ErrorEnvelope & { details: FieldError[] }`.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------------|--------------------------------|----------------------------------------------------|
|
||||
| `error` | `ZodError` | — | The Zod validation error to convert |
|
||||
| `code` | `string` | `"VALIDATION_FAILED"` | Stable machine-readable error code |
|
||||
| `message` | `string` | `"Request validation failed."` | Human-readable top-level message |
|
||||
|
||||
**Returns:** `ErrorEnvelope & { details: FieldError[] }` — a plain object (not a class instance).
|
||||
|
||||
**Throws:** Nothing — all ZodError issues are converted to FieldError entries.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Field paths from `ZodError.issues` are arrays of strings and numbers (for array indices). These are joined with `"."` to produce dot-notation paths (e.g. `"user.address.0.street"`), which aligns with the `FilterInput.field` convention and common form-library path formats. Issues with an empty `path` array represent root-level schema failures (e.g. a union discriminant mismatch) and are mapped to the sentinel field name `"_root"` to remain a valid non-empty string as required by the `FieldError` schema.
|
||||
Reference in New Issue
Block a user