Files
silo/internal/api/dependency_handlers.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

126 lines
3.7 KiB
Go

package api
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/kindredsystems/silo/internal/storage"
)
// DependencyResponse is the JSON representation for GET /dependencies.
type DependencyResponse struct {
UUID string `json:"uuid"`
PartNumber *string `json:"part_number"`
Revision *int `json:"revision"`
Quantity *float64 `json:"quantity"`
Label *string `json:"label"`
Relationship string `json:"relationship"`
}
// ResolvedDependencyResponse is the JSON representation for GET /dependencies/resolve.
type ResolvedDependencyResponse struct {
UUID string `json:"uuid"`
PartNumber *string `json:"part_number"`
Label *string `json:"label"`
Revision *int `json:"revision"`
Quantity *float64 `json:"quantity"`
Resolved bool `json:"resolved"`
FileAvailable bool `json:"file_available"`
}
// HandleGetDependencies returns the raw dependency list for an item.
// GET /api/items/{partNumber}/dependencies
func (s *Server) HandleGetDependencies(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
partNumber := chi.URLParam(r, "partNumber")
item, err := s.items.GetByPartNumber(ctx, partNumber)
if err != nil {
s.logger.Error().Err(err).Msg("failed to get item")
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get item")
return
}
if item == nil {
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
deps, err := s.deps.ListByItem(ctx, item.ID)
if err != nil {
s.logger.Error().Err(err).Msg("failed to list dependencies")
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list dependencies")
return
}
resp := make([]DependencyResponse, len(deps))
for i, d := range deps {
resp[i] = DependencyResponse{
UUID: d.ChildUUID,
PartNumber: d.ChildPartNumber,
Revision: d.ChildRevision,
Quantity: d.Quantity,
Label: d.Label,
Relationship: d.Relationship,
}
}
writeJSON(w, http.StatusOK, resp)
}
// HandleResolveDependencies returns dependencies with UUIDs resolved to part numbers
// and file availability status.
// GET /api/items/{partNumber}/dependencies/resolve
func (s *Server) HandleResolveDependencies(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
partNumber := chi.URLParam(r, "partNumber")
item, err := s.items.GetByPartNumber(ctx, partNumber)
if err != nil {
s.logger.Error().Err(err).Msg("failed to get item")
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get item")
return
}
if item == nil {
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
deps, err := s.deps.Resolve(ctx, item.ID)
if err != nil {
s.logger.Error().Err(err).Msg("failed to resolve dependencies")
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve dependencies")
return
}
resp := make([]ResolvedDependencyResponse, len(deps))
for i, d := range deps {
// Use resolved part number if available, fall back to .kc-provided value.
pn := d.ChildPartNumber
rev := d.ChildRevision
if d.Resolved {
pn = d.ResolvedPartNumber
rev = d.ResolvedRevision
}
fileAvailable := false
if d.Resolved && pn != nil && rev != nil && s.storage != nil {
key := storage.FileKey(*pn, *rev)
if exists, err := s.storage.Exists(ctx, key); err == nil {
fileAvailable = exists
}
}
resp[i] = ResolvedDependencyResponse{
UUID: d.ChildUUID,
PartNumber: pn,
Label: d.Label,
Revision: rev,
Quantity: d.Quantity,
Resolved: d.Resolved,
FileAvailable: fileAvailable,
}
}
writeJSON(w, http.StatusOK, resp)
}