CUB-113: implement core CRUD API endpoints
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
This commit is contained in:
2026-05-06 14:24:58 -04:00
parent 1b86d617cd
commit fca2ef5b84
19 changed files with 1496 additions and 3 deletions

View File

@@ -1 +0,0 @@
# Services

View File

@@ -0,0 +1,82 @@
// Package services contains business logic and application services.
package services
import (
"context"
"github.com/CubeCraft-Creations/Extrudex/backend/internal/models"
"github.com/CubeCraft-Creations/Extrudex/backend/internal/repositories"
)
// FilamentService wraps FilamentRepository with business logic and validation.
type FilamentService struct {
repo *repositories.FilamentRepository
}
// NewFilamentService creates a FilamentService backed by the given repository.
func NewFilamentService(repo *repositories.FilamentRepository) *FilamentService {
return &FilamentService{repo: repo}
}
// List returns paginated filament spools filtered by the given criteria.
func (s *FilamentService) List(ctx context.Context, filter repositories.FilamentFilter) ([]models.FilamentSpool, int, error) {
return s.repo.GetAll(ctx, filter)
}
// GetByID returns a single filament spool by ID.
func (s *FilamentService) GetByID(ctx context.Context, id int) (*models.FilamentSpool, error) {
return s.repo.GetByID(ctx, id)
}
// Create validates and creates a new filament spool.
func (s *FilamentService) Create(ctx context.Context, spool *models.FilamentSpool) (*models.FilamentSpool, error) {
if err := validateFilamentSpool(spool); err != nil {
return nil, err
}
return s.repo.Create(ctx, spool)
}
// Update applies partial updates to a filament spool after validation.
func (s *FilamentService) Update(ctx context.Context, id int, updates map[string]interface{}) (*models.FilamentSpool, error) {
return s.repo.Update(ctx, id, updates)
}
// SoftDelete marks a filament spool as deleted.
func (s *FilamentService) SoftDelete(ctx context.Context, id int) (bool, error) {
return s.repo.SoftDelete(ctx, id)
}
// PrinterService wraps PrinterRepository.
type PrinterService struct {
repo *repositories.PrinterRepository
}
// NewPrinterService creates a PrinterService backed by the given repository.
func NewPrinterService(repo *repositories.PrinterRepository) *PrinterService {
return &PrinterService{repo: repo}
}
// List returns all printers.
func (s *PrinterService) List(ctx context.Context) ([]models.Printer, error) {
return s.repo.GetAll(ctx)
}
// PrintJobService wraps PrintJobRepository.
type PrintJobService struct {
repo *repositories.PrintJobRepository
}
// NewPrintJobService creates a PrintJobService backed by the given repository.
func NewPrintJobService(repo *repositories.PrintJobRepository) *PrintJobService {
return &PrintJobService{repo: repo}
}
// List returns paginated print jobs filtered by the given criteria.
func (s *PrintJobService) List(ctx context.Context, filter repositories.PrintJobFilter) ([]models.PrintJob, int, error) {
return s.repo.GetAll(ctx, filter)
}
// GetByID returns a single print job by ID.
func (s *PrintJobService) GetByID(ctx context.Context, id int) (*models.PrintJob, error) {
return s.repo.GetByID(ctx, id)
}

View File

@@ -0,0 +1,74 @@
package services
import (
"errors"
"fmt"
"regexp"
"github.com/CubeCraft-Creations/Extrudex/backend/internal/dtos"
"github.com/CubeCraft-Creations/Extrudex/backend/internal/models"
)
// colorHexPattern validates hex color strings like #FF0000 or #ff0000.
var colorHexPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
// validateFilamentSpool performs validation on a FilamentSpool entity.
// Returns a descriptive error on failure.
func validateFilamentSpool(s *models.FilamentSpool) error {
if s.Name == "" {
return errors.New("name is required")
}
if s.MaterialBaseID <= 0 {
return errors.New("material_base_id is required")
}
if s.MaterialFinishID <= 0 {
return errors.New("material_finish_id is required")
}
if !colorHexPattern.MatchString(s.ColorHex) {
return fmt.Errorf("color_hex must be a valid hex color (e.g., #FF0000)")
}
if s.InitialGrams <= 0 {
return errors.New("initial_grams must be greater than 0")
}
if s.RemainingGrams < 0 {
return errors.New("remaining_grams must be >= 0")
}
return nil
}
// ValidateCreateFilamentRequest validates a creation DTO.
func ValidateCreateFilamentRequest(req dtos.CreateFilamentRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.MaterialBaseID <= 0 {
return errors.New("material_base_id is required")
}
if req.MaterialFinishID <= 0 {
return errors.New("material_finish_id is required")
}
if !colorHexPattern.MatchString(req.ColorHex) {
return fmt.Errorf("color_hex must be a valid hex color (e.g., #FF0000)")
}
if req.InitialGrams <= 0 {
return errors.New("initial_grams must be greater than 0")
}
if req.RemainingGrams < 0 {
return errors.New("remaining_grams must be >= 0")
}
return nil
}
// ValidateUpdateFilamentRequest validates partial update fields.
func ValidateUpdateFilamentRequest(req dtos.UpdateFilamentRequest) error {
if req.ColorHex != nil && !colorHexPattern.MatchString(*req.ColorHex) {
return fmt.Errorf("color_hex must be a valid hex color (e.g., #FF0000)")
}
if req.InitialGrams != nil && *req.InitialGrams <= 0 {
return errors.New("initial_grams must be greater than 0")
}
if req.RemainingGrams != nil && *req.RemainingGrams < 0 {
return errors.New("remaining_grams must be >= 0")
}
return nil
}