feat: Add PUT and PATCH datasets endpoints
This commit is contained in:
@@ -35,6 +35,7 @@ var (
|
||||
type DatasetRepository interface {
|
||||
Create(ctx context.Context, d domain.Dataset) (domain.Dataset, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (domain.Dataset, error)
|
||||
UpdateFields(ctx context.Context, id, categoryID uuid.UUID, name string, description, unit *string) (domain.Dataset, error)
|
||||
ListSummaries(ctx context.Context, filter domain.DatasetFilter, limit, offset int) ([]domain.DatasetSummary, error)
|
||||
Count(ctx context.Context, filter domain.DatasetFilter) (int, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
@@ -183,10 +184,7 @@ func (s *DatasetService) Upload(ctx context.Context, in UploadInput) (domain.Dat
|
||||
}
|
||||
content := io.MultiReader(bytes.NewReader(head), in.Reader)
|
||||
|
||||
if _, err := s.categories.GetByID(ctx, in.CategoryID); err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return domain.Dataset{}, fmt.Errorf("%w: category does not exist", domain.ErrValidation)
|
||||
}
|
||||
if err := s.ensureCategoryExists(ctx, in.CategoryID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
|
||||
@@ -946,6 +944,81 @@ func (s *DatasetService) Get(ctx context.Context, id uuid.UUID) (domain.Dataset,
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// DatasetUpdateInput carries the full set of editable metadata fields for a
|
||||
// PUT replace. Description and Unit being nil clears those (nullable)
|
||||
// columns.
|
||||
type DatasetUpdateInput struct {
|
||||
CategoryID uuid.UUID
|
||||
Name string
|
||||
Description *string
|
||||
Unit *string
|
||||
}
|
||||
|
||||
// Update replaces a dataset's editable metadata (category, name, description,
|
||||
// unit). File-related and lifecycle fields are untouched.
|
||||
func (s *DatasetService) Update(ctx context.Context, id uuid.UUID, in DatasetUpdateInput) (domain.Dataset, error) {
|
||||
if err := s.ensureCategoryExists(ctx, in.CategoryID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
return s.repo.UpdateFields(ctx, id, in.CategoryID, in.Name, in.Description, in.Unit)
|
||||
}
|
||||
|
||||
// DatasetPatchInput carries a sparse set of metadata field changes. A nil
|
||||
// CategoryID/Name leaves that column unchanged. Description/Unit are only
|
||||
// applied when their paired Set flag is true; a nil value with Set true
|
||||
// clears the (nullable) column.
|
||||
type DatasetPatchInput struct {
|
||||
CategoryID *uuid.UUID
|
||||
Name *string
|
||||
Description *string
|
||||
DescriptionSet bool
|
||||
Unit *string
|
||||
UnitSet bool
|
||||
}
|
||||
|
||||
// Patch applies a sparse update to a dataset's editable metadata (category,
|
||||
// name, description, unit). Fields absent from the input are left unchanged.
|
||||
func (s *DatasetService) Patch(ctx context.Context, id uuid.UUID, in DatasetPatchInput) (domain.Dataset, error) {
|
||||
current, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
|
||||
categoryID := current.CategoryID
|
||||
if in.CategoryID != nil {
|
||||
categoryID = *in.CategoryID
|
||||
if err := s.ensureCategoryExists(ctx, categoryID); err != nil {
|
||||
return domain.Dataset{}, err
|
||||
}
|
||||
}
|
||||
name := current.Name
|
||||
if in.Name != nil {
|
||||
name = *in.Name
|
||||
}
|
||||
description := current.Description
|
||||
if in.DescriptionSet {
|
||||
description = in.Description
|
||||
}
|
||||
unit := current.Unit
|
||||
if in.UnitSet {
|
||||
unit = in.Unit
|
||||
}
|
||||
|
||||
return s.repo.UpdateFields(ctx, id, categoryID, name, description, unit)
|
||||
}
|
||||
|
||||
// ensureCategoryExists validates that categoryID references an existing
|
||||
// category, mirroring the check performed on upload.
|
||||
func (s *DatasetService) ensureCategoryExists(ctx context.Context, categoryID uuid.UUID) error {
|
||||
if _, err := s.categories.GetByID(ctx, categoryID); err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("%w: category does not exist", domain.ErrValidation)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status-wait bounds and polling cadence for long polling.
|
||||
const (
|
||||
DefaultStatusWait = 25 * time.Second
|
||||
|
||||
@@ -56,6 +56,19 @@ func (r *stubDatasetRepo) GetByID(_ context.Context, id uuid.UUID) (domain.Datas
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *stubDatasetRepo) UpdateFields(_ context.Context, id, categoryID uuid.UUID, name string, description, unit *string) (domain.Dataset, error) {
|
||||
d, ok := r.store[id]
|
||||
if !ok {
|
||||
return domain.Dataset{}, domain.ErrNotFound
|
||||
}
|
||||
d.CategoryID = categoryID
|
||||
d.Name = name
|
||||
d.Description = description
|
||||
d.Unit = unit
|
||||
r.store[id] = d
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *stubDatasetRepo) ListSummaries(_ context.Context, filter domain.DatasetFilter, limit, offset int) ([]domain.DatasetSummary, error) {
|
||||
r.lastFilter = filter
|
||||
r.lastLimit = limit
|
||||
@@ -1298,3 +1311,121 @@ func TestDatasetService_Delete_RemovesObject(t *testing.T) {
|
||||
t.Fatalf("object not removed: %v", store.removed)
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
func TestDatasetService_Update_Success(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
id := uuid.New()
|
||||
repo.store[id] = domain.Dataset{ID: id, Name: "Old", Description: strPtr("old desc"), Unit: strPtr("old unit")}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
newCategory := uuid.New()
|
||||
got, err := svc.Update(context.Background(), id, DatasetUpdateInput{
|
||||
CategoryID: newCategory,
|
||||
Name: "New",
|
||||
Description: strPtr("new desc"),
|
||||
Unit: nil,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.CategoryID != newCategory || got.Name != "New" {
|
||||
t.Fatalf("update not applied: %+v", got)
|
||||
}
|
||||
if got.Description == nil || *got.Description != "new desc" {
|
||||
t.Fatalf("description not updated: %v", got.Description)
|
||||
}
|
||||
if got.Unit != nil {
|
||||
t.Fatalf("unit should be cleared, got %v", *got.Unit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Update_MissingCategory(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
id := uuid.New()
|
||||
repo.store[id] = domain.Dataset{ID: id, Name: "Old"}
|
||||
svc := newDatasetService(repo, &stubStore{}, false)
|
||||
|
||||
_, err := svc.Update(context.Background(), id, DatasetUpdateInput{CategoryID: uuid.New(), Name: "New"})
|
||||
if !errors.Is(err, domain.ErrValidation) {
|
||||
t.Fatalf("want ErrValidation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Update_NotFound(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
_, err := svc.Update(context.Background(), uuid.New(), DatasetUpdateInput{CategoryID: uuid.New(), Name: "New"})
|
||||
if !errors.Is(err, domain.ErrNotFound) {
|
||||
t.Fatalf("want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Patch_PartialUpdate(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
id := uuid.New()
|
||||
origCategory := uuid.New()
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, CategoryID: origCategory, Name: "Old",
|
||||
Description: strPtr("old desc"), Unit: strPtr("old unit"),
|
||||
}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
// Only Name is set; everything else must be left unchanged.
|
||||
got, err := svc.Patch(context.Background(), id, DatasetPatchInput{Name: strPtr("New")})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.Name != "New" {
|
||||
t.Fatalf("name not updated: %+v", got)
|
||||
}
|
||||
if got.CategoryID != origCategory {
|
||||
t.Fatalf("category should be unchanged, got %v", got.CategoryID)
|
||||
}
|
||||
if got.Description == nil || *got.Description != "old desc" {
|
||||
t.Fatalf("description should be unchanged, got %v", got.Description)
|
||||
}
|
||||
if got.Unit == nil || *got.Unit != "old unit" {
|
||||
t.Fatalf("unit should be unchanged, got %v", got.Unit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Patch_ClearsNullableField(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
id := uuid.New()
|
||||
repo.store[id] = domain.Dataset{ID: id, Name: "Old", Description: strPtr("old desc")}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
got, err := svc.Patch(context.Background(), id, DatasetPatchInput{DescriptionSet: true, Description: nil})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.Description != nil {
|
||||
t.Fatalf("description should be cleared, got %v", *got.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Patch_MissingCategory(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
id := uuid.New()
|
||||
repo.store[id] = domain.Dataset{ID: id, Name: "Old"}
|
||||
svc := newDatasetService(repo, &stubStore{}, false)
|
||||
|
||||
newCategory := uuid.New()
|
||||
_, err := svc.Patch(context.Background(), id, DatasetPatchInput{CategoryID: &newCategory})
|
||||
if !errors.Is(err, domain.ErrValidation) {
|
||||
t.Fatalf("want ErrValidation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_Patch_NotFound(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
_, err := svc.Patch(context.Background(), uuid.New(), DatasetPatchInput{Name: strPtr("New")})
|
||||
if !errors.Is(err, domain.ErrNotFound) {
|
||||
t.Fatalf("want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user