Files
skills/docs/api-stack/api/api-plugin-events/src/routes/events-routes/events-routes.md
2026-05-29 08:20:56 +01:00

3.9 KiB

events-routes

(no submodules)

Purpose

Builds the nine-route OpenAPI Hono module mounted by createEventsPlugin. Each route is wrapped by @hono/zod-openapi's createRoute so the package's local Zod schemas drive both input-validation and generated OpenAPI doc shape. The route layer is the single point at which auth (c.get("user")), authorisation (eventRepository.isCreator), maxInvitesPerRequest enforcement, OTel emission, and EventBus fan-out converge.

The handlers themselves are deliberately thin: every meaningful side-effect (CREATE row, UPDATE counter, DELETE row, EMIT event) funnels through the two repositories and the optional emitEvent callback. The callback is the integration point the plugin's onReady uses to forward route events onto the shared ctx.eventBus — that indirection is what fixes the upstream "local emitter" smell flagged in enterprise/api-plugin-events.md.

Features

  • 9 routes: list / get / create / update / delete / rsvp / cancel-rsvp / my-rsvp / attendees / invite.
  • Zod validation on every body / param / query.
  • Auth guard on every route via c.get("user").
  • Authorisation guard on every mutating route via eventRepository.isCreator.
  • maxInvitesPerRequest enforcement inside the /invite handler.
  • OTel counter emission on every successful mutation.
  • Typed emitEvent callback for cross-plugin fan-out.

Usage: Basic Example

import { createEventRoutes } from "@suaveplan/api-plugin-events";
import { OpenAPIHono } from "@hono/zod-openapi";

const routes = createEventRoutes({
    eventRepository,
    rsvpRepository,
    maxInvitesPerRequest: 50,
});

const parent = new OpenAPIHono();
parent.use(async (c, next) => {
    c.set("user", await resolveUser(c));
    await next();
});
parent.route("/events", routes);

Usage: Advanced Example

import { createEventRoutes } from "@suaveplan/api-plugin-events";

const routes = createEventRoutes({
    eventRepository,
    rsvpRepository,
    maxInvitesPerRequest: 25,
    emitEvent: (event) => {
        // Forward onto the host's typed bus
        host.eventBus.emit(event.type, event.payload);
        // Side-channel: persistent audit log
        auditLog.write({
            kind: event.type,
            payload: event.payload,
            at: new Date(),
        });
    },
});

// `routes` is an OpenAPIHono instance — feed it into the OpenAPI
// document generator:
host.openAPIRegistry.attach(routes);

API Reference

createEventRoutes(config: EventRoutesConfig): OpenAPIHono

Returns an OpenAPIHono<{ Variables: EventsRoutesVariables }> with the nine routes registered.

interface EventRoutesConfig

  • eventRepository: IEventRepository
  • rsvpRepository: IRsvpRepository
  • maxInvitesPerRequest: number
  • emitEvent?: (event: EventsRouteEvent) => void

interface EventsRoutesVariables

{ user: { id: string } & Record<string, unknown> | null | undefined } — the route handler reads c.get("user") from this slot; the host supplies the auth middleware that populates it.

Implementation Notes

The handlers manually try { getUser(c) } catch { return c.json(...) } rather than relying on Hono's onError because the OpenAPI route spec requires every documented response status to be reachable from the handler body. Catching EventsPluginError(UNAUTHENTICATED) and returning a 401 inline keeps the spec honest. The pattern repeats on every route — verbose, but each block is a single 3-line try/catch/json triple.

The app.route("/", routes) line in test fixtures and in the plugin mount mirrors the parent/child Hono pattern: the parent app owns the auth middleware, the child app owns the routes. Hono middleware registered on the parent BEFORE the .route() call fires before the child's handlers, so c.get("user") resolves correctly during the child's c.req.valid(...) parsing phase.