Introduce typed token value objects (EventToken, OrganizerToken,
RsvpToken) and refactor all existing Event code to use them.
Add POST /events/{token}/rsvps endpoint that persists an RSVP and
returns an rsvpToken. Populate attendeeCount in GET /events/{token}
from a real count query instead of hardcoded 0.
Includes: OpenAPI spec, Liquibase migration (rsvps table with
ON DELETE CASCADE), domain model, hexagonal ports/adapters,
service layer, and full test coverage (unit + integration).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
294 lines
7.6 KiB
YAML
294 lines
7.6 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: fete API
|
|
description: Privacy-focused event announcements and RSVPs
|
|
version: 0.1.0
|
|
license:
|
|
name: GPL-3.0-or-later
|
|
identifier: GPL-3.0-or-later
|
|
|
|
servers:
|
|
- url: /api
|
|
|
|
paths:
|
|
/events:
|
|
post:
|
|
operationId: createEvent
|
|
summary: Create a new event
|
|
tags:
|
|
- events
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateEventRequest"
|
|
responses:
|
|
"201":
|
|
description: Event created successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateEventResponse"
|
|
"400":
|
|
description: Validation failed
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ValidationProblemDetail"
|
|
|
|
/events/{token}/rsvps:
|
|
post:
|
|
operationId: createRsvp
|
|
summary: Submit an RSVP for an event
|
|
tags:
|
|
- events
|
|
parameters:
|
|
- name: token
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
description: Public event token
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateRsvpRequest"
|
|
responses:
|
|
"201":
|
|
description: RSVP created successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateRsvpResponse"
|
|
"400":
|
|
description: Validation failed (e.g. blank name)
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ValidationProblemDetail"
|
|
"404":
|
|
description: Event not found
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ProblemDetail"
|
|
"409":
|
|
description: Event has expired — RSVPs no longer accepted
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ProblemDetail"
|
|
|
|
/events/{token}:
|
|
get:
|
|
operationId: getEvent
|
|
summary: Get public event details by token
|
|
tags:
|
|
- events
|
|
parameters:
|
|
- name: token
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
description: Public event token
|
|
responses:
|
|
"200":
|
|
description: Event found
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GetEventResponse"
|
|
"404":
|
|
description: Event not found
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ProblemDetail"
|
|
|
|
components:
|
|
schemas:
|
|
CreateEventRequest:
|
|
type: object
|
|
required:
|
|
- title
|
|
- dateTime
|
|
- timezone
|
|
- expiryDate
|
|
properties:
|
|
title:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description:
|
|
type: string
|
|
maxLength: 2000
|
|
dateTime:
|
|
type: string
|
|
format: date-time
|
|
description: Event date and time with UTC offset (ISO 8601)
|
|
example: "2026-03-15T20:00:00+01:00"
|
|
timezone:
|
|
type: string
|
|
description: IANA timezone of the organizer
|
|
example: "Europe/Berlin"
|
|
location:
|
|
type: string
|
|
maxLength: 500
|
|
expiryDate:
|
|
type: string
|
|
format: date
|
|
description: Date after which event data is deleted. Must be in the future.
|
|
example: "2026-06-15"
|
|
|
|
CreateEventResponse:
|
|
type: object
|
|
required:
|
|
- eventToken
|
|
- organizerToken
|
|
- title
|
|
- dateTime
|
|
- timezone
|
|
- expiryDate
|
|
properties:
|
|
eventToken:
|
|
type: string
|
|
format: uuid
|
|
description: Public token for the event URL
|
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
organizerToken:
|
|
type: string
|
|
format: uuid
|
|
description: Secret token for organizer access
|
|
example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
|
|
title:
|
|
type: string
|
|
example: "Summer BBQ"
|
|
dateTime:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-15T20:00:00+01:00"
|
|
timezone:
|
|
type: string
|
|
description: IANA timezone of the organizer
|
|
example: "Europe/Berlin"
|
|
expiryDate:
|
|
type: string
|
|
format: date
|
|
example: "2026-06-15"
|
|
|
|
GetEventResponse:
|
|
type: object
|
|
required:
|
|
- eventToken
|
|
- title
|
|
- dateTime
|
|
- timezone
|
|
- attendeeCount
|
|
- expired
|
|
properties:
|
|
eventToken:
|
|
type: string
|
|
format: uuid
|
|
description: Public event token
|
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
title:
|
|
type: string
|
|
description: Event title
|
|
example: "Summer BBQ"
|
|
description:
|
|
type: string
|
|
description: Event description (absent if not set)
|
|
example: "Bring your own drinks!"
|
|
dateTime:
|
|
type: string
|
|
format: date-time
|
|
description: Event date/time with organizer's UTC offset
|
|
example: "2026-03-15T20:00:00+01:00"
|
|
timezone:
|
|
type: string
|
|
description: IANA timezone name of the organizer
|
|
example: "Europe/Berlin"
|
|
location:
|
|
type: string
|
|
description: Event location (absent if not set)
|
|
example: "Central Park, NYC"
|
|
attendeeCount:
|
|
type: integer
|
|
minimum: 0
|
|
description: Number of confirmed attendees (attending=true)
|
|
example: 12
|
|
expired:
|
|
type: boolean
|
|
description: Whether the event's expiry date has passed
|
|
example: false
|
|
|
|
CreateRsvpRequest:
|
|
type: object
|
|
required:
|
|
- name
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
description: Guest's display name
|
|
example: "Max Mustermann"
|
|
|
|
CreateRsvpResponse:
|
|
type: object
|
|
required:
|
|
- rsvpToken
|
|
- name
|
|
properties:
|
|
rsvpToken:
|
|
type: string
|
|
format: uuid
|
|
description: Token identifying this RSVP (store client-side for future updates)
|
|
example: "d4e5f6a7-b8c9-0123-4567-890abcdef012"
|
|
name:
|
|
type: string
|
|
description: Guest's display name as stored
|
|
example: "Max Mustermann"
|
|
|
|
ProblemDetail:
|
|
type: object
|
|
properties:
|
|
type:
|
|
type: string
|
|
format: uri
|
|
default: "about:blank"
|
|
title:
|
|
type: string
|
|
status:
|
|
type: integer
|
|
detail:
|
|
type: string
|
|
instance:
|
|
type: string
|
|
format: uri
|
|
additionalProperties: true
|
|
|
|
ValidationProblemDetail:
|
|
allOf:
|
|
- $ref: "#/components/schemas/ProblemDetail"
|
|
- type: object
|
|
properties:
|
|
fieldErrors:
|
|
type: array
|
|
items:
|
|
type: object
|
|
required:
|
|
- field
|
|
- message
|
|
properties:
|
|
field:
|
|
type: string
|
|
message:
|
|
type: string
|