feat(revisions): auto-create revision on item metadata changes #177
@@ -860,6 +860,7 @@ type UpdateItemRequest struct {
|
||||
}
|
||||
|
||||
// HandleUpdateItem updates an item's fields and/or creates a new revision.
|
||||
// Any change to item metadata or properties triggers a new revision for audit trail.
|
||||
func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
partNumber := chi.URLParam(r, "partNumber")
|
||||
@@ -900,6 +901,30 @@ func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
fields.Description = req.Description
|
||||
}
|
||||
|
||||
// Detect which metadata fields actually changed
|
||||
var metadataChanges []string
|
||||
if req.PartNumber != "" && req.PartNumber != item.PartNumber {
|
||||
metadataChanges = append(metadataChanges, "part_number")
|
||||
}
|
||||
if req.ItemType != "" && req.ItemType != item.ItemType {
|
||||
metadataChanges = append(metadataChanges, "item_type")
|
||||
}
|
||||
if req.Description != "" && req.Description != item.Description {
|
||||
metadataChanges = append(metadataChanges, "description")
|
||||
}
|
||||
if req.SourcingType != nil && *req.SourcingType != item.SourcingType {
|
||||
metadataChanges = append(metadataChanges, "sourcing_type")
|
||||
}
|
||||
if req.LongDescription != nil {
|
||||
oldLD := ""
|
||||
if item.LongDescription != nil {
|
||||
oldLD = *item.LongDescription
|
||||
}
|
||||
if *req.LongDescription != oldLD {
|
||||
metadataChanges = append(metadataChanges, "long_description")
|
||||
}
|
||||
}
|
||||
|
||||
// Update the item record (UUID stays the same)
|
||||
if user := auth.UserFromContext(ctx); user != nil {
|
||||
fields.UpdatedBy = &user.Username
|
||||
@@ -910,12 +935,38 @@ func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create new revision if properties provided
|
||||
if req.Properties != nil {
|
||||
// Create a revision if anything changed (metadata or properties)
|
||||
metadataChanged := len(metadataChanges) > 0
|
||||
propertiesChanged := req.Properties != nil
|
||||
|
||||
if metadataChanged || propertiesChanged {
|
||||
// Determine properties for the new revision
|
||||
props := req.Properties
|
||||
if props == nil {
|
||||
// Carry forward properties from the latest revision
|
||||
latestRev, err := s.items.GetLatestRevision(ctx, item.ID)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to get latest revision")
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get latest revision")
|
||||
return
|
||||
}
|
||||
if latestRev != nil {
|
||||
props = latestRev.Properties
|
||||
} else {
|
||||
props = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-generate comment if not provided and only metadata changed
|
||||
comment := req.Comment
|
||||
if comment == "" && metadataChanged {
|
||||
comment = "updated " + strings.Join(metadataChanges, ", ")
|
||||
}
|
||||
|
||||
rev := &db.Revision{
|
||||
ItemID: item.ID,
|
||||
Properties: req.Properties,
|
||||
Comment: &req.Comment,
|
||||
Properties: props,
|
||||
Comment: &comment,
|
||||
}
|
||||
if user := auth.UserFromContext(ctx); user != nil {
|
||||
rev.CreatedBy = &user.Username
|
||||
@@ -926,6 +977,12 @@ func (s *Server) HandleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "revision_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.broker.Publish("revision.created", mustMarshal(map[string]any{
|
||||
"part_number": fields.PartNumber,
|
||||
"revision_number": rev.RevisionNumber,
|
||||
}))
|
||||
go s.triggerJobs(context.Background(), "revision_created", item.ID, item)
|
||||
}
|
||||
|
||||
// Get updated item (use new part number if changed)
|
||||
|
||||
@@ -329,6 +329,38 @@ func (r *ItemRepository) CreateRevision(ctx context.Context, rev *Revision) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatestRevision retrieves the most recent revision for an item.
|
||||
func (r *ItemRepository) GetLatestRevision(ctx context.Context, itemID string) (*Revision, error) {
|
||||
rev := &Revision{}
|
||||
var propsJSON []byte
|
||||
|
||||
err := r.db.pool.QueryRow(ctx, `
|
||||
SELECT id, item_id, revision_number, properties, file_key, file_version,
|
||||
file_checksum, file_size, COALESCE(file_storage_backend, 'filesystem'),
|
||||
thumbnail_key, created_at, created_by, comment,
|
||||
COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels
|
||||
FROM revisions
|
||||
WHERE item_id = $1
|
||||
ORDER BY revision_number DESC
|
||||
LIMIT 1
|
||||
`, itemID).Scan(
|
||||
&rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion,
|
||||
&rev.FileChecksum, &rev.FileSize, &rev.FileStorageBackend,
|
||||
&rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
|
||||
&rev.Status, &rev.Labels,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying latest revision: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(propsJSON, &rev.Properties); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling properties: %w", err)
|
||||
}
|
||||
return rev, nil
|
||||
}
|
||||
|
||||
// GetRevisions retrieves all revisions for an item.
|
||||
func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Revision, error) {
|
||||
// Check if status column exists (migration 007 applied)
|
||||
|
||||
Reference in New Issue
Block a user