From 7bab2b3fb7d41e96c86ac16dc0b9fe565cccd6e8 Mon Sep 17 00:00:00 2001 From: openclaw Date: Thu, 5 Mar 2026 15:13:48 +0000 Subject: [PATCH] Add API versioning policy and sync docs OpenAPI spec --- README.md | 6 + docs/api-versioning.md | 57 ++++ docs/openapi.yaml | 689 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 748 insertions(+), 4 deletions(-) create mode 100644 docs/api-versioning.md diff --git a/README.md b/README.md index 7e72913..1a465f9 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,9 @@ npm run start - #2 Datenmodell für Request/Offer/Negotiation/Deal - #3 Bewertungssystem 2-14 Tage Verzögerung - #4 Adressänderung nur per Briefbestätigung + +## API Governance + +- OpenAPI: `openapi.yaml` (Spiegel: `docs/openapi.yaml`) +- Versioning/Deprecation Policy: `docs/api-versioning.md` + diff --git a/docs/api-versioning.md b/docs/api-versioning.md new file mode 100644 index 0000000..8d79bb0 --- /dev/null +++ b/docs/api-versioning.md @@ -0,0 +1,57 @@ +# API Versionierung und Deprecation Policy + +## Ziel + +Diese Policy definiert eine reproduzierbare Versionierungsstrategie fuer `helpyourneighbour`, damit API-Clients stabil bleiben und Breaking Changes planbar sind. + +## Versionierungsmodell + +- Basispfad-Versionierung: `/v1/...`, `/v2/...` +- Aktueller Stand dieses Repos ist funktional `v1`. +- Fuer die laufende Migration gilt: bestehende Routen ohne Prefix bleiben bis zur vollstaendigen Umstellung als Legacy-Compat aktiv. + +## Semantik + +- **Minor Change (non-breaking):** neue optionale Felder, neue Endpunkte, neue optionale Query-Parameter. +- **Major Change (breaking):** Umbenennung/Entfernung von Feldern, strictere Validierung mit API-Break, geaenderte Fehlersemantik. +- Non-breaking Aenderungen bleiben innerhalb derselben Major API (`v1`). +- Breaking Aenderungen erzeugen eine neue Major API (`v2`). + +## Deprecation-Regeln + +Bei geplanter Entfernung eines Endpunkts/Felds: + +1. Markierung als `deprecated: true` in OpenAPI. +2. Deprecation-Hinweis in Response-Headern: + - `Deprecation: true` + - `Sunset: ` + - optional `Link: ; rel=\"deprecation\"` +3. Changelog-Eintrag und Migrationshinweise im Repo (`docs/`). +4. Mindestfrist bis Removal: **90 Tage**. + +## Kompatibilitaetsgarantien + +- JSON-Felder werden nicht ohne neue Major API entfernt. +- Typen werden nicht stillschweigend geaendert. +- Neue Pflichtfelder werden nicht in bestehende Request-Bodies von `v1` eingefuehrt. + +## Fehlerverhalten + +- Fehlerobjekte folgen stabil dem Muster `{ "error": ... }`. +- Neue Fehlercodes werden additive eingefuehrt. +- Bereits genutzte HTTP-Statuscodes bleiben stabil, ausser bei neuer Major API. + +## Rollout-Prozess fuer Breaking Changes + +1. ADR erstellen (Design + Impact). +2. `v2` Endpunkt parallel einfuehren. +3. `v1` als deprecated markieren (Header + OpenAPI). +4. 90-Tage-Frist einhalten. +5. Nach Frist `v1` entfernen. + +## Definition of Done fuer API-Aenderungen + +- OpenAPI aktualisiert (`/openapi.yaml` und `docs/openapi.yaml`). +- Changelog/Migrationshinweis vorhanden. +- Contract-Tests fuer geaenderte Endpunkte aktualisiert. +- Breaking vs non-breaking explizit im PR beschrieben. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index dcea6e1..e6d2c12 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,5 +1,686 @@ -openapi: 3.0.0 +openapi: 3.1.0 info: - title: Helpyourneighbour API - version: 0.1.0 -paths: {} + title: HelpYourNeighbour API + version: 0.2.0 + description: API contract for authentication, requests, offers, contacts, profile, addresses, and reviews. +servers: + - url: http://localhost:3000 + description: Local development +security: + - bearerAuth: [] +paths: + /health: + get: + tags: [system] + summary: Health check + security: [] + responses: + '200': + description: Service healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + + /auth/register: + post: + tags: [auth] + summary: Register a new user + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: User created + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Email already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Registration failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/login: + post: + tags: [auth] + summary: Login with email/password + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login success + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /requests: + get: + tags: [requests] + summary: List help requests + security: [] + responses: + '200': + description: Request list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/HelpRequest' + post: + tags: [requests] + summary: Create help request + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateHelpRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/{requestId}: + post: + tags: [offers] + summary: Create offer for help request + parameters: + - $ref: '#/components/parameters/RequestId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOfferRequest' + responses: + '201': + description: Offer created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/negotiation/{offerId}: + post: + tags: [offers] + summary: Create counter-offer/negotiation message + parameters: + - $ref: '#/components/parameters/OfferId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOfferRequest' + responses: + '201': + description: Negotiation entry created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/accept/{offerId}: + post: + tags: [offers] + summary: Accept offer and create deal + parameters: + - $ref: '#/components/parameters/OfferId' + responses: + '201': + description: Deal created + content: + application/json: + schema: + $ref: '#/components/schemas/DealCreatedResponse' + '400': + description: Invalid offer id + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: Offer not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/request: + post: + tags: [contacts] + summary: Request contact exchange for a deal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContactRequestCreate' + responses: + '201': + description: Request created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Deal not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Request already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/respond: + post: + tags: [contacts] + summary: Accept or reject contact exchange request + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContactRequestRespond' + responses: + '200': + description: Response processed + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/deal/{dealId}: + get: + tags: [contacts] + summary: List accepted contact exchanges for deal + parameters: + - $ref: '#/components/parameters/DealId' + responses: + '200': + description: Accepted contact exchange rows + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ContactExchangeRow' + '400': + description: Invalid dealId + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Deal not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /addresses/change-request: + post: + tags: [addresses] + summary: Request address change with postal verification code + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddressChangeRequest' + responses: + '201': + description: Change request created + content: + application/json: + schema: + $ref: '#/components/schemas/AddressChangeCreatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /addresses/verify: + post: + tags: [addresses] + summary: Verify address change with 6-digit code + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddressVerifyRequest' + responses: + '200': + description: Address verified + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid payload or code + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Request not pending + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /profile/phone: + post: + tags: [profile] + summary: Update encrypted phone number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PhoneUpdateRequest' + responses: + '200': + description: Phone updated + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /reviews/{dealId}: + post: + tags: [reviews] + summary: Create review for deal + parameters: + - $ref: '#/components/parameters/DealId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateReviewRequest' + responses: + '201': + description: Review created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + parameters: + RequestId: + name: requestId + in: path + required: true + schema: + type: integer + minimum: 1 + OfferId: + name: offerId + in: path + required: true + schema: + type: integer + minimum: 1 + DealId: + name: dealId + in: path + required: true + schema: + type: integer + minimum: 1 + + responses: + BadRequest: + description: Validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + BadRequestSimple: + description: Invalid payload + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Unauthorized: + description: Missing or invalid token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Forbidden: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + schemas: + HealthResponse: + type: object + additionalProperties: false + properties: + status: + type: string + example: ok + required: [status] + + ErrorResponse: + type: object + properties: + error: + oneOf: + - type: string + - type: object + + StatusResponse: + type: object + additionalProperties: false + properties: + status: + type: string + required: [status] + + IdResponse: + type: object + additionalProperties: false + properties: + id: + type: integer + minimum: 1 + required: [id] + + DealCreatedResponse: + type: object + additionalProperties: false + properties: + dealId: + type: integer + minimum: 1 + required: [dealId] + + AuthTokenResponse: + type: object + additionalProperties: false + properties: + token: + type: string + required: [token] + + RegisterRequest: + type: object + additionalProperties: false + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + displayName: + type: string + minLength: 2 + maxLength: 120 + required: [email, password, displayName] + + LoginRequest: + type: object + additionalProperties: false + properties: + email: + type: string + format: email + password: + type: string + minLength: 1 + required: [email, password] + + HelpRequest: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + value_chf: + type: number + status: + type: string + created_at: + type: string + format: date-time + requester_name: + type: string + + CreateHelpRequest: + type: object + additionalProperties: false + properties: + title: + type: string + minLength: 3 + maxLength: 180 + description: + type: string + minLength: 5 + valueChf: + type: number + exclusiveMinimum: 0 + required: [title, description, valueChf] + + CreateOfferRequest: + type: object + additionalProperties: false + properties: + amountChf: + type: number + exclusiveMinimum: 0 + message: + type: string + maxLength: 2000 + required: [amountChf] + + ContactRequestCreate: + type: object + additionalProperties: false + properties: + dealId: + type: integer + minimum: 1 + targetUserId: + type: integer + minimum: 1 + required: [dealId, targetUserId] + + ContactRequestRespond: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + accept: + type: boolean + required: [requestId, accept] + + ContactExchangeRow: + type: object + properties: + id: + type: integer + requester_id: + type: integer + target_id: + type: integer + accepted: + type: boolean + requester_phone_encrypted: + type: string + target_phone_encrypted: + type: string + + AddressChangeRequest: + type: object + additionalProperties: false + properties: + newAddress: + type: string + minLength: 10 + required: [newAddress] + + AddressChangeCreatedResponse: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + postalDispatch: + type: string + example: pending_letter + note: + type: string + verificationCode: + type: string + pattern: '^\\d{6}$' + required: [requestId, postalDispatch, note, verificationCode] + + AddressVerifyRequest: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + code: + type: string + pattern: '^\\d{6}$' + required: [requestId, code] + + PhoneUpdateRequest: + type: object + additionalProperties: false + properties: + phone: + type: string + minLength: 6 + maxLength: 40 + required: [phone] + + CreateReviewRequest: + type: object + additionalProperties: false + properties: + revieweeId: + type: integer + minimum: 1 + rating: + type: integer + minimum: 1 + maximum: 5 + comment: + type: string + maxLength: 2000 + required: [revieweeId, rating]