Zum Inhalt springen

Soft Delete

Wenn softDelete aktiviert ist, versieht ein Löschvorgang eine Spalte mit einem Zeitstempel, anstatt die Zeile zu entfernen, und jeder Lesevorgang filtert die so markierten Zeilen heraus. Ansonsten ändert sich an der Operation nichts: Es ist dieselbe Berechtigung erforderlich, beforeDelete kann weiterhin ein Veto einlegen und afterDelete wird weiterhin ausgelöst. Aus Sicht des Aufrufers wurde die Zeile gelöscht; wie die Tabelle dies festhält, ist Aufgabe dieses Flags.

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 verwendet deletedAt (Spalte deleted_at). Die Objektform benennt sie um: softDelete: { field: "archivedAt" }.

Nur für Postgres, genau wie search und indexes.

Zeilen mit Zeitstempel werden standardmäßig vor find, findById, count, den Aggregaten, dem Realtime-Refetch und beim Laden dieser Collection über eine Relation ausgeblendet. Dieser Standardwert ist genau der Zweck: Code, der vor der Existenz des Flags geschrieben wurde, funktioniert weiterhin, und niemand muss daran denken, manuell zu filtern.

Zwei Query-Parameter ermöglichen den Zugriff darauf:

Parameter Liefert
?deleted=include Aktive Zeilen und solche mit Zeitstempel
?deleted=only Nur Zeilen mit Zeitstempel – die Papierkorb-Ansicht

Jeder andere Wert führt zu einem 400-Fehler statt eines stillen Fallbacks. Würde ?deleted=true stillschweigend alle gelöschten Zeilen ausblenden, würde es so aussehen, als hätte es funktioniert, und stattdessen die gegenteilige Frage beantworten.

Eine Wiederherstellung ist ein ganz normales Update, das das Feld wieder auf null setzt. Es gibt kein spezielles Verb, da es keinen speziellen Status gibt – die Zeile war nie weg. Über REST ist es ein PATCH – einzeln, /bulk oder _batch –, dessen Body das Feld auf null setzt:

curl -X PATCH /api/data/invoices/7 -d '{"deletedAt": null}'

Dieses Update ist dasjenige, das eine gestempelte Zeile erreicht. Jede andere Bearbeitung davon antwortet mit 404, so wie die Zeile es bei jedem Standard-Lesezugriff tut; stellen Sie sie zuerst wieder her.

Die Umkehrung ist kein Update. Das Setzen des Feldes auf einen Wert wird mit 400 FIELD_NOT_WRITABLE abgelehnt – löschen Sie die Zeile stattdessen –, sodass die delete-Berechtigung, beforeDelete und afterDelete immer dafür gelten. Ein Upsert kann eine Zeile erstellen, die bereits gestempelt ist, stempelt aber nie eine, die gespeichert war.

Ebenso wenig schreibt ein Upsert in eine Zeile im Papierkorb: Wenn sein Schlüssel zu einer gestempelten Zeile gehört, wird er mit 409 ROW_IN_TRASH abgelehnt, und nichts wird geschrieben. Stellen Sie die Zeile zuerst wieder her, oder löschen Sie sie endgültig mit ?hard=true und führen Sie das Upsert erneut aus. Auf 0.23 schreibt das Upsert seine Werte in die verborgene Zeile und antwortet mit 201.

Ein Löschvorgang über einen Many-to-Many-Pfad wie DELETE /api/data/posts/1/tags/5 entfernt die Verknüpfung von Post 1 mit dem Tag. Er führt kein Soft Delete des Tags durch, den andere Posts weiterhin verwenden.

Ein echtes DELETE erfolgt über ?hard=true beim Delete-Aufruf. Es erfordert exakt dieselbe Berechtigung wie ein normaler Löschvorgang: Es ist dasselbe Verb, und eine separate Absicherung wäre eine zweite Zugriffskontrollfläche für ein und dieselbe Operation. Was sich ändert, ist, ob die Zeile wiederhergestellt werden kann. Nur das Literal true oder 1 bedeutet Ja; ein Tippfehler führt zu einem 400-Fehler, denn ein Aufrufer, der ein vollständiges Löschen angefordert hat und stattdessen ein Soft Delete erhält, geht davon aus, dass die Daten endgültig gelöscht sind.

Der typisierte Client erreicht den Papierkorb über trash an jeder Collection. Er sendet die oben beschriebenen Anfragen: ?deleted=only bei den Lesezugriffen, das Update auf null für eine Wiederherstellung und ?hard=true für ein endgültiges Löschen.

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=true
await client.data.invoices.trash.purgeMany([8, 9]);

trash.find und trash.count nehmen dieselben Parameter wie find und count. Eine Wiederherstellung setzt deletedAt, sofern Sie das Feld nicht nennen: Eine Collection, die mit softDelete: { field: "archivedAt" } deklariert ist, stellt mit trash.restore(7, { field: "archivedAt" }) wieder her. Der Client kann den Feldnamen nicht vom Server lesen.

Verwenden Sie trash an einer Collection, die softDelete deklariert. An einer, die es nicht tut, ignoriert der Server ?deleted=, sodass trash.find die aktiven Zeilen zurückgibt und trash.purge ein gewöhnliches Löschen ist.

Servercode hat denselben trash: context.data.invoices.trash in einem Callback und rebase.data.invoices.trash in einer Funktion oder einem Cron. Er liest und löscht endgültig über den Driver, mit den Berechtigungen des Aufrufers, wie der Rest von context.data.

Ist Offline aktiviert, wird der Papierkorb nie zwischengespeichert, und nichts darin wird in die Warteschlange gestellt. Jeder Aufruf braucht den Server und schlägt ohne ihn mit dem Fehler OFFLINE fehl. Eine wiederhergestellte Zeile kommt in die lokale Datenbank, eine endgültig gelöschte verlässt sie.