CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
// Package bulkeditapi implementiert MET-03s Backend-Voraussetzung für die
|
|
// Massenbearbeitung: EIN HTTP-Endpunkt über internal/bulkedit (DOC-15).
|
|
// Keine eigene Massenbearbeitungslogik.
|
|
package bulkeditapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/bulkedit"
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/metadatafield"
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
|
|
)
|
|
|
|
type Server struct {
|
|
pool *pgxpool.Pool
|
|
lockChecker bulkedit.LockChecker
|
|
}
|
|
|
|
func NewServer(pool *pgxpool.Pool, lockChecker bulkedit.LockChecker) *Server {
|
|
return &Server{pool: pool, lockChecker: lockChecker}
|
|
}
|
|
|
|
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("POST /bulk-edit", s.handleApply)
|
|
}
|
|
|
|
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})
|
|
}
|
|
|
|
// Mit Modul-Token gilt allein dessen Nutzer; ein abweichendes Feld "actor" im Body wird ignoriert.
|
|
func resolveActor(ctx context.Context, bodyActor string) string {
|
|
if id, ok := moduleauth.IdentityFrom(ctx); ok && id.UserID != "" {
|
|
return id.UserID
|
|
}
|
|
return bodyActor
|
|
}
|
|
|
|
type metadataChangeDTO struct {
|
|
FieldID string `json:"field_id"`
|
|
Value *string `json:"value"`
|
|
}
|
|
|
|
type applyRequest struct {
|
|
Actor string `json:"actor"`
|
|
DocumentIDs []string `json:"document_ids"`
|
|
AddTagIDs []string `json:"add_tag_ids"`
|
|
RemoveTagIDs []string `json:"remove_tag_ids"`
|
|
MetadataChanges []metadataChangeDTO `json:"metadata_changes"`
|
|
}
|
|
|
|
type skippedDTO struct {
|
|
DocumentID string `json:"document_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type applyResult struct {
|
|
Applied []string `json:"applied"`
|
|
Skipped []skippedDTO `json:"skipped"`
|
|
}
|
|
|
|
// Akzeptanzkriterium 3 (MET-03) / Akzeptanzkriterium 1 (DOC-15):
|
|
// Massenbearbeitung über eine Dokumentauswahl, gesperrte Dokumente werden
|
|
// einzeln übersprungen statt die Operation als Ganzes abzulehnen.
|
|
func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
|
|
var req applyRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error())
|
|
return
|
|
}
|
|
req.Actor = resolveActor(r.Context(), req.Actor)
|
|
if req.Actor == "" {
|
|
writeError(w, http.StatusBadRequest, "fehlendes feld \"actor\"")
|
|
return
|
|
}
|
|
|
|
changes := make([]bulkedit.MetadataChange, len(req.MetadataChanges))
|
|
for i, c := range req.MetadataChanges {
|
|
changes[i] = bulkedit.MetadataChange{FieldID: c.FieldID, Value: c.Value}
|
|
}
|
|
|
|
result, err := bulkedit.Apply(r.Context(), s.pool, s.lockChecker, req.Actor, bulkedit.Request{
|
|
DocumentIDs: req.DocumentIDs, AddTagIDs: req.AddTagIDs, RemoveTagIDs: req.RemoveTagIDs, MetadataChanges: changes,
|
|
})
|
|
if err != nil {
|
|
var ve *metadatafield.ErrValidation
|
|
if errors.As(err, &ve) {
|
|
writeError(w, http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
dto := applyResult{Applied: result.Applied, Skipped: make([]skippedDTO, len(result.Skipped))}
|
|
for i, sk := range result.Skipped {
|
|
dto.Skipped[i] = skippedDTO{DocumentID: sk.DocumentID, Reason: sk.Reason}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(dto)
|
|
}
|