Repository: apigee/registry (go)

Issue:
Registry API: DeleteApiSpecRevision can delete the only remaining revision of a spec
[AIP-162](https://google.aip.dev/162#deleting-revisions) states the following. We don't currently support this expected behavior.

> The method must not cause the resource to be deleted. Because a resource should not exist with zero revisions, the method must fail with FAILED_PRECONDITION if the user attempts to delete the only revision.

We have a [test](https://github.com/apigee/registry/blob/9c0e693d871ddb18e31137a59949bbcebf3fb1f5/server/actions_spec_revisions_test.go#L237) for this case that is currently skipped.

Pull request:
disallow delete of only SpecRevision
Fixes #257

Patch:
diff --git a/server/registry/actions_spec_revisions.go b/server/registry/actions_spec_revisions.go
index a5bd436..be5ecd8 100644
--- a/server/registry/actions_spec_revisions.go
+++ b/server/registry/actions_spec_revisions.go
@@ -93,7 +93,7 @@ func (s *RegistryServer) DeleteApiSpecRevision(ctx context.Context, req *rpc.Del
 	}
 
 	if err := db.DeleteSpecRevision(ctx, name); err != nil {
-		return nil, status.Error(codes.Internal, err.Error())
+		return nil, err
 	}
 
 	s.notify(ctx, rpc.Notification_DELETED, name.String())
diff --git a/server/registry/internal/storage/delete.go b/server/registry/internal/storage/delete.go
index e810b50..fc72d0c 100644
--- a/server/registry/internal/storage/delete.go
+++ b/server/registry/internal/storage/delete.go
@@ -245,13 +245,41 @@ func (c *Client) DeleteSpecRevision(ctx context.Context, name names.SpecRevision
 		models.Spec{},
 		models.SpecRevisionTag{},
 	} {
-		op := c.db.WithContext(ctx).
-			Where("project_id = ?", name.ProjectID).
-			Where("api_id = ?", name.ApiID).
-			Where("version_id = ?", name.VersionID).
-			Where("spec_id = ?", name.SpecID).
-			Where("revision_id = ?", name.RevisionID)
-		if err := op.Delete(model).Error; err != nil {
+		err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+			op := tx.
+				Where("project_id = ?", name.ProjectID).
+				Where("api_id = ?", name.ApiID).
+				Where("version_id = ?", name.VersionID).
+				Where("spec_id = ?", name.SpecID).
+				Where("revision_id = ?", name.RevisionID)
+			if err := op.Delete(model).Error; err != nil {
+				return grpcErrorForDBError(ctx, err)
+			}
+
+			// if we deleted the last revision, abort
+			op = tx.
+				Where("project_id = ?", name.ProjectID).
+				Where("api_id = ?", name.ApiID).
+				Where("version_id = ?", name.VersionID).
+				Where("spec_id = ?", name.SpecID).
+				Order("revision_create_time desc")
+
+			v := new(models.Spec)
+			if err := op.First(v).Error; err == gorm.ErrRecordNotFound {
+				return status.Errorf(codes.FailedPrecondition, "cannot delete the only revision: %s", name)
+			} else if err != nil {
+				return grpcErrorForDBError(ctx, err)
+			}
+
+			return nil
+		})
+
+		switch status.Code(err) {
+		case codes.OK:
+			return nil
+		case codes.FailedPrecondition:
+			return err
+		default:
 			return grpcErrorForDBError(ctx, err)
 		}
 	}


Test patch:
diff --git a/server/registry/actions_spec_revisions_test.go b/server/registry/actions_spec_revisions_test.go
index c0fac5f..3e50670 100644
--- a/server/registry/actions_spec_revisions_test.go
+++ b/server/registry/actions_spec_revisions_test.go
@@ -235,11 +235,17 @@ func TestDeleteApiSpecRevision(t *testing.T) {
 		t.Fatalf("Setup/Seeding: Failed to seed registry: %s", err)
 	}
 
-	t.Run("only remaining revision", func(t *testing.T) {
-		t.Skip("not yet supported")
+	getReq := &rpc.GetApiSpecRequest{
+		Name: "projects/my-project/locations/global/apis/my-api/versions/v1/specs/my-spec",
+	}
+	firstRevision, err := server.GetApiSpec(ctx, getReq)
+	if err != nil {
+		t.Fatalf("Setup: GetApiSpecRequest(%+v) returned error: %s", getReq, err)
+	}
 
+	t.Run("only remaining revision", func(t *testing.T) {
 		req := &rpc.DeleteApiSpecRevisionRequest{
-			Name: "projects/my-project/locations/global/apis/my-api/versions/v1/specs/my-spec",
+			Name: fmt.Sprintf("projects/my-project/locations/global/apis/my-api/versions/v1/specs/my-spec@%s", firstRevision.GetRevisionId()),
 		}
 
 		if _, err := server.DeleteApiSpecRevision(ctx, req); status.Code(err) != codes.FailedPrecondition {