Files
suaveplan/openspec/archive/2026-05-19-add-api-plugin-events/proposal.md
2026-05-27 23:29:53 +01:00

11 KiB

add-api-plugin-events

Package: @suaveplan/api-plugin-events Status: PROPOSED Type: new-package Design: (none — straightforward migration; see this proposal + spec delta) Spec delta: ./specs/api-plugin-events/spec.md

Why

Genesis ships the foundational pieces of @suaveplan/api (database, search, auth, etc.) but is missing the event-management plugin (calendar-style entity: schedule events, RSVP, invite users). Without this plugin, downstream apps that need meetup / RSVP / attendee flows have to hand-roll the surface — there is no other @suaveplan/* capability that covers it. Upstream enterprise/api-plugin-events.md documents the audit (3 *.test.ts files, parity-only migration target). This change brings the package across at full functional parity, adapted to the genesis @suaveplan/api plugin shape (PluginDescriptor, ctx.getService("app"), ctx.getService("db")) and genesis DatabaseAdapter positional-param surface, with the named-emitter wart replaced by the shared ctx.eventBus so other plugins can subscribe.

The event-management plugin is distinct from @suaveplan/events (the typed event-bus, already shipped on genesis) and from @suaveplan/api-plugin-event-bus (the API-side adapter for the event-bus, also shipped). The name collision is unfortunate but the plugin's defineCodes namespace is EVENTS_PLUGIN (not EVENTS) precisely to avoid the registry clash with @suaveplan/events.

What changes

  • ADDED: createEventsPlugin(options?) — factory returning a PluginDescriptor<EventsPluginConfig> for @suaveplan/api.
  • ADDED: EventsPluginOptionsSchema / EventsPluginConfig / EventsPluginConfigInput — Zod-validated config (basePath, enableNotifications, maxInvitesPerRequest).
  • ADDED: EventRepository + IEventRepository — SurrealDB-backed event CRUD repository over the genesis SurrealAdapter service slot.
  • ADDED: RsvpRepository + IRsvpRepository — SurrealDB-backed RSVP / invitation / attendee-counting repository.
  • ADDED: createEventRoutes — Hono OpenAPI route module with 9 endpoints (list / get / create / update / delete / rsvp / cancel-rsvp / my-rsvp / attendees / invite).
  • ADDED: EVENTS_PLUGIN_CODES registry + EventsPluginError class — 12 stable codes spanning configuration, lifecycle, repository, and route paths.
  • ADDED: OTel instruments — 5 counters / histograms emitted from repository + route code (events created, events updated, events deleted, rsvps written, invitations sent).
  • ADDED: ServiceRegistry augmentation — eventRepository and rsvpRepository slots become available to downstream plugins via typed ctx.getService(...).
  • ADDED: Local Zod schemas (EventSchema, CreateEventSchema, UpdateEventSchema, EventQuerySchema, EventIdParamSchema, EventLocationSchema, EventStatusSchema, AttendeeCountSchema, EventListResponseSchema, RsvpSchema, RsvpStatusSchema, CreateRsvpSchema, RsvpResponseSchema, AttendeeSchema, AttendeeQuerySchema, AttendeeListResponseSchema, InviteUsersSchema, InviteResponseSchema) — @suaveplan/api-schemas on genesis only ships event-bus envelope schemas, not event-entity schemas, so the plugin owns its boundary schemas.
  • ADDED: surql/tables/event.surql migration — event and event_rsvp SCHEMAFULL tables plus the two indexes the repositories rely on (event_start_idx, event_rsvp_unique_idx). See ## Out of scope for the deferred upstream permission rules / helper functions / event_update table.
  • ADDED: EventsPluginEvents event-map keys — events:initialized, events:ready, event:created, event:updated, event:cancelled, rsvp:created, rsvp:cancelled, event:invitations:sent.

Out of scope

Items from enterprise/api-plugin-events.md "Required enterprise-quality improvements" and "Feature-rich enhancements" are deferred to follow-up changes because they each require new cross-package dependencies that don't exist yet on genesis (or are migrated under their own change):

  • ICS / iCalendar export — requires a @suaveplan/templates ICS builder or a hand-rolled module; deferred to add-api-plugin-events-ics-export.
  • Recurring events (RRULE / RFC-5545) — requires @suaveplan/datetime RRULE expansion which is not yet wired; deferred to add-api-plugin-events-recurrence.
  • Notification fan-out via @suaveplan/notification-channel — enableNotifications is wired through the plugin config and accepted at construction, but the actual channel binding lands when @suaveplan/notification-channel migrates (see STATUS.md). The config flag is non-load-bearing in this change but reserved by Zod so consumers configuring it today won't fail validation later.
  • Rate-limiter on /events/:id/invite — depends on @suaveplan/api-plugin-rate-limit which is migrating concurrently; will be composed at the host level once both land.
  • Audit-log on create / cancel / RSVP — depends on @suaveplan/api-plugin-audit (concurrent migration).
  • Auth gating via @suaveplan/api-plugin-auth — this plugin reads c.get("user") (set by the host's auth middleware) and raises UNAUTHENTICATED when missing; explicit requireAuth() composition is a host-level concern.
  • Calendar sync (Google / Outlook / iCloud), waitlist / capacity, invite tokens, reminders, geo, check-in QR, ticketing, SSE live updates — each is a feature-rich enhancement called out in the upstream enterprise audit, none of which are present in the upstream code itself; they belong in follow-up changes that don't ride on this parity migration.
  • Upstream surql/tables/event.surql ships row-level PERMISSIONS rules and stored helper functions (fn::event::can_view, fn::event::can_moderate, fn::event::create, fn::event::update_rsvp, fn::event::invite, fn::event::post_update, fn::event::get_upcoming) plus an event_update table — all of which transitively depend on fn::bitwise::*, fn::notification::*, fn::group::*, and record<site> / record<group> references from sibling plugins (api-plugin-site, api-plugin-notifications, api-plugin-groups, api-plugin-bitwise). None of those plugins exist on genesis yet, and the upstream test harness papers over the gap with a loadCrossPluginStubs helper that is itself absent from genesis's @suaveplan/surrealdb. To avoid hand-rolling four downstream plugins as a side-quest, the migration ships a slimmer surql/tables/event.surql covering only the event and event_rsvp tables that the repositories actually read / write; permissions enforcement moves to the route layer (auth guard + creator check) and the event_update table + helper functions migrate when their dependent plugins land.

Source mapping

This change derives from ../suaveplan/enterprise/api-plugin-events.md and the upstream source at ../suaveplan/packages/server/api-plugin-events/.

Requirement Source lines Notes
Plugin factory createEventsPlugin(config?) enterprise.md L18-L23; src/index.ts L35-L101 reworded — genesis uses PluginDescriptor not Plugin<EventsConfig>
Config schema (basePath, enableNotifications, maxInvitesPerRequest) enterprise.md L12; src/index.ts L23-L29 verbatim
Plugin dependencies: ["database"] enterprise.md L16; src/index.ts L46 verbatim
migrationsPath: "./surql" enterprise.md L16; src/index.ts L58 verbatim — surql payload migrated unchanged
EventRepository: findAll/findById/create/update/delete/isCreator/updateAttendeeCount src/repositories/event.repository/index.ts verbatim — semantics preserved, query helper rewritten for genesis adapter
RsvpRepository: findByEventAndUser/findAttendees/upsert/delete/createInvitations/countByStatus src/repositories/rsvp.repository/index.ts verbatim
createEventRoutes (9 endpoints) src/routes/events/index.ts verbatim — ctx.eventBus replaces local EventEmitter
event / event_rsvp / event_update tables + indexes surql/tables/event.surql verbatim
Public re-export of repositories + routes + types src/index.ts L103-L106 reworded — explicit named exports per genesis rule 10

Requirements with no source line

Requirement Justification
100% test / branch / function / line coverage Genesis-wide policy (CLAUDE.md "Quality", AGENTS.md §7).
defineCodes("EVENTS_PLUGIN", [...]) namespace Genesis convention; EVENTS collides with the @suaveplan/events event-bus registry.
OTel instruments via metrics.getMeter("@suaveplan/api-plugin-events") Genesis-wide observability policy; emitted from production paths and asserted with a real in-memory MeterProvider (AGENTS.md §13).
Use ctx.eventBus from @suaveplan/api-plugin context, not a local EventEmitter Fixes the smell flagged in enterprise/api-plugin-events.md "Notable smells" L41 (local emitter not visible to other plugins).
Subpath exports per feature module AGENTS.md §6 — required so consumers can tree-shake.
Schemas owned locally @suaveplan/api-schemas on genesis ships only event-bus envelope schemas, not event-entity schemas (verified by inspection: packages/api/api-schemas/src/events/ contains domain-event.ts, envelope.ts, metadata.ts only). Migrating the entity schemas into @suaveplan/api-schemas is out of scope for a plugin migration.
Positional $p0-style params instead of named $title Genesis DatabaseAdapter.query(sql, params: readonly unknown[]) wraps params as {p0: ..., p1: ...} via paramsToRecord. Upstream uses named placeholders that don't survive the canonical surface (Path-A escape valve documented in CLAUDE.md).

Verification approach

  • src/repositories/event.repository/event.repository.test.ts — covers EventRepository requirements (CRUD, normalisation, isCreator branches, attendee count).
  • src/repositories/rsvp.repository/rsvp.repository.test.ts — covers RsvpRepository requirements (find / upsert / delete / invitations / countByStatus).
  • src/routes/events-routes/events-routes.test.ts — covers route surface, status codes, auth guard, event emission.
  • src/plugin/plugin.test.ts — covers plugin lifecycle (onInit / onReady / onClose), service registration, route mounting.
  • src/error-codes/error-codes.test.ts — pins every code is reachable from a real call site.
  • src/otel/instruments.test.ts — real MeterProvider, asserts instrument shape and emission.
  • src/schemas/event-schemas.test.ts + src/schemas/rsvp-schemas.test.ts + src/schemas/options.test.ts — Zod boundary tests.
  • surql/surql-validation.test.ts — discovers and loads the surql migration against a real SurrealDB testcontainer (isSurrealDBAvailable skip-aware).
  • tests/integration/full-boot.test.ts — full plugin boot + CRUD roundtrip against a real SurrealDB container.
  • src/api-plugin-events.spec-conformance.test.ts — pins every public symbol re-exported by the package root.

Migration impact

None — additive only. This is a brand-new package on genesis. Consumers will opt-in via createEventsPlugin() once they need event-management routes.

Impact

Affected dependents

None on genesis today — package is additive. Downstream apps that previously had no event-management capability gain access.