API: Add POST /api/items/{partNumber}/bom/merge endpoint #45

Closed
opened 2026-02-09 00:16:56 +00:00 by forbes · 0 comments
Owner

Summary

Add a server-side BOM merge endpoint that accepts an assembly-derived BOM from silo-mod and intelligently merges it with the existing server BOM. The merge is synchronous — the server computes the diff, applies safe changes, and returns the full diff in the response.

Endpoint

POST /api/items/{partNumber}/bom/merge

Requires: Editor role + RequireWritable (blocked in read-only mode)

Request Body

{
  "entries": [
    {"child_part_number": "PRT-100", "quantity": 1},
    {"child_part_number": "ASM-200", "quantity": 2},
    {"child_part_number": "F01-001", "quantity": 12}
  ]
}

Each entry represents a direct child from the assembly. All entries are treated as source = "assembly".

Response Body

{
  "status": "merged",
  "diff": {
    "added": [{"part_number": "PRT-300", "quantity": 1}],
    "removed": [{"part_number": "PRT-400", "quantity": 2}],
    "quantity_changed": [{
      "part_number": "F01-001",
      "old_quantity": 8,
      "new_quantity": 12
    }],
    "unchanged": [{"part_number": "PRT-100", "quantity": 1}]
  },
  "warnings": [
    {"type": "unreferenced", "part_number": "PRT-400", "message": "Present in server BOM but not in assembly"}
  ],
  "resolve_url": "/items/ASM-001/bom"
}

Merge Rules

The merge only considers entries where source = 'assembly' on the server side. Manual entries are untouched.

Scenario Action
Added — in request, not in server (assembly-sourced) Create new relationship with source = 'assembly'
Quantity changed — in both, different quantity Update relationship quantity
Removed — in server (assembly-sourced), not in request Do NOT delete. Include in warnings array as unreferenced
Unchanged — in both, same quantity No action
Manual entries on server Completely ignored by merge (never touched)

Critical rule

Assembly-sourced entries that disappear from the assembly are never auto-deleted. They appear as "unreferenced" warnings. The user must explicitly remove them via the web UI or API. This protects against:

  • Purchased parts tracked in Silo but not modeled in CAD
  • Parts added via web UI for BOM completeness
  • Items from other disciplines (electrical, software BOMs)

Implementation

Database layer (internal/db/relationships.go)

New method:

func (r *RelationshipRepository) GetBOMBySource(
    ctx context.Context, parentItemID string, source string,
) ([]*BOMEntry, error)

This filters GetBOM by the source column. Needed so the merge can compare only assembly-sourced entries.

Handler (internal/api/bom_handlers.go)

New handler HandleMergeBOM:

  1. Resolve {partNumber} to parent item ID
  2. Validate request body (all child_part_number values must exist)
  3. Fetch current assembly-sourced BOM via GetBOMBySource(ctx, parentID, "assembly")
  4. Build maps: local[child_pn] = qty vs remote[child_pn] = qty
  5. Compute diff (added / removed / quantity_changed / unchanged)
  6. In a transaction:
    • INSERT new relationships for added entries (source = 'assembly')
    • UPDATE quantity for changed entries
    • Do NOT delete removed entries
  7. Emit SSE bom.merged event (see #45)
  8. Return diff response

Route (internal/api/routes.go)

Register in the editor-gated BOM group:

r.Post("/bom/merge", server.HandleMergeBOM)

Dependencies

  • #44 (source column on relationships table) must be implemented first
  • #45 (SSE bom.merged event) can be done in parallel

Error Cases

  • 404 — parent part number not found
  • 400 — invalid request body or unknown child part numbers
  • 409 — cycle detected (adding a child would create circular BOM)
  • 503 — server in read-only mode
## Summary Add a server-side BOM merge endpoint that accepts an assembly-derived BOM from silo-mod and intelligently merges it with the existing server BOM. The merge is synchronous — the server computes the diff, applies safe changes, and returns the full diff in the response. ## Endpoint ``` POST /api/items/{partNumber}/bom/merge ``` **Requires:** Editor role + RequireWritable (blocked in read-only mode) ### Request Body ```json { "entries": [ {"child_part_number": "PRT-100", "quantity": 1}, {"child_part_number": "ASM-200", "quantity": 2}, {"child_part_number": "F01-001", "quantity": 12} ] } ``` Each entry represents a direct child from the assembly. All entries are treated as `source = "assembly"`. ### Response Body ```json { "status": "merged", "diff": { "added": [{"part_number": "PRT-300", "quantity": 1}], "removed": [{"part_number": "PRT-400", "quantity": 2}], "quantity_changed": [{ "part_number": "F01-001", "old_quantity": 8, "new_quantity": 12 }], "unchanged": [{"part_number": "PRT-100", "quantity": 1}] }, "warnings": [ {"type": "unreferenced", "part_number": "PRT-400", "message": "Present in server BOM but not in assembly"} ], "resolve_url": "/items/ASM-001/bom" } ``` ## Merge Rules The merge only considers entries where `source = 'assembly'` on the server side. Manual entries are untouched. | Scenario | Action | |----------|--------| | **Added** — in request, not in server (assembly-sourced) | Create new relationship with `source = 'assembly'` | | **Quantity changed** — in both, different quantity | Update relationship quantity | | **Removed** — in server (assembly-sourced), not in request | **Do NOT delete.** Include in `warnings` array as `unreferenced` | | **Unchanged** — in both, same quantity | No action | | **Manual entries on server** | Completely ignored by merge (never touched) | ### Critical rule Assembly-sourced entries that disappear from the assembly are never auto-deleted. They appear as "unreferenced" warnings. The user must explicitly remove them via the web UI or API. This protects against: - Purchased parts tracked in Silo but not modeled in CAD - Parts added via web UI for BOM completeness - Items from other disciplines (electrical, software BOMs) ## Implementation ### Database layer (`internal/db/relationships.go`) New method: ```go func (r *RelationshipRepository) GetBOMBySource( ctx context.Context, parentItemID string, source string, ) ([]*BOMEntry, error) ``` This filters `GetBOM` by the `source` column. Needed so the merge can compare only assembly-sourced entries. ### Handler (`internal/api/bom_handlers.go`) New handler `HandleMergeBOM`: 1. Resolve `{partNumber}` to parent item ID 2. Validate request body (all `child_part_number` values must exist) 3. Fetch current assembly-sourced BOM via `GetBOMBySource(ctx, parentID, "assembly")` 4. Build maps: `local[child_pn] = qty` vs `remote[child_pn] = qty` 5. Compute diff (added / removed / quantity_changed / unchanged) 6. In a transaction: - INSERT new relationships for added entries (`source = 'assembly'`) - UPDATE quantity for changed entries - Do NOT delete removed entries 7. Emit SSE `bom.merged` event (see #45) 8. Return diff response ### Route (`internal/api/routes.go`) Register in the editor-gated BOM group: ```go r.Post("/bom/merge", server.HandleMergeBOM) ``` ## Dependencies - #44 (source column on relationships table) must be implemented first - #45 (SSE bom.merged event) can be done in parallel ## Error Cases - `404` — parent part number not found - `400` — invalid request body or unknown child part numbers - `409` — cycle detected (adding a child would create circular BOM) - `503` — server in read-only mode
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/silo#45