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>
142 lines
4.5 KiB
Go
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)
|
|
}
|