Add null safety checks to prevent TypeError when backend returns null instead of empty array for files list. Initialize empty slices on backend and add null coalescing on frontend when accessing files state. - Backend: Initialize files slice to always return [] instead of null - Frontend: Add null checks for files state in all map/filter operations
177 lines
4.5 KiB
Go
177 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/evanreichard/markdown-editor/internal/logging"
|
|
"github.com/evanreichard/markdown-editor/internal/storage"
|
|
)
|
|
|
|
// Handlers contains the HTTP handlers
|
|
type Handlers struct {
|
|
storage *storage.Storage
|
|
}
|
|
|
|
// New creates a new Handlers instance
|
|
func New(s *storage.Storage) *Handlers {
|
|
logging.Logger.Info("API handlers initialized")
|
|
return &Handlers{storage: s}
|
|
}
|
|
|
|
// ErrorResponse represents an error response
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// sendError sends a JSON error response
|
|
func (h *Handlers) sendError(w http.ResponseWriter, r *http.Request, statusCode int, err error) {
|
|
logging.Logger.WithFields(map[string]interface{}{
|
|
"path": r.URL.Path,
|
|
"status": statusCode,
|
|
"error": err.Error(),
|
|
}).Warn("API error")
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
|
|
}
|
|
|
|
// ListFiles handles GET /api/files
|
|
func (h *Handlers) ListFiles(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
|
return
|
|
}
|
|
|
|
files, err := h.storage.List()
|
|
if err != nil {
|
|
h.sendError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
// Ensure we always encode an array, never null
|
|
if files == nil {
|
|
files = []*storage.File{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(files)
|
|
}
|
|
|
|
// GetFile handles GET /api/files/:name
|
|
func (h *Handlers) GetFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
|
return
|
|
}
|
|
|
|
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
|
|
if name == "" {
|
|
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
|
|
return
|
|
}
|
|
|
|
file, err := h.storage.Get(name)
|
|
if err != nil {
|
|
if err == storage.ErrFileNotFound {
|
|
h.sendError(w, r, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
h.sendError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(file)
|
|
}
|
|
|
|
// CreateFile handles POST /api/files
|
|
func (h *Handlers) CreateFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
|
return
|
|
}
|
|
|
|
var file storage.File
|
|
if err := json.NewDecoder(r.Body).Decode(&file); err != nil {
|
|
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
|
return
|
|
}
|
|
|
|
result, err := h.storage.Create(file.Name, file.Content)
|
|
if err != nil {
|
|
if err == storage.ErrInvalidName {
|
|
h.sendError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
h.sendError(w, r, http.StatusConflict, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// UpdateFile handles PUT /api/files/:name
|
|
func (h *Handlers) UpdateFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut {
|
|
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
|
return
|
|
}
|
|
|
|
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
|
|
if name == "" {
|
|
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
|
|
return
|
|
}
|
|
|
|
var file storage.File
|
|
if err := json.NewDecoder(r.Body).Decode(&file); err != nil {
|
|
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
|
return
|
|
}
|
|
|
|
result, err := h.storage.Update(name, file.Content)
|
|
if err != nil {
|
|
if err == storage.ErrFileNotFound {
|
|
h.sendError(w, r, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
h.sendError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// DeleteFile handles DELETE /api/files/:name
|
|
func (h *Handlers) DeleteFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
|
return
|
|
}
|
|
|
|
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
|
|
if name == "" {
|
|
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
|
|
return
|
|
}
|
|
|
|
err := h.storage.Delete(name)
|
|
if err != nil {
|
|
if err == storage.ErrFileNotFound {
|
|
h.sendError(w, r, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
h.sendError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|