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,105 @@
|
||||
# event-schemas
|
||||
|
||||
## Purpose
|
||||
|
||||
The package-local Zod schemas for the `event` entity surface. Owns
|
||||
every input / output shape touching the events routes and
|
||||
repositories: the persisted `EventSchema`, the create / update
|
||||
request bodies, the list-query parameters, the path param shape,
|
||||
the `EventLocationSchema` value object, and the `EventStatusSchema`
|
||||
0..2 integer encoding. The `@suaveplan/api-schemas` package
|
||||
ships only event-bus envelope schemas (`DomainEvent`,
|
||||
`EventEnvelope`, `EventMetadata`); the event-management entity
|
||||
schemas are owned here because no other package needs them.
|
||||
|
||||
The schemas double as the type sources — every exported `*Schema`
|
||||
has a matching `type Foo = z.infer<typeof FooSchema>` so consumers
|
||||
get a single source of truth for both runtime validation and
|
||||
compile-time narrowing. The `CreateEventSchema` and
|
||||
`UpdateEventSchema` carry a `.refine()` constraint that asserts
|
||||
`end_time > start_time` at the parser boundary so the repository
|
||||
never sees a malformed time pair.
|
||||
|
||||
## Features
|
||||
|
||||
- 11 schemas spanning persistence, request, response, and value
|
||||
objects.
|
||||
- `CreateEventSchema` / `UpdateEventSchema` enforce `end_time > start_time`
|
||||
via `.refine()`.
|
||||
- `EventStatusSchema` constrains status to `0..2` (draft / active /
|
||||
cancelled) matching the upstream encoding.
|
||||
- `EventQuerySchema` coerces query-string `limit` / `offset` /
|
||||
`upcoming` / `past` to typed numbers / booleans.
|
||||
- `EventLocationSchema` validates `(lat, lng)` coordinates against
|
||||
geographic bounds.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CreateEventSchema, EventSchema } from "@suaveplan/api-plugin-events";
|
||||
|
||||
const parsed = CreateEventSchema.parse({
|
||||
title: "Meetup",
|
||||
start_time: "2026-06-01T18:00:00Z",
|
||||
end_time: "2026-06-01T22:00:00Z",
|
||||
});
|
||||
console.log(parsed.timezone); // "UTC"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
EventQuerySchema,
|
||||
EventListResponseSchema,
|
||||
UpdateEventSchema,
|
||||
} from "@suaveplan/api-plugin-events";
|
||||
|
||||
// Coerce from URL search params
|
||||
const url = new URL("https://api.example.com/events?limit=10&past=true");
|
||||
const query = EventQuerySchema.parse(Object.fromEntries(url.searchParams));
|
||||
|
||||
// Build a typed response payload
|
||||
const body = EventListResponseSchema.parse({
|
||||
data: [
|
||||
/* events from repository */
|
||||
],
|
||||
total: 42,
|
||||
});
|
||||
|
||||
// Forbid time inversion at the boundary
|
||||
const result = UpdateEventSchema.safeParse({
|
||||
start_time: "2026-06-01T22:00:00Z",
|
||||
end_time: "2026-06-01T18:00:00Z",
|
||||
});
|
||||
if (!result.success) {
|
||||
// result.error.issues[0].path === ["end_time"]
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `EventStatusSchema: z.ZodNumber` — integer 0..2.
|
||||
- `EventLocationSchema: z.ZodObject` — `{ name, address?, coordinates? }`.
|
||||
- `AttendeeCountSchema: z.ZodObject` — `{ going, maybe, invited }`.
|
||||
- `EventSchema: z.ZodObject` — the persisted event row.
|
||||
- `CreateEventSchema: z.ZodEffects<...>` — POST `/` body.
|
||||
- `UpdateEventSchema: z.ZodEffects<...>` — PUT `/{id}` body.
|
||||
- `EventQuerySchema: z.ZodObject` — GET `/` query params.
|
||||
- `EventIdParamSchema: z.ZodObject` — `{ id: string }`.
|
||||
- `EventListResponseSchema: z.ZodObject` — GET `/` response.
|
||||
|
||||
Every schema exports a sibling `type Foo = z.infer<typeof FooSchema>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`status` is encoded as an integer (`0=draft`, `1=active`,
|
||||
`2=cancelled`) rather than a string enum to match the upstream
|
||||
SurrealDB schema and to keep label cardinality low when emitted as
|
||||
OTel attributes. The two cross-field `.refine()` rules
|
||||
(`end_time > start_time`) are duplicated on `CreateEventSchema` and
|
||||
`UpdateEventSchema` rather than factored to a shared helper because
|
||||
the `UpdateEventSchema` variant must tolerate a single-field update
|
||||
(`{ title }`) without raising the refinement, which means the rule
|
||||
fires only when both `start_time` and `end_time` appear in the same
|
||||
payload.
|
||||
@@ -0,0 +1,101 @@
|
||||
# options
|
||||
|
||||
## Purpose
|
||||
|
||||
The Zod schema for `createEventsPlugin`'s options bundle. Three
|
||||
fields: `basePath` (mount prefix, default `/events`),
|
||||
`enableNotifications` (reserved for a future
|
||||
`@suaveplan/notification-channel` binding, default `false`),
|
||||
`maxInvitesPerRequest` (runtime cap on the `/invite` endpoint,
|
||||
default `50`, range `1..100`).
|
||||
|
||||
The schema is the boundary between caller input and the resolved
|
||||
config that flows through every lifecycle hook. The plugin factory
|
||||
calls `EventsPluginOptionsSchema.safeParse(options)` at construction
|
||||
time and raises `EventsPluginError(INVALID_CONFIG)` on failure so an
|
||||
invalid `basePath` or out-of-range `maxInvitesPerRequest` fails
|
||||
before the descriptor leaves the factory and never reaches the
|
||||
registry.
|
||||
|
||||
## Features
|
||||
|
||||
- Every field has a default — `EventsPluginOptionsSchema.parse({})`
|
||||
produces the canonical resolved config.
|
||||
- `basePath` is a non-empty string.
|
||||
- `maxInvitesPerRequest` is an integer in `[1, 100]`.
|
||||
- `EventsPluginConfigInput` (the `z.input` type) is partial-by-default
|
||||
for the caller; `EventsPluginConfig` (the `z.infer` type) is
|
||||
fully-populated for the runtime.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
EventsPluginOptionsSchema,
|
||||
type EventsPluginConfig,
|
||||
} from "@suaveplan/api-plugin-events";
|
||||
|
||||
const config: EventsPluginConfig = EventsPluginOptionsSchema.parse({});
|
||||
console.log(config.basePath); // "/events"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
EventsPluginOptionsSchema,
|
||||
type EventsPluginConfigInput,
|
||||
} from "@suaveplan/api-plugin-events";
|
||||
|
||||
function deriveConfig(env: Record<string, string | undefined>) {
|
||||
const input: EventsPluginConfigInput = {
|
||||
basePath: env.EVENTS_BASE_PATH ?? "/events",
|
||||
enableNotifications: env.EVENTS_NOTIFY === "true",
|
||||
maxInvitesPerRequest: env.EVENTS_INVITE_CAP
|
||||
? Number.parseInt(env.EVENTS_INVITE_CAP, 10)
|
||||
: 50,
|
||||
};
|
||||
const result = EventsPluginOptionsSchema.safeParse(input);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`events plugin misconfigured: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EventsPluginOptionsSchema: z.ZodObject`
|
||||
|
||||
- `basePath: z.ZodDefault<z.ZodString>` — default `/events`.
|
||||
- `enableNotifications: z.ZodDefault<z.ZodBoolean>` — default `false`.
|
||||
- `maxInvitesPerRequest: z.ZodDefault<z.ZodNumber>` — int `[1, 100]`,
|
||||
default `50`.
|
||||
|
||||
### `type EventsPluginConfig`
|
||||
|
||||
`z.infer<typeof EventsPluginOptionsSchema>` — the resolved, defaults-
|
||||
applied shape passed to lifecycle hooks.
|
||||
|
||||
### `type EventsPluginConfigInput`
|
||||
|
||||
`z.input<typeof EventsPluginOptionsSchema>` — the caller-facing
|
||||
partial shape accepted by `createEventsPlugin`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`enableNotifications` is reserved for the future
|
||||
`@suaveplan/notification-channel` binding (see `## Out of scope` in
|
||||
the proposal). The flag is validated and threaded through the
|
||||
resolved config today so consumers can opt-in without a breaking
|
||||
change later. The schema enforces the type contract; the binding
|
||||
itself lands when the notification-channel package migrates.
|
||||
|
||||
`maxInvitesPerRequest` is intentionally capped at the schema layer
|
||||
(100) below the body schema's hard cap (`InviteUsersSchema.user_ids`
|
||||
max 100) so the route handler can return a structured
|
||||
`INVITE_LIMIT_EXCEEDED` error instead of letting Zod raise a more
|
||||
opaque `Array must contain at most 100 element(s)` validation
|
||||
failure. The two caps coexist by design.
|
||||
@@ -0,0 +1,99 @@
|
||||
# rsvp-schemas
|
||||
|
||||
## Purpose
|
||||
|
||||
Local Zod schemas for the RSVP / invitation surface. Owns the
|
||||
persisted `RsvpSchema`, the `CreateRsvpSchema` request body for
|
||||
`POST /:id/rsvp`, the `InviteUsersSchema` body for
|
||||
`POST /:id/invite`, the `AttendeeQuerySchema` for the attendee
|
||||
listing endpoint, and the joined `AttendeeSchema` that combines the
|
||||
RSVP row with optional sparse user fields (`user_name`,
|
||||
`user_email`, `user_image`).
|
||||
|
||||
The RSVP status is a four-value string enum (`invited`, `going`,
|
||||
`maybe`, `not_going`) — not the integer encoding used for event
|
||||
status — because RSVPs are read-heavy and the integer encoding gave
|
||||
no real-world benefit when the upstream surface was originally
|
||||
specced. The `InviteUsersSchema` enforces a 1..100 length range as
|
||||
the schema-level hard cap; the runtime cap (default 50) is enforced
|
||||
separately inside the `POST /:id/invite` handler so deployments can
|
||||
narrow without recompiling.
|
||||
|
||||
## Features
|
||||
|
||||
- 9 schemas spanning persistence, request, response, and pagination.
|
||||
- `RsvpStatusSchema` constrains status to the four documented values.
|
||||
- `InviteUsersSchema` enforces `1 <= length <= 100` on `user_ids`
|
||||
with non-empty-string members.
|
||||
- `AttendeeSchema` extends `RsvpSchema` with optional sparse user
|
||||
fields for joined listing endpoints.
|
||||
- `AttendeeQuerySchema` coerces query-string parameters.
|
||||
- `RsvpResponseSchema` carries a nullable `rsvp` field for
|
||||
"no current RSVP" representations.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CreateRsvpSchema, RsvpStatusSchema } from "@suaveplan/api-plugin-events";
|
||||
|
||||
const body = CreateRsvpSchema.parse({ status: "going" });
|
||||
const isValid = RsvpStatusSchema.safeParse("yes").success; // false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AttendeeListResponseSchema,
|
||||
AttendeeQuerySchema,
|
||||
InviteResponseSchema,
|
||||
InviteUsersSchema,
|
||||
} from "@suaveplan/api-plugin-events";
|
||||
|
||||
// Validate an invite body, then enforce the runtime cap separately
|
||||
const parsed = InviteUsersSchema.parse({ user_ids: ["user:1", "user:2"] });
|
||||
if (parsed.user_ids.length > config.maxInvitesPerRequest) {
|
||||
return res.status(400).json({ error: "INVITE_LIMIT_EXCEEDED" });
|
||||
}
|
||||
|
||||
// Build a paginated attendee response
|
||||
const query = AttendeeQuerySchema.parse({ status: "going", limit: "100" });
|
||||
const response = AttendeeListResponseSchema.parse({
|
||||
data: [
|
||||
/* joined attendees */
|
||||
],
|
||||
total: 42,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RsvpStatusSchema: z.ZodEnum<["invited", "going", "maybe", "not_going"]>`.
|
||||
- `RsvpSchema: z.ZodObject` — persisted RSVP row.
|
||||
- `CreateRsvpSchema: z.ZodObject` — `POST /:id/rsvp` body.
|
||||
- `InviteUsersSchema: z.ZodObject` — `POST /:id/invite` body
|
||||
(`user_ids: string[]`, length 1..100, non-empty members).
|
||||
- `AttendeeQuerySchema: z.ZodObject` — pagination + status filter.
|
||||
- `AttendeeSchema: z.ZodObject` — RSVP + sparse user fields.
|
||||
- `AttendeeListResponseSchema: z.ZodObject` — `{ data, total }`.
|
||||
- `RsvpResponseSchema: z.ZodObject` — `{ rsvp: RsvpSchema | null }`.
|
||||
- `InviteResponseSchema: z.ZodObject` — `{ invitations, total }`.
|
||||
|
||||
Every schema exports a matching `type Foo = z.infer<typeof FooSchema>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema-level cap on `InviteUsersSchema.user_ids` is 100 and the
|
||||
runtime cap (`config.maxInvitesPerRequest`) defaults to 50. The two
|
||||
limits coexist because the schema-level cap protects against
|
||||
abusive payloads (10 000-element arrays that would still parse
|
||||
correctly but exhaust the server) and the runtime cap encodes a
|
||||
per-deployment policy. The route handler enforces the runtime cap
|
||||
after the schema parse so the response code is `400` with
|
||||
`EVENTS_PLUGIN.INVITE_LIMIT_EXCEEDED`, not `422`/`400` from Zod.
|
||||
|
||||
`AttendeeSchema` extends `RsvpSchema` with optional joined columns
|
||||
because the suaveplan SurrealDB driver doesn't materialise FETCH
|
||||
expansions into nested objects by default — the route handler reads
|
||||
the raw projection from `findAttendees` which mixes RSVP fields with
|
||||
optional joined user fields.
|
||||
Reference in New Issue
Block a user