Files
suaveplan/packages/api-stack/api/api-plugin-events/src/schemas/rsvp-schemas.md
2026-05-27 23:29:53 +01:00

3.9 KiB

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

import { CreateRsvpSchema, RsvpStatusSchema } from "@suaveplan/api-plugin-events";

const body = CreateRsvpSchema.parse({ status: "going" });
const isValid = RsvpStatusSchema.safeParse("yes").success; // false

Usage: Advanced Example

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.