ADR-004: Patró d'eliminació de recursos amb safeguards (preview + confirm-by-name)
| Camp | Valor |
|---|---|
| Data | 2026-04-26 |
| Estat | Accepted |
| Decisors | Equip LDP |
| Relacionat | (cap dependència directa) |
Context
Eliminar recursos centrals del catàleg (Dance, Song, i futurs Choreographer / Event) toca moltes taules amb FKs amb diferents accions (CASCADE, SET NULL, RESTRICT). Els riscos d'una eliminació mal pensada són grans:
- Pèrdua silenciosa de dades curades: una
DELETEsense informació pot esborrar associacions, links, fitxes tècniques (LineDanceSpec), històric de fusions, etc., en cascada, sense que l'admin sigui conscient. - Trencar la integritat de l'historial: si un recurs és el "survivor" d'una fusió (
*_merge_history.survivor_id), eliminar-lo trenca la traçabilitat dels merges. - Trencar referències d'agenda real: events ja celebrats que tenen un ball al setlist no han de poder perdre la referència ni l'event silenciosament.
- Eliminacions accidentals: un click descuidat amb un sol botó "Eliminar" pot ser destructiu i no recuperable.
A la sessió del 2026-04-26 es va plantejar la necessitat d'eliminar dades de proves de manera segura, sense renunciar a la integritat del sistema en producció.
Decisió
Tots els recursos centrals que necessitin esborrat dur (hard-delete) implementen el mateix patró de 3 capes:
1. Pre-flight check (preview)
Endpoint admin-only: GET /api/admin/{resource}/{id}/delete-preview.
Retorna un Delete{Resource}PreviewDto amb:
blockers: llista de raons que prohibeixen l'esborrat. Si conté algun element, l'esborrat ha de fallar a 409 Conflict.willCascade: resum de files relacionades que es perdran o es desvincularan (counts per categoria).
Tipus de blockers definits actualment:
| Tipus | Què bloqueja | Per què |
|---|---|---|
EVENT_SETLIST | Dance: present a event_setlist_items | Els events ja celebrats no han de perdre referències. |
MERGE_SURVIVOR | Dance/Song: és survivor_id a *_merge_history | Trencar la història de fusions invalida l'auditoria. |
DANCE_USING_SONG | Song: alguna fila a dance_song | Coherència catalogada; si cal, primer cal arxivar/fusionar. |
2. Confirmació pel nom exacte
Endpoint admin-only: DELETE /api/admin/{resource}/{id}?confirmationName={exactValue}.
El servei refusa amb ClaimException (codi *_DELETE_CONFIRMATION_MISMATCH) si el text rebut no coincideix amb el nom canònic del recurs:
- Dance:
dance.nameexacte. - Song:
"{title} — {artist}"(em-dash U+2014 entre títol i artista). - (Futurs) Choreographer / Event: definir el nom canònic en el moment d'implementar-se.
3. Safeguards al servei
Dins @Transactional, el servei:
- Valida el
confirmationName. - Crida internament
previewDelete()i refusa si hi ha cap blocker (codi*_IN_USE). - Loggeja
INFOamb email admin +cascadeSummaryper traçabilitat. - Executa l'esborrat (la majoria de cascades es fan a nivell DB via constraints
ON DELETE CASCADE/SET NULL). - Si hi ha decisió específica (e.g. links orfes post-cascade per a Dance), neteja addicional dins la mateixa transacció.
4. Soft-delete com a alternativa preferent
Quan el recurs ho permet (té listing_status o equivalent), la UI sempre ofereix l'opció d'arxivar com a alternativa abans de l'esborrat dur.
- Dance → soft-delete via
PATCH /api/admin/dances/{id}/listing-statusambARCHIVED. Reversible, preserva tot. - Song → soft-delete pendent (no té encara
listing_status); a futur, afegir migració.
L'admin tria entre arxivar (preferit, no destructiu) i eliminar permanentment (destructiu, amb 2 passos de confirmació).
5. UX: modal en 2 passos
DeleteDanceModal / DeleteSongModal:
- Pas 1: mostra el resum del preview. Si hi ha blockers, només permet "Cancel·lar" / "Arxivar". Si no, ofereix "Continuar amb l'esborrat".
- Pas 2: input
TextInputamb el nom canònic com a placeholder. El botó vermell "Eliminar permanentment" està desactivat fins que el text exactament coincideix.
Decisions aplicades concretes (Dance, 2026-04-26)
- Els events que tinguin un dance al setlist mai permeten esborrar el dance (decisió A: dura).
- Si un dance és survivor de merge, mai es permet esborrar (decisió B: dura).
- Tant arxivar com eliminar dur estan disponibles a la UI (decisió C: les dues).
- Els links que quedin orfes a
links.dance_song_idpost-CASCADE es netegen (decisió D: sí).
Decisions aplicades concretes (Song, 2026-04-26)
- Songs en ús per algun dance NO es poden eliminar (constraint DB
dance_song.song_id NO ACTIONa V48 + check al servei). - Songs survivor de merge NO es poden eliminar.
- Soft-delete pendent (Songs no té encara
listing_status); s'ha deixat la porta oberta per a una futura migració.
Conseqüències
Positives
- Sense pèrdua silenciosa: l'admin sempre veu què s'esborrarà abans d'executar.
- Sense esborrats accidentals: cal un nom exacte per confirmar.
- Auditoria preservada: la història de merges i events celebrats mai es trenca per error.
- Patró replicable: els nous recursos eliminables (Choreographer, Event, Venue, Person, Organization...) poden seguir el mateix patró sense reinventar.
- Frontend coherent: dos modals quasi idèntics (
DeleteDanceModal,DeleteSongModal) amb la mateixa UX.
Negatives
- Cost d'implementació per recurs: cada recurs eliminable necessita endpoint preview, count methods als repos, DTO de preview i modal. ~3-4 hores per recurs.
- No 100% genèric: Dance i Song tenen DTOs separats (
DeleteDancePreviewDtovsDeleteSongPreviewDto) perquè els camps de cascade summary són diferents. Si en algun moment apareix un tercer recurs amb estructura idèntica, refactoritzar a un DTO genèric és viable, però per ara els 2 separats són clars i petits.
Implementació de referència
Notes per a futurs recursos
Quan s'implementi el patró per a un nou recurs:
- Mapejar totes les FKs que apunten a la taula del recurs (
grep "REFERENCES <taula>" V*.sqlabackend/src/main/resources/db/migration). - Classificar cada FK: BLOCKER (RESTRICT/NO ACTION), CASCADE (silenciós), SET NULL (orfes).
- Decidir per cada CASCADE/SET NULL si cal mostrar el count al preview o si és prou irrellevant per amagar-lo.
- Afegir count methods als repos corresponents.
- Crear
Delete{Resource}PreviewDtoamb la mateixa estructura (blockers+willCascade). - Crear el modal frontend copiant-ne un d'existent.
- Afegir ErrorCodes
{RESOURCE}_IN_USEi{RESOURCE}_DELETE_CONFIRMATION_MISMATCH. - Documentar les decisions específiques d'aquest recurs en aquest mateix ADR (afegint una nova secció).