Files
silo/internal/db/item_dependencies.go
Forbes cffcf56085 feat(api): item dependency extraction, indexing, and resolve endpoints
- Add Dependency type to internal/kc and extract silo/dependencies.json
  from .kc files on commit
- Create ItemDependencyRepository with ReplaceForRevision, ListByItem,
  and Resolve (LEFT JOIN against items table)
- Add GET /{partNumber}/dependencies and
  GET /{partNumber}/dependencies/resolve endpoints
- Index dependencies in extractKCMetadata with SSE broadcast
- Pack real dependency data into .kc files on checkout
- Update PackInput.Dependencies from []any to []Dependency

Closes #143
2026-02-18 18:53:40 -06:00

128 lines
4.1 KiB
Go

package db
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// ItemDependency represents a row in the item_dependencies table.
type ItemDependency struct {
ID string
ParentItemID string
ChildUUID string
ChildPartNumber *string
ChildRevision *int
Quantity *float64
Label *string
Relationship string
RevisionNumber int
CreatedAt time.Time
}
// ResolvedDependency extends ItemDependency with resolution info from a LEFT JOIN.
type ResolvedDependency struct {
ItemDependency
ResolvedPartNumber *string
ResolvedRevision *int
Resolved bool
}
// ItemDependencyRepository provides item_dependencies database operations.
type ItemDependencyRepository struct {
db *DB
}
// NewItemDependencyRepository creates a new item dependency repository.
func NewItemDependencyRepository(db *DB) *ItemDependencyRepository {
return &ItemDependencyRepository{db: db}
}
// ReplaceForRevision atomically replaces all dependencies for an item's revision.
// Deletes existing rows for the parent item and inserts the new set.
func (r *ItemDependencyRepository) ReplaceForRevision(ctx context.Context, parentItemID string, revisionNumber int, deps []*ItemDependency) error {
return r.db.Tx(ctx, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, `DELETE FROM item_dependencies WHERE parent_item_id = $1`, parentItemID)
if err != nil {
return fmt.Errorf("deleting old dependencies: %w", err)
}
for _, d := range deps {
_, err := tx.Exec(ctx, `
INSERT INTO item_dependencies
(parent_item_id, child_uuid, child_part_number, child_revision,
quantity, label, relationship, revision_number)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, parentItemID, d.ChildUUID, d.ChildPartNumber, d.ChildRevision,
d.Quantity, d.Label, d.Relationship, revisionNumber)
if err != nil {
return fmt.Errorf("inserting dependency: %w", err)
}
}
return nil
})
}
// ListByItem returns all dependencies for an item.
func (r *ItemDependencyRepository) ListByItem(ctx context.Context, parentItemID string) ([]*ItemDependency, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT id, parent_item_id, child_uuid, child_part_number, child_revision,
quantity, label, relationship, revision_number, created_at
FROM item_dependencies
WHERE parent_item_id = $1
ORDER BY label NULLS LAST
`, parentItemID)
if err != nil {
return nil, fmt.Errorf("listing dependencies: %w", err)
}
defer rows.Close()
var deps []*ItemDependency
for rows.Next() {
d := &ItemDependency{}
if err := rows.Scan(
&d.ID, &d.ParentItemID, &d.ChildUUID, &d.ChildPartNumber, &d.ChildRevision,
&d.Quantity, &d.Label, &d.Relationship, &d.RevisionNumber, &d.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scanning dependency: %w", err)
}
deps = append(deps, d)
}
return deps, nil
}
// Resolve returns dependencies with child UUIDs resolved against the items table.
// Unresolvable UUIDs (external or deleted items) have Resolved=false.
func (r *ItemDependencyRepository) Resolve(ctx context.Context, parentItemID string) ([]*ResolvedDependency, error) {
rows, err := r.db.pool.Query(ctx, `
SELECT d.id, d.parent_item_id, d.child_uuid, d.child_part_number, d.child_revision,
d.quantity, d.label, d.relationship, d.revision_number, d.created_at,
i.part_number, i.current_revision
FROM item_dependencies d
LEFT JOIN items i ON i.id = d.child_uuid AND i.archived_at IS NULL
WHERE d.parent_item_id = $1
ORDER BY d.label NULLS LAST
`, parentItemID)
if err != nil {
return nil, fmt.Errorf("resolving dependencies: %w", err)
}
defer rows.Close()
var deps []*ResolvedDependency
for rows.Next() {
d := &ResolvedDependency{}
if err := rows.Scan(
&d.ID, &d.ParentItemID, &d.ChildUUID, &d.ChildPartNumber, &d.ChildRevision,
&d.Quantity, &d.Label, &d.Relationship, &d.RevisionNumber, &d.CreatedAt,
&d.ResolvedPartNumber, &d.ResolvedRevision,
); err != nil {
return nil, fmt.Errorf("scanning resolved dependency: %w", err)
}
d.Resolved = d.ResolvedPartNumber != nil
deps = append(deps, d)
}
return deps, nil
}