Callback delle Entità
Overview
Sezione intitolata “Overview”I callback ti consentono di agganciarti al ciclo di vita dell’entità per:
- Sincronizzare i dati tra collezioni — copiare o spostare entità tra tabelle in base ai cambiamenti di stato
- Trasformare i dati prima del salvataggio (campi calcolati, slugificazione)
- Validare le regole di business oltre la validazione dello schema
- Attivare effetti collaterali dopo le scritture (inviare email, sincronizzare API, aggiornare cache)
- Filtrare/trasformare i dati dopo la lettura
- Operazioni a cascata — pulire i record correlati in caso di eliminazione
Definire i Callback
Sezione intitolata “Definire i Callback”import type { PostgresCollectionConfig } from "@rebasepro/types";
// The row shape. Without it every `values.x` below is `unknown`.type Article = { title: string; slug: string; createdAt: string; updatedAt: string;};
const articlesCollection: PostgresCollectionConfig<Article> = { slug: "articles", name: "Articles", table: "articles", properties: { title: { name: "Title", type: "string" }, slug: { name: "Slug", type: "string" }, createdAt: { name: "Created at", type: "string" }, updatedAt: { name: "Updated at", type: "string" } }, callbacks: { beforeSave: async ({ values, id, 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.createdAt = new Date().toISOString(); } values.updatedAt = new Date().toISOString();
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: { /* ... */ }});Riferimento ai Callback
Sezione intitolata “Riferimento ai Callback”beforeSave
Sezione intitolata “beforeSave”Chiamato prima che un’entità venga scritta nel database. Restituisce i valori modificati.
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, updatedAt: new Date() };}Lancia un errore per bloccare il salvataggio:
beforeSave: async ({ values }) => { if (values.price < 0) { throw new Error("Price cannot be negative"); } return values;}afterSave
Sezione intitolata “afterSave”Chiamato dopo un salvataggio riuscito. Utilizzare per effetti collaterali.
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
Sezione intitolata “afterSaveError”Chiamato quando un’operazione di salvataggio fallisce.
afterSaveError: async ({ values, entityId, error, context}) => { console.error("Save failed:", error);}afterRead
Sezione intitolata “afterRead”Chiamato dopo aver letto le entità dal database. Trasforma i dati per la visualizzazione.
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
Sezione intitolata “beforeDelete”Chiamato prima che un’entità venga eliminata. Lancia un errore per bloccare l’eliminazione.
beforeDelete: async ({ entityId, entity, context}) => { if (entity.values.status === "published") { throw new Error("Cannot delete published articles. Unpublish first."); }}afterDelete
Sezione intitolata “afterDelete”Chiamato dopo un’eliminazione riuscita.
afterDelete: async ({ entityId, entity, context}) => { // Cleanup related data console.log(`Article ${entityId} deleted`);}Callback delle Proprietà
Sezione intitolata “Callback delle Proprietà”Puoi anche definire callback a livello di proprietà per trasformazioni specifiche del campo:
properties: { email: { type: "string", name: "Email", callbacks: { beforeSave: ({ value }) => value?.toLowerCase().trim(), afterRead: ({ value }) => value // Could decrypt, etc. } }}L’API context.data
Sezione intitolata “L’API context.data”Ogni callback riceve un oggetto context che include context.data — un livello di accesso ai dati unificato per eseguire operazioni tra collezioni all’interno dei hook del ciclo di vita.
Accesso alle Collezioni
Sezione intitolata “Accesso alle Collezioni”context.data utilizza un Proxy JavaScript, quindi puoi accedere a qualsiasi collezione tramite il suo slug come proprietà:
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);}Metodi Disponibili
Sezione intitolata “Metodi Disponibili”Ogni accessor di collezione (context.data.<slug>) fornisce questi metodi:
| Metodo | Firma | Descrizione |
|---|---|---|
.find() |
find(params?: FindParams) → FindResponse |
Interroga le entità con filtri, ordinamento e paginazione |
.findById() |
findById(id: string | number) → Entity | undefined |
Recupera una singola entità tramite ID |
.create() |
create(data: Partial<Values>, id?: string) → Entity |
Crea una nuova entità |
.update() |
update(id: string | number, data: Partial<Values>) → Entity |
Aggiorna un’entità esistente |
.delete() |
delete(id: string | number) → void |
Elimina un’entità |
.count() |
count(params?: FindParams) → number |
Conta le entità corrispondenti |
.listen() |
listen(params, onUpdate, onError?) → unsubscribe |
Sottoscrizione in tempo reale (dove supportato) |
.listenById() |
listenById(id, onUpdate, onError?) → unsubscribe |
Ascolta una singola entità |
Interrogare con .find()
Sezione intitolata “Interrogare con .find()”Il metodo find() supporta il filtraggio avanzato:
afterSave: async ({ values, context }) => { // Simple equality const { data: activeJobs } = await context.data.jobs.find({ where: { status: "published" }, limit: 10, orderBy: ["createdAt", "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"]] } });}Creare Entità
Sezione intitolata “Creare Entità”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, }); }}Sicurezza: con quali privilegi viene eseguito context.data
Sezione intitolata “Sicurezza: con quali privilegi viene eseguito context.data”context.data eredita i privilegi di ciò che ha attivato il callback. Non è un livello di fiducia fisso.
- Attivato da una richiesta utente (REST, realtime, una modifica nel pannello di amministrazione) → con ambito utente. Il callback viene eseguito all’interno della transazione vincolata da RLS aperta per quella richiesta, quindi le policy si applicano sia alle letture sia alle scritture. Un callback non può vedere una riga che il suo chiamante non poteva vedere.
- Attivato da
rebase.dataAsAdmino da un job cron (lo stesso singleton) → con ambito amministratore, non senza ambito. Quel driver è limitato a{ uid: "service", roles: ["admin"] }, quindi il callback viene comunque eseguito in una transazione vincolata da RLS: le tue policy vengono valutate, rispetto a quell’identità. - Attivato dal driver base (i flussi di autenticazione integrati, le migrazioni) → senza ambito. Viene eseguito sulla connessione proprietaria e bypassa RLS.
Questo conta soprattutto nella direzione che fallisce in silenzio. RLS filtra, non solleva errori — quindi un callback che legge una riga adiacente la troverà quando salva un’attività amministrativa e potrebbe non trovare nulla quando salva un utente finale, senza errori in nessuno dei due casi. Scrivi callback che tollerino un risultato vuoto, oppure ricorri deliberatamente al piano di amministrazione:
afterSave: async ({ context }) => { // Con ambito utente quando è un utente ad aver attivato questo salvataggio: // RLS si applica. await context.data.audit_logs.create({ action: "approved" });
// Ambito amministratore deliberato — per lavoro che il chiamante davvero // non deve vedere, come un log di audit che non può leggere né modificare. // Attenzione: è la portata di un amministratore, non un bypass di RLS: una // collection la cui unica regola è `policy.serverContext()` resta chiusa // anche a lui, perché quella compila in `rebase.uid() IS NULL` e l'uid di // questo accessor è `service`. await context.client.dataAsAdmin.audit_logs.create({ action: "approved" });}Semantica delle Transazioni
Sezione intitolata “Semantica delle Transazioni”Le operazioni context.data NON sono automaticamente incluse nella stessa transazione del salvataggio che le attiva.
Il salvataggio originale dell’entità completa prima la sua transazione di database. Quindi afterSave viene eseguito e qualsiasi chiamata context.data apre transazioni separate. Se un’operazione context.data fallisce in afterSave, il salvataggio originale non viene annullato.
Questo significa:
- ✅ Il salvataggio che attiva l’operazione ha sempre successo indipendentemente
- ⚠️ Le scritture con effetti collaterali potrebbero fallire senza influenzare l’operazione originale
- ⚠️ Non c’è garanzia di atomicità tra il salvataggio originale e le successive chiamate a
context.data
Per operazioni che devono essere atomiche, avvolgile nella gestione degli errori:
afterSave: async ({ values, entityId, context }) => { try { await context.data.jobs.create({ title: values.title, status: "published", }); } catch (error) { // Log the failure — the original save already succeeded console.error(`Failed to promote job from submission ${entityId}:`, error); // Optionally: mark the submission as "promotion_failed" await context.data["job-submissions"].update(entityId, { promotion_status: "failed", promotion_error: String(error), }); }}Sincronizzazione dei Dati tra Collezioni
Sezione intitolata “Sincronizzazione dei Dati tra Collezioni”Uno degli usi più potenti dei callback è la sincronizzazione dei dati tra collezioni utilizzando context.data:
import type { PostgresCollectionConfig } from "@rebasepro/types";
type Submission = { title: string; description: string; company_id: string; status: string; promoted_job_id: string;};
const submissionsCollection: PostgresCollectionConfig<Submission> = { slug: "job_submissions", name: "Job Submissions", table: "job_submissions", properties: { title: { name: "Title", type: "string" }, description: { name: "Description", type: "string" }, company_id: { name: "Company", type: "string" }, status: { name: "Status", type: "string" }, promoted_job_id: { name: "Promoted job", type: "string" } }, callbacks: { afterSave: async ({ values, id, previousValues, context }) => { // When a submission is approved, create a published job if (values.status === "approved" && previousValues?.status !== "approved") { const newJob = await context.data.collection<Record<string, unknown>>("jobs").create({ title: values.title, description: values.description, company_id: values.company_id, status: "published", source_submission_id: id, });
// Update the submission with the promoted job reference await context.data["job-submissions"].update(entityId, { promoted_job_id: newJob.id, }); } } }, properties: { /* ... */ }});Altri pattern tra collezioni:
- Eliminazione a cascata: Usa
afterDeleteper rimuovere i record correlati nelle collezioni figlie - Denormalizzazione: Usa
afterSaveper aggiornare i campi riepilogativi in una collezione padre - Registrazione audit: Usa
afterSave/afterDeleteper scrivere in una collezione di log di audit - Contatori: Usa
afterSave/afterDeleteper aggiornare i campi contatore sulle entità correlate
Riferimento Completo al Contesto
Sezione intitolata “Riferimento Completo al Contesto”Ogni callback riceve un oggetto context di tipo RebaseCallContext:
interface RebaseCallContext { /** L'utente autenticato, se presente */ user?: User; /** Il driver dati sottostante (PostgresBackendDriver) */ driver: DataDriver; /** Accesso dati unificato — context.data.<slug>.create/update/find/delete */ data: RebaseData;}Prossimi Passi
Sezione intitolata “Prossimi Passi”- Regole di Sicurezza — Sicurezza a Livello di Riga
- Cronologia delle Entità — Traccia di audit
- Funzioni Personalizzate — Aggiungere endpoint API personalizzati
