130 lines
3.0 KiB
Go
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
|
|
}
|