Some checks failed
Dev Build / build-test (pull_request) Failing after 2m4s
- Add dtos package with request/response structs
- Add repositories: Material, Filament, Printer, PrintJob, UsageLog
- Add services: FilamentService, PrinterService, PrintJobService
- Add handlers for all 5 resources with consistent error responses
- Wire all endpoints into Chi router under /api
- Validation on POST/PUT filament endpoints
- Filter/pagination support on list endpoints
- Soft-delete for filaments (DELETE /api/filaments/{id})
- go build ./... && go vet ./... → PASS
274 lines
7.7 KiB
Go
274 lines
7.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/CubeCraft-Creations/Extrudex/backend/internal/dtos"
|
|
"github.com/CubeCraft-Creations/Extrudex/backend/internal/models"
|
|
"github.com/CubeCraft-Creations/Extrudex/backend/internal/repositories"
|
|
"github.com/CubeCraft-Creations/Extrudex/backend/internal/services"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// FilamentHandler handles HTTP requests for filament spool CRUD operations.
|
|
type FilamentHandler struct {
|
|
service *services.FilamentService
|
|
}
|
|
|
|
// NewFilamentHandler creates a FilamentHandler with the given service.
|
|
func NewFilamentHandler(service *services.FilamentService) *FilamentHandler {
|
|
return &FilamentHandler{service: service}
|
|
}
|
|
|
|
// List handles GET /api/filaments — returns paginated, filtered spools.
|
|
func (h *FilamentHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
limit, offset := parsePagination(r)
|
|
filter := repositories.FilamentFilter{
|
|
Material: r.URL.Query().Get("material"),
|
|
Finish: r.URL.Query().Get("finish"),
|
|
Color: r.URL.Query().Get("color"),
|
|
LowStock: r.URL.Query().Get("low_stock") == "true",
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}
|
|
|
|
spools, total, err := h.service.List(r.Context(), filter)
|
|
if err != nil {
|
|
slog.Error("failed to list filaments", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{
|
|
Error: "internal server error",
|
|
Code: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, dtos.ListResponse{
|
|
Data: spools,
|
|
Total: total,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
}
|
|
|
|
// Get handles GET /api/filaments/{id} — returns a single spool.
|
|
func (h *FilamentHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "invalid filament ID",
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
spool, err := h.service.GetByID(r.Context(), id)
|
|
if err != nil {
|
|
slog.Error("failed to get filament", "id", id, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{
|
|
Error: "internal server error",
|
|
Code: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
if spool == nil {
|
|
writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{
|
|
Error: "filament not found",
|
|
Code: http.StatusNotFound,
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, dtos.SingleResponse{Data: spool})
|
|
}
|
|
|
|
// Create handles POST /api/filaments — creates a new filament spool.
|
|
func (h *FilamentHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
var req dtos.CreateFilamentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "invalid request body",
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate required fields.
|
|
if err := services.ValidateCreateFilamentRequest(req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "validation failed: " + err.Error(),
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Build domain model.
|
|
spool := models.FilamentSpool{
|
|
Name: req.Name,
|
|
MaterialBaseID: req.MaterialBaseID,
|
|
MaterialFinishID: req.MaterialFinishID,
|
|
MaterialModifierID: req.MaterialModifierID,
|
|
ColorHex: req.ColorHex,
|
|
Brand: req.Brand,
|
|
DiameterMM: 1.75, // default
|
|
InitialGrams: req.InitialGrams,
|
|
RemainingGrams: req.RemainingGrams,
|
|
SpoolWeightGrams: req.SpoolWeightGrams,
|
|
CostUSD: req.CostUSD,
|
|
LowStockThresholdGrams: 50, // default
|
|
Notes: req.Notes,
|
|
Barcode: req.Barcode,
|
|
}
|
|
if req.DiameterMM != nil {
|
|
spool.DiameterMM = *req.DiameterMM
|
|
}
|
|
if req.LowStockThresholdGrams != nil {
|
|
spool.LowStockThresholdGrams = *req.LowStockThresholdGrams
|
|
}
|
|
|
|
created, err := h.service.Create(r.Context(), &spool)
|
|
if err != nil {
|
|
slog.Error("failed to create filament", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{
|
|
Error: "internal server error",
|
|
Code: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, dtos.SingleResponse{Data: created})
|
|
}
|
|
|
|
// Update handles PUT /api/filaments/{id} — partially updates a spool.
|
|
func (h *FilamentHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "invalid filament ID",
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
var req dtos.UpdateFilamentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "invalid request body",
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate update fields.
|
|
if err := services.ValidateUpdateFilamentRequest(req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "validation failed: " + err.Error(),
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Build updates map (only non-nil fields).
|
|
updates := buildFilamentUpdates(req)
|
|
|
|
updated, err := h.service.Update(r.Context(), id, updates)
|
|
if err != nil {
|
|
slog.Error("failed to update filament", "id", id, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{
|
|
Error: "internal server error",
|
|
Code: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
if updated == nil {
|
|
writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{
|
|
Error: "filament not found",
|
|
Code: http.StatusNotFound,
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, dtos.SingleResponse{Data: updated})
|
|
}
|
|
|
|
// Delete handles DELETE /api/filaments/{id} — soft-deletes a spool.
|
|
func (h *FilamentHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, dtos.ErrorResponse{
|
|
Error: "invalid filament ID",
|
|
Code: http.StatusBadRequest,
|
|
})
|
|
return
|
|
}
|
|
|
|
deleted, err := h.service.SoftDelete(r.Context(), id)
|
|
if err != nil {
|
|
slog.Error("failed to delete filament", "id", id, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, dtos.ErrorResponse{
|
|
Error: "internal server error",
|
|
Code: http.StatusInternalServerError,
|
|
})
|
|
return
|
|
}
|
|
if !deleted {
|
|
writeJSON(w, http.StatusNotFound, dtos.ErrorResponse{
|
|
Error: "filament not found",
|
|
Code: http.StatusNotFound,
|
|
})
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// buildFilamentUpdates converts an UpdateFilamentRequest to a map of column→value.
|
|
func buildFilamentUpdates(req dtos.UpdateFilamentRequest) map[string]interface{} {
|
|
updates := make(map[string]interface{})
|
|
if req.Name != nil {
|
|
updates["name"] = *req.Name
|
|
}
|
|
if req.MaterialBaseID != nil {
|
|
updates["material_base_id"] = *req.MaterialBaseID
|
|
}
|
|
if req.MaterialFinishID != nil {
|
|
updates["material_finish_id"] = *req.MaterialFinishID
|
|
}
|
|
if req.MaterialModifierID != nil {
|
|
updates["material_modifier_id"] = *req.MaterialModifierID
|
|
}
|
|
if req.ColorHex != nil {
|
|
updates["color_hex"] = *req.ColorHex
|
|
}
|
|
if req.Brand != nil {
|
|
updates["brand"] = *req.Brand
|
|
}
|
|
if req.DiameterMM != nil {
|
|
updates["diameter_mm"] = *req.DiameterMM
|
|
}
|
|
if req.InitialGrams != nil {
|
|
updates["initial_grams"] = *req.InitialGrams
|
|
}
|
|
if req.RemainingGrams != nil {
|
|
updates["remaining_grams"] = *req.RemainingGrams
|
|
}
|
|
if req.SpoolWeightGrams != nil {
|
|
updates["spool_weight_grams"] = *req.SpoolWeightGrams
|
|
}
|
|
if req.CostUSD != nil {
|
|
updates["cost_usd"] = *req.CostUSD
|
|
}
|
|
if req.LowStockThresholdGrams != nil {
|
|
updates["low_stock_threshold_grams"] = *req.LowStockThresholdGrams
|
|
}
|
|
if req.Notes != nil {
|
|
updates["notes"] = *req.Notes
|
|
}
|
|
if req.Barcode != nil {
|
|
updates["barcode"] = *req.Barcode
|
|
}
|
|
return updates
|
|
}
|