package db import ( "context" "database/sql" "net/http" "github.com/pkg/errors" "github.com/uptrace/bun" ) type deleter[T any] struct { tx bun.Tx q *bun.DeleteQuery resourceID any // Store ID before deletion for audit auditCallback AuditCallback auditRequest *http.Request } type systemType interface { isSystem() bool } func DeleteItem[T any](tx bun.Tx) *deleter[T] { return &deleter[T]{ tx: tx, q: tx.NewDelete(). Model((*T)(nil)), } } func (d *deleter[T]) Where(query string, args ...any) *deleter[T] { d.q = d.q.Where(query, args...) // Try to capture resource ID from WHERE clause if it's a simple "id = ?" pattern if query == "id = ?" && len(args) > 0 { d.resourceID = args[0] } return d } // WithAudit enables audit logging for this delete operation // The callback will be invoked after successful deletion with auto-generated audit info // If the callback returns an error, the transaction will be rolled back func (d *deleter[T]) WithAudit(r *http.Request, callback AuditCallback) *deleter[T] { d.auditRequest = r d.auditCallback = callback return d } func (d *deleter[T]) Delete(ctx context.Context) error { _, err := d.q.Exec(ctx) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil } return errors.Wrap(err, "bun.DeleteQuery.Exec") } // Handle audit logging if enabled if d.auditCallback != nil && d.auditRequest != nil { tableName := extractTableName[T]() resourceType := extractResourceType(tableName) action := buildAction(resourceType, "delete") info := &AuditInfo{ Action: action, ResourceType: resourceType, ResourceID: d.resourceID, Details: nil, // Delete doesn't need details } // Call audit callback - if it fails, return error to trigger rollback if err := d.auditCallback(ctx, d.tx, info, d.auditRequest); err != nil { return errors.Wrap(err, "audit.callback") } } return nil } func DeleteByID[T any](tx bun.Tx, id int) *deleter[T] { return DeleteItem[T](tx).Where("id = ?", id) } func DeleteWithProtection[T systemType](ctx context.Context, tx bun.Tx, id int) error { deleter := DeleteByID[T](tx, id) item, err := GetByID[T](tx, id).GetFirst(ctx) if err != nil { return errors.Wrap(err, "GetByID") } if item == nil { return errors.New("record not found") } if (*item).isSystem() { return errors.New("record is system protected") } return deleter.Delete(ctx) }