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
This commit is contained in:
125
internal/api/dependency_handlers.go
Normal file
125
internal/api/dependency_handlers.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ type Server struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
settings *db.SettingsRepository
|
settings *db.SettingsRepository
|
||||||
metadata *db.ItemMetadataRepository
|
metadata *db.ItemMetadataRepository
|
||||||
|
deps *db.ItemDependencyRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new API server.
|
// NewServer creates a new API server.
|
||||||
@@ -85,6 +86,7 @@ func NewServer(
|
|||||||
settings := db.NewSettingsRepository(database)
|
settings := db.NewSettingsRepository(database)
|
||||||
locations := db.NewLocationRepository(database)
|
locations := db.NewLocationRepository(database)
|
||||||
metadata := db.NewItemMetadataRepository(database)
|
metadata := db.NewItemMetadataRepository(database)
|
||||||
|
itemDeps := db.NewItemDependencyRepository(database)
|
||||||
seqStore := &dbSequenceStore{db: database, schemas: schemas}
|
seqStore := &dbSequenceStore{db: database, schemas: schemas}
|
||||||
partgen := partnum.NewGenerator(schemas, seqStore)
|
partgen := partnum.NewGenerator(schemas, seqStore)
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ func NewServer(
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
deps: itemDeps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,38 @@ func (s *Server) extractKCMetadata(ctx context.Context, item *db.Item, fileKey s
|
|||||||
"updated_by": username,
|
"updated_by": username,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Index dependencies from silo/dependencies.json.
|
||||||
|
if result.Dependencies != nil {
|
||||||
|
dbDeps := make([]*db.ItemDependency, len(result.Dependencies))
|
||||||
|
for i, d := range result.Dependencies {
|
||||||
|
pn := d.PartNumber
|
||||||
|
rev := d.Revision
|
||||||
|
qty := d.Quantity
|
||||||
|
label := d.Label
|
||||||
|
rel := d.Relationship
|
||||||
|
if rel == "" {
|
||||||
|
rel = "component"
|
||||||
|
}
|
||||||
|
dbDeps[i] = &db.ItemDependency{
|
||||||
|
ParentItemID: item.ID,
|
||||||
|
ChildUUID: d.UUID,
|
||||||
|
ChildPartNumber: &pn,
|
||||||
|
ChildRevision: &rev,
|
||||||
|
Quantity: &qty,
|
||||||
|
Label: &label,
|
||||||
|
Relationship: rel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.deps.ReplaceForRevision(ctx, item.ID, rev.RevisionNumber, dbDeps); err != nil {
|
||||||
|
s.logger.Warn().Err(err).Str("part_number", item.PartNumber).Msg("kc: failed to index dependencies")
|
||||||
|
} else {
|
||||||
|
s.broker.Publish("dependencies.changed", mustMarshal(map[string]any{
|
||||||
|
"part_number": item.PartNumber,
|
||||||
|
"count": len(dbDeps),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.logger.Info().Str("part_number", item.PartNumber).Msg("kc: metadata indexed successfully")
|
s.logger.Info().Str("part_number", item.PartNumber).Msg("kc: metadata indexed successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,33 @@ func (s *Server) packKCFile(ctx context.Context, data []byte, item *db.Item, rev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build dependencies from item_dependencies table.
|
||||||
|
var deps []kc.Dependency
|
||||||
|
dbDeps, err := s.deps.ListByItem(ctx, item.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn().Err(err).Str("part_number", item.PartNumber).Msg("kc: failed to query dependencies for packing")
|
||||||
|
} else {
|
||||||
|
deps = make([]kc.Dependency, len(dbDeps))
|
||||||
|
for i, d := range dbDeps {
|
||||||
|
deps[i] = kc.Dependency{
|
||||||
|
UUID: d.ChildUUID,
|
||||||
|
PartNumber: derefStr(d.ChildPartNumber, ""),
|
||||||
|
Revision: derefInt(d.ChildRevision, 0),
|
||||||
|
Quantity: derefFloat(d.Quantity, 0),
|
||||||
|
Label: derefStr(d.Label, ""),
|
||||||
|
Relationship: d.Relationship,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deps == nil {
|
||||||
|
deps = []kc.Dependency{}
|
||||||
|
}
|
||||||
|
|
||||||
input := &kc.PackInput{
|
input := &kc.PackInput{
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
History: history,
|
History: history,
|
||||||
Dependencies: []any{}, // empty for Phase 2
|
Dependencies: deps,
|
||||||
}
|
}
|
||||||
|
|
||||||
return kc.Pack(data, input)
|
return kc.Pack(data, input)
|
||||||
@@ -95,3 +117,19 @@ func derefStr(p *string, fallback string) string {
|
|||||||
}
|
}
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derefInt returns the value of a *int pointer, or fallback if nil.
|
||||||
|
func derefInt(p *int, fallback int) int {
|
||||||
|
if p != nil {
|
||||||
|
return *p
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// derefFloat returns the value of a *float64 pointer, or fallback if nil.
|
||||||
|
func derefFloat(p *float64, fallback float64) float64 {
|
||||||
|
if p != nil {
|
||||||
|
return *p
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler {
|
|||||||
r.Get("/bom/export.csv", server.HandleExportBOMCSV)
|
r.Get("/bom/export.csv", server.HandleExportBOMCSV)
|
||||||
r.Get("/bom/export.ods", server.HandleExportBOMODS)
|
r.Get("/bom/export.ods", server.HandleExportBOMODS)
|
||||||
r.Get("/metadata", server.HandleGetMetadata)
|
r.Get("/metadata", server.HandleGetMetadata)
|
||||||
|
r.Get("/dependencies", server.HandleGetDependencies)
|
||||||
|
r.Get("/dependencies/resolve", server.HandleResolveDependencies)
|
||||||
|
|
||||||
// DAG (gated by dag module)
|
// DAG (gated by dag module)
|
||||||
r.Route("/dag", func(r chi.Router) {
|
r.Route("/dag", func(r chi.Router) {
|
||||||
|
|||||||
127
internal/db/item_dependencies.go
Normal file
127
internal/db/item_dependencies.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -30,10 +30,21 @@ type Metadata struct {
|
|||||||
Fields map[string]any `json:"fields"`
|
Fields map[string]any `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dependency represents one entry in silo/dependencies.json.
|
||||||
|
type Dependency 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"`
|
||||||
|
}
|
||||||
|
|
||||||
// ExtractResult holds the parsed silo/ directory contents from a .kc file.
|
// ExtractResult holds the parsed silo/ directory contents from a .kc file.
|
||||||
type ExtractResult struct {
|
type ExtractResult struct {
|
||||||
Manifest *Manifest
|
Manifest *Manifest
|
||||||
Metadata *Metadata
|
Metadata *Metadata
|
||||||
|
Dependencies []Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
// HistoryEntry represents one entry in silo/history.json.
|
// HistoryEntry represents one entry in silo/history.json.
|
||||||
@@ -52,7 +63,7 @@ type PackInput struct {
|
|||||||
Manifest *Manifest
|
Manifest *Manifest
|
||||||
Metadata *Metadata
|
Metadata *Metadata
|
||||||
History []HistoryEntry
|
History []HistoryEntry
|
||||||
Dependencies []any // empty [] for Phase 2; structured types in Phase 3+
|
Dependencies []Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract opens a ZIP archive from data and parses the silo/ directory.
|
// Extract opens a ZIP archive from data and parses the silo/ directory.
|
||||||
@@ -64,7 +75,7 @@ func Extract(data []byte) (*ExtractResult, error) {
|
|||||||
return nil, fmt.Errorf("kc: open zip: %w", err)
|
return nil, fmt.Errorf("kc: open zip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestFile, metadataFile *zip.File
|
var manifestFile, metadataFile, dependenciesFile *zip.File
|
||||||
hasSiloDir := false
|
hasSiloDir := false
|
||||||
|
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
@@ -76,6 +87,8 @@ func Extract(data []byte) (*ExtractResult, error) {
|
|||||||
manifestFile = f
|
manifestFile = f
|
||||||
case "silo/metadata.json":
|
case "silo/metadata.json":
|
||||||
metadataFile = f
|
metadataFile = f
|
||||||
|
case "silo/dependencies.json":
|
||||||
|
dependenciesFile = f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +114,16 @@ func Extract(data []byte) (*ExtractResult, error) {
|
|||||||
result.Metadata = m
|
result.Metadata = m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dependenciesFile != nil {
|
||||||
|
deps, err := readJSON[[]Dependency](dependenciesFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("kc: parse dependencies.json: %w", err)
|
||||||
|
}
|
||||||
|
if deps != nil {
|
||||||
|
result.Dependencies = *deps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func TestPack_RoundTrip(t *testing.T) {
|
|||||||
Manifest: newManifest,
|
Manifest: newManifest,
|
||||||
Metadata: newMetadata,
|
Metadata: newMetadata,
|
||||||
History: history,
|
History: history,
|
||||||
Dependencies: []any{},
|
Dependencies: []Dependency{},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Pack error: %v", err)
|
t.Fatalf("Pack error: %v", err)
|
||||||
@@ -193,7 +193,7 @@ func TestPack_EmptyDependencies(t *testing.T) {
|
|||||||
|
|
||||||
packed, err := Pack(original, &PackInput{
|
packed, err := Pack(original, &PackInput{
|
||||||
Manifest: &Manifest{UUID: "x"},
|
Manifest: &Manifest{UUID: "x"},
|
||||||
Dependencies: []any{},
|
Dependencies: []Dependency{},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Pack error: %v", err)
|
t.Fatalf("Pack error: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user