Files
sub-store/internal/database/recycle_repo.go
T

130 lines
3.0 KiB
Go

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
}