initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
113
backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs
Normal file
113
backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
78
backend/Infrastructure/Data/ExtrudexDbContext.cs
Normal file
78
backend/Infrastructure/Data/ExtrudexDbContext.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Extrudex.Domain.Base;
|
||||
using Extrudex.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Extrudex.Infrastructure.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Main EF Core database context for the Extrudex system.
|
||||
/// Handles entity registration, snake_case naming, and automatic timestamp management.
|
||||
/// </summary>
|
||||
public class ExtrudexDbContext : DbContext
|
||||
{
|
||||
public ExtrudexDbContext(DbContextOptions<ExtrudexDbContext> options) : base(options) { }
|
||||
|
||||
// Lookup tables
|
||||
public DbSet<MaterialBase> MaterialBases => Set<MaterialBase>();
|
||||
public DbSet<MaterialFinish> MaterialFinishes => Set<MaterialFinish>();
|
||||
public DbSet<MaterialModifier> MaterialModifiers => Set<MaterialModifier>();
|
||||
|
||||
// Core entities
|
||||
public DbSet<Spool> Spools => Set<Spool>();
|
||||
public DbSet<Printer> Printers => Set<Printer>();
|
||||
public DbSet<AmsUnit> AmsUnits => Set<AmsUnit>();
|
||||
public DbSet<AmsSlot> AmsSlots => Set<AmsSlot>();
|
||||
public DbSet<PrintJob> PrintJobs => Set<PrintJob>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Apply all entity type configurations from the assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ExtrudexDbContext).Assembly);
|
||||
|
||||
// Apply seed data
|
||||
modelBuilder.Entity<MaterialBase>().HasData(SeedData.MaterialBases);
|
||||
modelBuilder.Entity<MaterialFinish>().HasData(SeedData.MaterialFinishes);
|
||||
modelBuilder.Entity<MaterialModifier>().HasData(SeedData.MaterialModifiers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically set UpdatedAt on auditable entities during SaveChanges.
|
||||
/// </summary>
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
SetAuditTimestamps();
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(
|
||||
bool acceptAllChangesOnSuccess,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
SetAuditTimestamps();
|
||||
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets UpdatedAt on all auditable entities that have been modified.
|
||||
/// Sets CreatedAt on all auditable entities that are being added.
|
||||
/// </summary>
|
||||
private void SetAuditTimestamps()
|
||||
{
|
||||
var entries = ChangeTracker.Entries<AuditableEntity>();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.CreatedAt = DateTime.UtcNow;
|
||||
entry.Entity.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
else if (entry.State == EntityState.Modified)
|
||||
{
|
||||
entry.Entity.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
backend/Infrastructure/Data/Seed/SeedData.cs
Normal file
121
backend/Infrastructure/Data/Seed/SeedData.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Extrudex.Domain.Entities;
|
||||
|
||||
namespace Extrudex.Infrastructure.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Static seed data for all material lookup tables.
|
||||
/// These are inserted via EF Core HasData during the initial migration.
|
||||
///
|
||||
/// IDs are deterministic GUIDs to ensure idempotent seed operations.
|
||||
/// MaterialFinish is REQUIRED on every spool — default value is "Basic" (not "Standard").
|
||||
/// </summary>
|
||||
public static class SeedData
|
||||
{
|
||||
// ─────────────────────────────────────────────
|
||||
// MaterialBase — deterministic GUIDs
|
||||
// ─────────────────────────────────────────────
|
||||
public static readonly Guid PlaId = Guid.Parse("10000000-0000-0000-0000-000000000001");
|
||||
public static readonly Guid PetgId = Guid.Parse("10000000-0000-0000-0000-000000000002");
|
||||
public static readonly Guid AbsId = Guid.Parse("10000000-0000-0000-0000-000000000003");
|
||||
public static readonly Guid AsaId = Guid.Parse("10000000-0000-0000-0000-000000000004");
|
||||
public static readonly Guid TpuId = Guid.Parse("10000000-0000-0000-0000-000000000005");
|
||||
public static readonly Guid NylonId = Guid.Parse("10000000-0000-0000-0000-000000000006");
|
||||
|
||||
public static readonly MaterialBase[] MaterialBases =
|
||||
[
|
||||
new() { Id = PlaId, Name = "PLA", DensityGperCm3 = 1.24m },
|
||||
new() { Id = PetgId, Name = "PETG", DensityGperCm3 = 1.27m },
|
||||
new() { Id = AbsId, Name = "ABS", DensityGperCm3 = 1.04m },
|
||||
new() { Id = AsaId, Name = "ASA", DensityGperCm3 = 1.07m },
|
||||
new() { Id = TpuId, Name = "TPU", DensityGperCm3 = 1.21m },
|
||||
new() { Id = NylonId, Name = "Nylon", DensityGperCm3 = 1.14m }
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// MaterialFinish — "Basic" is the default, NOT "Standard"
|
||||
// ─────────────────────────────────────────────
|
||||
public static readonly Guid PlaBasicId = Guid.Parse("20000000-0000-0000-0000-000000000001");
|
||||
public static readonly Guid PlaMatteId = Guid.Parse("20000000-0000-0000-0000-000000000002");
|
||||
public static readonly Guid PlaSilkId = Guid.Parse("20000000-0000-0000-0000-000000000003");
|
||||
public static readonly Guid PlaGlitterId = Guid.Parse("20000000-0000-0000-0000-000000000004");
|
||||
public static readonly Guid PlaMarbleId = Guid.Parse("20000000-0000-0000-0000-000000000005");
|
||||
public static readonly Guid PlaSparkleId = Guid.Parse("20000000-0000-0000-0000-000000000006");
|
||||
public static readonly Guid PetgBasicId = Guid.Parse("20000000-0000-0000-0000-000000000007");
|
||||
public static readonly Guid PetgMatteId = Guid.Parse("20000000-0000-0000-0000-000000000008");
|
||||
public static readonly Guid PetgSilkId = Guid.Parse("20000000-0000-0000-0000-000000000009");
|
||||
public static readonly Guid AbsBasicId = Guid.Parse("20000000-0000-0000-0000-000000000010");
|
||||
public static readonly Guid AbsMatteId = Guid.Parse("20000000-0000-0000-0000-000000000011");
|
||||
public static readonly Guid AsaBasicId = Guid.Parse("20000000-0000-0000-0000-000000000012");
|
||||
public static readonly Guid AsaMatteId = Guid.Parse("20000000-0000-0000-0000-000000000013");
|
||||
public static readonly Guid TpuBasicId = Guid.Parse("20000000-0000-0000-0000-000000000014");
|
||||
public static readonly Guid NylonBasicId = Guid.Parse("20000000-0000-0000-0000-000000000015");
|
||||
|
||||
public static readonly MaterialFinish[] MaterialFinishes =
|
||||
[
|
||||
// PLA finishes
|
||||
new() { Id = PlaBasicId, Name = "Basic", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaMatteId, Name = "Matte", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaSilkId, Name = "Silk", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaGlitterId, Name = "Glitter", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaMarbleId, Name = "Marble", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaSparkleId, Name = "Sparkle", MaterialBaseId = PlaId },
|
||||
|
||||
// PETG finishes
|
||||
new() { Id = PetgBasicId, Name = "Basic", MaterialBaseId = PetgId },
|
||||
new() { Id = PetgMatteId, Name = "Matte", MaterialBaseId = PetgId },
|
||||
new() { Id = PetgSilkId, Name = "Silk", MaterialBaseId = PetgId },
|
||||
|
||||
// ABS finishes
|
||||
new() { Id = AbsBasicId, Name = "Basic", MaterialBaseId = AbsId },
|
||||
new() { Id = AbsMatteId, Name = "Matte", MaterialBaseId = AbsId },
|
||||
|
||||
// ASA finishes
|
||||
new() { Id = AsaBasicId, Name = "Basic", MaterialBaseId = AsaId },
|
||||
new() { Id = AsaMatteId, Name = "Matte", MaterialBaseId = AsaId },
|
||||
|
||||
// TPU finishes
|
||||
new() { Id = TpuBasicId, Name = "Basic", MaterialBaseId = TpuId },
|
||||
|
||||
// Nylon finishes
|
||||
new() { Id = NylonBasicId, Name = "Basic", MaterialBaseId = NylonId }
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// MaterialModifier — optional additives
|
||||
// ─────────────────────────────────────────────
|
||||
public static readonly Guid PlaCarbonFiberId = Guid.Parse("30000000-0000-0000-0000-000000000001");
|
||||
public static readonly Guid PlaGlassFiberId = Guid.Parse("30000000-0000-0000-0000-000000000002");
|
||||
public static readonly Guid PlaWoodFillId = Guid.Parse("30000000-0000-0000-0000-000000000003");
|
||||
public static readonly Guid PlaGlowInDarkId = Guid.Parse("30000000-0000-0000-0000-000000000004");
|
||||
public static readonly Guid PetgCarbonFiberId = Guid.Parse("30000000-0000-0000-0000-000000000005");
|
||||
public static readonly Guid PetgGlassFiberId = Guid.Parse("30000000-0000-0000-0000-000000000006");
|
||||
public static readonly Guid AbsCarbonFiberId = Guid.Parse("30000000-0000-0000-0000-000000000007");
|
||||
public static readonly Guid AbsGlassFiberId = Guid.Parse("30000000-0000-0000-0000-000000000008");
|
||||
public static readonly Guid AsaCarbonFiberId = Guid.Parse("30000000-0000-0000-0000-000000000009");
|
||||
public static readonly Guid NylonCarbonFiberId = Guid.Parse("30000000-0000-0000-0000-000000000010");
|
||||
public static readonly Guid NylonGlassFiberId = Guid.Parse("30000000-0000-0000-0000-000000000011");
|
||||
|
||||
public static readonly MaterialModifier[] MaterialModifiers =
|
||||
[
|
||||
// PLA modifiers
|
||||
new() { Id = PlaCarbonFiberId, Name = "Carbon Fiber", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaGlassFiberId, Name = "Glass Fiber", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaWoodFillId, Name = "Wood Fill", MaterialBaseId = PlaId },
|
||||
new() { Id = PlaGlowInDarkId, Name = "Glow-in-the-Dark", MaterialBaseId = PlaId },
|
||||
|
||||
// PETG modifiers
|
||||
new() { Id = PetgCarbonFiberId, Name = "Carbon Fiber", MaterialBaseId = PetgId },
|
||||
new() { Id = PetgGlassFiberId, Name = "Glass Fiber", MaterialBaseId = PetgId },
|
||||
|
||||
// ABS modifiers
|
||||
new() { Id = AbsCarbonFiberId, Name = "Carbon Fiber", MaterialBaseId = AbsId },
|
||||
new() { Id = AbsGlassFiberId, Name = "Glass Fiber", MaterialBaseId = AbsId },
|
||||
|
||||
// ASA modifiers
|
||||
new() { Id = AsaCarbonFiberId, Name = "Carbon Fiber", MaterialBaseId = AsaId },
|
||||
|
||||
// Nylon modifiers
|
||||
new() { Id = NylonCarbonFiberId, Name = "Carbon Fiber", MaterialBaseId = NylonId },
|
||||
new() { Id = NylonGlassFiberId, Name = "Glass Fiber", MaterialBaseId = NylonId }
|
||||
];
|
||||
}
|
||||
67
backend/Infrastructure/Services/QrCodeService.cs
Normal file
67
backend/Infrastructure/Services/QrCodeService.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Extrudex.Domain.Enums;
|
||||
using Extrudex.Domain.Interfaces;
|
||||
using QRCoder;
|
||||
|
||||
namespace Extrudex.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Generates high-contrast QR codes encoding deep links to Extrudex resources.
|
||||
/// Optimized for small label printing with dark modules on white background.
|
||||
/// Uses QRCoder library with ECC-level High for robust scanning on tiny labels.
|
||||
/// </summary>
|
||||
public class QrCodeService : IQrCodeService
|
||||
{
|
||||
private const string BaseUrl = "https://extrudex.app";
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GeneratePng(QrResourceType resourceType, Guid id, int pixelsPerModule = 20)
|
||||
{
|
||||
var url = GetResourceUrl(resourceType, id);
|
||||
|
||||
using var qrGenerator = new QRCodeGenerator();
|
||||
var qrCodeData = qrGenerator.CreateQrCode(
|
||||
url,
|
||||
QRCodeGenerator.ECCLevel.H); // High error correction — critical for small labels
|
||||
|
||||
using var qrCode = new PngByteQRCode(qrCodeData);
|
||||
return qrCode.GetGraphic(
|
||||
pixelsPerModule,
|
||||
darkColorRgba: new byte[] { 0, 0, 0, 255 }, // Pure black — maximum contrast
|
||||
lightColorRgba: new byte[] { 255, 255, 255, 255 }, // Pure white background
|
||||
drawQuietZones: true); // Quiet zones improve scan reliability
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GenerateSvg(QrResourceType resourceType, Guid id)
|
||||
{
|
||||
var url = GetResourceUrl(resourceType, id);
|
||||
|
||||
using var qrGenerator = new QRCodeGenerator();
|
||||
var qrCodeData = qrGenerator.CreateQrCode(
|
||||
url,
|
||||
QRCodeGenerator.ECCLevel.H);
|
||||
|
||||
using var svgQrCode = new SvgQRCode(qrCodeData);
|
||||
return svgQrCode.GetGraphic(
|
||||
pixelsPerModule: 20,
|
||||
darkColorHex: "#000000", // Pure black — maximum contrast
|
||||
lightColorHex: "#FFFFFF", // Pure white background
|
||||
drawQuietZones: true,
|
||||
sizingMode: SvgQRCode.SizingMode.WidthHeightAttribute);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetResourceUrl(QrResourceType resourceType, Guid id)
|
||||
{
|
||||
var path = resourceType switch
|
||||
{
|
||||
QrResourceType.Spool => $"/spools/{id}",
|
||||
QrResourceType.Printer => $"/printers/{id}",
|
||||
QrResourceType.Location => $"/locations/{id}",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(resourceType),
|
||||
resourceType, $"Unsupported QR resource type: {resourceType}")
|
||||
};
|
||||
|
||||
return $"{BaseUrl}{path}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user