Files
Extrudex/backend/internal/repositories/filament_repository.go

286 lines
8.6 KiB
Go
Raw Normal View History

package repositories
import (
"context"
"fmt"
"strings"
"time"
"github.com/CubeCraft-Creations/Extrudex/backend/internal/models"
"github.com/jackc/pgx/v5/pgxpool"
)
// FilamentRepository handles database queries for filament_spools.
type FilamentRepository struct {
pool *pgxpool.Pool
}
// NewFilamentRepository creates a FilamentRepository backed by the given pool.
func NewFilamentRepository(pool *pgxpool.Pool) *FilamentRepository {
return &FilamentRepository{pool: pool}
}
// FilamentFilter holds query parameters for listing filament spools.
type FilamentFilter struct {
Material string // filter by material_base name (case-insensitive)
Finish string // filter by material_finish name (case-insensitive)
Color string // filter by exact color_hex match
LowStock bool // if true, filter for remaining_grams <= low_stock_threshold_grams
Limit int
Offset int
}
// spoolScanFields is the common SELECT column list for filament spools with JOINs.
const spoolScanFields = `
s.id, s.name,
s.material_base_id,
COALESCE(mb.name, '') as material_base_name,
COALESCE(mb.density_g_cm3, 0) as material_base_density_g_cm3,
COALESCE(mb.extrusion_temp_min, NULL::int) as material_base_extrusion_temp_min,
COALESCE(mb.extrusion_temp_max, NULL::int) as material_base_extrusion_temp_max,
COALESCE(mb.bed_temp_min, NULL::int) as material_base_bed_temp_min,
COALESCE(mb.bed_temp_max, NULL::int) as material_base_bed_temp_max,
COALESCE(mb.created_at, s.created_at) as material_base_created_at,
COALESCE(mb.updated_at, s.created_at) as material_base_updated_at,
s.material_finish_id,
COALESCE(mf.name, '') as material_finish_name,
mf.description as material_finish_description,
COALESCE(mf.created_at, s.created_at) as material_finish_created_at,
COALESCE(mf.updated_at, s.created_at) as material_finish_updated_at,
s.material_modifier_id,
mm.name as material_modifier_name,
mm.description as material_modifier_description,
mm.created_at as material_modifier_created_at,
mm.updated_at as material_modifier_updated_at,
s.color_hex, s.brand, s.diameter_mm,
s.initial_grams, s.remaining_grams, s.spool_weight_grams,
s.cost_usd, s.low_stock_threshold_grams,
s.notes, s.barcode,
s.deleted_at, s.created_at, s.updated_at`
const spoolFromJoins = `
FROM filament_spools s
LEFT JOIN material_bases mb ON s.material_base_id = mb.id
LEFT JOIN material_finishes mf ON s.material_finish_id = mf.id
LEFT JOIN material_modifiers mm ON s.material_modifier_id = mm.id`
// scanSpoolWithJoins scans a full spool row including all JOINed tables.
func scanSpoolWithJoins(row interface{ Scan(...interface{}) error }) (models.FilamentSpool, error) {
var s models.FilamentSpool
var mb models.MaterialBase
var mf models.MaterialFinish
var mfDesc *string
var modifierID *int
var modName, modDesc *string
var modCreatedAt, modUpdatedAt *time.Time
err := row.Scan(
&s.ID, &s.Name,
&s.MaterialBaseID,
&mb.Name, &mb.DensityGCm3,
&mb.ExtrusionTempMin, &mb.ExtrusionTempMax,
&mb.BedTempMin, &mb.BedTempMax,
&mb.CreatedAt, &mb.UpdatedAt,
&s.MaterialFinishID,
&mf.Name, &mfDesc,
&mf.CreatedAt, &mf.UpdatedAt,
&modifierID,
&modName, &modDesc,
&modCreatedAt, &modUpdatedAt,
&s.ColorHex, &s.Brand, &s.DiameterMM,
&s.InitialGrams, &s.RemainingGrams, &s.SpoolWeightGrams,
&s.CostUSD, &s.LowStockThresholdGrams,
&s.Notes, &s.Barcode,
&s.DeletedAt, &s.CreatedAt, &s.UpdatedAt,
)
if err != nil {
return s, err
}
mb.ID = s.MaterialBaseID
s.MaterialBase = &mb
mf.ID = s.MaterialFinishID
if mfDesc != nil {
mf.Description = mfDesc
}
s.MaterialFinish = &mf
s.MaterialModifierID = modifierID
if modifierID != nil && modName != nil {
mm := models.MaterialModifier{
ID: *modifierID,
Name: *modName,
}
if modDesc != nil {
mm.Description = modDesc
}
if modCreatedAt != nil {
mm.CreatedAt = *modCreatedAt
}
if modUpdatedAt != nil {
mm.UpdatedAt = *modUpdatedAt
}
s.MaterialModifier = &mm
}
return s, nil
}
// GetAll returns filament spools matching the given filters, with pagination.
// Returns results, total matching count, and any error.
func (r *FilamentRepository) GetAll(ctx context.Context, filter FilamentFilter) ([]models.FilamentSpool, int, error) {
conditions := []string{"s.deleted_at IS NULL"}
args := []interface{}{}
argIdx := 1
if filter.Material != "" {
conditions = append(conditions, fmt.Sprintf("LOWER(mb.name) = LOWER($%d)", argIdx))
args = append(args, filter.Material)
argIdx++
}
if filter.Finish != "" {
conditions = append(conditions, fmt.Sprintf("LOWER(mf.name) = LOWER($%d)", argIdx))
args = append(args, filter.Finish)
argIdx++
}
if filter.Color != "" {
conditions = append(conditions, fmt.Sprintf("s.color_hex = $%d", argIdx))
args = append(args, filter.Color)
argIdx++
}
if filter.LowStock {
conditions = append(conditions, "s.remaining_grams <= s.low_stock_threshold_grams")
}
whereClause := ""
if len(conditions) > 0 {
whereClause = "WHERE " + strings.Join(conditions, " AND ")
}
// Count total.
var total int
countQuery := "SELECT COUNT(*) " + spoolFromJoins + " " + whereClause
if err := r.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
return nil, 0, err
}
// Query with pagination.
dataQuery := "SELECT " + spoolScanFields + " " + spoolFromJoins + " " +
whereClause +
" ORDER BY s.name ASC" +
fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
dataArgs := make([]interface{}, len(args))
copy(dataArgs, args)
dataArgs = append(dataArgs, filter.Limit, filter.Offset)
rows, err := r.pool.Query(ctx, dataQuery, dataArgs...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var spools []models.FilamentSpool
for rows.Next() {
s, err := scanSpoolWithJoins(rows)
if err != nil {
return nil, 0, err
}
spools = append(spools, s)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
if spools == nil {
spools = []models.FilamentSpool{}
}
return spools, total, nil
}
// GetByID returns a single filament spool by ID with JOINed data.
// Returns nil if not found or soft-deleted.
func (r *FilamentRepository) GetByID(ctx context.Context, id int) (*models.FilamentSpool, error) {
query := "SELECT " + spoolScanFields + " " + spoolFromJoins +
" WHERE s.id = $1 AND s.deleted_at IS NULL"
row := r.pool.QueryRow(ctx, query, id)
s, err := scanSpoolWithJoins(row)
if err != nil {
return nil, err
}
return &s, nil
}
// Create inserts a new filament spool and returns the created spool with JOINed data.
func (r *FilamentRepository) Create(ctx context.Context, spool *models.FilamentSpool) (*models.FilamentSpool, error) {
var id int
err := r.pool.QueryRow(ctx, `
INSERT INTO filament_spools (
name, material_base_id, material_finish_id, material_modifier_id,
color_hex, brand, diameter_mm, initial_grams, remaining_grams,
spool_weight_grams, cost_usd, low_stock_threshold_grams,
notes, barcode
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
RETURNING id
`,
spool.Name, spool.MaterialBaseID, spool.MaterialFinishID, spool.MaterialModifierID,
spool.ColorHex, spool.Brand, spool.DiameterMM, spool.InitialGrams, spool.RemainingGrams,
spool.SpoolWeightGrams, spool.CostUSD, spool.LowStockThresholdGrams,
spool.Notes, spool.Barcode,
).Scan(&id)
if err != nil {
return nil, err
}
return r.GetByID(ctx, id)
}
// Update applies partial updates to an existing filament spool.
// Only non-nil fields in the update map are applied.
// Returns the updated spool.
func (r *FilamentRepository) Update(ctx context.Context, id int, updates map[string]interface{}) (*models.FilamentSpool, error) {
if len(updates) == 0 {
return r.GetByID(ctx, id)
}
setClauses := []string{"updated_at = NOW()"}
args := []interface{}{}
argIdx := 1
for col, val := range updates {
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, argIdx))
args = append(args, val)
argIdx++
}
args = append(args, id)
query := fmt.Sprintf("UPDATE filament_spools SET %s WHERE id = $%d AND deleted_at IS NULL",
strings.Join(setClauses, ", "), argIdx)
result, err := r.pool.Exec(ctx, query, args...)
if err != nil {
return nil, err
}
if result.RowsAffected() == 0 {
return nil, nil // not found or deleted
}
return r.GetByID(ctx, id)
}
// SoftDelete marks a filament spool as deleted by setting deleted_at = NOW().
// Returns true if a row was affected.
func (r *FilamentRepository) SoftDelete(ctx context.Context, id int) (bool, error) {
result, err := r.pool.Exec(ctx, `
UPDATE filament_spools
SET deleted_at = NOW(), updated_at = NOW()
WHERE id = $1 AND deleted_at IS NULL
`, id)
if err != nil {
return false, err
}
return result.RowsAffected() > 0, nil
}