09c61ea9b4
support delete for client, document and folder * support delete for client, document and folder * remove batch cancel conflict not used Approved-by: Jacob Mathison
177 lines
5.7 KiB
Go
177 lines
5.7 KiB
Go
// 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)
|
|
}
|