Merged in feature/add-deletes (pull request #214)
support delete for client, document and folder * support delete for client, document and folder * remove batch cancel conflict not used Approved-by: Jacob Mathison
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// delete.go implements the hard-delete API handlers for client, document, and folder resources.
|
||||
// These handlers permanently remove data from the database. Source documents in S3 are
|
||||
// intentionally NOT deleted; their paths are logged and optionally returned in verbose mode.
|
||||
// Delete operations are restricted to super_admin via Permit.io middleware.
|
||||
package queryapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
// DeleteClient permanently deletes a client and all associated data.
|
||||
// DELETE /client/{id}?confirm=true&verbose=false
|
||||
// Requires confirm=true query parameter as a safety guard.
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteClient(ctx echo.Context, id ClientID, params DeleteClientParams) error {
|
||||
if !params.Confirm {
|
||||
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
||||
Message: "confirm=true query parameter is required to delete a client",
|
||||
})
|
||||
}
|
||||
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
slog.Info("client hard-delete requested",
|
||||
"clientId", id,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Client.HardDelete(ctx.Request().Context(), string(id))
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "client", string(id))
|
||||
}
|
||||
|
||||
slog.Info("client hard-deleted",
|
||||
"clientId", id,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// DeleteDocument permanently deletes a document and all dependent data.
|
||||
// DELETE /document/{id}?verbose=false
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteDocument(ctx echo.Context, id DocumentID, params DeleteDocumentParams) error {
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
docUUID := uuid.UUID(id)
|
||||
|
||||
slog.Info("document hard-delete requested",
|
||||
"documentId", docUUID,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Document.HardDelete(ctx.Request().Context(), docUUID)
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "document", docUUID.String())
|
||||
}
|
||||
|
||||
slog.Info("document hard-deleted",
|
||||
"documentId", docUUID,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// DeleteFolder permanently deletes a folder tree and optionally its documents.
|
||||
// DELETE /folders/{folderId}?include_documents=false&verbose=false
|
||||
// The root folder (path '/') cannot be deleted (returns 409).
|
||||
// If the folder tree contains documents and include_documents is false, returns 409.
|
||||
// When verbose=true, returns 200 with S3 paths of orphaned source documents.
|
||||
// When verbose=false (default), returns 204 No Content.
|
||||
func (s *Controllers) DeleteFolder(ctx echo.Context, folderId openapi_types.UUID, params DeleteFolderParams) error {
|
||||
adminUser, err := getAdminUserForAudit(ctx)
|
||||
if err != nil {
|
||||
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
||||
Message: "Could not identify admin user",
|
||||
})
|
||||
}
|
||||
|
||||
includeDocuments := params.IncludeDocuments != nil && *params.IncludeDocuments
|
||||
|
||||
slog.Info("folder hard-delete requested",
|
||||
"folderId", folderId,
|
||||
"includeDocuments", includeDocuments,
|
||||
"requestedBy", adminUser.Email,
|
||||
)
|
||||
|
||||
result, err := s.svc.Folder.HardDelete(ctx.Request().Context(), uuid.UUID(folderId), includeDocuments)
|
||||
if err != nil {
|
||||
return handleDeleteError(ctx, err, "folder", uuid.UUID(folderId).String())
|
||||
}
|
||||
|
||||
slog.Info("folder hard-deleted",
|
||||
"folderId", folderId,
|
||||
"includeDocuments", includeDocuments,
|
||||
"requestedBy", adminUser.Email,
|
||||
"orphanedS3Objects", len(result),
|
||||
)
|
||||
|
||||
return respondDelete(ctx, params.Verbose, result)
|
||||
}
|
||||
|
||||
// handleDeleteError maps service-layer errors to appropriate HTTP responses.
|
||||
// Returns 404 for not-found, 409 for conflict, and 500 for unexpected errors.
|
||||
func handleDeleteError(ctx echo.Context, err error, resourceType string, resourceID string) error {
|
||||
var conflictErr *harddelete.ConflictError
|
||||
if errors.As(err, &conflictErr) {
|
||||
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
||||
Message: conflictErr.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var notFoundErr *harddelete.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
||||
Message: notFoundErr.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
slog.Error("hard-delete failed",
|
||||
"resourceType", resourceType,
|
||||
"resourceId", resourceID,
|
||||
"error", err,
|
||||
)
|
||||
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
||||
Message: fmt.Sprintf("failed to delete %s: internal error", resourceType),
|
||||
})
|
||||
}
|
||||
|
||||
// respondDelete sends the appropriate HTTP response based on the verbose flag.
|
||||
// When verbose is true, returns 200 with a DeleteResponse containing S3 paths.
|
||||
// When verbose is false or nil, returns 204 No Content.
|
||||
func respondDelete(ctx echo.Context, verbose *bool, s3Paths []harddelete.S3PathInfo) error {
|
||||
if verbose != nil && *verbose {
|
||||
docs := make([]struct {
|
||||
DocumentId openapi_types.UUID `json:"documentId"`
|
||||
S3Path string `json:"s3Path"`
|
||||
}, len(s3Paths))
|
||||
|
||||
for i, p := range s3Paths {
|
||||
docs[i].DocumentId = openapi_types.UUID(p.DocumentID)
|
||||
docs[i].S3Path = fmt.Sprintf("s3://%s/%s", p.Bucket, p.Key)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, DeleteResponse{
|
||||
DeletedDocuments: docs,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user