package database import ( "database/sql" "time" "github.com/jmoiron/sqlx" "github.com/google/uuid" "github.com/peterqiu0516/sub-store/internal/util" ) type RecycleRepo struct { db *sqlx.DB } func NewRecycleRepo(db *sqlx.DB) *RecycleRepo { return &RecycleRepo{db: db} } type recycleRow struct { ID string `db:"id"` ResourceType string `db:"resource_type"` ResourceID string `db:"resource_id"` SnapshotJSON string `db:"snapshot_json"` DeletedAt int64 `db:"deleted_at"` } // ArchiveAndDelete archives a resource snapshot to recycle_bin and deletes it in one transaction. // Per review-resolution #29: BeginTx wrapping archive + delete + trim. func (r *RecycleRepo) ArchiveAndDelete(resourceType, resourceID string, snapshot map[string]any, deleteSQL string, deleteArgs ...any) (string, error) { deletedAt := time.Now().UnixMilli() id := uuid.New().String() snapshotJSON := marshalJSON(snapshot) maxEntries := util.MaxRecycleEntries tx, err := r.db.Beginx() if err != nil { return "", err } defer tx.Rollback() // Step 1: Insert into recycle_bin _, err = tx.Exec( "INSERT INTO recycle_bin (id, resource_type, resource_id, snapshot_json, deleted_at) VALUES (?, ?, ?, ?, ?)", id, resourceType, resourceID, snapshotJSON, deletedAt, ) if err != nil { return "", err } // Step 2: Delete the resource _, err = tx.Exec(deleteSQL, deleteArgs...) if err != nil { return "", err } // Step 3: Trim recycle_bin to maxEntries _, err = tx.Exec( `DELETE FROM recycle_bin WHERE id IN ( SELECT id FROM recycle_bin ORDER BY deleted_at DESC LIMIT -1 OFFSET ? )`, maxEntries, ) if err != nil { return "", err } if err := tx.Commit(); err != nil { return "", err } return id, nil } func (r *RecycleRepo) List() ([]map[string]any, error) { var rows []recycleRow if err := r.db.Select(&rows, "SELECT * FROM recycle_bin ORDER BY deleted_at DESC LIMIT ?", util.MaxRecycleEntries, ); err != nil { return nil, err } result := make([]map[string]any, 0, len(rows)) for _, row := range rows { entry := map[string]any{ "id": row.ID, "resourceType": row.ResourceType, "resourceId": row.ResourceID, "snapshot": parseSnapshot(row.SnapshotJSON), "deletedAt": row.DeletedAt, } result = append(result, entry) } return result, nil } func (r *RecycleRepo) Get(id string) (map[string]any, error) { var row recycleRow if err := r.db.Get(&row, "SELECT * FROM recycle_bin WHERE id = ?", id); err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return map[string]any{ "id": row.ID, "resourceType": row.ResourceType, "resourceId": row.ResourceID, "snapshot": parseSnapshot(row.SnapshotJSON), "deletedAt": row.DeletedAt, }, nil } func (r *RecycleRepo) Delete(id string) error { _, err := r.db.Exec("DELETE FROM recycle_bin WHERE id = ?", id) return err } func parseSnapshot(value string) map[string]any { if value == "" { return map[string]any{} } var m map[string]any jsonUnmarshal(value, &m) if m == nil { m = map[string]any{} } return m }