Soft delete
Qué cambia
Sección titulada «Qué cambia»Con softDelete activado, un borrado marca una columna en lugar de eliminar la fila, y cada lectura filtra las filas marcadas. Nada más cambia en la operación: se requiere el mismo permiso, beforeDelete todavía puede vetarlo y afterDelete aún se ejecuta. Desde el punto de vista de quien realiza la llamada, la fila fue eliminada; cómo lo registra la tabla es asunto de este 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 utiliza deletedAt (columna deleted_at). La forma de objeto permite renombrarlo: softDelete: { field: "archivedAt" }.
Solo para Postgres, al igual que la búsqueda y los índices.
Qué ve una lectura
Sección titulada «Qué ve una lectura»Las filas marcadas se ocultan por defecto de find, findById, count, las agregaciones, el refetch en tiempo real y de esta colección cuando se carga a través de una relación. Ese comportamiento por defecto es el objetivo: el código escrito antes de que existiera este flag sigue funcionando y nadie tiene que acordarse de filtrar.
Dos parámetros de consulta permiten acceder a ellas:
| Parámetro | Qué devuelve |
|---|---|
?deleted=include |
Filas activas y marcadas |
?deleted=only |
Solo filas marcadas — la vista de papelera |
Cualquier otra cosa devuelve un 400 en lugar de un fallback silencioso. Que ?deleted=true ocultara silenciosamente cada fila eliminada daría la impresión de funcionar y respondería a la pregunta opuesta.
Restaurar y eliminar de verdad
Sección titulada «Restaurar y eliminar de verdad»Una restauración es una actualización ordinaria que vuelve a establecer el campo en null. No hay un verbo especial, porque no hay un estado especial: la fila nunca se fue a ningún lado. Por REST es un PATCH — individual, /bulk o _batch — cuyo cuerpo establece el campo en null:
curl -X PATCH /api/data/invoices/7 -d '{"deletedAt": null}'Esa actualización es la que llega a una fila marcada. Cualquier otra edición de ella responde 404, igual que la fila responde a toda lectura por defecto; restáurala primero.
Lo contrario no es una actualización. Establecer el campo en un valor se rechaza con 400 FIELD_NOT_WRITABLE — elimina la fila en su lugar —, de modo que el permiso delete, beforeDelete y afterDelete siempre se le aplican. Un upsert puede crear una fila que ya esté marcada, pero nunca marca una que ya estaba almacenada.
Tampoco escribe un upsert en una fila que está en la papelera: cuando su clave pertenece a una fila marcada, se rechaza con 409 ROW_IN_TRASH y no se escribe nada. Restaura la fila primero, o púrgala con ?hard=true y vuelve a hacer el upsert. En 0.23 el upsert escribe sus valores en la fila oculta y responde 201.
Una eliminación a través de una ruta muchos a muchos, como DELETE /api/data/posts/1/tags/5, elimina el vínculo del post 1 con la etiqueta. No hace un soft delete de la etiqueta, que otros posts siguen usando.
Un DELETE real es ?hard=true en la llamada de eliminación. Requiere exactamente el mismo permiso que un borrado ordinario: es el mismo verbo, y restringirlo por separado supondría una segunda superficie de control de acceso para una sola operación. Lo que cambia es si la fila puede recuperarse o no. Solo el valor literal true o 1 significa que sí; un error tipográfico devuelve un 400, porque quien realiza la llamada solicitó purgar y, si obtuviera un soft delete, creería que los datos han desaparecido.
Desde el SDK
Sección titulada «Desde el SDK»El cliente tipado llega a la papelera a través de trash en cada colección. Envía
las peticiones anteriores: ?deleted=only en las lecturas, la actualización a null para
restaurar y ?hard=true para purgar.
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 y trash.count aceptan los mismos parámetros que find y count.
Una restauración establece deletedAt salvo que nombres el campo: una colección declarada
con softDelete: { field: "archivedAt" } se restaura con
trash.restore(7, { field: "archivedAt" }). El cliente no puede leer el nombre
del campo del servidor.
Usa trash en una colección que declare softDelete. En una que no lo haga,
el servidor ignora ?deleted=, así que trash.find devuelve las filas vivas y
trash.purge es un borrado normal.
El código del servidor tiene el mismo trash: context.data.invoices.trash en un callback,
y rebase.data.invoices.trash en una función o un cron. Lee y purga
a través del driver, con los privilegios de quien llama, como el resto de
context.data.
Con offline activado, la papelera nunca se almacena en caché y nada
de ella se encola. Cada llamada necesita el servidor y falla con el error OFFLINE
sin él. Una fila restaurada se une a la base de datos local, y una purgada la abandona.
Siguientes pasos
Sección titulada «Siguientes pasos»- Definición de colecciones — dónde se declara
softDelete - API REST — los endpoints de eliminación y consulta a los que pertenecen estos parámetros
- Reglas de seguridad (RLS) — quién puede eliminar una fila en primer lugar