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,129 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/document"
|
||||
"queryorchestration/internal/harddelete"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// HardDelete permanently removes a client and ALL associated data from the database.
|
||||
// This includes all documents (with full cascade), folders, collector configurations,
|
||||
// batch uploads, and sync records. The entire operation executes in a single transaction.
|
||||
//
|
||||
// S3 source files are intentionally NOT deleted. Their paths are collected before
|
||||
// the transaction, logged at INFO level, and returned for optional verbose responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: request context
|
||||
// - clientID: the client identifier to delete
|
||||
//
|
||||
// Returns:
|
||||
// - []harddelete.S3PathInfo: S3 paths of orphaned source documents
|
||||
// - error: NotFoundError if client does not exist, or a database error
|
||||
func (s *Service) HardDelete(ctx context.Context, clientID string) ([]harddelete.S3PathInfo, error) {
|
||||
queries := s.cfg.GetDBQueries()
|
||||
|
||||
// Get all document IDs for the client
|
||||
docIDs, err := queries.GetAllDocumentIDsForClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get document IDs for client %s: %w", clientID, err)
|
||||
}
|
||||
|
||||
// Collect S3 paths for all documents before the transaction
|
||||
allS3Paths, err := harddelete.CollectS3PathsForDocuments(ctx, docIDs, wrapS3Query(queries))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
// 1. Delete all documents with full cascade
|
||||
for _, docID := range docIDs {
|
||||
if err := document.DeleteDocumentCascade(ctx, q, docID); err != nil {
|
||||
return fmt.Errorf("cascade delete document %s: %w", docID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete all documentUploads for the client
|
||||
if err := q.DeleteDocumentUploadsForClient(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete document uploads: %w", err)
|
||||
}
|
||||
|
||||
// 3. Delete all folders for the client (safe now that all documents are gone)
|
||||
if err := q.DeleteAllFoldersForClient(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete folders: %w", err)
|
||||
}
|
||||
|
||||
// 4-6. Delete collector data, batch uploads, and sync records
|
||||
if err := deleteClientDependencies(ctx, q, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. Delete the client row
|
||||
rowsAffected, err := q.HardDeleteClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete client: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return &harddelete.NotFoundError{
|
||||
ResourceType: "client",
|
||||
ResourceID: clientID,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
harddelete.LogOrphanedS3Paths(allS3Paths)
|
||||
return allS3Paths, nil
|
||||
}
|
||||
|
||||
// clientDependencyDelete pairs a description with a delete function for use in
|
||||
// deleteClientDependencies. Each entry represents one step in the FK-ordered cascade.
|
||||
type clientDependencyDelete struct {
|
||||
name string
|
||||
fn func(ctx context.Context, clientID string) error
|
||||
}
|
||||
|
||||
// deleteClientDependencies removes collector data, batch uploads, and sync records
|
||||
// for a client within an existing transaction. Must be called after documents and
|
||||
// folders are already deleted. Executes deletes in FK dependency order.
|
||||
func deleteClientDependencies(ctx context.Context, q *repository.Queries, clientID string) error {
|
||||
deletes := []clientDependencyDelete{
|
||||
{"collector min clean versions", q.DeleteCollectorMinCleanVersions},
|
||||
{"collector active versions", q.DeleteCollectorActiveVersions},
|
||||
{"collector versions", q.DeleteCollectorVersions},
|
||||
{"batch uploads", q.DeleteBatchUploads},
|
||||
{"client can sync", q.DeleteClientCanSync},
|
||||
}
|
||||
for _, d := range deletes {
|
||||
if err := d.fn(ctx, clientID); err != nil {
|
||||
return fmt.Errorf("delete %s: %w", d.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapS3Query adapts the repository's CollectDocumentS3Paths to the function signature
|
||||
// expected by harddelete.CollectS3PathsForDocuments.
|
||||
func wrapS3Query(queries *repository.Queries) func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
return func(ctx context.Context, docID uuid.UUID) ([]harddelete.S3Row, error) {
|
||||
rows, err := queries.CollectDocumentS3Paths(ctx, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]harddelete.S3Row, len(rows))
|
||||
for i, r := range rows {
|
||||
result[i] = harddelete.S3Row{Bucket: r.Bucket, Key: r.Key}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user