Files
nib-v1/internal/api/helpers.go
lerko e9ecc4c1f7
CI / test (pull_request) Successful in 2m13s
fix: address code review findings across backend and frontend
Fix goroutine-unsafe ULID entropy by wrapping in LockedMonotonicReader.
Move PRAGMA foreign_keys outside transaction in v3 migration where
SQLite was silently ignoring it. Escape LIKE wildcards in link
resolution to prevent false matches. Add non-localhost binding warning,
log writeJSON encoder errors, add ?permanent=true for explicit hard
delete, preserve title/description during absorb, use millisecond
backup timestamps, add path.Clean to spaHandler. Frontend gains
checkedJSON() for resp.ok validation, consistent stopPropagation, and
shared renderCardSections() to eliminate duplicate rendering.
2026-05-21 16:02:57 -04:00

99 lines
2.6 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/lerko/nib/internal/db"
)
const maxBodySize = 1 << 20 // 1 MB
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type EntityResponse struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
ModifiedAt string `json:"modified_at"`
Body string `json:"body"`
Title *string `json:"title"`
Description *string `json:"description"`
Glyph string `json:"glyph"`
TimeAnchor *string `json:"time_anchor"`
CompletedAt *string `json:"completed_at"`
Pinned bool `json:"pinned"`
DeletedAt *string `json:"deleted_at"`
Tags []string `json:"tags"`
CardType *string `json:"card_type"`
CardData *string `json:"card_data"`
UseCount int `json:"use_count"`
LastUsedAt *string `json:"last_used_at"`
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Printf("writeJSON encode error: %v", err)
}
}
func writeError(w http.ResponseWriter, status int, code, message string) {
writeJSON(w, status, ErrorResponse{Error: code, Message: message})
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "malformed JSON: "+err.Error())
return false
}
return true
}
func writeInternalError(w http.ResponseWriter, err error) {
log.Printf("internal error: %v", err)
writeError(w, http.StatusInternalServerError, "internal", "internal server error")
}
func entityToResponse(e *db.Entity) EntityResponse {
resp := EntityResponse{
ID: e.ID,
CreatedAt: e.CreatedAt.Format(time.RFC3339),
ModifiedAt: e.ModifiedAt.Format(time.RFC3339),
Body: e.Body,
Title: e.Title,
Description: e.Description,
Glyph: string(e.Glyph),
Pinned: e.Pinned,
Tags: e.Tags,
UseCount: e.UseCount,
}
if resp.Tags == nil {
resp.Tags = []string{}
}
resp.TimeAnchor = e.TimeAnchor
resp.CompletedAt = formatTimeRespPtr(e.CompletedAt)
resp.DeletedAt = formatTimeRespPtr(e.DeletedAt)
resp.LastUsedAt = formatTimeRespPtr(e.LastUsedAt)
if e.CardType != nil {
s := string(*e.CardType)
resp.CardType = &s
}
resp.CardData = e.CardData
return resp
}
func formatTimeRespPtr(t *time.Time) *string {
if t == nil {
return nil
}
s := t.Format(time.RFC3339)
return &s
}