L'API context.data
Ogni callback riceve un oggetto context che include context.data — un livello unificato di accesso ai dati per eseguire operazioni tra collezioni dall’interno degli 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, 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);}Una proprietà è l’accessor della collection, il nome che l’SDK generato le dà:
context.data.myNotes è la collection my-notes, qualunque sia la tabella in cui è
archiviata. Un nome che non è l’accessor di nessuna collection viene letto come il suo snake_case,
quindi context.data.auditLog è audit_log.
Le righe non sono tipizzate (Record<string, unknown>) finché il progetto non registra il suo
Database generato; da quel momento ogni accessor è tipizzato a partire da esso. Vedi
Dati lato server tipizzati.
Metodi Disponibili
Sezione intitolata “Metodi Disponibili”Ogni funzione di accesso alla collezione (context.data.<slug>) fornisce questi metodi:
| Metodo | Firma | Descrizione |
|---|---|---|
.find() |
find(params?: FindParams) → FindResponse |
Interroga entità con filtri, ordinamento e paginazione |
.findById() |
findById(id: string | number) → Entity | undefined |
Recupera una singola entità per 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 record |
.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à |
.trash |
trash.find(), .count(), .restore(id), .restoreMany(ids), .purge(id), .purgeMany(ids) |
Le righe che un soft delete ha contrassegnato |
Interrogare con .find()
Sezione intitolata “Interrogare con .find()”Il metodo find() filtra con tuple [operatore, valore] — la forma tipizzata della stringa di query ?status=eq.published letta dall’API REST:
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"]] } });}Creazione di Entità
Sezione intitolata “Creazione di Entità”.create() e .update() prendono i valori da scrivere, con le firme indicate sopra. Sincronizzazione dei dati tra collezioni usa entrambi: una candidatura approvata crea un’offerta di lavoro pubblicata e viene ricollegata a essa.
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 si tratta di un livello di attendibilità fisso.
- Attivato da una richiesta utente (REST, realtime, una modifica dal pannello di amministrazione) → con ambito utente. Il callback viene eseguito all’interno della transazione vincolata a RLS aperta per quella richiesta, pertanto le policy si applicano sia alle letture che alle scritture. Un callback non può vedere una riga che il suo chiamante non potrebbe vedere.
- Attivato da
rebase.dataAsAdmino da un cron job (lo stesso singleton) → con ambito admin, non privo di ambito. Quel driver ha come ambito{ uid: "service", roles: ["admin"] }, quindi il callback viene comunque eseguito su una transazione vincolata a RLS — le tue policy vengono valutate a fronte di tale identità. - Attivato dal driver di base (flussi di autenticazione integrati, migrazioni) → senza ambito. Viene eseguito sulla connessione proprietaria e ignora RLS.
Questo ha un impatto notevole soprattutto nel caso che fallisce silenziosamente. RLS filtra, non solleva errori — quindi un callback che legge una riga di pari livello la troverà quando salva un’attività di amministrazione e potrebbe non trovare nulla quando salva un utente finale, senza alcun errore in entrambi i casi. Scrivi callback che tollerano un risultato vuoto, oppure ricorri deliberatamente al piano di amministrazione:
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" });}Semantica delle Transazioni
Sezione intitolata “Semantica delle Transazioni”Le scritture di context.data di un callback fanno parte della scrittura che lo ha attivato. Su Postgres, beforeSave, il salvataggio e afterSave — oppure beforeDelete, l’eliminazione e afterDelete — vengono eseguiti all’interno di un’unica transazione, ogni callback viene atteso prima del commit e context.data scrive tramite quella stessa transazione.
Di conseguenza, la scrittura scatenante e tutto ciò che i suoi callback hanno scritto vengono confermati insieme oppure per niente:
- Un errore lanciato da
afterSaveoafterDeleteannulla la scrittura scatenante, insieme a ogni scrittura dicontext.dataeffettuata dai callback. Al chiamante viene restituito 400CALLBACK_REJECTEDcondetails.stageche indica il nome dell’hook — o con lo stato stesso dell’errore quando ne include uno: unRebaseApiErrorche hai lanciato, il 409 per violazione di unicità. - I sottoscrittori realtime ricevono la notifica della riga solo dopo il commit, pertanto una scrittura che ha subito un rollback non viene mai annunciata.
- Un callback mantiene aperta la transazione durante la sua esecuzione, quindi un callback lento equivale a un blocco trattenuto e a una connessione del pool impegnata —
beforeSaveebeforeDeletetanto quanto gli hookafter. Vedi Un callback tiene una connessione. - Una scrittura di
context.dataesegue anche i callback della collezione di destinazione, quindi unafterSaveche aggiorna la propria riga esegue nuovamente se stesso. Le scritture annidate per più di 16 livelli vengono rifiutate con 500CALLBACK_RECURSION, indicando l’hook e la collezione, e l’intera scrittura subisce un rollback. Rendi condizionale una scrittura di questo tipo, come fa l’esempio sotto.
Lascia che un fallimento generi un’eccezione quando la scrittura scatenante non deve sopravvivere ad esso. Intercettalo quando invece deve sopravvivere, ma solo attorno a una scrittura di context.data: una creazione, un aggiornamento o un’eliminazione che il database rifiuta (una violazione di unicità o di chiave esterna, un trigger) viene annullata da sola, e il resto viene salvato tramite commit.
Qualsiasi altra istruzione che fallisce sulla transazione della scrittura — una ricerca, la lettura che un aggiornamento o un’eliminazione effettua per trovare la propria riga (un id che la colonna chiave non può contenere), l’accodamento di un job rifiutato dal database — interrompe quella transazione in Postgres, e intercettare l’errore in JavaScript non lo annulla. La scrittura viene rifiutata con 500 TRANSACTION_ABORTED e non viene memorizzato nulla, anziché rispondere con un successo per una scrittura che ha subito un rollback. Lascia che un fallimento di questo tipo generi un’eccezione, oppure verifica la condizione prima di eseguire l’istruzione.
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) }); }}Le operazioni che devono uscire dal database — un’email, un webhook, una chiamata a un’API di terze parti — non appartengono al corpo del callback. Manterrebbero la transazione aperta per un tempo di andata e ritorno sulla rete, e nulla potrebbe annullarle in caso di rollback della scrittura. Accoda un job a questo scopo, oppure eseguile dopo che la scrittura ha restituito la risposta: pubblica su un canale realtime, oppure usa waitUntil in una funzione personalizzata. La sezione Hook descrive quale soluzione sia più adatta.
Su MongoDB nulla di tutto questo è valido. Quel driver esegue gli stessi callback senza una transazione, pertanto la scrittura è già archiviata quando viene eseguito afterSave, e un’eccezione generata lì segnala il fallimento senza annullarla.
Un callback tiene una connessione
Sezione intitolata “Un callback tiene una connessione”Su Postgres una scrittura viene eseguita su una connessione prelevata dal pool del database,
e ogni callback viene eseguito su di essa: beforeSave e beforeDelete dopo che la
transazione è iniziata, afterSave e afterDelete prima del suo commit. Per tutto il
tempo in cui un callback è in esecuzione — in attesa di una ricerca, di un calcolo, di qualsiasi cosa — quella
connessione non serve nient’altro, e i lock che la scrittura ha preso restano presi:
in afterSave, la riga appena scritta; in una scrittura bulk o _batch,
ogni riga che la transazione ha scritto prima.
Il costo è la durata propria del callback, e le scritture concorrenti la sommano:
- Il pool contiene 20 connessioni per impostazione predefinita (
DB_POOL_MAX). 25 salvataggi contemporanei il cuibeforeSaveattendeva 300 ms hanno fatto durare 290 ms una lettura di una riga non correlata, quasi tutti spesi ad attendere una connessione. - Una creazione bulk di 100 righe il cui
beforeSaveattende 50 ms è una transazione aperta per 5,3 s, che tiene la sua connessione e i lock di ogni riga scritta finora per tutto quel tempo.
Quindi:
- Tieni fuori dai callback tutto ciò che è lento. Una chiamata a un altro servizio appartiene a un job, o a dopo il ritorno della scrittura.
- Cerca ciò di cui un hook ha bisogno prima della scrittura quando puoi — nel client, o in una funzione personalizzata che poi scrive.
- Invia le righe i cui hook sono lenti in bulk più piccoli.
- Se molti hook lenti sono eseguiti insieme per progetto, alza
DB_POOL_MAX, entro quanto ilmax_connectionsdel database consente per ogni istanza. - Da 0.25 Con
REBASE_METRICS=true,/metricsriportarebase_db_pool_wait_ms: quanto ha atteso ogni query per una connessione. Un pool sano lo tiene vicino a zero; una coda che cresce con il traffico sono richieste in attesa di connessioni che i callback tengono. Vedi Metriche.