504 lines
19 KiB
Go
504 lines
19 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"chat-agent-system/internal/app/docflow"
|
|
"chat-agent-system/internal/app/uow"
|
|
"chat-agent-system/internal/contracts/vocabulary"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/docrevision"
|
|
"chat-agent-system/internal/freedoc"
|
|
)
|
|
|
|
func (s *Server) docflowCommands() *docflow.Commands {
|
|
if s != nil && s.rt != nil && s.rt.Docflow != nil {
|
|
return s.rt.Docflow
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) registerDocumentRevisionRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/revision-state", s.handleDocRevisionState)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/versions", s.handleDocRevisionListVersions)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/versions/{version_no}", s.handleDocRevisionGetVersion)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/diff", s.handleDocRevisionDiff)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/manual-overlay", s.handleDocRevisionManualOverlay)
|
|
mux.HandleFunc("PATCH /api/v1/documents/{doc_id}/content", s.handleDocRevisionPatchContent)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/agent-candidates/{candidate_id}", s.handleDocRevisionGetCandidate)
|
|
mux.HandleFunc("POST /api/v1/documents/{doc_id}/agent-candidates/{candidate_id}/decision", s.handleDocRevisionCandidateDecision)
|
|
mux.HandleFunc("GET /api/v1/documents/{doc_id}/change-reports/{report_id}", s.handleDocRevisionGetReport)
|
|
mux.HandleFunc("PATCH /api/v1/notifications/{id}", s.handlePatchNotification)
|
|
}
|
|
|
|
func (s *Server) docRev() *docrevision.Service {
|
|
if s == nil || s.svc() == nil {
|
|
return nil
|
|
}
|
|
return s.rev()
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionState(w http.ResponseWriter, r *http.Request) {
|
|
user, docID, ref, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
st, err := s.bootstrapRevisionIfNeeded(r.Context(), user, docID, ref)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
if st != nil && st.PendingCandidateID != nil {
|
|
// Read path already authorized; decide capability requires write auth (author duty).
|
|
st.CanDecidePending = s.authorizeDocRevision(r.Context(), user, ref, true) == nil
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, st)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionListVersions(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
items, err := rev.ListVersions(r.Context(), docID)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, map[string]any{"versions": items})
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionGetVersion(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
vno, _ := strconv.Atoi(r.PathValue("version_no"))
|
|
view := r.URL.Query().Get("view")
|
|
if view == "" {
|
|
view = "final"
|
|
}
|
|
md, sha, err := rev.GetVersionContent(r.Context(), docID, vno, view)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
w.Header().Set("ETag", `"`+sha+`"`)
|
|
writeJSONResponse(w, http.StatusOK, map[string]any{
|
|
"version_no": vno, "view": view, "content_sha256": sha, "content_markdown": md,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionDiff(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
fromNo, _ := strconv.Atoi(r.URL.Query().Get("from_version"))
|
|
toNo, _ := strconv.Atoi(r.URL.Query().Get("to_version"))
|
|
fromView := r.URL.Query().Get("from_view")
|
|
toView := r.URL.Query().Get("to_view")
|
|
if fromView == "" {
|
|
fromView = "final"
|
|
}
|
|
if toView == "" {
|
|
toView = "final"
|
|
}
|
|
diff, err := rev.DiffVersions(r.Context(), docID, fromNo, toNo, fromView, toView)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, diff)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionManualOverlay(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
diff, err := rev.ManualOverlay(r.Context(), docID)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, diff)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Request) {
|
|
user, docID, ref, ok := s.requireDocRevisionAccess(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Markdown string `json:"markdown"`
|
|
EditSessionID string `json:"edit_session_id"`
|
|
ClientBlockMap []docrevision.BlockNode `json:"client_block_map"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
|
|
return
|
|
}
|
|
idemp := r.Header.Get("Idempotency-Key")
|
|
if idemp == "" {
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": "Idempotency-Key required"})
|
|
return
|
|
}
|
|
var expectedHead *int64
|
|
var expectedRev *int
|
|
if match := strings.Trim(r.Header.Get("If-Match"), `"`); match != "" {
|
|
parts := strings.Split(match, ":")
|
|
if len(parts) == 2 {
|
|
if sid, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
|
|
expectedHead = &sid
|
|
}
|
|
if rn, err := strconv.Atoi(parts[1]); err == nil {
|
|
expectedRev = &rn
|
|
}
|
|
}
|
|
}
|
|
|
|
lifecycle := "drafting"
|
|
workDir := ""
|
|
projectID := int64(0)
|
|
kind := "controlled"
|
|
if ref != nil {
|
|
lifecycle = ref.LifecycleState
|
|
workDir = ref.WorkDir
|
|
projectID = ref.ProjectID
|
|
kind = ref.Kind
|
|
}
|
|
if kind == "orphan" {
|
|
writeDocRevisionError(w, docrevision.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
// 1) Controlled: author duty + lease WITHOUT writing body (fail-closed before ledger).
|
|
// This UI has no edit-session heartbeat: release the request-scoped human lease on every exit
|
|
// (success, 409, projection failure) so a 15m residual cannot block a later squad run.
|
|
if kind == "controlled" && s.gov() != nil {
|
|
// ActorIsMember=true:上游 enforceDocGovWorkflowAction(Edit) 已按成员级放行。
|
|
if err := s.gov().AssertHumanCanEdit(r.Context(), docID, user.ID, vocabulary.RoleAuthor, true); err != nil {
|
|
writeDocGovError(w, r, err)
|
|
return
|
|
}
|
|
holderID := fmt.Sprintf("%d", user.ID)
|
|
if user.ID <= 0 {
|
|
holderID = "human_anon"
|
|
}
|
|
defer func() {
|
|
_, _ = s.gov().ReleaseLease(context.Background(), docID, string(vocabulary.LeaseHolderHuman), holderID)
|
|
}()
|
|
}
|
|
|
|
// 2) Bootstrap if needed, then commit ledger. No fact-source mutation yet.
|
|
if _, stErr := rev.GetRevisionState(r.Context(), docID); stErr != nil {
|
|
md := body.Markdown
|
|
if ref != nil && strings.TrimSpace(ref.Markdown) != "" {
|
|
md = ref.Markdown
|
|
}
|
|
if strings.TrimSpace(md) == "" {
|
|
writeDocRevisionError(w, docrevision.ErrNeedsContent)
|
|
return
|
|
}
|
|
if _, err := rev.EnsureBootstrap(r.Context(), docrevision.BootstrapRequest{
|
|
DocID: docID, Kind: kind, Markdown: md, ProjectID: projectID, WorkDir: workDir,
|
|
LifecycleState: lifecycle,
|
|
Actor: docrevision.ActorSnapshot{DisplayName: user.Name, Role: "author", Duty: "author"},
|
|
}); err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
}
|
|
cmds := s.docflowCommands()
|
|
if cmds == nil || cmds.Rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
res, err := cmds.HumanEdit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.HumanEditCommand{
|
|
DocID: docID,
|
|
Request: docrevision.HumanRevisionRequest{
|
|
DocID: docID, Markdown: body.Markdown, ExpectedHeadSnapshot: expectedHead, ExpectedRevisionNo: expectedRev,
|
|
IdempotencyKey: idemp, EditSessionID: body.EditSessionID, ActorUserID: user.ID,
|
|
ActorSnapshot: docrevision.ActorSnapshot{DisplayName: user.Name, Role: "author", Duty: "author"},
|
|
LifecycleState: lifecycle, WorkDir: workDir, ProjectID: projectID, AllowCreateBaseline: false,
|
|
ClientBlockMap: body.ClientBlockMap,
|
|
},
|
|
})
|
|
if errors.Is(err, docrevision.ErrHeadConflict) && res != nil && res.Conflict != nil {
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{
|
|
"code": docrevision.ErrCodeHeadConflict, "conflict": res.Conflict,
|
|
})
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
|
|
// 3) Project fact-source AFTER ledger commit. The projection intent was inserted in the
|
|
// same transaction as the ledger (document_content_projections). Failure keeps that row
|
|
// pending and returns DOCUMENT_PROJECTION_FAILED. The same Idempotency-Key retries
|
|
// projection without a second revision. Only a successful projection acks the row.
|
|
if kind == "controlled" && s.gov() != nil {
|
|
if _, err := s.gov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
|
DocID: docID,
|
|
ActorID: user.ID,
|
|
ActorRole: vocabulary.RoleAuthor,
|
|
ActorName: user.Name,
|
|
MarkdownBody: body.Markdown,
|
|
}); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_PROJECTION_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "账本已提交但 DocGov 正文/审计投影失败;请用同一 Idempotency-Key 重试保存。账本 head 仍为权威。",
|
|
"state": res.State,
|
|
})
|
|
return
|
|
}
|
|
} else if workDir != "" {
|
|
if err := rev.ProjectOutputMarkdown(docID, s.dataRoot()); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_PROJECTION_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "账本已提交但工作区投影失败;请重试保存。账本 head 仍为权威。",
|
|
"state": res.State,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
if kind == "freeform" && s.freeDoc() != nil {
|
|
md := body.Markdown
|
|
if _, err := s.freeDoc().UpdateDocument(r.Context(), int(user.ID), docID, freedoc.UpdateFreeformDocRequest{
|
|
ContentMarkdown: &md,
|
|
ChangeSummary: "revision ledger human save",
|
|
}); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_PROJECTION_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "账本已提交但自由文档正文投影失败;请重试保存。账本 head 仍为权威。",
|
|
"state": res.State,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
if res.State.HeadSnapshotID != 0 {
|
|
if markErr := rev.MarkContentProjected(r.Context(), docID, res.State.HeadSnapshotID); markErr != nil {
|
|
slog.Warn("content projection ack failed", "doc_id", docID, "snapshot_id", res.State.HeadSnapshotID, "err", markErr)
|
|
}
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, res)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionGetCandidate(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
cid, _ := strconv.ParseInt(r.PathValue("candidate_id"), 10, 64)
|
|
c, err := rev.GetCandidate(r.Context(), docID, cid)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, c)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *http.Request) {
|
|
user, docID, _, ok := s.requireDocRevisionAccess(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
cid, _ := strconv.ParseInt(r.PathValue("candidate_id"), 10, 64)
|
|
var body struct {
|
|
Decision string `json:"decision"`
|
|
Confirmed bool `json:"confirmed"`
|
|
ExpectedInputSHA string `json:"expected_input_sha"`
|
|
ExpectedOutputSHA string `json:"expected_output_sha"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
|
|
return
|
|
}
|
|
cand, _ := rev.GetCandidate(r.Context(), docID, cid)
|
|
cmds := s.docflowCommands()
|
|
if cmds == nil || cmds.Rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
st, err := cmds.AgentCommit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.AgentCommitCommand{
|
|
DocID: docID, CandidateID: cid,
|
|
Decision: docrevision.CommitAgentDecision{
|
|
Decision: body.Decision, Confirmed: body.Confirmed,
|
|
ExpectedInputSHA: body.ExpectedInputSHA, ExpectedOutputSHA: body.ExpectedOutputSHA,
|
|
Reason: body.Reason, ActorUserID: user.ID,
|
|
ActorSnapshot: docrevision.ActorSnapshot{DisplayName: user.Name},
|
|
},
|
|
})
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
|
|
// Always project ledger head to fact source after decision:
|
|
// commit → new head; reject → keep/restore previous head (never leave squad draft on disk).
|
|
if err := rev.ProjectOutputMarkdown(docID, s.dataRoot()); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_PROJECTION_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "候选决策已写入账本,但工作区交付物投影失败;请重试决策或人工修复。",
|
|
"state": st,
|
|
})
|
|
return
|
|
}
|
|
if s.gov() != nil && st != nil {
|
|
if err := s.gov().MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_PROJECTION_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "候选决策已写入账本,但 document_records 哈希投影失败;请重试。",
|
|
"state": st,
|
|
})
|
|
return
|
|
}
|
|
if body.Decision == "commit" && cand != nil && strings.TrimSpace(cand.ProposalID) != "" {
|
|
if err := s.gov().MarkProposalApplied(r.Context(), cand.ProposalID); err != nil {
|
|
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
|
"code": "DOCUMENT_GOVERNANCE_FAILED",
|
|
"error": err.Error(),
|
|
"hint": "候选账本已提交且事实源已投影,但变更提案标记失败。请用同一 decision(相同 expected_*_sha)重试:已 committed 候选幂等,仅补 MarkProposalApplied;勿换候选或改正文。",
|
|
"state": st,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, st)
|
|
}
|
|
|
|
func (s *Server) handleDocRevisionGetReport(w http.ResponseWriter, r *http.Request) {
|
|
_, docID, _, ok := s.requireDocRevisionAccess(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
rev := s.docRev()
|
|
if rev == nil {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
|
|
return
|
|
}
|
|
rid, _ := strconv.ParseInt(r.PathValue("report_id"), 10, 64)
|
|
rep, err := rev.GetChangeReport(r.Context(), docID, rid)
|
|
if err != nil {
|
|
writeDocRevisionError(w, err)
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, rep)
|
|
}
|
|
|
|
func (s *Server) handlePatchNotification(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := s.requirePrincipal(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
|
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "notifications unavailable"})
|
|
return
|
|
}
|
|
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
var body struct {
|
|
Status string `json:"status"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
|
|
return
|
|
}
|
|
if err := s.notifySvc().UpdateNotificationStatus(r.Context(), user.ID, id, body.Status); err != nil {
|
|
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSONResponse(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
func writeDocRevisionError(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, docrevision.ErrInvalidDocID):
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"code": docrevision.ErrCodeInvalidDocID, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrNotFound):
|
|
writeJSONResponse(w, http.StatusNotFound, map[string]any{"code": docrevision.ErrCodeNotFound, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrNeedsContent):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{
|
|
"code": docrevision.ErrCodeNeedsContent,
|
|
"error": err.Error(),
|
|
"hint": "文档尚无服务端正文事实源;请先保存正文或注册自由文档后再读取版本账本",
|
|
})
|
|
case errors.Is(err, docrevision.ErrHeadConflict):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{"code": docrevision.ErrCodeHeadConflict, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrLifecycleLocked):
|
|
writeJSONResponse(w, http.StatusLocked, map[string]any{"code": docrevision.ErrCodeLifecycleLocked, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrCandidateStale):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{"code": docrevision.ErrCodeCandidateStale, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrNeedsConfirmation):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{"code": docrevision.ErrCodeNeedsConfirmation, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrReportInvalid):
|
|
writeJSONResponse(w, http.StatusUnprocessableEntity, map[string]any{"code": docrevision.ErrCodeReportInvalid, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrTooLarge):
|
|
writeJSONResponse(w, http.StatusRequestEntityTooLarge, map[string]any{"code": docrevision.ErrCodeTooLarge, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrForbidden), errors.Is(err, uow.ErrPermissionDenied):
|
|
writeJSONResponse(w, http.StatusForbidden, map[string]any{"code": docrevision.ErrCodeEditForbidden, "error": err.Error()})
|
|
case errors.Is(err, docrevision.ErrIdempotencyConflict), errors.Is(err, uow.ErrDigestConflict):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{"code": "DOCUMENT_IDEMPOTENCY_CONFLICT", "error": err.Error()})
|
|
case errors.Is(err, uow.ErrBusy):
|
|
writeJSONResponse(w, http.StatusConflict, map[string]any{"code": "DOCUMENT_BUSY", "error": err.Error()})
|
|
default:
|
|
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
|
|
}
|
|
}
|