Skip to content

Metacore

Recetario de addons

Recetas cortas para los patrones que aparecen al construir addons. Cada entrada es una pregunta, el snippet mínimo que funciona, y una nota de una línea sobre el por qué. Para contexto profundo de cualquier feature, seguí los links a manifest-spec.md y dynamic-ui.md.

Tabla de contenidos ​

¿Cómo agrego una relación foreign-key? ​

Declará ref en la columna. El host genera la constraint FOREIGN KEY y expone un endpoint /options/<model> que el relation picker del modal usa.

json
{
  "table_name": "ticket_comments",
  "columns": [
    { "name": "ticket_id", "type": "uuid", "required": true,
      "ref": "addon_tickets.tickets" }
  ]
}

El diálogo de edit va a renderizar un combobox buscable para ticket_id. Los targets pueden ser cross-schema (addon_<other>.<table>) o core (users, organizations).

¿Cómo hago una columna buscable? ​

Dos capas — filtrabilidad a nivel columna (manejada por metadata, surfaceada como un chip de filtro por columna en el toolbar) y búsqueda global (manejada por el input de texto libre del toolbar).

json
{
  "name": "title",
  "type": "string",
  "size": 255,
  "searchable": true,
  "filterable": true
}

searchable: true incluye la columna en la búsqueda global estilo ILIKE que el toolbar emite como ?search=. filterable: true hace que el kernel produzca un FilterDefinition para que los usuarios vean un botón de filtro por columna.

¿Cómo agrego una validación custom? ​

Para fields de acción, agregá validation (regex aplicado después de normalize):

json
"input_schema": [
  { "name": "rfc", "type": "string", "required": true,
    "normalize": "uppercase",
    "validation": "^[A-ZÑ&]{3,4}\\d{6}[A-Z0-9]{3}$" }
]

Para constraints de columna más allá de lo que expresa el manifest (NOT NULL, UNIQUE, length), validá en tu handler de acción / export WASM. Mantené las constraints de schema declarativas; mantené las reglas de negocio en código.

¿Cómo creo una acción custom con un modal? ​

Declará la acción bajo el modelo con fields[]:

json
"actions": {
  "tickets": [
    {
      "key": "reassign",
      "label": "Reassign",
      "icon": "UserPlus",
      "fields": [
        { "name": "assignee_id", "label": "New assignee", "type": "user", "required": true },
        { "name": "note", "label": "Note", "type": "text" }
      ]
    }
  ]
}

<DynamicTable> agrega "Reassign" al dropdown de fila. Clickearla dispara <ActionModalDispatcher>, que renderiza un modal con los inputs declarados y hace POST a /data/tickets/<id>/action/reassign. Cableá el lado servidor vía hooks (webhook), un export WASM, o un ActionInterceptor compilado — ver manifest-spec.md.

Para UI totalmente custom registrá un componente:

tsx
import { actionRegistry } from '@asteby/metacore-sdk'
actionRegistry.register('tickets', 'reassign', ReassignDialog)

El dispatcher rutea a tu componente cuando (model, action.key) coincide.

¿Cómo requiero un permiso para un botón? ​

Envolvé el affordance en <CapabilityGate>:

tsx
import { CapabilityGate } from '@asteby/metacore-runtime-react'

<CapabilityGate require="db:write addon_tickets.tickets">
  <Button onClick={createTicket}>New ticket</Button>
</CapabilityGate>

El kernel sigue enforciando la misma capability server-side — gatear UI es puramente una cortesía de UX. Ver dynamic-ui.md para los modos all / any / invert.

¿Cómo emito un evento cuando un registro cambia? ​

Declará la capability y el topic del evento:

json
"capabilities": [
  { "kind": "event:emit", "target": "ticket.*", "reason": "Notify state changes" }
],
"events": ["ticket.created", "ticket.resolved"]

En un webhook / export WASM, llamá la API de eventos del host con { topic: 'ticket.resolved', payload: {…} }. El kernel chequea la capability, persiste el evento, y lo distribuye a los suscriptores.

Para eventos automáticos en cada operación CRUD, usá lifecycle_hooks:

json
"lifecycle_hooks": {
  "tickets": [
    { "event": "after_create",
      "target": { "type": "webhook", "url": "/webhooks/ticket_created" },
      "async": true }
  ]
}

¿Cómo me suscribo a eventos de otro addon? ​

Declará la capability y traé eventos del bus en tu handler:

json
"capabilities": [
  { "kind": "event:subscribe", "target": "invoice.stamped" }
]

El addon publicador debe declarar events: ["invoice.stamped"] para que el host conozca el schema. Suscribite vía la API de bus del kernel en tu módulo WASM o vía un webhook que el kernel llama cuando el evento dispara.

¿Cómo muestro una UI distinta para create vs edit? ​

<DynamicRecordDialog> ya intercambia título y label de submit por mode. Si necesitás fields distintos, branchá en el call site y renderizá dos componentes distintos (o dos modelos de manifest — uno para el funnel de creación, otro para editar el registro persistido).

tsx
{mode === 'create'
  ? <FullCreationWizard onDone={refetch} />
  : <DynamicRecordDialog open mode="edit" model="tickets" recordId={id} />}

¿Cómo agrego una columna de soft-delete? ​

json
{
  "table_name": "tickets",
  "soft_delete": true,
  "columns": [ /* … */ ]
}

El host agrega una columna deleted_at timestamptz, la filtra fuera de las queries por defecto, y rutea DELETE /data/tickets/<id> a un UPDATE … SET deleted_at = now(). Sin cableado a nivel app requerido.

¿Cómo scopeo registros por organización? ​

json
{
  "table_name": "tickets",
  "org_scoped": true,
  "columns": [ /* … */ ]
}

El host agrega una columna organization_id, le pone NOT NULL + index, aplica una policy RLS de Postgres, y stampea la columna en insert. Las lecturas cross-tenant son negadas por el kernel incluso si una capability las permitiría de otro modo.

Para data regulada preferí tenant_isolation: "schema-per-tenant" — ver manifest-spec.md.

¿Cómo bundleo una extensión de frontend con mi addon? ​

Declará un entry de federación en el manifest:

json
"frontend": {
  "entry": "/api/metacore/addons/tickets/frontend/remoteEntry.js",
  "format": "federation",
  "expose": "./plugin",
  "container": "metacore_tickets"
}

Buildeá el frontend con @originjs/vite-plugin-federation y un name que matchee container. El módulo expuesto debe exportar register(api: AddonAPI), que recibe el SDK del host y registra contribuciones de slot, handlers de acción, items de navegación, etc.

tsx
// frontend/src/plugin.tsx
import type { AddonAPI } from '@asteby/metacore-sdk'

export function register(api: AddonAPI) {
  api.slot.register('dashboard.widgets', RevenueWidget, { priority: 10 })
  api.action.register('tickets', 'reassign', ReassignDialog)
  api.nav.add({ key: 'tickets', label: 'Tickets', to: '/m/tickets' })
}

El host lo carga vía <AddonLoader> desde @asteby/metacore-runtime-react.

¿Cómo testeo mi addon localmente? ​

bash
metacore validate         # checks estáticos: regex, semver, capabilities, defaults
metacore build --strict   # produce my-addon-0.1.0.tar.gz
metacore inspect *.tar.gz # imprime manifest + migraciones + tamaños del bundle

Corré un host con una referencia file: a tu directorio de addon y recargá — el kernel re-corre AutoMigrate en cada restart en dev. Los webhooks apuntando a http://localhost:7101/webhooks/... funcionan directo; para WASM, usá metacore compile-wasm para producir un backend/backend.wasm fresco antes de recargar.

¿Cómo precargo metadata al boot de la app? ​

tsx
import { useMetadataCache } from '@asteby/metacore-runtime-react'

function PrefetchMetadata() {
  const { prefetchAll } = useMetadataCache()
  const api = useApi()
  useEffect(() => { prefetchAll(api) }, [api])
  return null
}

prefetchAll emite un solo GET /metadata/all y siembra los caches de tabla y modal. Mounts subsiguientes de <DynamicTable> renderizan sin round-trip de red. El cache está namespaceado por metadataVersion — cuando el kernel lo bumpea, el cache se invalida automáticamente.

¿Cómo agrego una acción de dropdown de fila que linkea a otra página? ​

Declará una acción con type: "link" y un template linkUrl:

json
{
  "key": "open_invoice",
  "label": "Open invoice",
  "icon": "ExternalLink",
  "type": "link",
  "linkUrl": "/invoices/{invoice_id}"
}

Tokens como {invoice_id} son reemplazados con el valor de la fila antes de la navegación. <DynamicTable> reconoce type: "link" y usa el navigate() de TanStack Router del host en vez de abrir un modal.

¿Cómo gateo una acción según el estado actual de la fila? ​

Usá condition para esconder la acción del dropdown cuando la fila no matchea, y requiresState para asegurarlo en el servidor:

json
{
  "key": "resolve",
  "label": "Resolve",
  "icon": "CheckCircle2",
  "confirm": true,
  "condition": { "field": "status", "operator": "in", "value": ["open", "in_progress"] },
  "requiresState": ["open", "in_progress"]
}

condition filtra el dropdown del lado cliente (eq, neq, in, not_in); requiresState hace que el kernel rechace ejecuciones obsoletas donde el estado de la fila cambió entre el fetch y el click.


¿Tenés una receta para agregar? Mandá un PR — las recetas viven en este archivo como una lista plana, sin anidar.

Metacore es open-source. Apache-2.0.