Entitäts-Callbacks
Übersicht
Abschnitt betitelt „Übersicht“Callbacks ermöglichen es Ihnen, sich in den Entitätslebenszyklus einzuhängen, um:
- Daten zwischen Sammlungen synchronisieren — Entitäten bei Statusänderungen über Tabellen hinweg kopieren oder verschieben
- Daten transformieren vor dem Speichern (berechnete Felder, Slug-Erstellung)
- Geschäftsregeln validieren über die Schema-Validierung hinaus
- Nebeneffekte auslösen nach Schreibvorgängen (E-Mails senden, APIs synchronisieren, Caches aktualisieren)
- Daten filtern/transformieren nach dem Lesen
- Kaskadenoperationen — verwandte Datensätze beim Löschen bereinigen
Callbacks definieren
Abschnitt betitelt „Callbacks definieren“const articlesCollection: EntityCollection = { slug: "articles", callbacks: { beforeSave: async ({ values, entityId, status }) => { // Auto-generate slug from title if (values.title) { values.slug = values.title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); }
// Set timestamps if (status === "new") { values.created_at = new Date(); } values.updated_at = new Date();
return values; },
afterSave: async ({ values, entityId }) => { // Send notification console.log(`Article ${entityId} saved: ${values.title}`); },
beforeDelete: async ({ entityId }) => { // Prevent deletion of published articles // Throw to block the deletion },
afterRead: async ({ entity }) => { // Transform data after loading return entity; } }, properties: { /* ... */ }};Callback-Referenz
Abschnitt betitelt „Callback-Referenz“beforeSave
Abschnitt betitelt „beforeSave“Wird aufgerufen, bevor eine Entität in die Datenbank geschrieben wird. Geben Sie die geänderten Werte zurück.
beforeSave: async ({ values, // Entity values entityId, // Entity ID (null for new entities) status, // "new" | "existing" | "copy" previousValues, // Previous values (for updates) context // Full Rebase context}) => { // Return modified values return { ...values, updated_at: new Date() };}Werfen Sie einen Fehler, um das Speichern zu blockieren:
beforeSave: async ({ values }) => { if (values.price < 0) { throw new Error("Price cannot be negative"); } return values;}afterSave
Abschnitt betitelt „afterSave“Wird nach einem erfolgreichen Speichervorgang aufgerufen. Für Nebeneffekte verwenden.
afterSave: async ({ values, // Saved values entityId, // Entity ID previousValues, // Previous values (null for new entities) status, // "new" | "existing" | "copy" context}) => { // Send webhook await fetch("https://api.slack.com/webhook", { method: "POST", body: JSON.stringify({ text: `New article: ${values.title}` }) });}afterSaveError
Abschnitt betitelt „afterSaveError“Wird aufgerufen, wenn ein Speichervorgang fehlschlägt.
afterSaveError: async ({ values, entityId, error, context}) => { console.error("Save failed:", error);}afterRead
Abschnitt betitelt „afterRead“Wird nach dem Lesen von Entitäten aus der Datenbank aufgerufen. Transformieren Sie die Daten für die Anzeige.
afterRead: async ({ entity, // The entity to transform context}) => { // Add computed fields return { ...entity, values: { ...entity.values, displayName: `${entity.values.first_name} ${entity.values.last_name}` } };}beforeDelete
Abschnitt betitelt „beforeDelete“Wird aufgerufen, bevor eine Entität gelöscht wird. Werfen Sie einen Fehler, um das Löschen zu blockieren.
beforeDelete: async ({ entityId, entity, context}) => { if (entity.values.status === "published") { throw new Error("Cannot delete published articles. Unpublish first."); }}afterDelete
Abschnitt betitelt „afterDelete“Wird nach einem erfolgreichen Löschvorgang aufgerufen.
afterDelete: async ({ entityId, entity, context}) => { // Cleanup related data console.log(`Article ${entityId} deleted`);}Eigenschaften-Callbacks
Abschnitt betitelt „Eigenschaften-Callbacks“Sie können Callbacks auch auf Eigenschaftsebene für feldspezifische Transformationen definieren:
properties: { email: { type: "string", name: "Email", callbacks: { beforeSave: ({ value }) => value?.toLowerCase().trim(), afterRead: ({ value }) => value // Could decrypt, etc. } }}Die context.data API
Abschnitt betitelt „Die context.data API“Jeder Callback erhält ein context-Objekt, das context.data enthält – eine vereinheitlichte Datenschicht für die Durchführung von sammlungsübergreifenden Operationen innerhalb von Lebenszyklus-Hooks.
Auf Sammlungen zugreifen
Abschnitt betitelt „Auf Sammlungen zugreifen“context.data verwendet einen JavaScript Proxy, sodass Sie auf jede Sammlung über ihren Slug als Eigenschaft zugreifen können:
afterSave: async ({ values, entityId, 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);}Verfügbare Methoden
Abschnitt betitelt „Verfügbare Methoden“Jeder Sammlungs-Accessor (context.data.<slug>) bietet diese Methoden:
| 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 nach 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 | Eine Entität löschen |
.count() | count(params?: FindParams) → number | Übereinstimmende Entitäten zählen |
.listen() | listen(params, onUpdate, onError?) → unsubscribe | Echtzeit-Abonnement (wo unterstützt) |
.listenById() | listenById(id, onUpdate, onError?) → unsubscribe | Einer einzelnen Entität lauschen |
Abfragen mit .find()
Abschnitt betitelt „Abfragen mit .find()“Die find()-Methode unterstützt umfangreiche Filterung:
afterSave: async ({ values, context }) => { // Simple equality const { data: activeJobs } = await context.data.jobs.find({ where: { status: "published" }, limit: 10, orderBy: "created_at:desc" });
// PostgREST-style operators const { data: recentJobs } = await context.data.jobs.find({ where: { status: "eq.published", salary: "gte.50000" } });
// Tuple syntax const { data: expensiveJobs } = await context.data.jobs.find({ where: { salary: [">=", 100000], role: ["in", ["admin", "manager"]] } });}Entitäten erstellen
Abschnitt betitelt „Entitäten erstellen“afterSave: async ({ values, entityId, previousValues, context }) => { // Promote an approved submission to a published job if (values.status === "approved" && previousValues?.status !== "approved") { const newJob = await context.data.jobs.create({ title: values.title, description: values.description, company_id: values.company_id, status: "published", source_submission_id: entityId, });
// Link back to the original submission await context.data["job-submissions"].update(entityId, { promoted_job_id: newJob.id, }); }}Sicherheit: RLS-Bypass-Verhalten
Abschnitt betitelt „Sicherheit: RLS-Bypass-Verhalten“context.data-Operationen in Callbacks umgehen die Row Level Security (RLS).
Wenn Callbacks im Backend ausgeführt werden, laufen sie über den Basis-PostgresBackendDriver – nicht über den authentifizierten Wrapper. Das bedeutet, dass context.data vollen Datenbankzugriff hat, unabhängig von den Berechtigungen des auslösenden Benutzers.
Dies ist beabsichtigt: Server-seitige Lebenszyklus-Hooks sind vertrauenswürdiger Code, der oft in Sammlungen schreiben muss, auf die der Endbenutzer keinen direkten Zugriff hat (z. B. das Erstellen eines Audit-Log-Eintrags, das Aktualisieren eines Zählers in einem übergeordneten Datensatz).
Wenn Sie RLS-gebundene Operationen innerhalb eines Callbacks benötigen, verwenden Sie den authentifizierten Treiber direkt:
afterSave: async ({ context }) => { // Dies umgeht RLS (normal für Callbacks): await context.data.audit_logs.create({ action: "approved" });
// Um RLS zu erzwingen, greifen Sie auf den Treiber zu und rufen Sie withAuth() auf: const authDriver = await context.driver.withAuth(context.user); // authDriver.data-Operationen berücksichtigen RLS}Transaktionssemantik
Abschnitt betitelt „Transaktionssemantik“context.data-Operationen werden NICHT automatisch in dieselbe Transaktion eingeschlossen wie der auslösende Speichervorgang.
Der ursprüngliche Entitätsspeichervorgang schließt zuerst seine Datenbanktransaktion ab. Dann läuft afterSave, und alle context.data-Aufrufe öffnen separate Transaktionen. Wenn eine context.data-Operation in afterSave fehlschlägt, wird der ursprüngliche Speichervorgang nicht rückgängig gemacht.
Das bedeutet:
- ✅ Der auslösende Speichervorgang ist immer unabhängig erfolgreich
- ⚠️ Schreibvorgänge mit Nebeneffekten können fehlschlagen, ohne die ursprüngliche Operation zu beeinflussen
- ⚠️ Es gibt keine Atomizitätsgarantie zwischen dem ursprünglichen Speichervorgang und nachfolgenden
context.data-Aufrufen
Für Operationen, die atomar sein müssen, umwickeln Sie diese mit Fehlerbehandlung:
afterSave: async ({ values, entityId, context }) => { try { await context.data.jobs.create({ title: values.title, status: "published", }); } catch (error) { // Den Fehler protokollieren — der ursprüngliche Speichervorgang war bereits erfolgreich console.error(`Failed to promote job from submission ${entityId}:`, error); // Optional: Die Einreichung als "promotion_failed" markieren await context.data["job-submissions"].update(entityId, { promotion_status: "failed", promotion_error: String(error), }); }}Daten zwischen Sammlungen synchronisieren
Abschnitt betitelt „Daten zwischen Sammlungen synchronisieren“Eine der mächtigsten Anwendungen von Callbacks ist das Synchronisieren von Daten über Sammlungen hinweg mit context.data:
const submissionsCollection: EntityCollection = { slug: "job_submissions", callbacks: { afterSave: async ({ values, entityId, previousValues, context }) => { // Wenn eine Einreichung genehmigt wird, einen veröffentlichten Job erstellen if (values.status === "approved" && previousValues?.status !== "approved") { const newJob = await context.data.jobs.create({ title: values.title, description: values.description, company_id: values.company_id, status: "published", source_submission_id: entityId, });
// Die Einreichung mit der Referenz des beworbenen Jobs aktualisieren await context.data["job-submissions"].update(entityId, { promoted_job_id: newJob.id, }); } } }, properties: { /* ... */ }};Andere sammlungsübergreifende Muster:
- Kaskadierendes Löschen: Verwenden Sie
afterDelete, um verknüpfte Datensätze in Kind-Sammlungen zu entfernen - Denormalisierung: Verwenden Sie
afterSave, um Übersichtsfelder in einer übergeordneten Sammlung zu aktualisieren - Audit-Protokollierung: Verwenden Sie
afterSave/afterDelete, um in eine Audit-Log-Sammlung zu schreiben - Zähler: Verwenden Sie
afterSave/afterDelete, um Zählerfelder in verknüpften Entitäten zu aktualisieren
Vollständige Kontextreferenz
Abschnitt betitelt „Vollständige Kontextreferenz“Jeder Callback erhält ein context-Objekt vom Typ RebaseCallContext:
interface RebaseCallContext { /** Der authentifizierte Benutzer, falls vorhanden */ user?: User; /** Der zugrunde liegende Datentreiber (PostgresBackendDriver) */ driver: DataDriver; /** Vereinheitlichter Datenzugriff — context.data.<slug>.create/update/find/delete */ data: RebaseData;}Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- Sicherheitsregeln — Zeilenebene Sicherheit
- Entitätshistorie — Audit-Trail
- Benutzerdefinierte Funktionen — Benutzerdefinierte API-Endpunkte hinzufügen