fix: CreateSection/CreateRequest/CreateRequestList now use lib.EntryWrite — no more raw DB calls in handlers

This commit is contained in:
James 2026-03-17 13:21:49 -04:00
parent eb37f682eb
commit ec0b60d44c
1 changed files with 58 additions and 87 deletions

View File

@ -2057,35 +2057,28 @@ func (h *Handlers) CreateSection(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Name) == "" || body.ParentID == "" { if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Name) == "" || body.ParentID == "" {
ErrorResponse(w, http.StatusBadRequest, "invalid_body", "name and parent_id required"); return ErrorResponse(w, http.StatusBadRequest, "invalid_body", "name and parent_id required"); return
} }
projectKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID) parent, err := lib.EntryByID(h.DB, h.Cfg, body.ParentID)
if err != nil { ErrorResponse(w, http.StatusInternalServerError, "internal", "key error"); return } if err != nil || parent == nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_parent", "Parent entry not found"); return
var parentDepth int }
h.DB.Conn.QueryRow(`SELECT depth FROM entries WHERE entry_id=?`, body.ParentID).Scan(&parentDepth) siblings, _ := lib.EntriesByParent(h.DB, h.Cfg, body.ParentID)
var maxSort int
h.DB.Conn.QueryRow(
`SELECT COALESCE(MAX(sort_order),0) FROM entries WHERE parent_id=? AND deleted_at IS NULL`, body.ParentID,
).Scan(&maxSort)
now := time.Now().UnixMilli()
secID := uuid.New().String()
name := strings.TrimSpace(body.Name) name := strings.TrimSpace(body.Name)
secData := lib.SectionData{Name: name} secData := lib.SectionData{Name: name}
secDataJSON, _ := json.Marshal(secData) secDataJSON, _ := json.Marshal(secData)
sumPacked, _ := lib.Pack(projectKey, name) entry := &lib.Entry{
dataPacked, _ := lib.Pack(projectKey, string(secDataJSON)) ProjectID: projectID,
ParentID: body.ParentID,
_, err = h.DB.Conn.Exec( Type: lib.TypeSection,
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order, Depth: parent.Depth + 1,
search_key, search_key2, summary, data, stage, SortOrder: (len(siblings) + 1) * 1000,
assignee_id, return_to_id, origin_id, version, deleted_at, deleted_by, key_version, created_at, updated_at, created_by) SummaryText: name,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, DataText: string(secDataJSON),
secID, projectID, body.ParentID, lib.TypeSection, parentDepth+1, maxSort+1000, Stage: lib.StagePreDataroom,
nil, nil, sumPacked, dataPacked, lib.StagePreDataroom, }
"", "", "", 1, nil, nil, 1, now, now, actorID, if err := lib.EntryWrite(h.DB, h.Cfg, actorID, entry); err != nil {
) ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create section"); return
if err != nil { ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create section"); return } }
JSONResponse(w, http.StatusOK, map[string]any{"entry_id": secID, "name": name}) JSONResponse(w, http.StatusOK, map[string]any{"entry_id": entry.EntryID, "name": name})
} }
// CreateRequest creates a new blank request under a section or request_list. // CreateRequest creates a new blank request under a section or request_list.
@ -2102,36 +2095,28 @@ func (h *Handlers) CreateRequest(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Title) == "" || body.ParentID == "" { if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Title) == "" || body.ParentID == "" {
ErrorResponse(w, http.StatusBadRequest, "invalid_body", "title and parent_id required"); return ErrorResponse(w, http.StatusBadRequest, "invalid_body", "title and parent_id required"); return
} }
projectKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID) parent, err := lib.EntryByID(h.DB, h.Cfg, body.ParentID)
if err != nil { ErrorResponse(w, http.StatusInternalServerError, "internal", "key error"); return } if err != nil || parent == nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_parent", "Parent entry not found"); return
// Determine depth from parent }
var parentDepth int siblings, _ := lib.EntriesByParent(h.DB, h.Cfg, body.ParentID)
h.DB.Conn.QueryRow(`SELECT depth FROM entries WHERE entry_id=?`, body.ParentID).Scan(&parentDepth)
var maxSort int
h.DB.Conn.QueryRow(
`SELECT COALESCE(MAX(sort_order),0) FROM entries WHERE parent_id=? AND deleted_at IS NULL`, body.ParentID,
).Scan(&maxSort)
now := time.Now().UnixMilli()
reqID := uuid.New().String()
title := strings.TrimSpace(body.Title) title := strings.TrimSpace(body.Title)
reqData := lib.RequestData{Title: title, Status: "open", Priority: "medium"} reqData := lib.RequestData{Title: title, Status: "open", Priority: "medium"}
reqDataJSON, _ := json.Marshal(reqData) reqDataJSON, _ := json.Marshal(reqData)
sumPacked, _ := lib.Pack(projectKey, title) entry := &lib.Entry{
dataPacked, _ := lib.Pack(projectKey, string(reqDataJSON)) ProjectID: projectID,
ParentID: body.ParentID,
_, err = h.DB.Conn.Exec( Type: lib.TypeRequest,
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order, Depth: parent.Depth + 1,
search_key, search_key2, summary, data, stage, SortOrder: (len(siblings) + 1) * 100,
assignee_id, return_to_id, origin_id, version, deleted_at, deleted_by, key_version, created_at, updated_at, created_by) SummaryText: title,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, DataText: string(reqDataJSON),
reqID, projectID, body.ParentID, lib.TypeRequest, parentDepth+1, maxSort+100, Stage: lib.StagePreDataroom,
nil, nil, sumPacked, dataPacked, lib.StagePreDataroom, }
"", "", "", 1, nil, nil, 1, now, now, actorID, if err := lib.EntryWrite(h.DB, h.Cfg, actorID, entry); err != nil {
) ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create request"); return
if err != nil { ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create request"); return } }
JSONResponse(w, http.StatusOK, map[string]any{"entry_id": reqID, "title": title}) JSONResponse(w, http.StatusOK, map[string]any{"entry_id": entry.EntryID, "title": title})
} }
// CreateRequestList creates a new empty request list for a project. // CreateRequestList creates a new empty request list for a project.
@ -2152,48 +2137,34 @@ func (h *Handlers) CreateRequestList(w http.ResponseWriter, r *http.Request) {
return return
} }
projectKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Could not derive project key")
return
}
now := time.Now().UnixMilli()
listName := strings.TrimSpace(body.Name) listName := strings.TrimSpace(body.Name)
requestListID := uuid.New().String() // Sort after existing lists: use sibling count * 1000
siblings, _ := lib.EntriesByParent(h.DB, h.Cfg, projectID)
// Compute sort_order: place after last existing list listCount := 0
var maxSort int for _, s := range siblings {
h.DB.Conn.QueryRow( if s.Type == lib.TypeRequestList {
`SELECT COALESCE(MAX(sort_order),0) FROM entries WHERE project_id=? AND type='request_list' AND deleted_at IS NULL`, projectID, listCount++
).Scan(&maxSort) }
sortOrder := maxSort + 1000 }
rlData := lib.RequestListData{Name: listName} rlData := lib.RequestListData{Name: listName}
rlDataJSON, _ := json.Marshal(rlData) rlDataJSON, _ := json.Marshal(rlData)
rlSummaryPacked, _ := lib.Pack(projectKey, listName) entry := &lib.Entry{
rlDataPacked, _ := lib.Pack(projectKey, string(rlDataJSON)) ProjectID: projectID,
ParentID: projectID,
_, err = h.DB.Conn.Exec( Type: lib.TypeRequestList,
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order, Depth: 1,
search_key, search_key2, summary, data, stage, SortOrder: (listCount + 1) * 1000,
assignee_id, return_to_id, origin_id, SummaryText: listName,
version, deleted_at, deleted_by, key_version, DataText: string(rlDataJSON),
created_at, updated_at, created_by) Stage: lib.StagePreDataroom,
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`, }
requestListID, projectID, projectID, lib.TypeRequestList, 1, sortOrder, if err := lib.EntryWrite(h.DB, h.Cfg, actorID, entry); err != nil {
nil, nil, rlSummaryPacked, rlDataPacked, lib.StagePreDataroom,
"", "", "",
1, nil, nil, 1,
now, now, actorID,
)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create request list") ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create request list")
return return
} }
JSONResponse(w, http.StatusOK, map[string]any{ JSONResponse(w, http.StatusOK, map[string]any{
"entry_id": requestListID, "entry_id": entry.EntryID,
"name": listName, "name": listName,
}) })
} }