Callbacks de Entidad
Resumen
Sección titulada «Resumen»Los callbacks le permiten integrar su lógica en el ciclo de vida de la entidad para:
- Sincronizar datos entre colecciones — copiar o mover entidades entre tablas en cambios de estado
- Transformar datos antes de guardar (campos calculados, slugificación)
- Validar reglas de negocio más allá de la validación de esquemas
- Disparar efectos secundarios después de las escrituras (enviar correos electrónicos, sincronizar APIs, actualizar cachés)
- Filtrar/transformar datos después de la lectura
- Operaciones en cascada — limpiar registros relacionados al eliminar
Definición de Callbacks
Sección titulada «Definición de Callbacks»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: { /* ... */ }};Referencia de Callbacks
Sección titulada «Referencia de Callbacks»beforeSave
Sección titulada «beforeSave»Se invoca antes de que una entidad sea escrita en la base de datos. Devuelve los valores modificados.
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() };}Lanza un error para bloquear la acción de guardar:
beforeSave: async ({ values }) => { if (values.price < 0) { throw new Error("Price cannot be negative"); } return values;}afterSave
Sección titulada «afterSave»Se invoca después de una operación de guardar exitosa. Utilícelo para efectos secundarios.
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
Sección titulada «afterSaveError»Se invoca cuando una operación de guardar falla.
afterSaveError: async ({ values, entityId, error, context}) => { console.error("Save failed:", error);}afterRead
Sección titulada «afterRead»Se invoca después de leer entidades de la base de datos. Transforma los datos para su visualización.
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
Sección titulada «beforeDelete»Se invoca antes de que una entidad sea eliminada. Lanza un error para bloquear la eliminación.
beforeDelete: async ({ entityId, entity, context}) => { if (entity.values.status === "published") { throw new Error("Cannot delete published articles. Unpublish first."); }}afterDelete
Sección titulada «afterDelete»Se invoca después de una eliminación exitosa.
afterDelete: async ({ entityId, entity, context}) => { // Cleanup related data console.log(`Article ${entityId} deleted`);}Callbacks de Propiedad
Sección titulada «Callbacks de Propiedad»También puede definir callbacks a nivel de propiedad para transformaciones específicas de campo:
properties: { email: { type: "string", name: "Email", callbacks: { beforeSave: ({ value }) => value?.toLowerCase().trim(), afterRead: ({ value }) => value // Could decrypt, etc. } }}La API context.data
Sección titulada «La API context.data»Cada callback recibe un objeto context que incluye context.data — una capa unificada de acceso a datos para realizar operaciones entre colecciones desde los hooks del ciclo de vida.
Acceso a Colecciones
Sección titulada «Acceso a Colecciones»context.data utiliza un Proxy de JavaScript, por lo que puede acceder a cualquier colección por su slug como una propiedad:
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);}Métodos Disponibles
Sección titulada «Métodos Disponibles»Cada accesor de colección (context.data.<slug>) proporciona estos métodos:
| Método | Firma | Descripción |
|---|---|---|
.find() | find(params?: FindParams) → FindResponse | Consulta entidades con filtros, ordenación y paginación |
.findById() | findById(id: string | number) → Entity | undefined | Obtiene una sola entidad por ID |
.create() | create(data: Partial<Values>, id?: string) → Entity | Crea una nueva entidad |
.update() | update(id: string | number, data: Partial<Values>) → Entity | Actualiza una entidad existente |
.delete() | delete(id: string | number) → void | Elimina una entidad |
.count() | count(params?: FindParams) → number | Cuenta las entidades coincidentes |
.listen() | listen(params, onUpdate, onError?) → unsubscribe | Suscripción en tiempo real (donde sea compatible) |
.listenById() | listenById(id, onUpdate, onError?) → unsubscribe | Escucha a una sola entidad |
Consultas con .find()
Sección titulada «Consultas con .find()»El método find() soporta filtrado avanzado:
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"]] } });}Creando Entidades
Sección titulada «Creando Entidades»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, }); }}Seguridad: Comportamiento de Omisión de RLS
Sección titulada «Seguridad: Comportamiento de Omisión de RLS»Las operaciones de context.data en los callbacks omiten la Seguridad a Nivel de Fila (RLS).
Cuando los callbacks se ejecutan en el backend, lo hacen a través del PostgresBackendDriver base — no el envoltorio autenticado. Esto significa que context.data tiene acceso completo a la base de datos independientemente de los permisos del usuario que lo activa.
Esto es intencional: los hooks del ciclo de vida del lado del servidor son código de confianza que a menudo necesita escribir en colecciones a las que el usuario final no tiene acceso directo (p. ej., crear una entrada de registro de auditoría, actualizar un contador en un registro padre).
Si necesita operaciones con ámbito RLS dentro de un callback, utilice el driver autenticado directamente:
afterSave: async ({ context }) => { // This bypasses RLS (normal for callbacks): await context.data.audit_logs.create({ action: "approved" });
// To enforce RLS, access the driver and call withAuth(): const authDriver = await context.driver.withAuth(context.user); // authDriver.data operations respect RLS}Semántica de Transacciones
Sección titulada «Semántica de Transacciones»Las operaciones de context.data NO se envuelven automáticamente en la misma transacción que el guardado que las activa.
El guardado de la entidad original completa primero su transacción de base de datos. Luego se ejecuta afterSave y cualquier llamada a context.data abre transacciones separadas. Si una operación de context.data falla en afterSave, el guardado original no se revierte.
Esto significa:
- ✅ El guardado que activa la operación siempre se realiza con éxito de forma independiente
- ⚠️ Las escrituras de efectos secundarios pueden fallar sin afectar la operación original
- ⚠️ No hay garantía de atomicidad entre el guardado original y las llamadas
context.datasubsiguientes
Para operaciones que deben ser atómicas, envuélvalas en manejo de errores:
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), }); }}Sincronización de Datos entre Colecciones
Sección titulada «Sincronización de Datos entre Colecciones»Uno de los usos más potentes de los callbacks es la sincronización de datos entre colecciones utilizando context.data:
const submissionsCollection: EntityCollection = { slug: "job_submissions", callbacks: { afterSave: async ({ values, entityId, previousValues, context }) => { // When a submission is approved, create 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, });
// Update the submission with the promoted job reference await context.data["job-submissions"].update(entityId, { promoted_job_id: newJob.id, }); } } }, properties: { /* ... */ }};Otros patrones entre colecciones:
- Eliminación en cascada: Utilice
afterDeletepara eliminar registros relacionados en colecciones secundarias - Desnormalización: Utilice
afterSavepara actualizar campos de resumen en una colección padre - Registro de auditoría: Utilice
afterSave/afterDeletepara escribir en una colección de registro de auditoría - Contadores: Utilice
afterSave/afterDeletepara actualizar campos de recuento en entidades relacionadas
Referencia Completa del Contexto
Sección titulada «Referencia Completa del Contexto»Cada callback recibe un objeto context de tipo RebaseCallContext:
interface RebaseCallContext { /** The authenticated user, if any */ user?: User; /** The underlying data driver (PostgresBackendDriver) */ driver: DataDriver; /** Unified data access — context.data.<slug>.create/update/find/delete */ data: RebaseData;}Próximos Pasos
Sección titulada «Próximos Pasos»- Reglas de Seguridad — Seguridad a Nivel de Fila
- Historial de Entidades — Registro de auditoría
- Funciones Personalizadas — Añadir endpoints de API personalizados