initial backend: Dokumente, Historie, API und Persistenz (D76)

- Liquibase-Schema (`document`, `document_history`) + Rollback-scripts
- Spring Boot with JPA-repositories, entities and services
- REST-API (`/documents`, `/documents/{id}`, `/documents/{id}/history`)
- OpenAPI-specifikation for CRUD-Operationen and history
- config files (`application.yaml`, `Liquibase`, H2 im PostgreSQL-Modus)
- preps for future live-editing/delta-updates
- Exceptions for conflikt- and not-found cases (409/404)
- keeping document hostory even after `delete` for RESTORE functionality
This commit is contained in:
mhoennig
2026-08-26 12:56:27 +02:00
parent 4e0ce51820
commit 0446e3d81e
31 changed files with 2060 additions and 7 deletions
@@ -0,0 +1,22 @@
spring:
application:
name: editor-backend
datasource:
# H2 im File-Modus mit PostgreSQL-Kompatibilitaet.
# Spaeterer Umstieg auf echtes PostgreSQL = im Wesentlichen nur diese URL aendern.
url: jdbc:h2:file:./data/editor;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
username: sa
password: ""
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: validate # Schema kommt ausschliesslich von Liquibase
open-in-view: false
liquibase:
change-log: classpath:db/changelog/db.changelog-master.sql
server:
port: 8080
@@ -0,0 +1,28 @@
--liquibase formatted sql
--changeset editor:001-create-document
CREATE TABLE document (
id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
version BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
--rollback DROP TABLE document;
--changeset editor:002-create-document-history
CREATE TABLE document_history (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
document_id UUID NOT NULL,
version BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
change_type VARCHAR(16) NOT NULL,
change_time TIMESTAMP WITH TIME ZONE NOT NULL
);
--rollback DROP TABLE document_history;
--changeset editor:003-index-document-history
CREATE INDEX idx_document_history_document_id ON document_history (document_id);
--rollback DROP INDEX idx_document_history_document_id;
+308
View File
@@ -0,0 +1,308 @@
openapi: 3.0.3
info:
title: Editor Backend API
description: |
CRUD-Grundgeruest fuer Dokumente.
Vorbereitete Erweiterungen (noch nicht aktiv):
- Autorisierung: securitySchemes.bearerAuth ist definiert, wird aber noch
auf keine Operation angewendet.
- Live-Editing: Das Feld `version` dient spaeter der Konflikterkennung
(Optimistic Locking) und als Basis fuer Delta-Updates via WebSocket.
- Clientseitige Verschluesselung: `content` ist ein opaker String. Der
Server interpretiert den Inhalt nicht, sodass spaeter Ciphertext
transportiert werden kann, ohne die API zu aendern.
version: 0.1.0
servers:
- url: /api/v1
tags:
- name: Documents
description: Verwaltung von Dokumenten
paths:
/documents:
get:
tags: [Documents]
operationId: listDocuments
summary: Alle Dokumente auflisten
responses:
"200":
description: Liste aller Dokumente
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Document"
post:
tags: [Documents]
operationId: createDocument
summary: Neues Dokument anlegen
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DocumentCreateRequest"
responses:
"201":
description: Dokument wurde angelegt
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
"400":
$ref: "#/components/responses/BadRequest"
/documents/{documentId}:
parameters:
- name: documentId
in: path
required: true
schema:
type: string
format: uuid
get:
tags: [Documents]
operationId: getDocument
summary: Einzelnes Dokument abrufen
responses:
"200":
description: Das angeforderte Dokument
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
"404":
$ref: "#/components/responses/NotFound"
put:
tags: [Documents]
operationId: updateDocument
summary: Dokument vollstaendig aktualisieren
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DocumentUpdateRequest"
responses:
"200":
description: Aktualisiertes Dokument
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: Versionskonflikt (fuer spaeteres Optimistic Locking reserviert)
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
delete:
tags: [Documents]
operationId: deleteDocument
summary: Dokument loeschen
responses:
"204":
description: Dokument wurde geloescht
"404":
$ref: "#/components/responses/NotFound"
/documents/{documentId}/history:
parameters:
- name: documentId
in: path
required: true
schema:
type: string
format: uuid
get:
tags: [Documents]
operationId: getDocumentHistory
summary: Historie eines Dokuments abrufen
description: >
Liefert alle Versionen eines Dokuments in chronologischer Reihenfolge.
Die Historie bleibt auch nach dem Loeschen des Dokuments erhalten.
responses:
"200":
description: Historie des Dokuments (aelteste zuerst)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DocumentHistoryEntry"
"404":
$ref: "#/components/responses/NotFound"
/documents/{documentId}/restore:
parameters:
- name: documentId
in: path
required: true
schema:
type: string
format: uuid
post:
tags: [Documents]
operationId: restoreDocument
summary: Geloeschtes Dokument aus der Historie wiederherstellen
description: >
Stellt ein geloeschtes Dokument unter derselben UUID wieder her.
Ohne Request-Body wird der letzte Stand vor dem Loeschen
wiederhergestellt; optional kann eine bestimmte Version angegeben
werden (auch als Rollback fuer ein noch existierendes Dokument).
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/RestoreRequest"
responses:
"200":
description: Wiederhergestelltes Dokument
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: >
Dokument existiert noch und es wurde keine Zielversion angegeben.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
components:
responses:
NotFound:
description: Ressource nicht gefunden
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
BadRequest:
description: Ungueltige Anfrage
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
schemas:
Document:
type: object
required: [id, title, content, version, createdAt, updatedAt]
properties:
id:
type: string
format: uuid
readOnly: true
title:
type: string
maxLength: 255
content:
type: string
description: >
Opaker Inhalt. Bei clientseitiger Verschluesselung enthaelt dieses
Feld spaeter den Ciphertext.
version:
type: integer
format: int64
description: Wird bei jeder Aenderung inkrementiert (Basis fuer Live-Editing/Konflikterkennung).
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
DocumentCreateRequest:
type: object
required: [title, content]
properties:
title:
type: string
minLength: 1
maxLength: 255
content:
type: string
DocumentUpdateRequest:
type: object
required: [title, content]
properties:
title:
type: string
minLength: 1
maxLength: 255
content:
type: string
expectedVersion:
type: integer
format: int64
description: Optional; wird spaeter fuer Optimistic Locking ausgewertet.
DocumentHistoryEntry:
type: object
required: [documentId, version, title, content, changeType, timestamp]
properties:
documentId:
type: string
format: uuid
version:
type: integer
format: int64
title:
type: string
content:
type: string
changeType:
type: string
enum: [CREATED, UPDATED, DELETED, RESTORED]
timestamp:
type: string
format: date-time
RestoreRequest:
type: object
properties:
version:
type: integer
format: int64
description: >
Optionale Zielversion. Ohne Angabe wird der letzte inhaltliche
Stand vor dem Loeschen wiederhergestellt.
ProblemDetail:
type: object
description: Fehlerformat nach RFC 9457 (Problem Details)
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >
Noch nicht aktiv. Wird bei Einfuehrung der Autorisierung auf die
Operationen angewendet (security: - bearerAuth: []).