Soft delete
Cosa cambia
Sezione intitolata “Cosa cambia”Con softDelete attivo, un’eliminazione contrassegna una colonna invece di rimuovere la riga,
e ogni operazione di lettura esclude le righe contrassegnate. Tutto il resto
dell’operazione rimane invariato: è richiesta la stessa autorizzazione, beforeDelete può
ancora porre il veto e afterDelete viene comunque eseguito. Dal punto di vista del chiamante
la riga è stata eliminata; il modo in cui la tabella registra l’operazione dipende da questo flag.
import { defineCollection } from "@rebasepro/cms-types";
const invoices = defineCollection({ slug: "invoices", name: "Invoices", table: "invoices", softDelete: true, properties: { reference: { name: "Reference", type: "string" }, deletedAt: { name: "Deleted at", type: "date", admin: { readOnly: true } } }});true utilizza deletedAt (colonna deleted_at). La forma a oggetto consente di rinominarlo:
softDelete: { field: "archivedAt" }.
Solo Postgres, come la ricerca e gli indici.
Cosa vede una lettura
Sezione intitolata “Cosa vede una lettura”Le righe contrassegnate sono nascoste per impostazione predefinita da find, findById, count,
dalle aggregazioni, dal refetch in tempo reale e da questa collection caricata tramite
una relazione. Tale comportamento predefinito è fondamentale: il codice scritto prima
dell’introduzione del flag continua a funzionare e nessuno deve ricordarsi di filtrare.
Due parametri di query consentono di accedervi:
| Parametro | Risultati |
|---|---|
?deleted=include |
Righe attive e quelle contrassegnate |
?deleted=only |
Solo le righe contrassegnate — la vista cestino |
Qualsiasi altro valore restituisce un 400 invece di un fallback silenzioso. ?deleted=true che
nasconde silenziosamente ogni riga eliminata sembrerebbe funzionare, ma risponderebbe
alla domanda opposta.
Ripristino ed eliminazione definitiva
Sezione intitolata “Ripristino ed eliminazione definitiva”Un ripristino è un normale aggiornamento che reimposta il campo su null. Non esiste
un verbo speciale, perché non esiste uno stato speciale: la riga non è mai stata spostata altrove.
Tramite REST è un PATCH — singolo, /bulk o _batch — il cui body imposta il
campo su null:
curl -X PATCH /api/data/invoices/7 -d '{"deletedAt": null}'Quell’aggiornamento è l’unico che raggiunge una riga contrassegnata. Qualsiasi altra modifica
di essa restituisce 404, come fa la riga per ogni lettura predefinita; ripristinala prima.
L’operazione inversa non è un aggiornamento. Impostare il campo su un valore viene rifiutato con 400
FIELD_NOT_WRITABLE — elimina invece la riga — così il permesso delete,
beforeDelete e afterDelete si applicano sempre. Un upsert può creare una riga
già contrassegnata, ma non contrassegna mai una riga già memorizzata.
Né un upsert scrive in una riga nel cestino: quando la sua chiave appartiene a
una riga contrassegnata, viene rifiutato con 409 ROW_IN_TRASH e non viene
scritto nulla. Ripristina prima la riga, oppure elimina definitivamente con
?hard=true e ripeti l’upsert. Sulla 0.23 l’upsert scrive i suoi valori nella
riga nascosta e risponde 201.
Un’eliminazione tramite un percorso many-to-many, come DELETE /api/data/posts/1/tags/5,
rimuove il collegamento del post 1 al tag. Non esegue il soft delete del tag, che altri
post utilizzano ancora.
Una vera DELETE corrisponde a ?hard=true nella chiamata di eliminazione. Richiede esattamente
gli stessi permessi di un’eliminazione ordinaria: si tratta dello stesso verbo e gestirlo
separatamente creerebbe una seconda superficie di controllo accessi per la medesima operazione. Ciò che
cambia è la possibilità che la riga venga recuperata. Solo il valore letterale true o 1
significa sì; un errore di digitazione produce un 400, poiché un chiamante che ha richiesto di eliminare
definitivamente e ha ottenuto un soft delete crederebbe erroneamente che i dati siano spariti.
Dall’SDK
Sezione intitolata “Dall’SDK”Il client tipizzato raggiunge il cestino tramite trash su ogni collection. Invia
le richieste sopra descritte: ?deleted=only sulle letture, l’aggiornamento a null per un
ripristino, e ?hard=true per un’eliminazione definitiva.
const trashed = await client.data.invoices.trash.find({ limit: 20 });const howMany = await client.data.invoices.trash.count();
await client.data.invoices.trash.restore(7); // PATCH { deletedAt: null }await client.data.invoices.trash.restoreMany([8, 9]);
await client.data.invoices.trash.purge(7); // DELETE ?hard=trueawait client.data.invoices.trash.purgeMany([8, 9]);trash.find e trash.count accettano gli stessi parametri di find e count.
Un ripristino imposta deletedAt a meno che tu non nomini il campo: una collection dichiarata
con softDelete: { field: "archivedAt" } si ripristina con
trash.restore(7, { field: "archivedAt" }). Il client non può leggere il nome del
campo dal server.
Usa trash su una collection che dichiara softDelete. Su una che non lo fa,
il server ignora ?deleted=, quindi trash.find restituisce le righe attive e
trash.purge è un’eliminazione ordinaria.
Il codice server ha lo stesso trash: context.data.invoices.trash in una callback,
e rebase.data.invoices.trash in una function o in un cron. Legge ed elimina
definitivamente tramite il driver, con i privilegi del chiamante, come fa il resto di
context.data.
Con offline abilitato, il cestino non viene mai messo in cache e nulla
al suo interno viene accodato. Ogni chiamata richiede il server, e fallisce con l’errore OFFLINE
senza di esso. Una riga ripristinata entra nel database locale, e una eliminata definitivamente ne esce.
Passaggi successivi
Sezione intitolata “Passaggi successivi”- Definire le Collection — dove viene dichiarato
softDelete - API REST — gli endpoint di eliminazione e query a cui appartengono questi parametri
- Regole di Sicurezza (RLS) — chi ha i permessi per eliminare una riga