feat(db): add source column to relationships table (#44)

Promote BOM source from metadata JSONB to a dedicated VARCHAR(20)
column with CHECK constraint ('manual' or 'assembly').

- Add migration 012_bom_source.sql (column, data migration, cleanup)
- Add Source field to Relationship and BOMEntry structs
- Update all SQL queries (GetBOM, GetWhereUsed, GetExpandedBOM, Create)
- Update API response/request types with source field
- Update CSV/ODS export to read e.Source instead of metadata
- Update CSV import to set source on relationship directly
- Update frontend types and BOMTab to use top-level source field
This commit is contained in:
Forbes
2026-02-08 18:45:41 -06:00
parent 80b334f308
commit 163dc9f0f0
6 changed files with 305 additions and 82 deletions

View File

@@ -29,6 +29,7 @@ type BOMEntryResponse struct {
ChildRevision *int `json:"child_revision,omitempty"`
EffectiveRevision int `json:"effective_revision"`
Depth *int `json:"depth,omitempty"`
Source string `json:"source"`
Metadata map[string]any `json:"metadata,omitempty"`
}
@@ -51,6 +52,7 @@ type AddBOMEntryRequest struct {
Unit *string `json:"unit,omitempty"`
ReferenceDesignators []string `json:"reference_designators,omitempty"`
ChildRevision *int `json:"child_revision,omitempty"`
Source string `json:"source,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
@@ -240,6 +242,7 @@ func (s *Server) HandleAddBOMEntry(w http.ResponseWriter, r *http.Request) {
Unit: req.Unit,
ReferenceDesignators: req.ReferenceDesignators,
ChildRevision: req.ChildRevision,
Source: req.Source,
Metadata: req.Metadata,
}
if user := auth.UserFromContext(ctx); user != nil {
@@ -273,6 +276,7 @@ func (s *Server) HandleAddBOMEntry(w http.ResponseWriter, r *http.Request) {
ReferenceDesignators: req.ReferenceDesignators,
ChildRevision: req.ChildRevision,
EffectiveRevision: child.CurrentRevision,
Source: rel.Source,
Metadata: req.Metadata,
}
if req.ChildRevision != nil {
@@ -434,6 +438,7 @@ func bomEntryToResponse(e *db.BOMEntry) BOMEntryResponse {
ReferenceDesignators: refDes,
ChildRevision: e.ChildRevision,
EffectiveRevision: e.EffectiveRevision,
Source: e.Source,
Metadata: e.Metadata,
}
}
@@ -686,14 +691,14 @@ func (s *Server) HandleExportBOMCSV(w http.ResponseWriter, r *http.Request) {
}
row := []string{
strconv.Itoa(i + 1), // Item
strconv.Itoa(e.Depth), // Level
getMetaString(e.Metadata, "source"), // Source
e.ChildPartNumber, // PN
strconv.Itoa(i + 1), // Item
strconv.Itoa(e.Depth), // Level
e.Source, // Source
e.ChildPartNumber, // PN
getMetaString(e.Metadata, "seller_description"), // Seller Description
unitCostStr, // Unit Cost
qtyStr, // QTY
extCost, // Ext Cost
unitCostStr, // Unit Cost
qtyStr, // QTY
extCost, // Ext Cost
getMetaString(e.Metadata, "sourcing_link"), // Sourcing Link
}
if err := writer.Write(row); err != nil {
@@ -853,12 +858,11 @@ func (s *Server) HandleImportBOMCSV(w http.ResponseWriter, r *http.Request) {
}
// Build metadata from CSV columns
metadata := make(map[string]any)
source := ""
if idx, ok := headerIdx["source"]; ok && idx < len(record) {
if v := strings.TrimSpace(record[idx]); v != "" {
metadata["source"] = v
}
source = strings.TrimSpace(record[idx])
}
metadata := make(map[string]any)
if idx, ok := headerIdx["seller description"]; ok && idx < len(record) {
if v := strings.TrimSpace(record[idx]); v != "" {
metadata["seller_description"] = v
@@ -942,6 +946,7 @@ func (s *Server) HandleImportBOMCSV(w http.ResponseWriter, r *http.Request) {
ChildItemID: child.ID,
RelType: "component",
Quantity: quantity,
Source: source,
Metadata: metadata,
CreatedBy: importUsername,
}

View File

@@ -599,7 +599,7 @@ func (s *Server) HandleExportBOMODS(w http.ResponseWriter, r *http.Request) {
}
}
source := getMetaString(e.Metadata, "source")
source := e.Source
if source == "" && childItem != nil {
st := childItem.SourcingType
if st == "manufactured" {
@@ -754,7 +754,7 @@ func (s *Server) HandleProjectSheetODS(w http.ResponseWriter, r *http.Request) {
if e.Quantity != nil {
qty = *e.Quantity
}
source := getMetaString(e.Metadata, "source")
source := e.Source
if source == "" && childItem != nil {
if childItem.SourcingType == "manufactured" {
source = "M"