CUB-113: implement core CRUD API endpoints
Some checks failed
Dev Build / build-test (pull_request) Failing after 2m4s
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:
@@ -1 +0,0 @@
|
||||
# Services
|
||||
82
backend/internal/services/services.go
Normal file
82
backend/internal/services/services.go
Normal 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)
|
||||
}
|
||||
74
backend/internal/services/validation.go
Normal file
74
backend/internal/services/validation.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user