initial commit

This commit is contained in:
cubecraft-agents[bot]
2026-04-25 18:51:05 +00:00
commit 230c3b295d
78 changed files with 8093 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class AmsSlotConfiguration : BaseEntityConfiguration<AmsSlot>
{
public override void Configure(EntityTypeBuilder<AmsSlot> builder)
{
base.Configure(builder);
builder.Property(e => e.TrayIndex)
.HasColumnName("tray_index")
.IsRequired();
builder.Property(e => e.AmsUnitId)
.HasColumnName("ams_unit_id")
.IsRequired();
builder.Property(e => e.SpoolId)
.HasColumnName("spool_id");
builder.Property(e => e.RemainingWeightG)
.HasColumnName("remaining_weight_g")
.HasPrecision(10, 2);
// Unique index on (ams_unit_id, tray_index) — each slot position is unique within its unit
builder.HasIndex(e => new { e.AmsUnitId, e.TrayIndex })
.IsUnique()
.HasDatabaseName("ix_ams_slots_ams_unit_id_tray_index");
// Index on spool_id for looking up which slot holds a given spool
builder.HasIndex(e => e.SpoolId)
.HasDatabaseName("ix_ams_slots_spool_id");
// Relationships
builder.HasOne(e => e.AmsUnit)
.WithMany(e => e.Slots)
.HasForeignKey(e => e.AmsUnitId)
.HasConstraintName("fk_ams_slots_ams_unit")
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(e => e.Spool)
.WithMany(e => e.AmsSlots)
.HasForeignKey(e => e.SpoolId)
.HasConstraintName("fk_ams_slots_spool")
.OnDelete(DeleteBehavior.SetNull);
}
}

View File

@@ -0,0 +1,38 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class AmsUnitConfiguration : BaseEntityConfiguration<AmsUnit>
{
public override void Configure(EntityTypeBuilder<AmsUnit> builder)
{
base.Configure(builder);
builder.Property(e => e.UnitIndex)
.HasColumnName("unit_index")
.IsRequired();
builder.Property(e => e.PrinterId)
.HasColumnName("printer_id")
.IsRequired();
// Unique index on (printer_id, unit_index) — no two units on the same printer share an index
builder.HasIndex(e => new { e.PrinterId, e.UnitIndex })
.IsUnique()
.HasDatabaseName("ix_ams_units_printer_id_unit_index");
// Relationships
builder.HasOne(e => e.Printer)
.WithMany(e => e.AmsUnits)
.HasForeignKey(e => e.PrinterId)
.HasConstraintName("fk_ams_units_printer")
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.Slots)
.WithOne(e => e.AmsUnit)
.HasForeignKey(e => e.AmsUnitId)
.HasConstraintName("fk_ams_slots_ams_unit");
}
}

View File

@@ -0,0 +1,63 @@
using Extrudex.Domain.Base;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
/// <summary>
/// Base configuration for all entities. Sets up common conventions:
/// - Table names in snake_case
/// - GUID primary keys stored as PostgreSQL UUID
/// - Automatic timestamp columns in snake_case
/// </summary>
/// <typeparam name="TEntity">The entity type to configure.</typeparam>
public abstract class BaseEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
where TEntity : BaseEntity
{
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// Table name in snake_case
builder.ToTable(ToSnakeCase(typeof(TEntity).Name));
// Primary key stored as UUID
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
// If the entity is auditable, configure the timestamp columns
if (typeof(AuditableEntity).IsAssignableFrom(typeof(TEntity)))
{
ConfigureAuditColumns(builder);
}
}
/// <summary>
/// Configures audit timestamp columns (created_at, updated_at) for auditable entities.
/// Uses string-based property names since the generic type constraint is BaseEntity
/// and cannot be cast to AuditableEntity at compile time.
/// </summary>
private static void ConfigureAuditColumns(EntityTypeBuilder<TEntity> builder)
{
builder.Property<DateTime>("CreatedAt")
.HasColumnName("created_at")
.HasDefaultValueSql("now() at time zone 'utc'");
builder.Property<DateTime>("UpdatedAt")
.HasColumnName("updated_at")
.HasDefaultValueSql("now() at time zone 'utc'");
}
/// <summary>
/// Converts PascalCase or camelCase to snake_case.
/// </summary>
protected static string ToSnakeCase(string name)
{
return string.Concat(
name.Select((ch, i) =>
i > 0 && char.IsUpper(ch) && (char.IsLower(name[i - 1]) || (i + 1 < name.Length && char.IsLower(name[i + 1])))
? "_" + ch
: ch.ToString()))
.ToLowerInvariant();
}
}

View File

@@ -0,0 +1,44 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class MaterialBaseConfiguration : BaseEntityConfiguration<MaterialBase>
{
public override void Configure(EntityTypeBuilder<MaterialBase> builder)
{
base.Configure(builder);
builder.Property(e => e.Name)
.HasColumnName("name")
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.DensityGperCm3)
.HasColumnName("density_g_per_cm3")
.HasPrecision(10, 4)
.IsRequired();
// Unique index on material base name
builder.HasIndex(e => e.Name)
.IsUnique()
.HasDatabaseName("ix_material_bases_name");
// Relationships
builder.HasMany(e => e.Finishes)
.WithOne(e => e.MaterialBase)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_material_finishes_material_base");
builder.HasMany(e => e.Modifiers)
.WithOne(e => e.MaterialBase)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_material_modifiers_material_base");
builder.HasMany(e => e.Spools)
.WithOne(e => e.MaterialBase)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_spools_material_base");
}
}

View File

@@ -0,0 +1,39 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class MaterialFinishConfiguration : BaseEntityConfiguration<MaterialFinish>
{
public override void Configure(EntityTypeBuilder<MaterialFinish> builder)
{
base.Configure(builder);
builder.Property(e => e.Name)
.HasColumnName("name")
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.MaterialBaseId)
.HasColumnName("material_base_id")
.IsRequired();
// Unique index on (material_base_id, name) — each finish name is unique per base material
builder.HasIndex(e => new { e.MaterialBaseId, e.Name })
.IsUnique()
.HasDatabaseName("ix_material_finishes_material_base_id_name");
// Relationship configured from MaterialBase side; navigation-only here
builder.HasOne(e => e.MaterialBase)
.WithMany(e => e.Finishes)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_material_finishes_material_base")
.OnDelete(DeleteBehavior.Restrict);
builder.HasMany(e => e.Spools)
.WithOne(e => e.MaterialFinish)
.HasForeignKey(e => e.MaterialFinishId)
.HasConstraintName("fk_spools_material_finish");
}
}

View File

@@ -0,0 +1,38 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class MaterialModifierConfiguration : BaseEntityConfiguration<MaterialModifier>
{
public override void Configure(EntityTypeBuilder<MaterialModifier> builder)
{
base.Configure(builder);
builder.Property(e => e.Name)
.HasColumnName("name")
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.MaterialBaseId)
.HasColumnName("material_base_id")
.IsRequired();
// Unique index on (material_base_id, name) — each modifier name is unique per base material
builder.HasIndex(e => new { e.MaterialBaseId, e.Name })
.IsUnique()
.HasDatabaseName("ix_material_modifiers_material_base_id_name");
builder.HasOne(e => e.MaterialBase)
.WithMany(e => e.Modifiers)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_material_modifiers_material_base")
.OnDelete(DeleteBehavior.Restrict);
builder.HasMany(e => e.Spools)
.WithOne(e => e.MaterialModifier!)
.HasForeignKey(e => e.MaterialModifierId)
.HasConstraintName("fk_spools_material_modifier");
}
}

View File

@@ -0,0 +1,108 @@
using Extrudex.Domain.Entities;
using Extrudex.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class PrintJobConfiguration : BaseEntityConfiguration<PrintJob>
{
public override void Configure(EntityTypeBuilder<PrintJob> builder)
{
base.Configure(builder);
builder.Property(e => e.PrinterId)
.HasColumnName("printer_id")
.IsRequired();
builder.Property(e => e.SpoolId)
.HasColumnName("spool_id")
.IsRequired();
builder.Property(e => e.PrintName)
.HasColumnName("print_name")
.IsRequired()
.HasMaxLength(500);
builder.Property(e => e.GcodeFilePath)
.HasColumnName("gcode_file_path")
.HasMaxLength(1000);
builder.Property(e => e.MmExtruded)
.HasColumnName("mm_extruded")
.HasPrecision(12, 2)
.IsRequired();
builder.Property(e => e.GramsDerived)
.HasColumnName("grams_derived")
.HasPrecision(10, 2)
.IsRequired();
builder.Property(e => e.CostPerPrint)
.HasColumnName("cost_per_print")
.HasPrecision(10, 4);
builder.Property(e => e.StartedAt)
.HasColumnName("started_at");
builder.Property(e => e.CompletedAt)
.HasColumnName("completed_at");
builder.Property(e => e.Status)
.HasColumnName("status")
.HasConversion<string>()
.HasMaxLength(50)
.HasDefaultValue(JobStatus.Queued)
.IsRequired();
builder.Property(e => e.DataSource)
.HasColumnName("data_source")
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
// Audit snapshots for COGS accuracy
builder.Property(e => e.FilamentDiameterAtPrintMm)
.HasColumnName("filament_diameter_at_print_mm")
.HasPrecision(6, 3)
.IsRequired();
builder.Property(e => e.MaterialDensityAtPrint)
.HasColumnName("material_density_at_print")
.HasPrecision(10, 4)
.IsRequired();
builder.Property(e => e.Notes)
.HasColumnName("notes")
.HasMaxLength(2000);
// Index on status for filtering active/completed jobs
builder.HasIndex(e => e.Status)
.HasDatabaseName("ix_print_jobs_status");
// Index on printer_id for querying jobs by printer
builder.HasIndex(e => e.PrinterId)
.HasDatabaseName("ix_print_jobs_printer_id");
// Index on spool_id for querying jobs by spool
builder.HasIndex(e => e.SpoolId)
.HasDatabaseName("ix_print_jobs_spool_id");
// Index on data_source for querying by integration path
builder.HasIndex(e => e.DataSource)
.HasDatabaseName("ix_print_jobs_data_source");
// Relationships
builder.HasOne(e => e.Printer)
.WithMany(e => e.PrintJobs)
.HasForeignKey(e => e.PrinterId)
.HasConstraintName("fk_print_jobs_printer")
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.Spool)
.WithMany(e => e.PrintJobs)
.HasForeignKey(e => e.SpoolId)
.HasConstraintName("fk_print_jobs_spool")
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@@ -0,0 +1,111 @@
using Extrudex.Domain.Entities;
using Extrudex.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class PrinterConfiguration : BaseEntityConfiguration<Printer>
{
public override void Configure(EntityTypeBuilder<Printer> builder)
{
base.Configure(builder);
builder.Property(e => e.Name)
.HasColumnName("name")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.Status)
.HasColumnName("status")
.HasConversion<string>()
.HasMaxLength(50)
.HasDefaultValue(PrinterStatus.Offline)
.IsRequired();
builder.Property(e => e.Manufacturer)
.HasColumnName("manufacturer")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.Model)
.HasColumnName("model")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.PrinterType)
.HasColumnName("printer_type")
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
builder.Property(e => e.ConnectionType)
.HasColumnName("connection_type")
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
builder.Property(e => e.HostnameOrIp)
.HasColumnName("hostname_or_ip")
.IsRequired()
.HasMaxLength(255);
builder.Property(e => e.Port)
.HasColumnName("port")
.IsRequired();
// MQTT credentials
builder.Property(e => e.MqttUsername)
.HasColumnName("mqtt_username")
.HasMaxLength(200);
builder.Property(e => e.MqttPassword)
.HasColumnName("mqtt_password")
.HasMaxLength(500);
builder.Property(e => e.MqttUseTls)
.HasColumnName("mqtt_use_tls")
.HasDefaultValue(false)
.IsRequired();
// Moonraker API key
builder.Property(e => e.ApiKey)
.HasColumnName("api_key")
.HasMaxLength(500);
builder.Property(e => e.IsActive)
.HasColumnName("is_active")
.HasDefaultValue(true)
.IsRequired();
builder.Property(e => e.LastSeenAt)
.HasColumnName("last_seen_at");
// Index on status for filtering online/offline printers
builder.HasIndex(e => e.Status)
.HasDatabaseName("ix_printers_status");
// Index on printer_type for filtering by printer hardware type
builder.HasIndex(e => e.PrinterType)
.HasDatabaseName("ix_printers_printer_type");
// Index on connection for querying by protocol
builder.HasIndex(e => e.ConnectionType)
.HasDatabaseName("ix_printers_connection_type");
// Index on is_active for active printer queries
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("ix_printers_is_active");
// Relationships
builder.HasMany(e => e.AmsUnits)
.WithOne(e => e.Printer)
.HasForeignKey(e => e.PrinterId)
.HasConstraintName("fk_ams_units_printer");
builder.HasMany(e => e.PrintJobs)
.WithOne(e => e.Printer)
.HasForeignKey(e => e.PrinterId)
.HasConstraintName("fk_print_jobs_printer");
}
}

View File

@@ -0,0 +1,113 @@
using Extrudex.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Extrudex.Infrastructure.Data.Configurations;
public class SpoolConfiguration : BaseEntityConfiguration<Spool>
{
public override void Configure(EntityTypeBuilder<Spool> builder)
{
base.Configure(builder);
builder.Property(e => e.MaterialBaseId)
.HasColumnName("material_base_id")
.IsRequired();
builder.Property(e => e.MaterialFinishId)
.HasColumnName("material_finish_id")
.IsRequired();
builder.Property(e => e.MaterialModifierId)
.HasColumnName("material_modifier_id");
builder.Property(e => e.Brand)
.HasColumnName("brand")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.ColorName)
.HasColumnName("color_name")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.ColorHex)
.HasColumnName("color_hex")
.IsRequired()
.HasMaxLength(7); // "#RRGGBB" format
builder.Property(e => e.WeightTotalGrams)
.HasColumnName("weight_total_grams")
.HasPrecision(10, 2)
.IsRequired();
builder.Property(e => e.WeightRemainingGrams)
.HasColumnName("weight_remaining_grams")
.HasPrecision(10, 2)
.IsRequired();
builder.Property(e => e.FilamentDiameterMm)
.HasColumnName("filament_diameter_mm")
.HasPrecision(6, 3)
.IsRequired();
builder.Property(e => e.SpoolSerial)
.HasColumnName("spool_serial")
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.PurchasePrice)
.HasColumnName("purchase_price")
.HasPrecision(10, 2);
builder.Property(e => e.PurchaseDate)
.HasColumnName("purchase_date");
builder.Property(e => e.IsActive)
.HasColumnName("is_active")
.HasDefaultValue(true)
.IsRequired();
// Unique index on spool_serial — critical for barcode/QR scanning
builder.HasIndex(e => e.SpoolSerial)
.IsUnique()
.HasDatabaseName("ix_spools_spool_serial");
// Index on material_base_id for spool filtering
builder.HasIndex(e => e.MaterialBaseId)
.HasDatabaseName("ix_spools_material_base_id");
// Index on is_active for active spool queries
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("ix_spools_is_active");
// Relationships
builder.HasOne(e => e.MaterialBase)
.WithMany(e => e.Spools)
.HasForeignKey(e => e.MaterialBaseId)
.HasConstraintName("fk_spools_material_base")
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.MaterialFinish)
.WithMany(e => e.Spools)
.HasForeignKey(e => e.MaterialFinishId)
.HasConstraintName("fk_spools_material_finish")
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.MaterialModifier)
.WithMany(e => e.Spools)
.HasForeignKey(e => e.MaterialModifierId)
.HasConstraintName("fk_spools_material_modifier")
.OnDelete(DeleteBehavior.SetNull);
builder.HasMany(e => e.AmsSlots)
.WithOne(e => e.Spool!)
.HasForeignKey(e => e.SpoolId)
.HasConstraintName("fk_ams_slots_spool");
builder.HasMany(e => e.PrintJobs)
.WithOne(e => e.Spool)
.HasForeignKey(e => e.SpoolId)
.HasConstraintName("fk_print_jobs_spool");
}
}