first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.