fix(backend): enforce gapless 1..N reindexing normalization across research areas CRUD
- Implement normalizeResearchAreasOrder inside transactions for Create, Update, and Delete operations - Prevent duplicate or corrupted display_order overlaps when inserting/moving items - Clean up existing database display_order sequence - Pass all unit tests and E2E test gates cleanly
This commit is contained in:
@@ -126,6 +126,35 @@ func (r *HomeRepository) GetResearchAreas(ctx context.Context) ([]models.Researc
|
||||
return areas, rows.Err()
|
||||
}
|
||||
|
||||
func (r *HomeRepository) normalizeResearchAreasOrder(ctx context.Context, tx *sql.Tx) error {
|
||||
rows, err := tx.QueryContext(ctx, `SELECT id FROM research_areas ORDER BY display_order ASC, id ASC`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query areas for normalization: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ids []int
|
||||
for rows.Next() {
|
||||
var id int
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return fmt.Errorf("scan area id: %w", err)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx, id := range ids {
|
||||
newOrder := idx + 1
|
||||
_, err := tx.ExecContext(ctx, `UPDATE research_areas SET display_order = ? WHERE id = ?`, newOrder, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update area %d to order %d: %w", id, newOrder, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HomeRepository) CreateResearchArea(ctx context.Context, a models.ResearchArea) (int, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
@@ -161,12 +190,20 @@ func (r *HomeRepository) CreateResearchArea(ctx context.Context, a models.Resear
|
||||
return 0, fmt.Errorf("insert research area: %w", err)
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get inserted area id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.normalizeResearchAreasOrder(ctx, tx); err != nil {
|
||||
return 0, fmt.Errorf("normalize display_order: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("commit tx: %w", err)
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
return int(id), err
|
||||
return int(id), nil
|
||||
}
|
||||
|
||||
func (r *HomeRepository) UpdateResearchArea(ctx context.Context, id int, a models.ResearchArea) error {
|
||||
@@ -202,6 +239,8 @@ func (r *HomeRepository) UpdateResearchArea(ctx context.Context, id int, a model
|
||||
if err != nil {
|
||||
return fmt.Errorf("shift research areas display_order on update: %w", err)
|
||||
}
|
||||
} else if a.DisplayOrder <= 0 {
|
||||
a.DisplayOrder = oldOrder
|
||||
}
|
||||
|
||||
res, err := tx.ExecContext(ctx, `
|
||||
@@ -216,11 +255,22 @@ func (r *HomeRepository) UpdateResearchArea(ctx context.Context, id int, a model
|
||||
if err == nil && rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
if err := r.normalizeResearchAreasOrder(ctx, tx); err != nil {
|
||||
return fmt.Errorf("normalize display_order on update: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *HomeRepository) DeleteResearchArea(ctx context.Context, id int) error {
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM research_areas WHERE id = ?`, id)
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
res, err := tx.ExecContext(ctx, `DELETE FROM research_areas WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete research area: %w", err)
|
||||
}
|
||||
@@ -228,7 +278,12 @@ func (r *HomeRepository) DeleteResearchArea(ctx context.Context, id int) error {
|
||||
if err == nil && rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return err
|
||||
|
||||
if err := r.normalizeResearchAreasOrder(ctx, tx); err != nil {
|
||||
return fmt.Errorf("normalize display_order on delete: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *HomeRepository) GetStatsSummary(ctx context.Context) (*models.StatsSummary, error) {
|
||||
|
||||
Reference in New Issue
Block a user