Zum Inhalt springen

Die context.data-API

Jeder Callback erhält ein context-Objekt, das context.data enthält — eine einheitliche Datenzugriffsschicht zur Durchführung von kollektionsübergreifenden Operationen innerhalb von Lifecycle-Hooks.

context.data verwendet einen JavaScript-Proxy, sodass Sie über den Slug als Eigenschaft auf jede Kollektion zugreifen können:

afterSave: async ({ values, id, context }) => {
// Dynamic property access — works for any collection slug
const jobs = context.data.jobs;
const users = context.data.users;
// Alternatively, use the .collection() method for dynamic slugs
const collectionName = "jobs";
const accessor = context.data.collection(collectionName);
}

Eine Eigenschaft ist der Accessor der Collection, der Name, den das generierte SDK ihr gibt: context.data.myNotes ist die Collection my-notes, in welcher Tabelle sie auch gespeichert ist. Ein Name, der kein Accessor einer Collection ist, wird als sein snake_case gelesen, sodass context.data.auditLog die Collection audit_log ist.

Zeilen sind untypisiert (Record<string, unknown>), bis das Projekt seine generierte Database registriert; dann ist jeder Accessor daraus typisiert. Siehe Typisierte serverseitige Daten.

Jeder Kollektions-Accessor (context.data.<slug>) stellt die folgenden Methoden bereit:

Methode Signatur Beschreibung
.find() find(params?: FindParams) → FindResponse Entitäten mit Filtern, Sortierung und Paginierung abfragen
.findById() findById(id: string | number) → Entity | undefined Eine einzelne Entität anhand der ID abrufen
.create() create(data: Partial<Values>, id?: string) → Entity Eine neue Entität erstellen
.update() update(id: string | number, data: Partial<Values>) → Entity Eine bestehende Entität aktualisieren
.delete() delete(id: string | number) → void Einen Datensatz löschen
.count() count(params?: FindParams) → number Übereinstimmende Entitäten zählen
.listen() listen(params, onUpdate, onError?) → unsubscribe Echtzeit-Abonnement (sofern unterstützt)
.listenById() listenById(id, onUpdate, onError?) → unsubscribe Eine einzelne Entität überwachen
.trash trash.find(), .count(), .restore(id), .restoreMany(ids), .purge(id), .purgeMany(ids) Die Zeilen, die ein Soft Delete markiert hat

Die find()-Methode filtert mit [operator, value]-Tupeln — der typisierten Form des Query-Strings ?status=eq.published, den die REST-API liest:

afterSave: async ({ values, context }) => {
// Equality
const { data: activeJobs } = await context.data.jobs.find({
where: { status: ["==", "published"] },
limit: 10,
orderBy: ["createdAt", "desc"]
});
// Several conditions, AND-ed
const { data: expensiveJobs } = await context.data.jobs.find({
where: {
salary: [">=", 100000],
role: ["in", ["admin", "manager"]]
}
});
}

.create() und .update() erwarten die zu schreibenden Werte, mit den oben genannten Signaturen. Daten zwischen Kollektionen synchronisieren verwendet beide: Eine genehmigte Einreichung erstellt einen veröffentlichten Job und wird mit ihm verknüpft.

Sicherheit: Mit welchen Berechtigungen context.data ausgeführt wird

Abschnitt betitelt „Sicherheit: Mit welchen Berechtigungen context.data ausgeführt wird“

context.data erbt die Berechtigungen dessen, was den Callback ausgelöst hat. Es handelt sich nicht um eine feste Vertrauensstufe.

  • Ausgelöst durch eine Benutzeranfrage (REST, Realtime, eine Bearbeitung im Admin-Panel) → User-Scoped. Der Callback wird innerhalb der RLS-gebundenen Transaktion ausgeführt, die für diese Anfrage geöffnet wurde, sodass Richtlinien für Lese- und Schreibvorgänge gelten. Ein Callback kann keine Zeile sehen, die sein Aufrufer nicht sehen konnte.
  • Ausgelöst durch rebase.dataAsAdmin oder einen Cron-Job (dasselbe Singleton) → Admin-Scoped, nicht unbeschränkt (unscoped). Dieser Treiber ist als { uid: "service", roles: ["admin"] } definiert, sodass der Callback dennoch in einer RLS-gebundenen Transaktion ausgeführt wird — Ihre Richtlinien werden anhand dieser Identität ausgewertet.
  • Ausgelöst durch den Basistreiber (integrierte Authentifizierungs-Flows, Migrationen) → Unscoped. Er wird über die Owner-Verbindung ausgeführt und umgeht RLS.

Dies ist besonders in jenen Fällen von Bedeutung, die stillschweigend fehlschlagen. RLS filtert, es wirft keine Fehler — ein Callback, der eine benachbarte Zeile liest, findet diese also, wenn eine Admin-Aufgabe speichert, findet jedoch möglicherweise nichts, wenn ein Endbenutzer speichert, jeweils ohne Fehlermeldung. Schreiben Sie Callbacks so, dass sie ein leeres Ergebnis tolerieren, oder greifen Sie bewusst auf die Admin-Ebene zu:

afterSave: async ({ context }) => {
// User-scoped when a user triggered this save: RLS applies.
await context.data.audit_logs.create({ action: "approved" });
// Deliberately admin-scoped — for work the caller genuinely may not see,
// such as an audit trail they must not be able to read or edit. Note this
// is an admin's reach, not a bypass: a collection whose only rule is
// `policy.serverContext()` stays closed to it, since that compiles to
// `rebase.uid() IS NULL` and this accessor's uid is `service`.
// `dataAsAdmin` is always there server-side; its type allows for the
// browser SDK, which has none — hence the `!`.
await context.client.dataAsAdmin!.audit_logs.create({ action: "approved" });
}

context.data-Schreibvorgänge eines Callbacks sind Teil des Schreibvorgangs, der ihn ausgelöst hat. Unter Postgres laufen beforeSave, das Speichern und afterSave — bzw. beforeDelete, das Löschen und afterDelete — innerhalb einer einzigen Transaktion. Jeder Callback wird vor dem Commit abgewartet, und context.data schreibt über dieselbe Transaktion.

Der auslösende Schreibvorgang und alles, was seine Callbacks geschrieben haben, werden also entweder gemeinsam oder gar nicht committet:

  • Ein Fehler (Throw) in afterSave oder afterDelete rollt den auslösenden Schreibvorgang sowie jeden context.data-Schreibvorgang der Callbacks zurück. Der Aufrufer erhält 400 CALLBACK_REJECTED mit details.stage, das den jeweiligen Hook benennt — oder den spezifischen Fehlerstatus, falls vorhanden: ein von Ihnen geworfener RebaseApiError, der 409-Fehler einer Eindeutigkeitsverletzung.
  • Realtime-Abonnenten erfahren erst nach dem Commit von der Zeile; ein zurückgerollter Schreibvorgang wird also niemals angekündigt.
  • Ein Callback hält die Transaktion während seiner Ausführung offen. Ein langsamer Callback bedeutet daher eine gehaltene Sperre und eine blockierte Verbindung im Connection-Pool — beforeSave und beforeDelete ebenso wie die after-Hooks. Siehe Ein Callback hält eine Verbindung.
  • Ein context.data-Schreibvorgang führt auch die Callbacks der Zielkollektion aus, sodass ein afterSave, das seine eigene Zeile aktualisiert, sich selbst erneut ausführt. Schreibvorgänge, die mehr als 16 Ebenen tief verschachtelt sind, werden mit 500 CALLBACK_RECURSION abgelehnt, unter Angabe des Hooks und der Collection, und der gesamte Schreibvorgang wird zurückgerollt. Machen Sie einen solchen Schreibvorgang bedingt, wie es das Beispiel unten tut.

Lassen Sie einen Fehler werfen, wenn der auslösende Schreibvorgang diesen nicht überstehen soll. Fangen Sie ihn ab, wenn er überstehen soll, aber nur um einen context.data-Schreibvorgang herum: Ein Create, Update oder Delete, das die Datenbank ablehnt (eine Unique- oder Foreign-Key-Verletzung, ein Trigger), wird isoliert rückgängig gemacht, und der Rest wird committet.

Jede andere Anweisung, die auf der Transaktion des Schreibvorgangs fehlschlägt — eine Abfrage, der Lesevorgang, den ein Update oder Delete zum Auffinden seiner Zeile durchführt (eine ID, die die Schlüsselspalte nicht aufnehmen kann), ein von der Datenbank abgelehntes Einreihen eines Jobs —, bricht diese Transaktion in Postgres ab, und das Abfangen des Fehlers in JavaScript macht das nicht rückgängig. Der Schreibvorgang wird mit 500 TRANSACTION_ABORTED abgelehnt und nichts wird gespeichert, statt Erfolg für einen Schreibvorgang zu melden, der zurückgerollt wurde. Lassen Sie einen solchen Fehler werfen, oder prüfen Sie die Bedingung, bevor Sie die Anweisung ausführen.

afterSave: async ({ values, id, status, context }) => {
// The update below saves this collection again, which runs this callback
// again: act on creates only, or it never stops.
if (status !== "new") return;
try {
await context.data.jobs.create({ title: values.title, status: "published" });
} catch (error) {
// Only the failed create is undone. The submission and this marker commit.
await context.data.job_submissions.update(id, {
promotion_status: "failed",
promotion_error: String(error)
});
}
}

Arbeiten, die die Datenbank verlassen müssen — eine E-Mail, ein Webhook, ein Aufruf einer Drittanbieter-API —, gehören nicht in den Callback-Body. Sie würden die Transaktion für einen Netzwerk-Roundtrip offenhalten, und nichts kann sie zurücknehmen, wenn der Schreibvorgang zurückgerollt wird. Stellen Sie dafür einen Job in die Warteschlange oder führen Sie die Aktion aus, nachdem der Schreibvorgang zurückgekehrt ist: Veröffentlichen Sie auf einem Realtime-Kanal oder verwenden Sie waitUntil in einer benutzerdefinierten Funktion. Unter Hooks erfahren Sie, was sich am besten eignet.

Bei MongoDB trifft nichts davon zu. Dieser Treiber führt dieselben Callbacks ohne Transaktion aus, sodass der Schreibvorgang bereits gespeichert ist, wenn afterSave ausgeführt wird, und ein dortiger Fehler lediglich das Scheitern meldet, ohne es rückgängig zu machen.

Unter Postgres läuft ein Schreibvorgang auf einer einzigen Verbindung, die aus dem Datenbank-Pool entnommen wird, und jeder Callback läuft darauf: beforeSave und beforeDelete, nachdem die Transaktion begonnen hat, afterSave und afterDelete, bevor sie committet. Solange ein Callback läuft — beim Abwarten einer Abfrage, einer Berechnung, irgendetwas —, bedient diese Verbindung nichts anderes, und die Sperren, die der Schreibvorgang genommen hat, bleiben genommen: in afterSave die gerade geschriebene Zeile; bei einem Bulk- oder _batch-Schreibvorgang jede Zeile, die die Transaktion zuvor geschrieben hat.

Die Kosten sind die eigene Dauer des Callbacks, und gleichzeitige Schreibvorgänge summieren sie:

  • Der Pool hält standardmäßig 20 Verbindungen (DB_POOL_MAX). 25 gleichzeitige Speichervorgänge, deren beforeSave 300 ms abwartete, ließen einen unbeteiligten Ein-Zeilen-Read 290 ms dauern, fast alles davon Warten auf eine Verbindung.
  • Ein Bulk-Create von 100 Zeilen, dessen beforeSave 50 ms abwartet, ist eine Transaktion, die 5,3 s offen ist und dabei ihre Verbindung und die Sperren jeder bisher geschriebenen Zeile die ganze Zeit hält.

Daher:

  • Halten Sie alles Langsame aus Callbacks heraus. Ein Aufruf eines anderen Dienstes gehört in einen Job oder nach der Rückkehr des Schreibvorgangs.
  • Schlagen Sie nach, was ein Hook braucht, bevor Sie schreiben, wenn möglich — im Client oder in einer benutzerdefinierten Funktion, die dann schreibt.
  • Senden Sie Zeilen, deren Hooks langsam sind, in kleineren Bulks.
  • Laufen viele langsame Hooks absichtlich gleichzeitig, erhöhen Sie DB_POOL_MAX, im Rahmen dessen, was max_connections der Datenbank für jede Instanz erlaubt.
  • Seit 0.25 Mit REBASE_METRICS=true meldet /metrics rebase_db_pool_wait_ms: wie lange jede Abfrage auf eine Verbindung wartete. Ein gesunder Pool hält ihn nahe null; ein Tail, der mit dem Traffic wächst, sind Anfragen, die auf Verbindungen warten, die Callbacks halten. Siehe Metriken.