CRUD dinámico
El kernel convierte manifest.tables[] en una capa REST + UI funcional sin código por tabla. Esta página explica cómo, desde ambos lados — qué hace el runtime en el backend, y qué hace el SDK en el frontend.
Por qué merece una página de concepto
CRUD es un problema resuelto a nivel de request-handler — pero resolverlo una vez por tabla se vuelve tedioso rápido. CRUD dinámico significa:
- El runtime lee la metadata al arranque (o después de cada install).
- Monta un store + handler genérico que sabe servir cualquier tabla que la metadata describa.
- El frontend lee la misma metadata y renderiza forms / tablas que matchean.
- Agregar una columna no requiere código en ninguno de los dos lados.
Este es el leverage central que provee Metacore. Cualquier otra pieza — actions, slots, lifecycle — extiende este loop.
La mitad backend
Qué se monta
Para cada addon, el kernel monta un set fijo de rutas:
GET /api/addons/:id/_meta/columns
GET /api/addons/:id/_meta/columns/:table
GET /api/addons/:id/:table?page=&size=&sort=&filter=
GET /api/addons/:id/:table/:rowId
POST /api/addons/:id/:table
PATCH /api/addons/:id/:table/:rowId
DELETE /api/addons/:id/:table/:rowId
POST /api/addons/:id/_actions/:actionIdHay un solo set de código de handler en el kernel, sin importar cuántos addons o tablas existan.
Qué hace una llamada de listado
Un GET /api/addons/tickets/tickets?page=2&size=20&sort=created_at:desc&filter=status:open:
- Auth. El middleware del host ya seteó
kernel.Identityen el contexto. - Resolución. El router parsea
addon=tickets,table=tickets. - Chequeo de capability. ¿El addon
ticketstienedb:readsobretickets? Si no, 403. - Chequeo de permission. ¿El usuario tiene
tickets.view? Si no, 403. - Scope de tenancy. El store filtra por
org_id = ctx.OrgIDautomáticamente (multi-tenant por defecto). - Build de query. Filter y sort se validan contra el schema de columnas del manifest; columnas desconocidas se rechazan.
- Ejecución. Una sola sentencia SQL contra la base de datos del addon.
- Serialización. Filas + cursor de paginación + total count, JSON.
- Audit (opcional). La llamada se loguea vía el hook de audit del kernel.
Los pasos 3 y 4 son las puertas que hacen a Metacore seguro por defecto. Los pasos 5 y 6 son la razón por la que no escribís WHERE org_id = ? AND ... en tus handlers.
Qué hace una escritura
POST /api/addons/tickets/tickets con {"title":"...", "status":"open"}:
- Auth + resolución como arriba.
- Chequeo de capability. ¿
db:writesobretickets? - Chequeo de permission. ¿
tickets.create? - Validación. El payload se chequea contra el schema de columnas del manifest: tipos, required, max length, valores enum, regex.
- Validadores personalizados. Cualquier validador del lado Go registrado en el addon corre.
- Insert. Dentro de una transacción; los campos de tenancy se autopopulan.
- Emisión de evento. El runtime publica
tickets.changeden el hub WebSocket. - Respuesta. La fila creada, incluyendo campos generados por el server.
Updates y deletes siguen la misma forma con los chequeos específicos del verbo correspondiente.
Dónde viven los datos
Las tablas del addon son tablas físicas en la base de datos del host. Cada tabla tiene namespace por addon ID a nivel schema o nombre de tabla (configurable). Las migraciones corren dentro de una transacción; los installs fallidos se rollbackean atómicamente.
La mitad frontend
<DynamicTable> desde el manifest
<DynamicTable addon="tickets" table="tickets" />Lo que hace el componente:
- Llama a
GET /api/addons/tickets/_meta/columns/ticketsuna vez, lo cachea. - Construye una config de columnas desde la metadata: label del header, tipo de cell, sortability, filterability.
- Llama a
GET /api/addons/tickets/ticketscon el estado actual de page/sort/filter. - Renderiza la tabla.
- Se suscribe a
tickets.changeden el hub WebSocket; invalida la query en cada evento.
Conseguís paginación, sorting, filtros multi-columna, click-en-fila-para-editar, botón de crear, confirmación de delete y updates en tiempo real. Nada de eso es código por tabla.
Personalizar sin escapar
<DynamicTable> acepta overrides:
<DynamicTable
addon="tickets"
table="tickets"
columns={{
status: { renderCell: (v) => <Badge tone={v}>{v}</Badge> },
title: { width: '40%' },
}}
onRowClick={(row) => navigate(`/tickets/${row.id}`)}
toolbar={<MyExtraButtons />}
/>Cuando los overrides no alcanzan, bajá a la capa de hooks:
import { useDynamicQuery, useDynamicMutation } from '@asteby/metacore-runtime-react'
const { rows, isLoading, page, setPage } = useDynamicQuery({
addon: 'tickets',
table: 'tickets',
filter: { status: 'open' },
sort: [{ col: 'priority', dir: 'desc' }],
})
const update = useDynamicMutation({ addon: 'tickets', table: 'tickets', op: 'update' })Los hooks manejan paginación, invalidación en tiempo real, updates optimistas y gating por permission; vos te encargás del rendering.
Forms
<DynamicForm> es la contraparte de create/edit:
<DynamicForm addon="tickets" table="tickets" rowId={id} />Lee la misma metadata, construye un form desde columns[], corre los mismos validadores del lado del cliente y envía al endpoint CRUD del kernel. Los inputs personalizados son pluggables por tipo (p.ej. un editor de rich-text para columnas text).
Dónde termina el CRUD dinámico
Algunos casos están deliberadamente fuera de scope:
- Joins entre addons. El store dinámico lee una tabla a la vez. Los joins se denormalizan en el manifest o se implementan como una action personalizada.
- Agregaciones. Reportes y dashboards los define el addon, no se autogeneran.
- Layouts a medida. Cuando la UI dirigida por metadata pelea con el diseño, montá un slot personalizado en su lugar.
Estos son los límites correctos — el CRUD dinámico es una solución del 80%; el 20% restante es donde los addons se ganan el sueldo.
Relacionado
- Manifest — el schema que lee el CRUD dinámico.
- Permisos — las puertas en cada llamada de CRUD.
- SDK docs / DynamicTable ↗ — cada prop, cada hook.
- Kernel docs / dynamic store ↗ — los internals del backend.