Files
nexarch/dms/internal/metadataapi/handler.go
T
patrickandClaude Sonnet 5 6a5a455b2c MET-03: metadaten- & tag-oberfläche
metadataapi/tagapi/bulkeditapi als Backend-Voraussetzung. MetadataForm
passt sich der Felddefinition an und warnt vor Verwerfen ungespeicherter
Aenderungen, TagInput schlaegt Tags vor und ist per Pfeiltasten/Enter
bedienbar, Massenbearbeitung wendet Tags auf beliebig grosse Auswahlen in
einem Aufruf an.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:17:13 +02:00

142 lines
4.5 KiB
Go

// Package metadataapi implementiert MET-03s Backend-Voraussetzung:
// Felddefinitionen listen und Metadatenwerte EINES Dokuments lesen/setzen
// — Grundlage für ein Formular, das sich automatisch an die Felddefinition
// anpasst. Ruft ausschließlich internal/metadatafield auf.
package metadataapi
import (
"encoding/json"
"net/http"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/metadatafield"
)
type Server struct {
pool *pgxpool.Pool
}
func NewServer(pool *pgxpool.Pool) *Server {
return &Server{pool: pool}
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /metadata-fields", s.handleListFields)
mux.HandleFunc("GET /documents/{id}/metadata", s.handleGetValues)
mux.HandleFunc("PUT /documents/{id}/metadata", s.handleSetValues)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
type fieldDTO struct {
ID string `json:"id"`
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Required bool `json:"required"`
DefaultValue *string `json:"default_value"`
}
// handleListFields liefert ALLE Felddefinitionen — Grundlage für ein
// Formular, das sich automatisch an sie anpasst (Akzeptanzkriterium 1).
func (s *Server) handleListFields(w http.ResponseWriter, r *http.Request) {
fields, err := metadatafield.ListAll(r.Context(), s.pool)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
dtos := make([]fieldDTO, len(fields))
for i, f := range fields {
dtos[i] = fieldDTO{ID: f.ID, Key: f.Key, Label: f.Label, Type: f.Type, Required: f.Required, DefaultValue: f.DefaultValue}
}
writeJSON(w, dtos)
}
// handleGetValues liefert die aktuellen Metadatenwerte EINES Dokuments als
// field_id -> value.
func (s *Server) handleGetValues(w http.ResponseWriter, r *http.Request) {
documentID := r.PathValue("id")
rows, err := s.pool.Query(r.Context(), `SELECT field_id, value FROM document_metadata_values WHERE document_id = $1`, documentID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
values := map[string]string{}
for rows.Next() {
var fieldID, value string
if err := rows.Scan(&fieldID, &value); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
values[fieldID] = value
}
if err := rows.Err(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, values)
}
// handleSetValues speichert MEHRERE Feldwerte EINES Dokuments in einem
// Aufruf (ein Formular-Speichern-Klick, ein Request) — jeder Wert wird
// gegen seine Felddefinition validiert (metadatafield.Validate), bevor
// irgendetwas geschrieben wird, damit kein Teilzustand entsteht.
func (s *Server) handleSetValues(w http.ResponseWriter, r *http.Request) {
documentID := r.PathValue("id")
var values map[string]string
if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error())
return
}
for fieldID, value := range values {
field, err := metadatafield.Get(r.Context(), s.pool, fieldID)
if err == metadatafield.ErrNotFound {
writeError(w, http.StatusBadRequest, "unbekanntes feld: "+fieldID)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if err := metadatafield.Validate(field, value); err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
}
tx, err := s.pool.Begin(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer func() { _ = tx.Rollback(r.Context()) }()
for fieldID, value := range values {
if _, err := tx.Exec(r.Context(), `
INSERT INTO document_metadata_values (document_id, field_id, value) VALUES ($1, $2, $3)
ON CONFLICT (document_id, field_id) DO UPDATE SET value = EXCLUDED.value
`, documentID, fieldID, value); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if err := tx.Commit(r.Context()); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}