Zum Inhalt springen

Entitäts-Callbacks

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
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: { /* ... */ }
};

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;
}

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}` })
});
}

Wird aufgerufen, wenn ein Speichervorgang fehlschlägt.

afterSaveError: async ({
values,
entityId,
error,
context
}) => {
console.error("Save failed:", error);
}

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}`
}
};
}

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.");
}
}

Wird nach einem erfolgreichen Löschvorgang aufgerufen.

afterDelete: async ({
entityId,
entity,
context
}) => {
// Cleanup related data
console.log(`Article ${entityId} deleted`);
}

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.
}
}
}

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.

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);
}

Jeder Sammlungs-Accessor (context.data.<slug>) bietet diese Methoden:

MethodeSignaturBeschreibung
.find()find(params?: FindParams) → FindResponseEntitäten mit Filtern, Sortierung und Paginierung abfragen
.findById()findById(id: string | number) → Entity | undefinedEine einzelne Entität nach ID abrufen
.create()create(data: Partial<Values>, id?: string) → EntityEine neue Entität erstellen
.update()update(id: string | number, data: Partial<Values>) → EntityEine bestehende Entität aktualisieren
.delete()delete(id: string | number) → voidEine Entität löschen
.count()count(params?: FindParams) → numberÜbereinstimmende Entitäten zählen
.listen()listen(params, onUpdate, onError?) → unsubscribeEchtzeit-Abonnement (wo unterstützt)
.listenById()listenById(id, onUpdate, onError?) → unsubscribeEiner einzelnen Entität lauschen

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"]]
}
});
}
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,
});
}
}

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
}

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),
});
}
}

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

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;
}