CRUD dinámico
El kernel convierte manifest.models[] 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
El kernel monta un set fijo de rutas centradas en el modelo (el host las cablea bajo un prefijo — típicamente /api):
GET /api/metadata/table/:model
GET /api/metadata/modal/:model
GET /api/metadata/all
GET /api/dynamic/:model?page=&size=&sort=&filter=
GET /api/dynamic/:model/:id
POST /api/dynamic/:model
PUT /api/dynamic/:model/:id
DELETE /api/dynamic/:model/:id
POST /api/dynamic/:model/:id/actions/:key
GET /api/options/:model # lookups de opciones de select (el host opta vía MountOptions)
GET /api/search/:model # búsqueda typeaheadHay un solo set de código de handler en el kernel, sin importar cuántos addons o modelos existan.
Qué hace una llamada de listado
Un GET /api/dynamic/tickets?page=2&size=20&sort=created_at:desc&filter=status:open:
- Auth. El middleware del host ya seteó la identidad del request en el contexto.
- Resolución. El router parsea
model=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. Toda respuesta del kernel usa el envelope
{ "success": true, "data": ..., "meta": ... }— las filas endata, paginación + total count enmeta. Los errores son{ "success": false, "message": "<razón>" }. Los consumers TS leen.datapara el payload. - 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/dynamic/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, con namespace en el schema del addon (addon_<key>). Las migraciones corren dentro de una transacción; los installs fallidos se rollbackean atómicamente.
La mitad frontend
<DynamicTable> desde el manifest
import { DynamicTable } from '@asteby/metacore-runtime-react'
<DynamicTable model="tickets" />Lo que hace el componente:
- Llama a
GET /api/metadata/table/ticketsuna vez, lo cachea (useMetadataCache). - Construye una config de columnas desde la metadata: label del header, estilo de cell, sortability, filterability. El
getDynamicColumnspor defecto maneja cada estilo de cell que emite el kernel — badge, avatar, teléfono, fecha, booleano, badges de relación, galería de medios, imagen, más un fallback de texto. - Llama a
GET /api/dynamic/ticketscon el estado actual de page/sort/filter. - Renderiza la tabla — paginación, sorting, filtros multi-columna, acciones de fila, diálogos de create/edit/delete.
- Se suscribe a los eventos de cambio del modelo en el hub WebSocket; invalida la query en cada evento.
Nada de eso es código por modelo.
Personalizar sin escapar
<DynamicTable> acepta una prop getDynamicColumns — una función pura que convierte la metadata en ColumnDef[] de TanStack — para que extiendas o sobrescribas el rendering de cells por defecto:
import { DynamicTable, makeDefaultGetDynamicColumns } from '@asteby/metacore-runtime-react'
<DynamicTable
model="tickets"
getDynamicColumns={makeDefaultGetDynamicColumns({
overrides: { status: (col) => ({ ...col, cell: StatusBadge }) },
})}
/>Cuando necesitás acceso crudo, el runtime expone useApi (el cliente tipado sobre el envelope del kernel), useOptions / useOptionsResolver (lookups de select) y useMetadataCache; manejás tus propias llamadas con TanStack Query y te encargás del rendering.
Forms
<DynamicForm> es la contraparte de create/edit:
import { DynamicForm } from '@asteby/metacore-runtime-react'
<DynamicForm model="tickets" rowId={id} />Lee la misma metadata, construye un form desde las columnas del modelo, corre los mismos validadores del lado del cliente y envía al endpoint CRUD del kernel. Los inputs personalizados son pluggables por tipo, y podés registrar validadores extra vía registerValidator.
Dónde termina el CRUD dinámico
La capa de query ya hace más que list/get plano. Soporta filtros por relación, group_by, agregaciones y preloads de relaciones — suficiente para respaldar dashboards y vistas agrupadas sin handlers a medida. Algunos casos quedan deliberadamente fuera de scope:
- Joins arbitrarios entre addons. El store dinámico lee un modelo y sus relaciones declaradas; un join ad-hoc entre addons no relacionados es una action personalizada.
- Reportes totalmente a medida. Cuando un reporte necesita matemática de dominio más allá de lo que expresa la API de agregaciones, escribilo como action o endpoint del host.
- Layouts a medida. Cuando la UI dirigida por metadata pelea con el diseño, aportá 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.