i18n - docs translations (#21789)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
22baf2c6c5
commit
2b3b2362db
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Configuración de la aplicación
|
||||
description: Declara la identidad de tu aplicación, el rol predeterminado, las variables y los metadatos del marketplace con defineApplication.
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
Cada aplicación debe tener exactamente una llamada a `defineApplication`. Declara:
|
||||
|
||||
* **Identidad** — identificador universal, nombre para mostrar, descripción.
|
||||
* **Permisos** — bajo qué rol se ejecutan sus funciones de lógica y componentes de frontend.
|
||||
* **Variables** *(opcionales)* — pares clave–valor expuestos a tu código como variables de entorno.
|
||||
* **Hooks de preinstalación / postinstalación** *(opcionales)* — consulta [Funciones de lógica](/l/es/developers/extend/apps/logic/logic-functions).
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Notas:
|
||||
|
||||
* Los campos `universalIdentifier` son identificadores deterministas que te pertenecen. Genéralos una vez y mantenlos estables entre sincronizaciones.
|
||||
* `applicationVariables` se convierten en variables de entorno para tus funciones y componentes de frontend. En las funciones lógicas (del lado del servidor), están disponibles como `process.env.VARIABLE_NAME`. En los componentes de frontend, usa `getApplicationVariable('VARIABLE_NAME')` de `twenty-sdk/front-component`. Las variables marcadas con `isSecret: true` solo se inyectan en las funciones lógicas. Los componentes de frontend solo reciben variables no secretas.
|
||||
* El rol predeterminado se detecta automáticamente a partir del archivo de rol marcado con [`defineApplicationRole()`](/l/es/developers/extend/apps/config/roles); no necesitas hacer referencia a él desde `defineApplication()`.
|
||||
* Las funciones de preinstalación y posinstalación se detectan automáticamente durante la compilación del manifiesto; no necesitas referenciarlas en `defineApplication()`.
|
||||
* Pasar `defaultRoleUniversalIdentifier` explícitamente sigue siendo compatible por motivos de retrocompatibilidad, pero está en desuso en favor de `defineApplicationRole()`.
|
||||
|
||||
## Rol de función predeterminado
|
||||
|
||||
El rol declarado con [`defineApplicationRole()`](/l/es/developers/extend/apps/config/roles) controla a qué pueden acceder las funciones de lógica y los componentes de interfaz de la aplicación:
|
||||
|
||||
* El token en tiempo de ejecución inyectado como `TWENTY_APP_ACCESS_TOKEN` se deriva de este rol.
|
||||
* El cliente de API tipado está restringido a los permisos otorgados a ese rol.
|
||||
* Sigue el principio de mínimo privilegio: declara solo los permisos que necesitan tus funciones.
|
||||
|
||||
Cuando generas una nueva aplicación, la CLI crea un archivo de rol inicial en `src/roles/default-role.ts`. Consulta [Roles y permisos](/l/es/developers/extend/apps/config/roles) para obtener la referencia completa.
|
||||
|
||||
## Metadatos del Marketplace
|
||||
|
||||
Si planeas [publicar tu aplicación](/l/es/developers/extend/apps/operations/publishing), estos campos opcionales controlan cómo aparece en el marketplace:
|
||||
|
||||
| Campo | Descripción |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `author` | Nombre del autor o de la empresa |
|
||||
| `category` | Categoría de la aplicación para el filtrado en el marketplace |
|
||||
| `logoUrl` | Ruta al logotipo de tu aplicación (p. ej., `public/logo.png`) |
|
||||
| `screenshots` | Arreglo de rutas de capturas de pantalla (p. ej., `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | Descripción en Markdown más extensa para la pestaña "Acerca de". Si se omite, el marketplace utiliza el `README.md` del paquete en npm |
|
||||
| `websiteUrl` | Enlace a tu sitio web |
|
||||
| `termsUrl` | Enlace a los términos del servicio |
|
||||
| `emailSupport` | Dirección de correo electrónico de soporte |
|
||||
| `issueReportUrl` | Enlace al rastreador de incidencias |
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Hooks de instalación
|
||||
description: "Ejecuta lógica antes o después de la instalación: introduce datos iniciales, haz copias de seguridad de los registros, valida la actualización."
|
||||
icon: llave inglesa
|
||||
---
|
||||
|
||||
Los hooks de instalación son funciones de lógica especiales que se ejecutan durante el ciclo de vida de la instalación o actualización. Comparten el mismo tiempo de ejecución del controlador que las [logic functions](/l/es/developers/extend/apps/logic/logic-functions) normales y reciben un `InstallPayload`, pero se declaran con sus propias funciones de definición — `definePostInstallLogicFunction()` y `definePreInstallLogicFunction()` — y están fuera del modelo de desencadenadores normal (HTTP, cron, eventos de base de datos).
|
||||
|
||||
Cada aplicación puede definir **como máximo una función de preinstalación** y **como máximo una función de posinstalación**. La compilación del manifiesto generará un error si se detecta más de una de cualquiera de las dos.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Se ejecuta después de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
|
||||
Una función de posinstalación se ejecuta automáticamente una vez que tu aplicación ha terminado de instalarse en un espacio de trabajo. El servidor la ejecuta **después** de que se hayan sincronizado los metadatos de la aplicación y se haya generado el cliente del SDK, de modo que el espacio de trabajo esté completamente listo para usarse y el nuevo esquema esté disponible. Los casos de uso típicos incluyen poblar datos predeterminados, crear registros iniciales, configurar los ajustes del espacio de trabajo o aprovisionar recursos en servicios de terceros.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
También puedes ejecutar manualmente la función de posinstalación en cualquier momento usando la CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* Las funciones de posinstalación usan `definePostInstallLogicFunction()` — una variante especializada que omite la configuración de desencadenadores (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
* El controlador recibe un `InstallPayload` con `{ previousVersion?: string; newVersion: string }` — `newVersion` es la versión que se está instalando, y `previousVersion` es la versión que se instaló previamente (o `undefined` en una instalación nueva). Use estos valores para distinguir instalaciones nuevas de actualizaciones y para ejecutar lógica de migración específica de la versión.
|
||||
* **Cuándo se ejecuta el hook**: solo en instalaciones nuevas, de forma predeterminada. Pase `shouldRunOnVersionUpgrade: true` si también quiere que se ejecute cuando la app se actualice desde una versión anterior. Si se omite, el indicador es `false` por defecto y las actualizaciones omiten el hook.
|
||||
* **Modelo de ejecución — asíncrono por defecto, sincronía opcional**: el indicador `shouldRunSynchronously` controla *cómo* se ejecuta la post-instalación.
|
||||
* `shouldRunSynchronously: false` *(predeterminado)* — el hook se **encola en la cola de mensajes** con `retryLimit: 3` y se ejecuta de forma asíncrona en un worker. La respuesta de instalación se devuelve tan pronto como el trabajo se encola, por lo que un controlador lento o con fallos no bloquea al solicitante. El worker reintentará hasta tres veces. **Úselo para trabajos de larga duración** — sembrar conjuntos de datos grandes, llamar a APIs de terceros lentas, aprovisionar recursos externos, cualquier cosa que pueda exceder una ventana de respuesta HTTP razonable.
|
||||
* `shouldRunSynchronously: true` — el hook se ejecuta **en línea durante el flujo de instalación** (el mismo ejecutor que la pre-instalación). La solicitud de instalación se bloquea hasta que el controlador finaliza y, si arroja una excepción, quien realiza la instalación recibe un `POST_INSTALL_ERROR`. Sin reintentos automáticos. **Úselo para trabajo rápido que debe completarse antes de la respuesta** — por ejemplo, emitir un error de validación al usuario, o una configuración rápida de la que el cliente dependerá inmediatamente después de que regrese la llamada de instalación. Tenga en cuenta que la migración de metadatos ya se ha aplicado cuando se ejecuta la post-instalación, por lo que un fallo en modo síncrono **no** revierte los cambios de esquema — solo expone el error.
|
||||
* Asegúrese de que su controlador sea idempotente. En modo asíncrono, la cola puede reintentar hasta tres veces; en cualquier modo, el hook puede ejecutarse de nuevo en las actualizaciones cuando `shouldRunOnVersionUpgrade: true`.
|
||||
* Las variables de entorno `APPLICATION_ID`, `APP_ACCESS_TOKEN` y `API_URL` están disponibles dentro del controlador (igual que en cualquier otra función de lógica), por lo que puede llamar a la API de Twenty con un token de acceso de aplicación con alcance a su app.
|
||||
* Solo se permite una función de posinstalación por aplicación. La compilación del manifiesto generará un error si se detecta más de una.
|
||||
* Los `universalIdentifier`, `shouldRunOnVersionUpgrade` y `shouldRunSynchronously` de la función se adjuntan automáticamente al manifiesto de la aplicación en el campo `postInstallLogicFunction` durante la compilación; no es necesario que los referencies en [`defineApplication()`](/l/es/developers/extend/apps/config/application).
|
||||
* El tiempo de espera predeterminado se establece en 300 segundos (5 minutos) para permitir tareas de configuración más largas como la carga inicial de datos.
|
||||
* **No se ejecuta en modo de desarrollo**: cuando una app se registra localmente (mediante `yarn twenty dev`), el servidor omite por completo el flujo de instalación y sincroniza archivos directamente a través del observador de la CLI — por lo tanto, la post-instalación nunca se ejecuta en modo de desarrollo, independientemente de `shouldRunSynchronously`. Use `yarn twenty dev:function:exec --postInstall` para activarlo manualmente en un espacio de trabajo en ejecución.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Se ejecuta antes de que se aplique la migración de metadatos del espacio de trabajo">
|
||||
|
||||
Una función de preinstalación se ejecuta automáticamente durante la instalación, **antes de que se aplique la migración de metadatos del espacio de trabajo**. Comparte la misma forma de payload que la post-instalación (`InstallPayload`), pero está situada antes en el flujo de instalación para poder preparar el estado del que depende la próxima migración — usos típicos incluyen hacer copias de seguridad de datos, validar la compatibilidad con el nuevo esquema o archivar registros que están a punto de ser reestructurados o eliminados.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
También puedes ejecutar manualmente la función de preinstalación en cualquier momento usando la CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* Las funciones de pre-instalación usan `definePreInstallLogicFunction()` — la misma configuración especializada que la post-instalación, solo que adjunta a un punto diferente del ciclo de vida.
|
||||
* Tanto los controladores de pre- como de post-instalación reciben el mismo tipo `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Impórtelo una vez y reutilícelo para ambos hooks.
|
||||
* **Cuándo se ejecuta el hook**: se ubica justo antes de la migración de metadatos del espacio de trabajo (`synchronizeFromManifest`). Antes de ejecutarse, el servidor realiza una "sincronización simplificada" puramente aditiva que registra la función de pre-instalación de la versión **nueva** en los metadatos del espacio de trabajo — no se toca nada más — y luego la ejecuta. Debido a que esta sincronización es solo aditiva, los objetos, campos y datos de la versión anterior siguen intactos cuando se ejecuta su controlador: puede leer y respaldar de forma segura el estado premigración.
|
||||
* **Modelo de ejecución**: la pre-instalación se ejecuta **de forma síncrona** y **bloquea la instalación**. Si el controlador lanza una excepción, la instalación se aborta antes de que se apliquen cambios de esquema — el espacio de trabajo permanece en la versión anterior en un estado consistente. Esto es intencional: la pre-instalación es su última oportunidad para rechazar una actualización arriesgada.
|
||||
* Al igual que con la post-instalación, solo se permite una función de preinstalación por aplicación. Se adjunta automáticamente al manifiesto de la aplicación bajo `preInstallLogicFunction` durante la compilación.
|
||||
* **No se ejecuta en modo de desarrollo**: igual que la post-instalación — el flujo de instalación se omite por completo para las apps registradas localmente, por lo que la pre-instalación nunca se ejecuta con `yarn twenty dev`. Use `yarn twenty dev:function:exec --preInstall` para activarlo manualmente.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-instalación vs post-instalación: cuándo usar cada una" description="Elegir el hook de instalación adecuado">
|
||||
|
||||
Ambos hooks forman parte del mismo flujo de instalación y reciben el mismo `InstallPayload`. La diferencia es **cuándo** se ejecutan con respecto a la migración de metadatos del espacio de trabajo, y eso cambia qué datos pueden tocar de forma segura.
|
||||
|
||||
La pre-instalación siempre es **síncrona** (bloquea la instalación y puede abortarla). La post-instalación es **asíncrona por defecto** — se pone en cola en un worker con reintentos automáticos — pero puede optar por ejecución síncrona con `shouldRunSynchronously: true`. Consulte el acordeón `definePostInstallLogicFunction` de arriba para saber cuándo usar cada modo.
|
||||
|
||||
**Use `post-install` para cualquier cosa que necesite que exista el nuevo esquema.** Este es el caso más común:
|
||||
|
||||
* Sembrar datos predeterminados (crear registros iniciales, vistas predeterminadas, contenido de demostración) sobre objetos y campos recién añadidos.
|
||||
* Registrar webhooks con servicios de terceros ahora que la app ya tiene sus credenciales.
|
||||
* Llamar a su propia API para finalizar una configuración que depende de los metadatos sincronizados.
|
||||
* Lógica idempotente de "asegurar que esto exista" que debe reconciliar el estado en cada actualización — combínela con `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Ejemplo — sembrar un registro `PostCard` predeterminado después de la instalación:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` cuando una migración, de otro modo, destruiría o corrompería datos existentes.** Como la pre-instalación se ejecuta contra el esquema *anterior* y su fallo revierte la actualización, es el lugar adecuado para cualquier cosa arriesgada:
|
||||
|
||||
* **Hacer copia de seguridad de datos que están a punto de eliminarse o reestructurarse** — p. ej., está quitando un campo en la v2 y necesita copiar sus valores a otro campo o exportarlos a almacenamiento antes de que se ejecute la migración.
|
||||
* **Archivar registros que una nueva restricción invalidaría** — p. ej., un campo pasará a ser `NOT NULL` y primero necesita eliminar o corregir filas con valores nulos.
|
||||
* **Validar la compatibilidad y rechazar la actualización si los datos actuales no pueden migrarse limpiamente** — lance desde el controlador y la instalación se abortará sin aplicar cambios. Esto es más seguro que descubrir la incompatibilidad a mitad de la migración.
|
||||
* **Renombrar o reasignar claves de datos** antes de un cambio de esquema que perdería la asociación.
|
||||
|
||||
Ejemplo — archivar registros antes de una migración destructiva:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Regla general:**
|
||||
|
||||
| Quiere... | Usar |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| Sembrar datos predeterminados, configurar el espacio de trabajo, registrar recursos externos | `post-install` |
|
||||
| Ejecutar siembras de larga duración o llamadas a terceros que no deberían bloquear la respuesta de instalación | `post-install` (predeterminado — `shouldRunSynchronously: false`, con reintentos del worker) |
|
||||
| Ejecutar una configuración rápida de la que el cliente dependerá inmediatamente después de que regrese la llamada de instalación | `post-install` con `shouldRunSynchronously: true` |
|
||||
| Leer o hacer copia de seguridad de datos que la próxima migración perdería | `pre-install` |
|
||||
| Rechazar una actualización que corrompería datos existentes | `pre-install` (lanzar desde el controlador) |
|
||||
| Ejecutar reconciliación en cada actualización | `post-install` con `shouldRunOnVersionUpgrade: true` |
|
||||
| Realizar una configuración única solo en la primera instalación | `post-install` con `shouldRunOnVersionUpgrade: false` (predeterminado) |
|
||||
|
||||
<Note>
|
||||
En caso de duda, elija **post-install** como predeterminado. Recurra a la pre-instalación solo cuando la propia migración sea destructiva y necesite interceptar el estado anterior antes de que desaparezca.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Resumen
|
||||
description: "Configura la propia app: su identidad, los permisos predeterminados y lo que se ejecuta en el momento de la instalación."
|
||||
icon: screwdriver-wrench
|
||||
---
|
||||
|
||||
La **capa de configuración** de una app de Twenty es lo que describe la app *a la plataforma*: su identidad, los permisos que posee y el código que se ejecuta durante la instalación o la actualización. Estas declaraciones no añaden nuevas estructuras de datos ni comportamiento en tiempo de ejecución; le indican a Twenty *quién es la app* y *cómo configurarla*.
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Application — identity, default role, variables, │
|
||||
│ marketplace metadata │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Role — what the app's logic functions can read │ │
|
||||
│ │ and write (referenced by Application) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (at install / upgrade time)
|
||||
┌──────────────────────────────────┐
|
||||
│ Pre-install hook │ before metadata migration
|
||||
└──────────────────────────────────┘
|
||||
┌──────────────────────────────────┐
|
||||
│ Post-install hook │ after metadata migration
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## En esta sección
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Configuración de la aplicación" icon="rocket" href="/l/es/developers/extend/apps/config/application">
|
||||
`defineApplication`: identidad, rol predeterminado, variables y metadatos del marketplace.
|
||||
</Card>
|
||||
<Card title="Roles y permisos" icon="shield-halved" href="/l/es/developers/extend/apps/config/roles">
|
||||
`defineRole`: declara qué pueden leer y escribir las funciones lógicas de tu app.
|
||||
</Card>
|
||||
<Card title="Hooks de instalación" icon="wrench" href="/l/es/developers/extend/apps/config/install-hooks">
|
||||
`definePreInstallLogicFunction` y `definePostInstallLogicFunction`: hacen copias de seguridad de los datos, cargan valores predeterminados y validan actualizaciones.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Cómo se relacionan las piezas
|
||||
|
||||
* **Application** es el punto de entrada. Cada app tiene exactamente una llamada a `defineApplication()`, y apunta a un **Role** como su valor predeterminado.
|
||||
* El **Role** controla qué pueden leer y escribir las funciones lógicas y los componentes de interfaz de la app. Sigue el principio de privilegios mínimos: concede solo los permisos que tu código realmente necesita.
|
||||
* Los **hooks de instalación** se ejecutan durante la instalación o la actualización: el hook de preinstalación antes de la migración de metadatos (para poder rechazar una actualización arriesgada) y el hook de postinstalación después de la migración (para poder cargar datos predeterminados con el nuevo esquema).
|
||||
|
||||
<Note>
|
||||
Los hooks de instalación comparten el entorno de ejecución de la [función lógica](/l/es/developers/extend/apps/logic/logic-functions): misma firma del handler, mismas variables de entorno, mismo cliente de API tipado, pero se declaran con sus propias funciones de definición y viven fuera del modelo de triggers habitual (HTTP, cron, eventos de base de datos).
|
||||
</Note>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Recursos públicos
|
||||
description: Distribuye archivos estáticos — imágenes, íconos, fuentes — junto con tu aplicación mediante la carpeta `public/`.
|
||||
icon: folder-open
|
||||
---
|
||||
|
||||
La carpeta `public/` en la raíz de tu aplicación contiene archivos estáticos: imágenes, íconos, fuentes o cualquier otro recurso que tu aplicación necesite en tiempo de ejecución. Estos archivos se incluyen automáticamente en las compilaciones, se sincronizan durante el modo de desarrollo y se suben al servidor.
|
||||
|
||||
Los archivos ubicados en `public/` son:
|
||||
|
||||
* **De acceso público** — una vez sincronizados con el servidor, los recursos se sirven en una URL pública. No se necesita autenticación para acceder a ellos.
|
||||
* **Disponibles en componentes de frontend** — usa las URLs de los recursos para mostrar imágenes, íconos o cualquier medio dentro de tus componentes de React.
|
||||
* **Disponibles en funciones de lógica** — referencia las URLs de los recursos en correos electrónicos, respuestas de API o cualquier lógica del lado del servidor.
|
||||
* **Usados para metadatos del marketplace** — los campos `logoUrl` y `screenshots` en `defineApplication()` referencian archivos de esta carpeta (p. ej., `public/logo.png`). Estos se muestran en el marketplace cuando se publica tu aplicación.
|
||||
* **Sincronizados automáticamente en modo de desarrollo** — cuando agregas, actualizas o eliminas un archivo en `public/`, se sincroniza automáticamente con el servidor. No se necesita reiniciar.
|
||||
* **Incluidos en las compilaciones** — `yarn twenty dev:build` agrupa todos los recursos públicos en la salida de distribución.
|
||||
|
||||
## Acceder a recursos públicos con `getPublicAssetUrl`
|
||||
|
||||
Usa el helper `getPublicAssetUrl` de `twenty-sdk` para obtener la URL completa de un archivo en tu directorio `public/`. Funciona tanto en **funciones de lógica** como en **componentes de frontend**.
|
||||
|
||||
**En una función de lógica:**
|
||||
|
||||
```ts src/logic-functions/send-invoice.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
|
||||
|
||||
// Fetch the file content (no auth required — public endpoint)
|
||||
const response = await fetch(invoiceUrl);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return { logoUrl, size: buffer.byteLength };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-...',
|
||||
name: 'send-invoice',
|
||||
description: 'Sends an invoice with the app logo',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**En un componente de frontend:**
|
||||
|
||||
```tsx src/front-components/company-card.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const CompanyCard = () => {
|
||||
const logoUrl = getPublicAssetUrl('logo.png');
|
||||
|
||||
return <img src={logoUrl} alt="App logo" />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'company-card',
|
||||
component: CompanyCard,
|
||||
});
|
||||
```
|
||||
|
||||
El argumento `path` es relativo a la carpeta `public/` de tu aplicación. Tanto `getPublicAssetUrl('logo.png')` como `getPublicAssetUrl('public/logo.png')` resuelven a la misma URL — el prefijo `public/` se elimina automáticamente si está presente.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Roles y permisos
|
||||
description: Declara qué objetos y campos pueden leer y escribir las funciones de lógica y los componentes de interfaz de tu aplicación.
|
||||
icon: shield-halved
|
||||
---
|
||||
|
||||
Un **rol** es un conjunto de permisos: qué objetos puede leer o escribir una aplicación, qué campos puede ver y qué capacidades a nivel de plataforma puede usar. Las funciones lógicas y los componentes de interfaz de cada aplicación heredan los permisos del rol marcado con `defineApplicationRole()` (consulta [El rol de función predeterminado](#the-default-function-role) más abajo).
|
||||
|
||||
```ts src/roles/restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||||
label: 'My new role',
|
||||
description: 'A role that can be used in your workspace',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
## El rol de función predeterminado
|
||||
|
||||
Cuando generas una nueva aplicación, la CLI crea un archivo de rol predeterminado declarado con `defineApplicationRole()`:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
});
|
||||
```
|
||||
|
||||
`defineApplicationRole()` es una envoltura ligera alrededor de `defineRole()` que marca **el** rol utilizado como predeterminado de tu aplicación en el momento de la instalación. La validación es idéntica a `defineRole`, pero la canalización de compilación conecta automáticamente su `universalIdentifier` con `defaultRoleUniversalIdentifier` del manifiesto de la aplicación, por lo que no necesitas hacer referencia a él desde [`defineApplication`](/l/es/developers/extend/apps/config/application).
|
||||
|
||||
Notas:
|
||||
|
||||
* Se permite exactamente **una** llamada a `defineApplicationRole(...)` por aplicación; la compilación del manifiesto fallará si encuentra más de una.
|
||||
* Usa `defineRole()` (no `defineApplicationRole()`) para cualquier rol **adicional** que distribuya tu aplicación.
|
||||
* Configurar `defaultRoleUniversalIdentifier` explícitamente en `defineApplication()` sigue siendo compatible por motivos de retrocompatibilidad, pero está en desuso en favor de `defineApplicationRole()`.
|
||||
|
||||
## Mejores prácticas
|
||||
|
||||
* Parte del rol generado automáticamente y luego restríngeelo progresivamente; el valor predeterminado concede un acceso amplio de lectura, lo cual rara vez es lo que quieres en producción.
|
||||
* Reemplaza `objectPermissions` y `fieldPermissions` con los objetos y campos que realmente necesitan tus funciones.
|
||||
* `permissionFlagUniversalIdentifiers` controla el acceso a capacidades a nivel de plataforma. Manténlos al mínimo.
|
||||
* Consulta un ejemplo funcional: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Extender objetos
|
||||
description: Añade campos a los objetos estándar de Twenty (Person, Company, …) o a objetos de otras aplicaciones usando defineField.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
Usa `defineField()` para añadir un campo a un objeto que no te pertenece — un objeto estándar de Twenty como Person o Company, u otro objeto proporcionado por otra aplicación instalada. A diferencia de los campos en línea declarados dentro de [`defineObject`](/l/es/developers/extend/apps/data/objects), los campos independientes requieren un `objectUniversalIdentifier` para especificar qué objeto extienden.
|
||||
|
||||
```ts src/fields/company-loyalty-tier.field.ts
|
||||
import { defineField, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||||
name: 'loyaltyTier',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Loyalty Tier',
|
||||
icon: 'IconStar',
|
||||
options: [
|
||||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Puntos clave
|
||||
|
||||
* `objectUniversalIdentifier` identifica el objeto de destino. Para los objetos estándar de Twenty, importa la constante desde `twenty-sdk`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
|
||||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
* Al definir campos **en línea dentro de `defineObject()`**, **no** necesitas `objectUniversalIdentifier` — se hereda del objeto padre.
|
||||
|
||||
* `defineField()` es la única forma de añadir campos a objetos que no creaste con `defineObject()`.
|
||||
|
||||
* La ubicación del archivo depende de ti. La convención es `src/fields/\<name>.field.ts`, pero el SDK detecta campos en cualquier lugar de `src/`.
|
||||
|
||||
* Para agregar una pestaña a un diseño de página estándar (por ejemplo, la página de detalles de la Tarea o de la Empresa), usa [`definePageLayoutTab`](/l/es/developers/extend/apps/layout/page-layouts#definepagelayouttab) con `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` de `twenty-sdk/define`.
|
||||
|
||||
## Añadir una relación a un objeto existente
|
||||
|
||||
Para añadir un campo de relación (por ejemplo, vinculando tu objeto personalizado a un `Person` estándar), usa `defineField()` con `FieldType.RELATION`. El patrón es el mismo que para las relaciones en línea, pero con `objectUniversalIdentifier` establecido explícitamente. Consulta [Relaciones](/l/es/developers/extend/apps/data/relations) para conocer el patrón bidireccional.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Objetos
|
||||
description: Declara nuevos tipos de registro (tablas personalizadas con sus propios campos) usando defineObject.
|
||||
icon: tabla
|
||||
---
|
||||
|
||||
Los **objetos** personalizados son nuevos tipos de registro que tu aplicación añade a un espacio de trabajo — Tarjeta postal, Factura, Suscripción, cualquier cosa específica de tu dominio. Cada objeto declara su esquema (campos, relaciones, valores predeterminados) y un identificador universal estable que se mantiene a través de sincronizaciones e implementaciones.
|
||||
|
||||
```ts src/objects/post-card.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
name: 'content',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
name: 'recipientName',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
name: 'recipientAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
name: 'status',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
name: 'deliveredAt',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Puntos clave
|
||||
|
||||
* El `universalIdentifier` debe ser único y estable entre implementaciones.
|
||||
* Cada campo requiere `name`, `type`, `label` y su propio `universalIdentifier` estable.
|
||||
* La matriz `fields` es opcional: puedes definir objetos sin campos personalizados.
|
||||
* Los campos en línea definidos aquí **no** necesitan un `objectUniversalIdentifier`, ya que se hereda del objeto padre. Usa [`defineField()`](/l/es/developers/extend/apps/data/extending-objects) para añadir campos a objetos que no te pertenecen.
|
||||
* Puedes generar nuevos objetos con `yarn twenty dev:add object`, que te guía en la asignación de nombres, los campos y las relaciones. Consulta [Arquitectura → Generación de entidades](/l/es/developers/extend/apps/getting-started/scaffolding).
|
||||
|
||||
<Note>
|
||||
**Los campos base se añaden automáticamente.** Cuando defines un objeto personalizado, Twenty crea campos estándar como `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` y `deletedAt` por ti. No necesitas declararlos en tu matriz `fields`, solo tus campos personalizados. Puedes sobrescribir un campo predeterminado declarando uno con el mismo nombre, pero esto rara vez es una buena idea.
|
||||
</Note>
|
||||
|
||||
## Valores predeterminados
|
||||
|
||||
Los valores predeterminados de cadenas literales deben ir entre comillas simples **dentro** de la cadena — `defaultValue: "'Draft'"`, no `defaultValue: "Draft"`. Por eso el campo `status` anterior utiliza `` `'${PostCardStatus.DRAFT}'` ``.
|
||||
|
||||
Las cadenas sin comillas se reservan para valores predeterminados calculados, evaluados cuando se crea un registro:
|
||||
|
||||
* `'uuid'` — genera un UUID (para campos `UUID`)
|
||||
* `'now'` — la marca de tiempo actual (para campos `DATE_TIME`)
|
||||
|
||||
La misma convención se aplica a los subcampos de tipo cadena de los valores predeterminados compuestos (por ejemplo, `{ source: "'MANUAL'" }` en un campo `ACTOR`) y a los valores de `SELECT`/`MULTI_SELECT`. Un valor predeterminado de tipo cadena literal dejado sin comillas genera una advertencia cuando se compila tu aplicación.
|
||||
|
||||
## ¿Qué sigue?
|
||||
|
||||
* **Conecta este objeto con otros**: consulta [Relaciones](/l/es/developers/extend/apps/data/relations) para el patrón de relación bidireccional.
|
||||
* **Añade campos a objetos de otras aplicaciones**: consulta [Extender objetos](/l/es/developers/extend/apps/data/extending-objects) para `defineField()`.
|
||||
* **Muestra este objeto en la interfaz de usuario**: consulta [Vistas](/l/es/developers/extend/apps/layout/views) y [Elementos del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items) para colocarlo en la barra lateral.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Resumen
|
||||
description: "Da forma a los datos que tu aplicación agrega a un espacio de trabajo: objetos, campos y relaciones."
|
||||
icon: database
|
||||
---
|
||||
|
||||
La **capa de datos** de una aplicación de Twenty es el conjunto de datos que tu aplicación *agrega* a un espacio de trabajo — los nuevos tipos de registros que declara, las columnas que agrega a los objetos existentes y cómo esos registros se conectan entre sí.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Object — a record type, e.g. PostCard │
|
||||
│ ├─ Field (name, type, label) │
|
||||
│ ├─ Field │
|
||||
│ └─ Relation (link to another object) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
│
|
||||
├── lives in your app, OR
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Standard / other apps' objects │
|
||||
│ └─ Field added by your app via defineField │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## En esta sección
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Objetos" icon="table" href="/l/es/developers/extend/apps/data/objects">
|
||||
`defineObject` — declara nuevos tipos de registros con sus propios campos.
|
||||
</Card>
|
||||
<Card title="Extender objetos" icon="wand-magic-sparkles" href="/l/es/developers/extend/apps/data/extending-objects">
|
||||
`defineField` — agrega campos a objetos estándar o de otras aplicaciones.
|
||||
</Card>
|
||||
<Card title="Relaciones" icon="diagram-project" href="/l/es/developers/extend/apps/data/relations">
|
||||
Conexiones bidireccionales `MANY_TO_ONE` / `ONE_TO_MANY` entre objetos.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Entidades de un vistazo
|
||||
|
||||
| Entidad | Propósito | Definido con |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| **Objeto** | Un nuevo tipo de registro personalizado (por ejemplo, PostCard, Invoice) con sus propios campos | `defineObject()` |
|
||||
| **Campo** | Una columna en un objeto. Los campos independientes pueden ampliar objetos que no creaste (por ejemplo, agregar `loyaltyTier` a Company) | `defineField()` |
|
||||
| **Relación** | Un vínculo bidireccional entre dos objetos: ambos lados se declaran como campos | `defineField()` con `FieldType.RELATION` |
|
||||
| **Índice** | Un índice de base de datos para acelerar una consulta recurrente sobre uno de tus objetos | `defineIndex()` |
|
||||
|
||||
El SDK detecta estos mediante análisis AST en tiempo de compilación, por lo que la organización de archivos depende de ti; la convención es `src/objects/`, `src/fields/` y `src/indexes/`. Los UUID `universalIdentifier` estables vinculan todo a través de los despliegues.
|
||||
|
||||
## Índices (opcional)
|
||||
|
||||
Las aplicaciones pueden incluir índices junto con sus objetos para mantener rápidas las consultas recurrentes. El caso más habitual es una columna de estado o de clave externa que lees con frecuencia.
|
||||
|
||||
```ts src/indexes/post-card-status.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/post-card.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
|
||||
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Índices únicos
|
||||
|
||||
`defineIndex` acepta `isUnique: true` tanto para unicidad de una sola columna como de varias columnas. Este es el elemento primitivo recomendado — `defineField({ isUnique: true })` está obsoleto y se eliminará en una versión futura.
|
||||
|
||||
```ts
|
||||
defineIndex({
|
||||
universalIdentifier: '…',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
|
||||
});
|
||||
```
|
||||
|
||||
### Otras restricciones
|
||||
|
||||
* Las cláusulas `WHERE` parciales permanecen bajo el control del administrador: las aplicaciones no pueden declararlas.
|
||||
* Cada objeto está limitado a 10 índices personalizados (los índices propios del framework no cuentan).
|
||||
|
||||
Ordena el arreglo `fields` de la forma en que Postgres debería usarlo: la columna más a la izquierda primero, como en una guía telefónica. Los índices no son gratuitos: cada escritura en la tabla los actualiza. Añade uno solo cuando tengas una consulta que lo necesite.
|
||||
|
||||
<Note>
|
||||
¿Buscas **Application Config** o **Roles & Permissions**? Esos describen la propia aplicación en lugar de los datos que agrega; se encuentran en [Config](/l/es/developers/extend/apps/config/overview). ¿Buscas **Connections** (Linear, GitHub, Slack OAuth)? Estas existen para ser llamadas *desde* las funciones lógicas y se encuentran en [Logic](/l/es/developers/extend/apps/logic/connections).
|
||||
</Note>
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Relaciones
|
||||
description: Conecta objetos entre sí con relaciones bidireccionales MANY_TO_ONE / ONE_TO_MANY.
|
||||
icon: diagram-project
|
||||
---
|
||||
|
||||
Las relaciones conectan dos objetos entre sí. En Twenty, las relaciones siempre son **bidireccionales**: cada relación tiene dos lados, y cada lado se declara como un campo que hace referencia al otro.
|
||||
|
||||
| Tipo de relación | Descripción | ¿Tiene clave foránea? |
|
||||
| ---------------- | --------------------------------------------------------------------------- | --------------------- |
|
||||
| `MANY_TO_ONE` | Muchos registros de este objeto apuntan a un registro del objeto de destino | Sí (`joinColumnName`) |
|
||||
| `ONE_TO_MANY` | Un registro de este objeto tiene muchos registros del objeto de destino | No (lado inverso) |
|
||||
|
||||
## Cómo funcionan las relaciones
|
||||
|
||||
Cada relación requiere **dos campos** que se referencian entre sí:
|
||||
|
||||
1. El lado **MANY_TO_ONE** — vive en el objeto que contiene la clave foránea.
|
||||
2. El lado **ONE_TO_MANY** — vive en el objeto que es propietario de la colección.
|
||||
|
||||
Ambos campos usan `FieldType.RELATION` y se hacen referencia cruzada mediante `relationTargetFieldMetadataUniversalIdentifier`.
|
||||
|
||||
## Ejemplo: la tarjeta postal tiene muchos destinatarios
|
||||
|
||||
Un `PostCard` puede enviarse a muchos registros `PostCardRecipient`. Cada destinatario pertenece exactamente a una tarjeta postal.
|
||||
|
||||
**Paso 1: Define el lado ONE_TO_MANY en PostCard** (el lado "uno"):
|
||||
|
||||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||||
// Import from the other side
|
||||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCardRecipients',
|
||||
label: 'Post Card Recipients',
|
||||
icon: 'IconUsers',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Paso 2: Define el lado MANY_TO_ONE en PostCardRecipient** (el lado "muchos" — contiene la clave foránea):
|
||||
|
||||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
// Export so the other side can reference it
|
||||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||||
// Import from the other side
|
||||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
icon: 'IconMail',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Importaciones circulares:** ambos campos de relación hacen referencia al `universalIdentifier` del otro. Para evitar problemas de importaciones circulares, exporta los ID de tus campos como constantes con nombre desde cada archivo e impórtalos en el otro. El sistema de compilación los resuelve en tiempo de compilación.
|
||||
</Note>
|
||||
|
||||
## Relacionar con objetos estándar
|
||||
|
||||
Para crear una relación con un objeto integrado de Twenty (Person, Company, etc.), usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
|
||||
|
||||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||||
|
||||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Propiedades del campo de relación
|
||||
|
||||
| Propiedad | Obligatorio | Descripción |
|
||||
| ------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `type` | Sí | Debe ser `FieldType.RELATION` |
|
||||
| `relationTargetObjectMetadataUniversalIdentifier` | Sí | El `universalIdentifier` del objeto de destino |
|
||||
| `relationTargetFieldMetadataUniversalIdentifier` | Sí | El `universalIdentifier` del campo correspondiente en el objeto de destino |
|
||||
| `universalSettings.relationType` | Sí | `RelationType.MANY_TO_ONE` o `RelationType.ONE_TO_MANY` |
|
||||
| `universalSettings.onDelete` | Solo para MANY_TO_ONE | Qué sucede cuando se elimina el registro referenciado: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
|
||||
| `universalSettings.joinColumnName` | Solo para MANY_TO_ONE | Nombre de la columna de base de datos para la clave foránea (p. ej., `postCardId`) |
|
||||
|
||||
## Campos de relación en línea
|
||||
|
||||
También puedes declarar una relación directamente dentro de [`defineObject`](/l/es/developers/extend/apps/data/objects). Cuando es en línea, omite `objectUniversalIdentifier` — se hereda del objeto padre:
|
||||
|
||||
```ts
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCardRecipient',
|
||||
// ...
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: POST_CARD_FIELD_ID,
|
||||
type: FieldType.RELATION,
|
||||
name: 'postCard',
|
||||
label: 'Post Card',
|
||||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'postCardId',
|
||||
},
|
||||
},
|
||||
// … other fields
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Conceptos
|
||||
description: "Cómo funcionan las aplicaciones de Twenty: modelo de entidad, sandboxing y ciclo de vida de la instalación."
|
||||
icon: sitemap
|
||||
---
|
||||
|
||||
Las aplicaciones de Twenty son paquetes de TypeScript que amplían tu espacio de trabajo con objetos personalizados, lógica, componentes de UI y capacidades de IA. Se ejecutan en la plataforma Twenty con sandboxing completo y controles de permisos.
|
||||
|
||||
## Cómo funcionan las aplicaciones
|
||||
|
||||
Una aplicación es un conjunto de **entidades** declaradas usando funciones `defineEntity()` del paquete `twenty-sdk`. El SDK detecta estas declaraciones mediante análisis de AST en tiempo de compilación y produce un **manifiesto** — una descripción completa de lo que tu aplicación agrega a un espacio de trabajo. Estas funciones validan tu configuración en tiempo de compilación y proporcionan autocompletado en el IDE y seguridad de tipos.
|
||||
|
||||
```
|
||||
your-app/
|
||||
├── src/
|
||||
│ ├── application-config.ts ← defineApplication (required, one per app)
|
||||
│ ├── roles/ ← defineRole
|
||||
│ ├── objects/ ← defineObject
|
||||
│ ├── fields/ ← defineField
|
||||
│ ├── logic-functions/ ← defineLogicFunction
|
||||
│ ├── front-components/ ← defineFrontComponent
|
||||
│ ├── skills/ ← defineSkill
|
||||
│ ├── agents/ ← defineAgent
|
||||
│ ├── views/ ← defineView
|
||||
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
|
||||
│ └── page-layouts/ ← definePageLayout
|
||||
├── public/ ← Static assets (images, icons)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
<Note>
|
||||
**La organización de archivos depende de ti.** La detección de entidades se basa en el AST — el SDK encuentra llamadas a `export default defineEntity(...)` sin importar dónde se encuentre el archivo. La estructura de carpetas anterior es una convención, no un requisito.
|
||||
</Note>
|
||||
|
||||
## Tipos de entidades
|
||||
|
||||
| Entidad | Propósito | Documentación |
|
||||
| ----------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **Aplicación** | Identidad de la aplicación, rol predeterminado, variables | [Configuración de la aplicación](/l/es/developers/extend/apps/config/application) |
|
||||
| **Rol** | Conjuntos de permisos para objetos y campos | [Roles y permisos](/l/es/developers/extend/apps/config/roles) |
|
||||
| **Objeto** | Tipos de registros personalizados con campos | [Objetos](/l/es/developers/extend/apps/data/objects) |
|
||||
| **Campo** | Agregar campos a objetos de otras aplicaciones | [Ampliar objetos](/l/es/developers/extend/apps/data/extending-objects) |
|
||||
| **Relación** | Vínculos bidireccionales entre objetos | [Relaciones](/l/es/developers/extend/apps/data/relations) |
|
||||
| **Función de lógica** | TypeScript del lado del servidor con activadores | [Funciones de lógica](/l/es/developers/extend/apps/logic/logic-functions) |
|
||||
| **Habilidad** | Instrucciones reutilizables para agentes de IA | [Habilidades y agentes](/l/es/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **Agente** | Asistentes de IA con prompts personalizados | [Habilidades y agentes](/l/es/developers/extend/apps/logic/skills-and-agents) |
|
||||
| **Proveedor de conexión** | Credenciales OAuth para APIs de terceros | [Conexiones](/l/es/developers/extend/apps/logic/connections) |
|
||||
| **Vista** | Vistas de listas de registros preconfiguradas | [Vistas](/l/es/developers/extend/apps/layout/views) |
|
||||
| **Elemento del menú de navegación** | Entradas personalizadas de la barra lateral | [Elementos del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items) |
|
||||
| **Diseño de página** | Pestañas y widgets en la página de detalles de un registro | [Diseños de página](/l/es/developers/extend/apps/layout/page-layouts) |
|
||||
| **Componente de frontend** | Interfaz de usuario de React en entorno aislado dentro de Twenty | [Componentes de frontend](/l/es/developers/extend/apps/layout/front-components) |
|
||||
| **Elemento del menú de comandos** | Acciones rápidas y entradas Cmd+K | [Elementos del menú de comandos](/l/es/developers/extend/apps/layout/command-menu-items) |
|
||||
|
||||
## Sandboxing
|
||||
|
||||
* **Las funciones de lógica** se ejecutan en procesos de Node.js aislados en el servidor. Solo acceden a los datos a través del cliente de API tipado, limitado a los permisos del rol de la aplicación.
|
||||
* **Los componentes de frontend** se ejecutan en Web Workers usando Remote DOM — aislados de la página principal pero renderizando elementos DOM nativos (no iframes). Se comunican con Twenty a través de una API de host de paso de mensajes.
|
||||
* **Los permisos** se aplican a nivel de API. El token de tiempo de ejecución (`TWENTY_APP_ACCESS_TOKEN`) se deriva del rol definido en `defineApplication()`.
|
||||
|
||||
## Ciclo de vida de la aplicación
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Development │
|
||||
│ npx create-twenty-app → yarn twenty dev (live sync) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Build & Deploy │
|
||||
│ yarn twenty dev:build → yarn twenty app:publish │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Install flow │
|
||||
│ upload → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Publish │
|
||||
│ npm publish → appears in Twenty marketplace │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
* **`yarn twenty dev`** — observa tus archivos fuente y sincroniza en tiempo real los cambios con un servidor de Twenty conectado. El cliente de API tipado se regenera automáticamente cuando cambia el esquema.
|
||||
* **`yarn twenty dev:build`** — compila TypeScript, agrupa las funciones de lógica y los componentes de frontend con esbuild, y produce un manifiesto.
|
||||
* **Hooks de pre/post-instalación** — funciones opcionales que se ejecutan durante la instalación. Consulta [Hooks de instalación](/l/es/developers/extend/apps/config/install-hooks) para más detalles.
|
||||
|
||||
## Próximos pasos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Configuración" icon="screwdriver-wrench" href="/l/es/developers/extend/apps/config/overview">
|
||||
Identidad de la aplicación, rol predeterminado y hooks de instalación.
|
||||
</Card>
|
||||
<Card title="Datos" icon="database" href="/l/es/developers/extend/apps/data/overview">
|
||||
Objetos, campos y relaciones bidireccionales.
|
||||
</Card>
|
||||
<Card title="Lógica" icon="bolt" href="/l/es/developers/extend/apps/logic/overview">
|
||||
Funciones de lógica, habilidades, agentes y conexiones OAuth.
|
||||
</Card>
|
||||
<Card title="Diseño" icon="table-columns" href="/l/es/developers/extend/apps/layout/overview">
|
||||
Vistas, navegación, diseños de página y componentes de frontend.
|
||||
</Card>
|
||||
<Card title="Operaciones" icon="rocket" href="/l/es/developers/extend/apps/operations/overview">
|
||||
CLI, pruebas, remotos, CI y publicación de tu aplicación.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Servidor local
|
||||
description: "Administra el servidor Docker local de Twenty: inícialo, deténlo, actualízalo, crea una instancia de prueba en paralelo y realiza la configuración manual del SDK."
|
||||
icon: server
|
||||
---
|
||||
|
||||
## Administrar el servidor local
|
||||
|
||||
Usa `yarn twenty docker:*` para controlar el contenedor local de Twenty:
|
||||
|
||||
| Comando | Qué hace |
|
||||
| -------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `yarn twenty docker:start` | Inicia el servidor local (descarga la imagen si es necesario) |
|
||||
| `yarn twenty docker:start 2.2.0` | Iniciar una versión específica del servidor |
|
||||
| `yarn twenty docker:start --port 3030` | Inicia en un puerto personalizado |
|
||||
| `yarn twenty docker:stop` | Detiene el servidor (conserva los datos) |
|
||||
| `yarn twenty docker:status` | Muestra la URL, la versión y las credenciales de inicio de sesión |
|
||||
| `yarn twenty docker:logs` | Transmite los registros del servidor |
|
||||
| `yarn twenty docker:reset` | Elimina los datos y comienza desde cero |
|
||||
| `yarn twenty docker:upgrade` | Descarga la última imagen `twenty-app-dev` |
|
||||
| `yarn twenty docker:upgrade 2.2.0` | Actualiza a una versión específica |
|
||||
|
||||
Los datos se conservan entre reinicios en dos volúmenes de Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para archivos). Usa `reset` para borrar todo.
|
||||
|
||||
## Fijar la versión del servidor
|
||||
|
||||
Cuando no se pasa ninguna versión, `docker:start` resuelve la versión a partir del rango `engines.twenty` de tu aplicación en `package.json`, el mismo rango con el que el servidor valida cuando tu aplicación se instala. Inicia la imagen publicada más reciente de `twenty-app-dev` que satisface el rango, recurriendo a `latest` cuando el campo está ausente o ninguna versión publicada coincide:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"engines": {
|
||||
"twenty": ">=2.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pasa una versión explícitamente para anular el rango en una sola ejecución: `yarn twenty docker:start 2.3.0`. Si ya existe un contenedor en una versión diferente, `docker:start` lo actualiza in situ (recreando el contenedor mientras conserva tus volúmenes de datos).
|
||||
|
||||
## Actualización de la imagen del servidor
|
||||
|
||||
`yarn twenty docker:upgrade` descarga la última imagen, compara los digests y solo recrea el contenedor si realmente cambió algo. Los volúmenes se conservan — solo se reemplaza el contenedor. Si se descargó una nueva imagen y el contenedor estaba en ejecución, la actualización inicia automáticamente un contenedor nuevo; ejecuta `yarn twenty docker:start` después para esperar a que esté en buen estado.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty docker:upgrade # Latest
|
||||
yarn twenty docker:upgrade 2.2.0 # Specific version
|
||||
```
|
||||
|
||||
Puedes verificar la versión en ejecución con `yarn twenty docker:status` (muestra el `APP_VERSION` incluido en el contenedor).
|
||||
|
||||
## Ejecutar una instancia de prueba en paralelo
|
||||
|
||||
Pasa `--test` a cualquier comando de `docker:*` para administrar una segunda instancia completamente aislada — útil para ejecutar pruebas de integración o experimentar sin tocar los datos principales de desarrollo:
|
||||
|
||||
| Comando | Qué hace |
|
||||
| ----------------------------------- | -------------------------------------------------------------- |
|
||||
| `yarn twenty docker:start --test` | Inicia la instancia de prueba (usa el puerto 2021 por defecto) |
|
||||
| `yarn twenty docker:stop --test` | Deténla |
|
||||
| `yarn twenty docker:status --test` | Muestra su estado |
|
||||
| `yarn twenty docker:logs --test` | Transmite sus registros |
|
||||
| `yarn twenty docker:reset --test` | Borra sus datos |
|
||||
| `yarn twenty docker:upgrade --test` | Actualiza su imagen |
|
||||
|
||||
La instancia de prueba se ejecuta en su propio contenedor de Docker (`twenty-app-dev-test`) con volúmenes dedicados (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) y configuración dedicada, por lo que puede ejecutarse en paralelo con la instancia principal sin conflictos. Combina `--test` con `--port` para anular el puerto predeterminado (2021).
|
||||
|
||||
## Configuración manual (sin el generador)
|
||||
|
||||
Omite el generador si vas a agregar el SDK a un proyecto existente:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
Agrega el script a `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ahora puedes ejecutar `yarn twenty dev`, `yarn twenty docker:start` y todos los demás comandos.
|
||||
|
||||
<Note>
|
||||
No instales `twenty-sdk` globalmente — fíjalo por proyecto para que cada app use su propia versión.
|
||||
</Note>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Estructura del proyecto
|
||||
description: Qué hay dentro de una app de Twenty generada mediante scaffolding — archivos, carpetas y lo que hace cada uno.
|
||||
icon: folder-tree
|
||||
---
|
||||
|
||||
Una nueva app generada por `npx create-twenty-app` se ve así:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
package.json
|
||||
src/
|
||||
application-config.ts # Required — your app's entry point
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
```
|
||||
|
||||
## Archivos clave
|
||||
|
||||
| Archivo / Carpeta | Propósito |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `src/application-config.ts` | **Obligatorio.** El archivo de configuración principal de tu app. |
|
||||
| `src/default-role.ts` | Rol predeterminado que controla a qué pueden acceder tus funciones lógicas. |
|
||||
| `src/constants/universal-identifiers.ts` | UUIDs generados automáticamente y metadatos de la app (nombre para mostrar, descripción). |
|
||||
| `src/__tests__/` | Pruebas de integración (configuración + prueba de ejemplo). |
|
||||
| `public/` | Recursos estáticos (imágenes, fuentes) servidos con tu app. |
|
||||
|
||||
<Note>
|
||||
**La organización de archivos depende de ti.** Las carpetas anteriores son convenciones: el SDK detecta entidades mediante análisis AST en llamadas a `export default defineEntity(...)`, sin importar dónde se encuentre el archivo.
|
||||
</Note>
|
||||
|
||||
## Dependencias
|
||||
|
||||
Ambos paquetes del SDK de Twenty pertenecen a `devDependencies`, no a `dependencies`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"twenty-client-sdk": "^2.13.0",
|
||||
"twenty-sdk": "^2.13.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **`twenty-sdk`** incluye el CLI `twenty` y las herramientas de build/scaffolding. Solo se ejecuta en el desarrollo y durante el build, y nunca lo importa el runtime de la app que publicas.
|
||||
* **`twenty-client-sdk`** *sí* es importado por el código de tu app (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), pero Twenty lo proporciona en tiempo de ejecución: las funciones lógicas lo obtienen de una capa SDK generada y los componentes de front lo resuelven desde módulos servidos por el servidor. Tu copia instalada solo se utiliza para la comprobación de tipos y el build en tiempo de despliegue, por lo que nunca necesita incluirse en el bundle desplegado.
|
||||
|
||||
Mantener cualquiera de los paquetes bajo `dependencies` lo introduce en el bundle de runtime de la app instalada, donde es peso muerto. `twenty build` emite una advertencia cuando cualquiera de ellos sigue listado bajo `dependencies`.
|
||||
|
||||
Añade las dependencias de runtime propias de tu app (las bibliotecas que tus funciones lógicas realmente importan en tiempo de ejecución) bajo `dependencies` como de costumbre.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Inicio rápido
|
||||
icon: rocket
|
||||
description: Crea tu primera aplicación de Twenty en minutos.
|
||||
---
|
||||
|
||||
## Prerrequisitos
|
||||
|
||||
* **Node.js 24+** — [Descargar](https://nodejs.org/)
|
||||
* **Yarn 4** — incluido con Node.js a través de Corepack. Actívalo: `corepack enable`
|
||||
* **Docker** — [Descargar](https://www.docker.com/products/docker-desktop/). Necesario para ejecutar un servidor local de Twenty. Omítelo si ya tienes Twenty ejecutándose en otro lugar.
|
||||
|
||||
La creación de una app de Twenty tiene tres fases. El generador las combina en un único comando de ruta ideal, pero cada fase es un concepto independiente — cuando algo falla, saber en qué fase estás te indica qué debes corregir.
|
||||
|
||||
| Fase | Qué haces | Herramienta | Resultado |
|
||||
| --------------------------- | --------------------------------------------------- | ----------------------------- | ------------------------------------ |
|
||||
| **1. Generar estructura** | Genera el código fuente de la app | `npx create-twenty-app` | Un proyecto de TypeScript en disco |
|
||||
| **2. Ejecutar un servidor** | Inicia un servidor de Twenty con el que sincronizar | Docker + `yarn twenty server` | Una instancia de Twenty en ejecución |
|
||||
| **3. Sincronizar** | Sincroniza en vivo tu código con el servidor | `yarn twenty dev` | Tus cambios aparecen en la UI |
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — Genera la estructura de tu proyecto
|
||||
|
||||
Crea una app nueva a partir de la plantilla:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
Se te pedirá un nombre y una descripción — pulsa **Enter** para usar los valores predeterminados. Esto genera un proyecto de TypeScript en `my-twenty-app/` con un `application-config.ts` inicial, un rol predeterminado, un flujo de trabajo de CI y una prueba de integración.
|
||||
|
||||
**Después de esta fase:** tienes el código fuente de tu app en tu máquina. Aún no se está ejecutando — esa es la Fase 2.
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — Ejecuta un servidor local de Twenty
|
||||
|
||||
Tu app necesita un servidor de Twenty con el que sincronizar. El servidor es una instancia completa de Twenty — UI, API GraphQL, PostgreSQL — ejecutándose localmente en Docker. Tu código local sube sus definiciones a ese servidor, lo que hace que aparezcan en la UI.
|
||||
|
||||
El generador ofrece iniciar uno por ti:
|
||||
|
||||
> **¿Te gustaría configurar una instancia local de Twenty?**
|
||||
|
||||
* **Sí (recomendado)** — descarga la imagen de Docker `twentycrm/twenty-app-dev` y la inicia en el puerto `2020`. Asegúrate de que Docker esté en ejecución antes.
|
||||
* **No** — elige esto si ya tienes un servidor de Twenty al que te quieres conectar. Puedes conectarlo más tarde con `yarn twenty remote:add`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="¿Debería iniciar una instancia local?" />
|
||||
</div>
|
||||
|
||||
Una vez que el servidor esté en marcha, se abrirá un navegador para iniciar sesión. Inicia sesión con la cuenta de demostración precargada:
|
||||
|
||||
* **Correo electrónico:** `tim@apple.dev`
|
||||
* **Contraseña:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Pantalla de inicio de sesión de Twenty" />
|
||||
</div>
|
||||
|
||||
Haz clic en **Authorize** en la siguiente pantalla — esto le da a la CLI acceso a tu espacio de trabajo.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Pantalla de autorización de la CLI de Twenty" />
|
||||
</div>
|
||||
|
||||
Tu terminal confirmará que todo está configurado.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplicación generada correctamente" />
|
||||
</div>
|
||||
|
||||
**Después de esta fase:** tienes un servidor de Twenty en ejecución en [http://localhost:2020](http://localhost:2020) con tu CLI autorizada para sincronizar con él.
|
||||
|
||||
<Note>
|
||||
Si Docker no está instalado o en ejecución, el generador te indicará el comando de inicio correcto para tu sistema operativo. Una vez que Docker esté en marcha, puedes reanudar con `yarn twenty docker:start` — no es necesario volver a generar la estructura.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Sincroniza tus cambios
|
||||
|
||||
Este es el ciclo interno en el que pasarás la mayor parte del tiempo.
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Esto observa `src/`, recompila en cada cambio y sincroniza el resultado con el servidor. Edita un archivo, guarda y, en cuestión de unos segundos, el servidor reflejará el cambio. Verás un panel de estado en vivo en tu terminal.
|
||||
|
||||
Para una salida más detallada (registros de compilación, solicitudes de sincronización, trazas de errores), añade `--verbose`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.png" alt="Salida del modo de desarrollo en la terminal" />
|
||||
</div>
|
||||
|
||||
Abre [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) en tu navegador. Deberías ver tu app listada en **Your Apps**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista de Your Apps que muestra My twenty app" />
|
||||
</div>
|
||||
|
||||
Haz clic en **My twenty app** para ver su **registro de la aplicación** — un registro a nivel de servidor que describe tu app (nombre, identificador, credenciales de OAuth, origen). Un único registro puede instalarse en varios espacios de trabajo del mismo servidor.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalles del registro de la aplicación" />
|
||||
</div>
|
||||
|
||||
Haz clic en **View installed app** para ver la instalación en el espacio de trabajo. La pestaña **About** muestra la versión actual y las opciones de gestión.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicación instalada" />
|
||||
</div>
|
||||
|
||||
**Después de esta fase:** tienes un ciclo de desarrollo en vivo. Edita cualquier archivo en `src/` y aparecerá en la UI.
|
||||
|
||||
### Sincronización de una sola vez para CI y scripts
|
||||
|
||||
Pasa `--once` para ejecutar una sola compilación + sincronización y salir — mismo pipeline, sin watcher:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Comando | Comportamiento | Cuándo usarlo |
|
||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Supervisa tus archivos fuente y vuelve a sincronizar en cada cambio. Se ejecuta hasta que lo detengas. | Desarrollo local interactivo. |
|
||||
| `yarn twenty dev --once` | Realiza una sola compilación + sincronización y luego sale con el código `0` si tiene éxito o `1` si falla. | CI, hooks de pre-commit, agentes de IA, flujos de trabajo con scripts. |
|
||||
| `yarn twenty dev --once --dry-run` | Genera y muestra los cambios de metadatos **sin aplicarlos**. | Inspeccionar qué cambiaría una sincronización antes de confirmarla. |
|
||||
|
||||
Ambos modos necesitan un remoto autenticado. Consulta [Sincronización y recuperación](/l/es/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run) para obtener más información sobre `--dry-run`.
|
||||
|
||||
### Opciones del modo de desarrollo
|
||||
|
||||
| Opción | Descripción |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `--once` | Compila y sincroniza una vez y luego finaliza. |
|
||||
| `--dry-run` | Con `--once`, obtén una vista previa de los cambios de metadatos sin aplicarlos. No escribe nada. |
|
||||
| `--debounceMs \<ms>` | Establece el tiempo de antirrebote para los cambios de archivo en milisegundos (valor predeterminado: `2000`). |
|
||||
| `--verbose` / `--debug` | Muestra registros de compilación detallados, solicitudes de sincronización y seguimientos de errores. |
|
||||
|
||||
## Lo que puedes crear
|
||||
|
||||
Las apps se componen de **entidades** — cada una definida como un archivo de TypeScript con un único `export default`:
|
||||
|
||||
| Entidad | Qué hace |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Objetos y campos** | Modelos de datos personalizados (Post Card, Invoice, etc.). con campos tipados |
|
||||
| **Funciones de lógica** | Funciones de TypeScript del lado del servidor activadas por rutas HTTP, programaciones de cron o eventos de base de datos |
|
||||
| **Componentes de frontend** | Componentes de React que se renderizan dentro de la interfaz de Twenty (panel lateral, widgets, menú de comandos) |
|
||||
| **Habilidades y agentes** | Capacidades de IA — instrucciones reutilizables y asistentes autónomos |
|
||||
| **Vistas y navegación** | Vistas de lista preconfiguradas y elementos del menú lateral |
|
||||
| **Diseños de página** | Páginas de detalle de registros personalizadas con pestañas y widgets |
|
||||
|
||||
Referencia completa: [Conceptos](/l/es/developers/extend/apps/getting-started/concepts).
|
||||
|
||||
## Próximos pasos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Configuración" icon="screwdriver-wrench" href="/l/es/developers/extend/apps/config/overview">
|
||||
Identidad de la aplicación, rol predeterminado, hooks de instalación y recursos públicos.
|
||||
</Card>
|
||||
<Card title="Datos" icon="database" href="/l/es/developers/extend/apps/data/overview">
|
||||
Objetos, campos y relaciones bidireccionales.
|
||||
</Card>
|
||||
<Card title="Lógica" icon="bolt" href="/l/es/developers/extend/apps/logic/overview">
|
||||
Funciones de lógica, habilidades, agentes y conexiones OAuth.
|
||||
</Card>
|
||||
<Card title="Diseño" icon="table-columns" href="/l/es/developers/extend/apps/layout/overview">
|
||||
Vistas, navegación, diseños de página y componentes de frontend.
|
||||
</Card>
|
||||
<Card title="Operaciones" icon="rocket" href="/l/es/developers/extend/apps/operations/overview">
|
||||
CLI, pruebas, remotos, CI y publicación de tu aplicación.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Andamiaje
|
||||
description: Genera archivos de entidad de forma interactiva con yarn twenty dev:add — objetos, campos, vistas, funciones de lógica y más.
|
||||
icon: wand-magic-sparkles
|
||||
---
|
||||
|
||||
En lugar de crear archivos de entidad a mano, usa el generador interactivo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add
|
||||
```
|
||||
|
||||
Te pide que elijas un tipo de entidad y te guía por los campos requeridos; luego escribe un archivo listo para usar con un `universalIdentifier` estable y la llamada correcta a `defineEntity()`.
|
||||
|
||||
También puedes pasar el tipo de entidad directamente para omitir la primera pregunta:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
yarn twenty dev:add logicFunction
|
||||
yarn twenty dev:add frontComponent
|
||||
```
|
||||
|
||||
## Tipos de entidad disponibles
|
||||
|
||||
| Tipo de entidad | Comando | Archivo generado |
|
||||
| ------------------------------- | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| Objeto | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
|
||||
| Campo | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
|
||||
| Función de lógica | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| Componente de frontend | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| Rol | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
|
||||
| Habilidad | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
|
||||
| Agente | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
|
||||
| Vista | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| Elemento del menú de navegación | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| Diseño de página | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
|
||||
## Qué genera el generador
|
||||
|
||||
Cada tipo de entidad tiene su propia plantilla. Por ejemplo, `yarn twenty dev:add object` solicita:
|
||||
|
||||
1. **Nombre (singular)** — p. ej., `invoice`
|
||||
2. **Nombre (plural)** — p. ej., `invoices`
|
||||
3. **Etiqueta (singular)** — se completa automáticamente a partir del nombre (p. ej., `Invoice`)
|
||||
4. **Etiqueta (plural)** — se completa automáticamente (p. ej., `Invoices`)
|
||||
5. **¿Crear una vista y un elemento de navegación?** — si respondes que sí, el generador también crea una vista correspondiente y un enlace en la barra lateral para el nuevo objeto.
|
||||
|
||||
Otros tipos de entidad tienen indicaciones más simples: la mayoría solo piden un nombre.
|
||||
|
||||
El tipo de entidad `field` es más detallado: pide el nombre del campo, etiqueta, tipo (de una lista de todos los tipos de campo disponibles como `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.) y el `universalIdentifier` del objeto de destino.
|
||||
|
||||
## Ruta de salida personalizada
|
||||
|
||||
Usa la opción `--path` para colocar el archivo generado en una ubicación personalizada:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add logicFunction --path src/custom-folder
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Solución de problemas
|
||||
description: "Problemas comunes en la primera ejecución: Docker, versión de Node, Yarn, dependencias."
|
||||
icon: llave inglesa
|
||||
---
|
||||
|
||||
* **Errores de Docker** — Asegúrate de que Docker Desktop (o el daemon) esté en ejecución antes de `yarn twenty docker:start`. El mensaje de error mostrará el comando de inicio correcto para tu sistema operativo.
|
||||
* **Versión de Node incorrecta** — Se requiere 24+. Compruébalo con `node -v`.
|
||||
* **Falta Yarn 4** — Ejecuta `corepack enable`.
|
||||
* **Dependencias rotas** — `rm -rf node_modules && yarn install`.
|
||||
* **Errores de `twenty-sdk` tras actualizar a la v2.8.0** — Pasó de `dependencies` a `devDependencies` en la v2.8.0. Consulta [Estructura del proyecto → Dependencias](/l/es/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
* **`twenty build` muestra una advertencia sobre `twenty-client-sdk` en `dependencies`** — Twenty lo proporciona en tiempo de ejecución, por lo que debería trasladarse a `devDependencies` junto con `twenty-sdk`. Consulta [Estructura del proyecto → Dependencias](/l/es/developers/extend/apps/getting-started/project-structure#dependencies).
|
||||
|
||||
¿Atascado? Pide ayuda en el [Discord de Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Elementos del menú de comandos
|
||||
description: Expón componentes de frontend como acciones rápidas y entradas del menú de comandos (Cmd+K) con defineCommandMenuItem.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
Un **elemento del menú de comandos** es el puente entre el usuario y un [componente de frontend](/l/es/developers/extend/apps/layout/front-components). Registra el componente en el menú de comandos de Twenty (Cmd+K) y, opcionalmente, como un botón de acción rápida anclado en la esquina superior derecha de la página.
|
||||
|
||||
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
## Campos de configuración
|
||||
|
||||
| Campo | Obligatorio | Descripción |
|
||||
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sí | ID único estable para el comando |
|
||||
| `label` | Sí | Etiqueta completa mostrada en el menú de comandos (Cmd+K) |
|
||||
| `frontComponentUniversalIdentifier` | Sí | El `universalIdentifier` del componente de frontend que abre este comando |
|
||||
| `shortLabel` | No | Etiqueta corta mostrada en el botón de acción rápida anclado |
|
||||
| `icon` | No | Nombre del ícono mostrado junto a la etiqueta (p. ej., 'IconBolt', 'IconSend') |
|
||||
| `isPinned` | No | Cuando es `true`, muestra el comando como un botón de acción rápida en la esquina superior derecha de la página |
|
||||
| `availabilityType` | No | Controla dónde aparece el comando: 'GLOBAL' (siempre disponible), 'RECORD_SELECTION' (solo cuando hay registros seleccionados) o 'FALLBACK' (se muestra cuando ningún otro comando coincide) |
|
||||
| `availabilityObjectUniversalIdentifier` | No | Restringe el comando a páginas de un tipo de objeto específico (p. ej., solo en registros de Company) |
|
||||
| `conditionalAvailabilityExpression` | No | Una expresión booleana que controla dinámicamente la visibilidad (ver abajo) |
|
||||
|
||||
## Comandos sin interfaz
|
||||
|
||||
Un elemento del menú de comandos emparejado con un [headless front component](/l/es/developers/extend/apps/layout/front-components#headless-vs-non-headless) es la forma idónea de ofrecer una acción de un solo clic: ejecutar código, navegar o confirmar y ejecutar. La página Front Components abarca los [SDK Command components](/l/es/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) que gestionan el patrón de acción y desmontaje.
|
||||
|
||||
Un flujo típico:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
## Expresiones de disponibilidad condicional
|
||||
|
||||
El campo `conditionalAvailabilityExpression` te permite controlar cuándo es visible un comando en función del contexto de la página actual. Importa variables tipadas y operadores desde `twenty-sdk` para construir expresiones:
|
||||
|
||||
```ts src/command-menu-items/bulk-update.command-menu-item.ts
|
||||
import {
|
||||
defineCommandMenuItem,
|
||||
objectPermissions,
|
||||
everyEquals,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: '...',
|
||||
label: 'Bulk Update',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: '...',
|
||||
conditionalAvailabilityExpression: everyEquals(
|
||||
objectPermissions,
|
||||
'canUpdateObjectRecords',
|
||||
true,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`RECORD_SELECTION` ya implica una selección no vacía; usa `numberOfSelectedRecords` solo para recuentos específicos (por ejemplo, `>= 2`).
|
||||
</Note>
|
||||
|
||||
### Variables de contexto
|
||||
|
||||
Estas representan el estado actual de la página:
|
||||
|
||||
| Variable | Tipo | Descripción |
|
||||
| ------------------------------ | --------- | ------------------------------------------------------------------- |
|
||||
| `pageType` | `string` | Tipo de página actual (p. ej., 'RecordIndexPage', 'RecordShowPage') |
|
||||
| `isInSidePanel` | `boolean` | Si el componente se renderiza en un panel lateral |
|
||||
| `numberOfSelectedRecords` | `number` | Número de registros seleccionados actualmente |
|
||||
| `isSelectAll` | `boolean` | Si "seleccionar todo" está activo |
|
||||
| `selectedRecords` | `array` | Los objetos de registro seleccionados |
|
||||
| `favoriteRecordIds` | `array` | IDs de registros marcados como favoritos |
|
||||
| `objectPermissions` | `object` | Permisos para el tipo de objeto actual |
|
||||
| `targetObjectReadPermissions` | `object` | Permisos de lectura para el objeto de destino |
|
||||
| `targetObjectWritePermissions` | `object` | Permisos de escritura para el objeto de destino |
|
||||
| `featureFlags` | `object` | Indicadores de características activos |
|
||||
| `objectMetadataItem` | `object` | Metadatos del tipo de objeto actual |
|
||||
| `hasAnySoftDeleteFilterOnView` | `boolean` | Si la vista actual tiene un filtro de eliminación lógica |
|
||||
|
||||
### Operadores
|
||||
|
||||
Combina variables en expresiones booleanas:
|
||||
|
||||
| Operador | Descripción |
|
||||
| ----------------------------------- | --------------------------------------------------------------------- |
|
||||
| `isDefined(value)` | `true` si el valor no es null/undefined |
|
||||
| `isNonEmptyString(value)` | `true` si el valor es una cadena no vacía |
|
||||
| `includes(array, value)` | `true` si el arreglo contiene el valor |
|
||||
| `includesEvery(array, prop, value)` | `true` si la propiedad de cada elemento incluye el valor |
|
||||
| `every(array, prop)` | `true` si la propiedad es truthy en cada elemento |
|
||||
| `everyDefined(array, prop)` | `true` si la propiedad está definida en cada elemento |
|
||||
| `everyEquals(array, prop, value)` | `true` si la propiedad es igual al valor en cada elemento |
|
||||
| `some(array, prop)` | `true` si la propiedad es truthy en al menos un elemento |
|
||||
| `someDefined(array, prop)` | `true` si la propiedad está definida en al menos un elemento |
|
||||
| `someEquals(array, prop, value)` | `true` si la propiedad es igual al valor en al menos un elemento |
|
||||
| `someNonEmptyString(array, prop)` | `true` si la propiedad es una cadena no vacía en al menos un elemento |
|
||||
| `none(array, prop)` | `true` si la propiedad es falsy en cada elemento |
|
||||
| `noneDefined(array, prop)` | `true` si la propiedad es undefined en cada elemento |
|
||||
| `noneEquals(array, prop, value)` | `true` si la propiedad no es igual al valor en ningún elemento |
|
||||
@@ -0,0 +1,545 @@
|
||||
---
|
||||
title: Componentes de frontend
|
||||
description: Crea componentes de React que se renderizan dentro de la UI de Twenty con aislamiento en entorno sandbox.
|
||||
icon: window-maximize
|
||||
---
|
||||
|
||||
Los componentes de frontend son componentes de React que se renderizan directamente dentro de la UI de Twenty. Se ejecutan en un **Web Worker aislado** usando Remote DOM: tu código está aislado (sandboxed) pero se renderiza de forma nativa en la página, no en un iframe.
|
||||
|
||||
## Dónde se pueden usar los componentes de front
|
||||
|
||||
Los componentes de front pueden renderizarse en dos ubicaciones dentro de Twenty:
|
||||
|
||||
* **Panel lateral** — Los componentes de front no headless se abren en el panel lateral derecho. Este es el comportamiento predeterminado cuando un componente de front se activa desde el menú de comandos.
|
||||
* **Widgets (tableros y páginas de registros)** — Los componentes de front pueden incrustarse como widgets dentro de los [diseños de página](/l/es/developers/extend/apps/layout/page-layouts). Al configurar un tablero o el diseño de una página de registro, los usuarios pueden agregar un widget de componente de front.
|
||||
|
||||
Un componente de front por sí solo no es accesible desde la interfaz de usuario; necesitas *exponerlo*. Las dos formas de hacerlo son:
|
||||
|
||||
* **Emparejarlo con un [elemento del menú de comandos](/l/es/developers/extend/apps/layout/command-menu-items)**: lo registra en el menú de comandos (Cmd+K) y, de forma opcional, como una acción rápida fijada.
|
||||
* **Incrustarlo como widget en un [diseño de página](/l/es/developers/extend/apps/layout/page-layouts)**: lo coloca en la página de detalles de un registro o en un tablero.
|
||||
|
||||
## Ejemplo básico
|
||||
|
||||
La forma más rápida de ver un componente de front en acción es emparejarlo con un [`defineCommandMenuItem`](/l/es/developers/extend/apps/layout/command-menu-items), de modo que aparezca como un botón de acción rápida en la esquina superior derecha de la página:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello from my app!</h1>
|
||||
<p>This component renders inside Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
name: 'hello-world',
|
||||
description: 'A simple front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/hello-world.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
Después de sincronizar con `yarn twenty dev` (o ejecutar una sola vez `yarn twenty dev --once`), la acción rápida aparece en la esquina superior derecha de la página:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Botón de acción rápida en la esquina superior derecha" />
|
||||
</div>
|
||||
|
||||
Haz clic para renderizar el componente en línea.
|
||||
|
||||
## Campos de configuración
|
||||
|
||||
| Campo | Obligatorio | Descripción |
|
||||
| --------------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | Sí | ID único estable para este componente |
|
||||
| `component` | Sí | Una función de componente de React |
|
||||
| `name` | No | Nombre para mostrar |
|
||||
| `description` | No | Descripción de lo que hace el componente |
|
||||
| `isHeadless` | No | Configura en `true` si el componente no tiene UI visible (ver abajo) |
|
||||
|
||||
## Colocar un componente de frontend en una página
|
||||
|
||||
Más allá de los comandos, puedes incrustar un componente de frontend directamente en una página de registro agregándolo como un widget en un **diseño de página**. Consulta [Diseños de página](/l/es/developers/extend/apps/layout/page-layouts) para más detalles.
|
||||
|
||||
## Headless vs no headless
|
||||
|
||||
Los componentes de front vienen en dos modos de renderizado controlados por la opción `isHeadless`:
|
||||
|
||||
**No headless (predeterminado)** — El componente renderiza una UI visible. Cuando se activa desde el menú de comandos, se abre en el panel lateral. Este es el comportamiento predeterminado cuando `isHeadless` es `false` o se omite.
|
||||
|
||||
**Headless (`isHeadless: true`)** — El componente se monta de forma invisible en segundo plano. No abre el panel lateral. Los componentes headless están diseñados para acciones que ejecutan lógica y luego se desmontan — por ejemplo, ejecutar una tarea asíncrona, navegar a una página o mostrar un modal de confirmación. Se combinan de forma natural con los componentes Command del SDK descritos a continuación.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
}, [recordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-tracker',
|
||||
description: 'Tracks record views silently',
|
||||
isHeadless: true,
|
||||
component: SyncTracker,
|
||||
});
|
||||
```
|
||||
|
||||
Como el componente devuelve `null`, Twenty omite renderizar un contenedor para él — no aparece espacio vacío en el diseño. El componente sigue teniendo acceso a todos los hooks y a la API de comunicación con el host.
|
||||
|
||||
## Componentes Command del SDK
|
||||
|
||||
El paquete `twenty-sdk` proporciona cuatro componentes auxiliares Command diseñados para componentes de front headless. Cada componente ejecuta una acción al montarse, gestiona los errores mostrando una notificación tipo snackbar y desmonta automáticamente el componente de front al finalizar.
|
||||
|
||||
Impórtalos desde `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — Ejecuta un callback asíncrono mediante la prop `execute`.
|
||||
* **`CommandLink`** — Navega a una ruta de la aplicación. Props: `to`, `params`, `queryParams`, `options`.
|
||||
* **`CommandModal`** — Abre un modal de confirmación. Si el usuario confirma, ejecuta el callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — Abre una página específica del panel lateral. Props: `page`, `pageTitle`, `pageIcon`.
|
||||
|
||||
Aquí tienes un ejemplo completo de un componente de front headless que usa `Command` para ejecutar una acción desde el menú de comandos:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
Y un ejemplo que usa `CommandModal` para pedir confirmación antes de ejecutar:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Llamar a una función de lógica
|
||||
|
||||
Los componentes de front se ejecutan en el navegador dentro de un Web Worker aislado (sandboxed), mientras que las [funciones de lógica](/l/es/developers/extend/apps/logic/logic-functions) se ejecutan en el servidor. No hay una llamada directa en el mismo proceso entre ambos; en su lugar, un componente de front accede a una función de lógica a través de HTTP.
|
||||
|
||||
Una función de lógica declarada con `httpRouteTriggerSettings` se expone bajo el endpoint `/s/` en `${TWENTY_API_URL}/s\<path>`. Tu componente de front llama a esa ruta con el `RestApiClient` de `twenty-client-sdk/rest`, que se autentica con el `TWENTY_APP_ACCESS_TOKEN` que Twenty inyecta en el worker.
|
||||
|
||||
El `RestApiClient` está diseñado precisamente para esto. Lee `TWENTY_API_URL` y `TWENTY_APP_ACCESS_TOKEN` del entorno del worker, añade la cabecera `Authorization: Bearer`, serializa y analiza JSON, y lanza un `RestApiClientError` cuando faltan el token o la URL o la respuesta no es 2xx, para que no tengas que volver a implementar ese código repetitivo en cada componente.
|
||||
|
||||
Un componente de front sin interfaz (headless) puede ejecutar la llamada al montar mediante el componente `Command` y luego desmontarse automáticamente:
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { RestApiClient } from 'twenty-client-sdk/rest';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
const client = new RestApiClient();
|
||||
|
||||
await client.post('/s/github/fetch-prs', {
|
||||
owner: 'twentyhq',
|
||||
repo: 'twenty',
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'sync-prs',
|
||||
description: 'Triggers the fetch-prs logic function',
|
||||
isHeadless: true,
|
||||
component: SyncPrs,
|
||||
});
|
||||
```
|
||||
|
||||
La ruta que se pasa al cliente es la ruta pública de la ruta: el `httpRouteTriggerSettings.path` de la función lógica con el prefijo `/s`. Mantén `isAuthRequired: true`; el cliente proporciona el token de acceso de la aplicación que Twenty emite para tu componente:
|
||||
|
||||
```ts src/logic-functions/fetch-prs.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
|
||||
// ...fetch from GitHub and persist records...
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-prs',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/github/fetch-prs',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`TWENTY_API_URL` y `TWENTY_APP_ACCESS_TOKEN` se inyectan automáticamente; consulta [Variables de la aplicación](#application-variables). Dado que las variables de aplicación secretas nunca se exponen a los componentes de front, mantén las claves de API y otra lógica confidencial en la función de lógica, no en el componente de front.
|
||||
</Note>
|
||||
|
||||
### Referencia de `RestApiClient`
|
||||
|
||||
Importa `RestApiClient` desde `twenty-client-sdk/rest`. Pertenece a la misma familia de clientes que `CoreApiClient` y `MetadataApiClient`, pero se dirige a las rutas HTTP de tu aplicación en lugar de a la API de GraphQL.
|
||||
|
||||
| Método | Descripción |
|
||||
| --------------------------------- | -------------------------------------------- |
|
||||
| `get(path, options?)` | Envía una solicitud `GET` |
|
||||
| `post(path, body?, options?)` | Envía una solicitud `POST` |
|
||||
| `put(path, body?, options?)` | Envía una solicitud `PUT` |
|
||||
| `patch(path, body?, options?)` | Envía una solicitud `PATCH` |
|
||||
| `delete(path, options?)` | Envía una solicitud `DELETE` |
|
||||
| `request(method, path, options?)` | Solicitud genérica con cualquier método HTTP |
|
||||
|
||||
`options` acepta `headers`, `query` (un registro de parámetros de cadena de consulta; los valores nulos o indefinidos se omiten) y un `AbortSignal` mediante `signal`. Un objeto `body` que no sea de tipo `FormData` se serializa automáticamente como JSON. Ante un `401`, el cliente actualiza el token de acceso una vez a través del host y vuelve a intentar la solicitud.
|
||||
|
||||
La URL base y el token se resuelven desde el entorno de forma predeterminada. Pasa opciones de sobrescritura al constructor cuando sea necesario — por ejemplo, en pruebas:
|
||||
|
||||
```ts
|
||||
const client = new RestApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
token: 'my-token',
|
||||
});
|
||||
```
|
||||
|
||||
Las solicitudes fallidas lanzan un `RestApiClientError` que expone `status`, `statusText`, `url` y el `body` analizado:
|
||||
|
||||
```tsx
|
||||
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
|
||||
|
||||
const client = new RestApiClient();
|
||||
|
||||
try {
|
||||
const prs = await client.get('/s/github/fetch-prs', {
|
||||
query: { state: 'open' },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof RestApiClientError) {
|
||||
console.error(error.status, error.body);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Acceder al contexto de ejecución
|
||||
|
||||
Dentro de tu componente, usa hooks del SDK para acceder al usuario actual, el registro y la instancia del componente:
|
||||
|
||||
```tsx src/front-components/record-info.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User: {userId}</p>
|
||||
<p>Record: {recordId ?? 'No record context'}</p>
|
||||
<p>Component: {componentId}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
|
||||
name: 'record-info',
|
||||
component: RecordInfo,
|
||||
});
|
||||
```
|
||||
|
||||
Hooks disponibles:
|
||||
|
||||
| Hook | Devuelve | Descripción |
|
||||
| --------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `useUserId()` | `string` o `null` | El ID del usuario actual |
|
||||
| `useSelectedRecordIds()` | `string[]` | Todos los ID de los registros seleccionados (array vacío si no hay ninguno seleccionado) |
|
||||
| `useRecordId()` | `string` o `null` | **Obsoleto.** Usa `useSelectedRecordIds()` en su lugar |
|
||||
| `useFrontComponentId()` | `string` | El ID de esta instancia del componente |
|
||||
| `useColorScheme()` | `'light'` o `'dark'` | La combinación de colores activa de la interfaz de usuario del host (`System` ya está resuelto) |
|
||||
| `useFrontComponentExecutionContext(selector)` | varía | Accede al contexto de ejecución completo con una función selectora |
|
||||
|
||||
## Variables de aplicación
|
||||
|
||||
Las variables de aplicación definidas en [`defineApplication()`](/l/es/developers/extend/apps/config/application) con `isSecret: false` están disponibles dentro de los componentes de front mediante la utilidad `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Las variables secretas (`isSecret: true`) **no** se exponen a los componentes de front. Solo están disponibles en las [funciones de lógica](/l/es/developers/extend/apps/logic/logic-functions), que se ejecutan del lado del servidor. Esto evita que valores confidenciales como las claves de API se envíen al navegador.
|
||||
</Warning>
|
||||
|
||||
Las siguientes variables de sistema siempre están disponibles a través de `process.env`:
|
||||
|
||||
| Variable | Descripción |
|
||||
| ------------------------- | -------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | URL base de la API de Twenty |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Token de corta duración limitado al rol de tu aplicación |
|
||||
|
||||
## API de comunicación con el host
|
||||
|
||||
Los componentes de frontend pueden activar navegación, modales y notificaciones usando funciones de `twenty-sdk`:
|
||||
|
||||
| Función | Descripción |
|
||||
| ----------------------------------------------- | -------------------------------------------- |
|
||||
| `navigate(to, params?, queryParams?, options?)` | Navegar a una página en la aplicación |
|
||||
| `openSidePanelPage(params)` | Abrir un panel lateral |
|
||||
| `closeSidePanel()` | Cerrar el panel lateral |
|
||||
| `openCommandConfirmationModal(params)` | Mostrar un cuadro de diálogo de confirmación |
|
||||
| `enqueueSnackbar(params)` | Mostrar una notificación tipo toast |
|
||||
| `unmountFrontComponent()` | Desmontar el componente |
|
||||
| `updateProgress(progress)` | Actualizar un indicador de progreso |
|
||||
|
||||
Aquí tienes un ejemplo que usa la API del host para mostrar un snackbar y cerrar el panel lateral después de que una acción finaliza:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
### Trabajar con varios registros
|
||||
|
||||
Usa `useSelectedRecordIds()` para manejar varios registros seleccionados. Esto es útil para operaciones por lotes:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
|
||||
const handleExport = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
for (const recordId of selectedRecordIds) {
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { exported: true } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: `Exported ${selectedRecordIds.length} records`,
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Export {selectedRecordIds.length} selected record(s)?</p>
|
||||
<button onClick={handleExport}>Export</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Recursos públicos
|
||||
|
||||
Los componentes de frontend pueden acceder a archivos del directorio `public/` de la aplicación usando `getPublicAssetUrl`:
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getPublicAssetUrl } from 'twenty-sdk/utils';
|
||||
|
||||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'logo',
|
||||
component: Logo,
|
||||
});
|
||||
```
|
||||
|
||||
Consulta la [sección de recursos públicos](/l/es/developers/extend/apps/config/public-assets) para más detalles.
|
||||
|
||||
## Estilo
|
||||
|
||||
Los componentes de frontend admiten varios enfoques de estilos. Puedes usar:
|
||||
|
||||
* **Estilos en línea** — `style={{ color: 'red' }}`
|
||||
* **Componentes de Twenty UI** — importa desde `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar y más)
|
||||
* **Emotion** — CSS-in-JS con `@emotion/react`
|
||||
* **Styled-components** — patrones de `styled.div`
|
||||
* **Tailwind CSS** — clases utilitarias
|
||||
* **Cualquier librería CSS-in-JS** compatible con React
|
||||
|
||||
```tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Button, Tag, Status } from 'twenty-sdk/ui';
|
||||
|
||||
const StyledWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
|
||||
<Button title="Click me" onClick={() => alert('Clicked!')} />
|
||||
<Tag text="Active" color="green" />
|
||||
<Status color="green" text="Online" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
|
||||
name: 'styled-widget',
|
||||
component: StyledWidget,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Elementos del menú de navegación
|
||||
description: Agrega entradas personalizadas a la barra lateral del espacio de trabajo — enlaces a vistas guardadas o URLs externas.
|
||||
icon: bars
|
||||
---
|
||||
|
||||
Un **elemento del menú de navegación** es una entrada en la barra lateral izquierda. Usa `defineNavigationMenuItem()` para distribuir enlaces personalizados en la barra lateral — normalmente uno por cada [vista](/l/es/developers/extend/apps/layout/views) que publiques — o para apuntar a URL externas.
|
||||
|
||||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
## Puntos clave
|
||||
|
||||
* `type` determina a qué enlaza el elemento del menú. Cada tipo se asocia con un campo identificador específico:
|
||||
|
||||
| Tipo | Qué hace | Campo obligatorio |
|
||||
| ------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `NavigationMenuItemType.VIEW` | Abre una vista guardada | `viewUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.LINK` | Abre una URL externa | `link` |
|
||||
| `NavigationMenuItemType.FOLDER` | Agrupa elementos anidados bajo una etiqueta | `name` (y los elementos secundarios hacen referencia a la carpeta mediante `folderUniversalIdentifier`) |
|
||||
| `NavigationMenuItemType.OBJECT` | Abre la página de índice predeterminada de un objeto | `targetObjectUniversalIdentifier` |
|
||||
| `NavigationMenuItemType.PAGE_LAYOUT` | Abre un diseño de página independiente | `pageLayoutUniversalIdentifier` |
|
||||
|
||||
* `position` controla el orden en la barra lateral.
|
||||
|
||||
* `icon` y `color` son opcionales y personalizan el aspecto de la entrada.
|
||||
|
||||
* `folderUniversalIdentifier` también está disponible en cualquier elemento para anidarlo dentro de un elemento padre de tipo `FOLDER`.
|
||||
|
||||
<Note>
|
||||
**Error común:** crear un objeto sin una vista asociada y un elemento del menú de navegación hace que ese objeto sea invisible para los usuarios. A menos que sea un objeto técnico/interno, cada objeto personalizado debería tener una vista predeterminada *y* una entrada en la barra lateral que apunte a ella.
|
||||
</Note>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Resumen
|
||||
description: Coloca tu aplicación dentro de la interfaz de usuario de Twenty — entradas de la barra lateral, vistas guardadas, pestañas de la página de registro y componentes React aislados.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
La **capa de diseño** de una aplicación de Twenty es todo lo que el usuario ve: dónde aparece la aplicación en la barra lateral, qué vistas de lista incluye, cómo se organizan sus páginas de detalles de registro y qué componentes React personalizados se renderizan dentro de esas páginas.
|
||||
|
||||
```text
|
||||
Sidebar Record list Record detail page
|
||||
─────── ─────────── ──────────────────
|
||||
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
|
||||
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
|
||||
[📋 Inbox ] │ ──────── │ │ [Notes ] │
|
||||
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
|
||||
│ │ Acme │ │ │ adds a tab...
|
||||
└ defineNavi- │ … │ │ ┌────────────────┐ │
|
||||
gationMenu- └────▲─────┘ │ │ │ │
|
||||
Item points │ │ │ React UI │◀── …with a
|
||||
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
|
||||
└ defineView │ │ a Worker) │ │ widget inside
|
||||
picks columns │ └────────────────┘ │
|
||||
and filters └─────────────────────┘
|
||||
```
|
||||
|
||||
## En esta sección
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Vistas" icon="list" href="/l/es/developers/extend/apps/layout/views">
|
||||
`defineView` — configuraciones de listas guardadas: columnas visibles, filtros, grupos.
|
||||
</Card>
|
||||
<Card title="Elementos del menú de navegación" icon="bars" href="/l/es/developers/extend/apps/layout/navigation-menu-items">
|
||||
`defineNavigationMenuItem` — entradas de la barra lateral que apuntan a vistas o URL externas.
|
||||
</Card>
|
||||
<Card title="Diseños de Página" icon="table-columns" href="/l/es/developers/extend/apps/layout/page-layouts">
|
||||
`definePageLayout` y `definePageLayoutTab` — pestañas y widgets en la página de detalles de un registro.
|
||||
</Card>
|
||||
<Card title="Componentes de frontend" icon="window-maximize" href="/l/es/developers/extend/apps/layout/front-components">
|
||||
`defineFrontComponent` — componentes React aislados que se renderizan dentro de Twenty.
|
||||
</Card>
|
||||
<Card title="Elementos del menú de comandos" icon="terminal" href="/l/es/developers/extend/apps/layout/command-menu-items">
|
||||
`defineCommandMenuItem` — registra componentes de frontend como entradas Cmd+K y acciones rápidas.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Dónde aparece la aplicación
|
||||
|
||||
| Ubicación | Qué controla | Entidad |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| **Barra lateral** | Una entrada personalizada que enlaza a una vista guardada o a una URL externa | `defineNavigationMenuItem` |
|
||||
| **Lista de registros** | Una configuración guardada para un objeto — columnas visibles, orden, filtros, grupos | `defineView` |
|
||||
| **Página de detalles del registro** | Las pestañas y widgets en una página de registro (de tu propio objeto o de uno estándar) | `definePageLayout`, `definePageLayoutTab` |
|
||||
| **Dentro de cualquiera de las anteriores** | Un widget React personalizado — botones, formularios, paneles, integraciones | `defineFrontComponent` |
|
||||
| **Menú de comandos (Cmd+K)** | Una acción rápida fijada o un comando oculto | `defineCommandMenuItem` |
|
||||
|
||||
Los componentes de frontend se ejecutan dentro de un Web Worker aislado usando Remote DOM — se renderizan de forma nativa en la página (no dentro de un iframe), pero no pueden acceder directamente a la página o al DOM del host. La comunicación con Twenty ocurre a través de una API de host de paso de mensajes.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Diseños de Página
|
||||
description: Personaliza las páginas de detalle de los registros — pestañas, widgets y dónde se renderizan los componentes de frontend — usando `definePageLayout` y `definePageLayoutTab`.
|
||||
icon: table-columns
|
||||
---
|
||||
|
||||
Un **page layout** controla cómo se organiza la página de detalle de un registro: qué pestañas aparecen y qué widgets contienen. Usa `definePageLayout()` para declarar un layout para un objeto que posees, o `definePageLayoutTab()` para agregar una sola pestaña a un layout que ya existe (tuyo o uno estándar de Twenty).
|
||||
|
||||
| Caso de uso | Entidad |
|
||||
| -------------------------------------------------------------------------- | --------------------- |
|
||||
| Define todo el layout para una página de registro en un objeto que posees | `definePageLayout` |
|
||||
| Agrega una pestaña a un layout existente (tu propio objeto o uno estándar) | `definePageLayoutTab` |
|
||||
|
||||
## definePageLayout
|
||||
|
||||
Usa esto cuando eres propietario de toda la página de detalle; normalmente para un objeto personalizado que definiste tú mismo.
|
||||
|
||||
```ts src/page-layouts/example-record-page-layout.ts
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Puntos clave
|
||||
|
||||
* `type` suele ser `'RECORD_PAGE'` para personalizar la vista de detalles de un objeto específico.
|
||||
* `objectUniversalIdentifier` especifica a qué objeto se aplica este diseño.
|
||||
* Cada `tab` define una sección de la página con un `title`, `position` y `layoutMode` (`CANVAS` para un diseño libre).
|
||||
* Cada `widget` dentro de una pestaña puede renderizar un [componente de frontend](/l/es/developers/extend/apps/layout/front-components), una lista de relaciones u otros tipos de widget integrados.
|
||||
* `position` en las pestañas controla su orden. Usa valores más altos (p. ej., 50) para colocar pestañas personalizadas después de las integradas.
|
||||
|
||||
## definePageLayoutTab
|
||||
|
||||
Usa esto cuando solo quieras **agregar** una pestaña a un layout existente; por ejemplo, una pestaña de analíticas en la página estándar de Company o una pestaña de resumen de IA añadida al layout de tu propio objeto.
|
||||
|
||||
```ts src/page-layouts/example-extra-tab.ts
|
||||
import {
|
||||
definePageLayoutTab,
|
||||
PageLayoutTabLayoutMode,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk/define';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||||
|
||||
export default definePageLayoutTab({
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
|
||||
pageLayoutUniversalIdentifier:
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
|
||||
.universalIdentifier,
|
||||
title: 'Hello World',
|
||||
position: 1000,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Puntos clave
|
||||
|
||||
* `pageLayoutUniversalIdentifier` es **obligatorio** y debe apuntar a un page layout que ya exista en el momento de la instalación, ya sea un layout estándar de Twenty o uno definido por tu propia app. Las referencias entre apps a layouts que pertenecen a otra app instalada no son compatibles hoy en día. Cuando falta el layout padre, la instalación falla con un error de validación claro.
|
||||
|
||||
* Para los diseños estándar de Twenty, importa los identificadores desde `twenty-sdk/define`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
|
||||
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
|
||||
// …
|
||||
```
|
||||
|
||||
Cada entrada de diseño también expone sus `tabs` y sus `widgets`, para que puedas hacer referencia a cualquier nivel:
|
||||
|
||||
```ts
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
|
||||
```
|
||||
|
||||
También hay disponible un alias corto `STANDARD_PAGE_LAYOUT`:
|
||||
|
||||
```ts
|
||||
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
|
||||
|
||||
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
|
||||
```
|
||||
|
||||
* `widgets` están limitados solo a esta pestaña: hacen referencia a [componentes de frontend](/l/es/developers/extend/apps/layout/front-components), vistas, etc., exactamente igual que los widgets definidos en línea en `definePageLayout`.
|
||||
|
||||
* `position` controla el orden con respecto a las pestañas existentes en el diseño de página de destino. Elige un valor que sitúe tu pestaña donde la quieras, en relación con las pestañas integradas.
|
||||
|
||||
* Usa esto en lugar de `definePageLayout` cuando solo quieras agregar a un layout existente. Usa `definePageLayout` cuando eres propietario de todo el layout.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Vistas
|
||||
description: Incluye vistas guardadas preconfiguradas — orden de columnas, filtros y grupos — para los objetos de tu aplicación.
|
||||
icon: lista
|
||||
---
|
||||
|
||||
Una **vista** es una configuración guardada de cómo se muestran los registros de un objeto: qué campos aparecen, su orden, si son visibles y qué filtros o grupos se aplican. Usa `defineView()` para incluir vistas preconfiguradas con tu aplicación — normalmente una vista de índice predeterminada para cada objeto personalizado que crees.
|
||||
|
||||
```ts src/views/example-view.ts
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Puntos clave
|
||||
|
||||
* `objectUniversalIdentifier` especifica a qué objeto se aplica esta vista. Puede ser un objeto personalizado que hayas definido o un objeto estándar de Twenty.
|
||||
* `key` determina el tipo de vista — `ViewKey.INDEX` es la vista de lista principal para el objeto.
|
||||
* `fields` controla qué columnas aparecen y en qué orden. Cada campo referencia un `fieldMetadataUniversalIdentifier`.
|
||||
* También puedes definir `filters`, `filterGroups`, `groups` y `fieldGroups` para configuraciones avanzadas.
|
||||
* `position` controla el orden cuando existen múltiples vistas para el mismo objeto.
|
||||
|
||||
## Filtros
|
||||
|
||||
Una vista puede incluir filtros preaplicados. Cada filtro tiene tres coordenadas: el **campo** que se está filtrando, el **operando** (cómo comparar) y el **valor** (contra qué comparar). Las tres deben alinearse: usar un operando que no aplique a un tipo de campo será rechazado en el momento de la sincronización.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Operandos admitidos por tipo de campo
|
||||
|
||||
| Tipo de campo | Operandos admitidos |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Los tipos de campo con nombres similares pueden usar operandos completamente diferentes; `SELECT` y `MULTI_SELECT` son un caso común.
|
||||
|
||||
### Forma del valor por operando
|
||||
|
||||
El campo `value` siempre es un valor serializable en JSON, pero su forma esperada depende del operando:
|
||||
|
||||
| Familia de operandos | Forma del valor | Ejemplo |
|
||||
| ------------------------------------------------------ | ------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` en `SELECT` | array de claves de opciones (cadenas) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` en `MULTI_SELECT` | array de claves de opciones (cadenas) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` en `RELATION` | array de IDs de registros (uuids) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` en campos de tipo texto | cadena | `'acme'` |
|
||||
| `IS`, `IS_NOT` en `NUMBER` | cadena (el valor) | `'5'` |
|
||||
| `IS` en `RATING` / `UUID` | cadena (el valor) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | cadena (el límite) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` en `DATE` / `DATE_TIME` | cadena ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | cadena vacía | `''` |
|
||||
| `IS` en `BOOLEAN` | `'true'` o `'false'` | `'true'` |
|
||||
|
||||
## Cómo aparecen las vistas en la interfaz de usuario
|
||||
|
||||
Una vista por sí sola no es accesible desde la barra lateral. Para que aparezca allí, vincúlala con un [elemento del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items) de tipo `VIEW` que apunte al `universalIdentifier` de la vista. Ese es el patrón canónico: cada objeto personalizado suele incluir una vista predeterminada + una entrada en la barra lateral que la abre.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Conexiones
|
||||
description: Permite que tu aplicación actúe en nombre de un usuario en servicios de terceros mediante OAuth.
|
||||
icon: plug
|
||||
---
|
||||
|
||||
Las conexiones son credenciales que un usuario posee para un servicio externo (Linear, GitHub, Slack, ...). Tu aplicación declara **cómo** se obtienen esas credenciales — un **proveedor de conexión** — y las consume en tiempo de ejecución para realizar llamadas autenticadas a la API de terceros.
|
||||
|
||||
Actualmente solo se admite OAuth 2.0. Los futuros tipos de credenciales (tokens de acceso personal, claves de API, autenticación básica) se integrarán en la misma interfaz — las aplicaciones que ya usan `defineConnectionProvider({ type: 'oauth', ... })` no necesitarán migrar.
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
<Accordion title="defineConnectionProvider" description="Declara cómo se obtienen las conexiones de tu aplicación">
|
||||
|
||||
Un proveedor de conexión describe el flujo de OAuth que tu aplicación necesita. El usuario hace clic en "Agregar conexión" en la configuración de tu aplicación, completa la pantalla de consentimiento del proveedor y se crea una fila `ConnectedAccount` en su espacio de trabajo.
|
||||
|
||||
Una configuración funcional necesita **dos archivos** — el proveedor de conexión y una declaración `serverVariables` correspondiente en `defineApplication` que contiene las credenciales del cliente OAuth.
|
||||
|
||||
```ts src/connection-providers/linear-connection.ts
|
||||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||||
|
||||
export default defineConnectionProvider({
|
||||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||||
name: 'linear',
|
||||
displayName: 'Linear',
|
||||
icon: 'IconBrandLinear',
|
||||
type: 'oauth',
|
||||
oauth: {
|
||||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
// These must match keys in `defineApplication.serverVariables` below.
|
||||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||||
// 'form-urlencoded' for the token request.
|
||||
tokenRequestContentType: 'form-urlencoded',
|
||||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||||
usePkce: false,
|
||||
// Optional: extra query params on the authorize URL.
|
||||
// authorizationParams: { prompt: 'consent' },
|
||||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts src/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'Linear',
|
||||
description: 'Connect Linear to Twenty.',
|
||||
// OAuth client credentials live on the app registration (one OAuth app per
|
||||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||||
// as serverVariables so the admin can fill them in once for all installs.
|
||||
serverVariables: {
|
||||
LINEAR_CLIENT_ID: {
|
||||
description: 'OAuth client ID from your Linear OAuth application.',
|
||||
isSecret: false,
|
||||
isRequired: true,
|
||||
},
|
||||
LINEAR_CLIENT_SECRET: {
|
||||
description: 'OAuth client secret from your Linear OAuth application.',
|
||||
isSecret: true,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
|
||||
* `name` es la cadena de identificador única utilizada en `listConnections({ providerName })` (kebab-case, debe coincidir con `^[a-z][a-z0-9-]*$`).
|
||||
* `displayName` se muestra en la pestaña de configuración por aplicación y en la lista de herramientas de IA.
|
||||
* `clientIdVariable` / `clientSecretVariable` son **nombres**, no valores — deben coincidir con las claves declaradas en `defineApplication.serverVariables`. Los `client_id` y `client_secret` reales los introduce el administrador del servidor a través de la interfaz de registro de la aplicación; nunca se incluyen en tu repositorio.
|
||||
* Usa `serverVariables` (no `applicationVariables`) — las credenciales de OAuth son a nivel de servidor y hay una aplicación OAuth por servidor de Twenty.
|
||||
* Hasta que ambos `serverVariables` estén completos, la pestaña de configuración por aplicación muestra un aviso de "requiere administrador del servidor" y el botón "Agregar conexión" está deshabilitado.
|
||||
* `type: 'oauth'` es el único valor admitido actualmente. El discriminador es compatible hacia adelante: tipos futuros (`'pat'`, `'api-key'`, ...) agregarán nuevos bloques de subconfiguración junto a `oauth`.
|
||||
|
||||
La URL de callback de OAuth que tu proveedor debe autorizar es:
|
||||
|
||||
```
|
||||
https://<your-twenty-server>/auth/apps/callback
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="listConnections / getConnection" description="Usa conexiones desde una función de lógica">
|
||||
|
||||
Dentro de un controlador de función de lógica, `listConnections({ providerName })` devuelve las filas `ConnectedAccount` de esta aplicación para el proveedor indicado, con tokens de acceso actualizados.
|
||||
|
||||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||||
import { listConnections } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const createLinearIssueHandler = async (input: {
|
||||
teamId?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (!input.teamId || !input.title) {
|
||||
return { success: false, error: 'teamId and title are required' };
|
||||
}
|
||||
|
||||
const connections = await listConnections({ providerName: 'linear' });
|
||||
|
||||
// Workspace-shared credentials win when present; fall back to the first
|
||||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||||
// request user's connection via event.userWorkspaceId instead.
|
||||
const connection =
|
||||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||||
};
|
||||
}
|
||||
|
||||
// Use connection.accessToken to call the third-party API.
|
||||
const response = await fetch('https://api.linear.app/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||||
}),
|
||||
});
|
||||
|
||||
return { success: response.ok };
|
||||
};
|
||||
```
|
||||
|
||||
Cada conexión tiene:
|
||||
|
||||
| Campo | Descripción |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ID de fila único; pásalo a `getConnection(id)` para volver a obtener una sola conexión |
|
||||
| `visibility` | `'user'` (privada para un miembro del espacio de trabajo) o `'workspace'` (compartida con todos los miembros) |
|
||||
| `scopes` | Permisos de OAuth concedidos por el proveedor de origen (distintos de `visibility` — no están relacionados) |
|
||||
| `userWorkspaceId` | El id de userWorkspace del propietario — útil para elegir "la conexión del usuario de la solicitud" en activadores de rutas HTTP |
|
||||
| `accessToken` | Token de acceso OAuth actualizado (se renueva automáticamente si ha expirado) |
|
||||
| `name` / `handle` | El nombre para mostrar de la conexión (derivado automáticamente en el callback de OAuth, el usuario puede cambiarlo) |
|
||||
| `authFailedAt` | Se establece cuando la actualización más reciente falló; el usuario debe reconectarse |
|
||||
|
||||
Puntos clave:
|
||||
|
||||
* Pasa `{ providerName }` para filtrar por proveedor; omítelo para obtener todas las conexiones que posee esta aplicación en todos los proveedores.
|
||||
* El servidor actualiza de forma transparente el token de acceso antes de devolver la respuesta. Tu controlador siempre ve un token utilizable (o `authFailedAt` establecido).
|
||||
* `getConnection(id)` es el equivalente de una sola fila.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Visibilidad por usuario vs. compartida con el espacio de trabajo" description="Cómo los usuarios eligen entre credenciales privadas y compartidas">
|
||||
|
||||
Cuando un usuario hace clic en "Agregar conexión", se le solicita que elija una visibilidad:
|
||||
|
||||
* **Solo para mí** — la credencial es privada para el usuario que se conecta. Cualquier función de lógica llamada en su nombre (activador de ruta HTTP con `isAuthRequired: true`) la ve; los activadores de cron y los eventos de base de datos no.
|
||||
* **Compartida en el espacio de trabajo** — cualquier miembro del espacio de trabajo puede usar la credencial. Los activadores de cron y de base de datos también la ven, ya que no tienen usuario de la solicitud.
|
||||
|
||||
Usa la adecuada para cada controlador:
|
||||
|
||||
```ts
|
||||
// HTTP-route trigger — prefer the request user's own connection.
|
||||
const conn =
|
||||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||||
connections.find((c) => c.visibility === 'workspace');
|
||||
|
||||
// Cron trigger — no request user; only shared credentials are sensible.
|
||||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||||
```
|
||||
|
||||
Se permiten múltiples conexiones por (usuario, proveedor), por lo que el mismo usuario puede tener "Linear personal" y "Linear de trabajo" a la vez.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configuración única del proveedor" description="Registra tu aplicación OAuth con el servicio de terceros">
|
||||
|
||||
Para cada proveedor de conexión, el administrador del servidor debe registrar primero una aplicación OAuth en el servicio de terceros.
|
||||
|
||||
1. Ve a la configuración de desarrollador del proveedor (p. ej., https://linear.app/settings/api/applications/new).
|
||||
2. Configura el **URI de redirección** en `\<SERVER_URL>/auth/apps/callback`.
|
||||
3. Copia el **Client ID** y el **Client Secret** generados.
|
||||
4. Abre la aplicación instalada en Twenty como administrador del servidor → establece los valores en los `serverVariables` correspondientes.
|
||||
5. Luego, los miembros del espacio de trabajo pueden agregar conexiones desde la sección **Conexiones** por aplicación.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,515 @@
|
||||
---
|
||||
title: Funciones de lógica
|
||||
description: Defina funciones de TypeScript del lado del servidor con activadores HTTP, de cron y de eventos de base de datos.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
Las funciones lógicas son funciones de TypeScript del lado del servidor que se ejecutan en la plataforma Twenty. Pueden activarse mediante solicitudes HTTP, programaciones de cron o eventos de base de datos — y también pueden exponerse como herramientas para agentes de IA.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineLogicFunction" description="Define funciones de lógica y sus desencadenadores">
|
||||
|
||||
Cada archivo de función usa `defineLogicFunction()` para exportar una configuración con un controlador y desencadenadores opcionales.
|
||||
|
||||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const body = (params.body ?? {}) as { name?: string };
|
||||
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
/*databaseEventTriggerSettings: {
|
||||
eventName: 'people.created',
|
||||
},*/
|
||||
/*cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},*/
|
||||
});
|
||||
```
|
||||
|
||||
Tipos de desencadenadores disponibles:
|
||||
* **httpRoute**: Expone tu función en una ruta y método HTTP **bajo el endpoint `/s/`**:
|
||||
> p. ej., `path: '/post-card/create'` se puede invocar en `https://your-twenty-server.com/s/post-card/create`
|
||||
|
||||
<Note>
|
||||
Para invocar una función de lógica activada por una ruta desde un componente de frontend (headless), consulta [Llamar a una función de lógica](/l/es/developers/extend/apps/layout/front-components#calling-a-logic-function).
|
||||
</Note>
|
||||
* **cron**: Ejecuta tu función en un horario usando una expresión CRON.
|
||||
* **databaseEvent**: Se ejecuta en eventos del ciclo de vida de objetos del espacio de trabajo. Cuando la operación del evento es `updated`, se pueden especificar campos específicos que se deben escuchar en la matriz `updatedFields`. Si se deja sin definir o vacío, cualquier actualización activará la función.
|
||||
> p. ej. `person.updated`, `*.created`, `company.*`
|
||||
|
||||
<Note>
|
||||
También puedes ejecutar manualmente una función usando la CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
|
||||
```
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
Puedes ver los registros con:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:logs
|
||||
```
|
||||
</Note>
|
||||
|
||||
#### Carga útil del disparador de ruta
|
||||
|
||||
Cuando un desencadenador de ruta invoca tu función de lógica, esta recibe un objeto `RoutePayload` que sigue el
|
||||
[formato AWS HTTP API v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
Importa el tipo `RoutePayload` desde `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||||
const { method, path } = event.requestContext.http;
|
||||
|
||||
return { message: 'Success' };
|
||||
};
|
||||
```
|
||||
|
||||
El tipo `RoutePayload` tiene la siguiente estructura:
|
||||
|
||||
| Propiedad | Tipo | Descripción | Ejemplo |
|
||||
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record\<string, string \| undefined>` | Encabezados HTTP (solo aquellos listados en `forwardedRequestHeaders`) | consulta la sección de abajo |
|
||||
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parámetros de consulta (valores múltiples unidos con comas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record\<string, string \| undefined>` | Parámetros de ruta extraídos del patrón de la ruta | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | Cuerpo de la solicitud analizado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `rawBody` | `string \| undefined` | Cuerpo de la solicitud UTF-8 original, antes del análisis de JSON. Útil para verificar firmas de webhooks de estilo HMAC (p. ej., `X-Hub-Signature-256` de GitHub, Stripe). `undefined` cuando el entorno de ejecución no lo conservó. | |
|
||||
| `isBase64Encoded` | `boolean` | Indica si el cuerpo está codificado en base64 | |
|
||||
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
|
||||
| `requestContext.http.path` | `string` | Ruta de la solicitud sin procesar | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
De forma predeterminada, los encabezados HTTP de las solicitudes entrantes **no** se pasan a tu función de lógica por razones de seguridad.
|
||||
Para acceder a encabezados específicos, enuméralos explícitamente en el arreglo `forwardedRequestHeaders`:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
En tu controlador, accede a los encabezados reenviados así:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const signature = event.headers['x-webhook-signature'];
|
||||
const contentType = event.headers['content-type'];
|
||||
|
||||
// Validate webhook signature...
|
||||
return { received: true };
|
||||
};
|
||||
```
|
||||
|
||||
<Note>
|
||||
Los nombres de los encabezados se normalizan a minúsculas. Accede a ellos usando claves en minúsculas (p. ej., `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Respuesta HTTP personalizada
|
||||
|
||||
De forma predeterminada, devolver un valor sencillo desde tu controlador lo envía de vuelta como una respuesta `200` (JSON para objetos, `text/plain` para cadenas). Para controlar el código de estado y los encabezados de la respuesta, devuelve un `Response` desde `twenty-sdk/logic-function`:
|
||||
|
||||
```ts
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
return new Response('<h1>Hello</h1>', {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Por razones de seguridad, los encabezados de la respuesta están restringidos a una lista de permitidos. Cualquier encabezado que no esté en la lista (por ejemplo, `Set-Cookie`, encabezados CORS como `Access-Control-Allow-Origin`, o encabezados personalizados `X-*`) se descarta silenciosamente antes de que se envíe la respuesta. Los encabezados de respuesta permitidos son:
|
||||
|
||||
* `content-type`
|
||||
* `content-language`
|
||||
* `content-disposition`
|
||||
* `cache-control`
|
||||
* `retry-after`
|
||||
|
||||
<Note>
|
||||
El código de estado debe ser un código de estado HTTP válido (entre 100 y 599). Los nombres de los encabezados de respuesta se comparan sin distinguir mayúsculas de minúsculas.
|
||||
</Note>
|
||||
|
||||
#### Payload del disparador de evento de base de datos
|
||||
|
||||
Cuando un disparador de evento de base de datos invoca tu función de lógica, esta recibe un `DatabaseEventPayload` por cada registro modificado. El payload combina metadatos sobre el espacio de trabajo y el objeto de origen con el evento a nivel de registro.
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DatabaseEventPayload,
|
||||
ObjectRecordCreateEvent,
|
||||
ObjectRecordDestroyEvent,
|
||||
ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk/logic-function';
|
||||
|
||||
type Person = {
|
||||
id: string;
|
||||
emails?: { primaryEmail?: string };
|
||||
};
|
||||
```
|
||||
|
||||
La carga útil incluye:
|
||||
|
||||
| Propiedad | Descripción |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| `name` | Nombre del evento, como `person.updated`. |
|
||||
| `workspaceId` | Espacio de trabajo donde ocurrió el evento. |
|
||||
| `objectMetadata` | Metadatos del objeto que cambió. |
|
||||
| `recordId` | Id del registro que cambió. |
|
||||
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Campos del actor cuando el evento fue causado por un usuario del espacio de trabajo. |
|
||||
| `propiedades` | Datos del registro para el evento, con `before`, `after`, `diff` y `updatedFields` según la operación. |
|
||||
|
||||
| Evento | Datos del registro |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `person.created` | `event.properties.after` |
|
||||
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
|
||||
| `person.destroyed` | `event.properties.before` |
|
||||
|
||||
Para eliminaciones lógicas (soft deletes), `.deleted` sigue la estructura de estilo de actualización porque el campo `deletedAt` del registro cambia.
|
||||
Para eliminaciones permanentes, usa `.destroyed`.
|
||||
|
||||
<Note>
|
||||
`databaseEventTriggerSettings.updatedFields` filtra qué eventos de actualización activan la función.
|
||||
`event.properties.updatedFields` te indica qué campos realmente cambiaron en el evento actual.
|
||||
</Note>
|
||||
|
||||
Ejemplo de evento de creación:
|
||||
|
||||
```ts
|
||||
type PersonCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonCreatedEvent) => {
|
||||
const person = event.properties.after;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: person.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Ejemplo de evento de actualización:
|
||||
|
||||
```ts
|
||||
type PersonUpdatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordUpdateEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonUpdatedEvent) => {
|
||||
const { before, after, diff, updatedFields } = event.properties;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
updatedFields,
|
||||
previousEmail: before.emails?.primaryEmail,
|
||||
currentEmail: after.emails?.primaryEmail,
|
||||
emailDiff: diff.emails,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Ejecutar solo en actualizaciones de correo electrónico:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['emails'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Ejemplo de evento de eliminación:
|
||||
|
||||
```ts
|
||||
type PersonDestroyedEvent = DatabaseEventPayload<
|
||||
ObjectRecordDestroyEvent<Person>
|
||||
>;
|
||||
|
||||
const handler = async (event: PersonDestroyedEvent) => {
|
||||
const personBeforeDestroy = event.properties.before;
|
||||
|
||||
return {
|
||||
personId: event.recordId,
|
||||
email: personBeforeDestroy.emails?.primaryEmail,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### Exponer una función como herramienta de IA o acción de flujo de trabajo
|
||||
|
||||
Las funciones lógicas pueden exponerse en dos ámbitos, cada uno con su propio disparador:
|
||||
|
||||
* **`toolTriggerSettings`** — hace que la función sea descubrible por las funciones de IA de Twenty (chat, MCP, llamadas a funciones). Usa el JSON Schema estándar, el formato que los LLM entienden de forma nativa.
|
||||
* **`workflowActionTriggerSettings`** — hace que la función aparezca como un paso en el constructor visual de flujos de trabajo. Usa el `InputSchema` completo de Twenty para que el constructor pueda renderizar editores de campos adecuados, selectores de variables y etiquetas.
|
||||
|
||||
Una función puede optar por una, por la otra o por ambas. Se ubican junto a `cronTriggerSettings`, `databaseEventTriggerSettings` y `httpRouteTriggerSettings` — mismo patrón, misma estructura.
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
toolTriggerSettings: {},
|
||||
});
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
|
||||
* Una función puede mezclar superficies — declara tanto `toolTriggerSettings` como `workflowActionTriggerSettings` para exponerla en el chat Y en el constructor de flujos de trabajo.
|
||||
* Ambos, `toolTriggerSettings.inputSchema` y `workflowActionTriggerSettings.inputSchema`, son opcionales. Cuando se omiten, el generador del manifiesto los infiere a partir del código fuente del controlador (JSON Schema para la herramienta de IA, `InputSchema` de Twenty para la acción de flujo de trabajo). Proporciona uno explícitamente cuando quieras un tipado más rico — por ejemplo, con campos compatibles con `FieldMetadataType` como `CURRENCY` o `RELATION` para el constructor de flujos de trabajo, o con campos `description` que el agente de IA pueda leer:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Escribe una buena `description`.** Los agentes de IA dependen del campo `description` de la función para decidir cuándo usar la herramienta. Sé específico acerca de lo que hace la herramienta y cuándo debe invocarse.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
**Hooks de instalación** — los controladores de preinstalación y postinstalación — comparten este entorno de ejecución, pero se declaran con sus propias funciones 'define' y no aceptan configuraciones de disparador. Consulta [Hooks de instalación](/l/es/developers/extend/apps/config/install-hooks) para `definePreInstallLogicFunction` y `definePostInstallLogicFunction`.
|
||||
</Note>
|
||||
|
||||
## Clientes de API tipados (twenty-client-sdk)
|
||||
|
||||
El paquete `twenty-client-sdk` proporciona dos clientes GraphQL tipados para interactuar con la API de Twenty desde tus funciones de lógica y componentes de frontend.
|
||||
|
||||
| Cliente | Importar | Endpoint | ¿Generado? |
|
||||
| ------------------- | ---------------------------- | ---------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — datos del espacio de trabajo (registros, objetos) | Sí, en tiempo de desarrollo/compilación |
|
||||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuración del espacio de trabajo, cargas de archivos | No, viene preconstruido |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="CoreApiClient" description="Consultar y modificar datos del espacio de trabajo (registros, objetos)">
|
||||
|
||||
`CoreApiClient` es el cliente principal para consultar y mutar datos del espacio de trabajo. Se **genera a partir del esquema de tu espacio de trabajo** durante `yarn twenty dev` o `yarn twenty dev:build`, por lo que está completamente tipado para coincidir con tus objetos y campos.
|
||||
|
||||
```ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Query records
|
||||
const { companies } = await client.query({
|
||||
companies: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
domainName: {
|
||||
primaryLinkLabel: true,
|
||||
primaryLinkUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a record
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
El cliente usa una sintaxis de conjunto de selección: pasa `true` para incluir un campo, usa `__args` para los argumentos y anida objetos para las relaciones. Obtienes autocompletado completo y verificación de tipos basados en el esquema de tu espacio de trabajo.
|
||||
|
||||
<Note>
|
||||
**CoreApiClient se genera en tiempo de desarrollo/compilación.** Si intentas usarlo sin ejecutar primero `yarn twenty dev` o `yarn twenty dev:build`, lanzará un error. La generación ocurre automáticamente: la CLI inspecciona el esquema GraphQL de tu espacio de trabajo y genera un cliente tipado usando `@genql/cli`.
|
||||
</Note>
|
||||
|
||||
#### Uso de CoreSchema para anotaciones de tipos
|
||||
|
||||
`CoreSchema` proporciona tipos de TypeScript que coinciden con los objetos de tu espacio de trabajo; útil para tipar el estado de componentes o parámetros de funciones:
|
||||
|
||||
```ts
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
const [company, setCompany] = useState<
|
||||
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
|
||||
>(undefined);
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
company: {
|
||||
__args: { filter: { position: { eq: 1 } } },
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
setCompany(result.company);
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MetadataApiClient" description="Configuración del espacio de trabajo, aplicaciones y cargas de archivos">
|
||||
|
||||
`MetadataApiClient` viene preconstruido con el SDK (no se requiere generación). Consulta el endpoint `/metadata` para la configuración del espacio de trabajo, las aplicaciones y las cargas de archivos.
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
// List first 10 objects in the workspace
|
||||
const { objects } = await metadataClient.query({
|
||||
objects: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
labelSingular: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {},
|
||||
paging: { first: 10 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Subir archivos
|
||||
|
||||
El `MetadataApiClient` incluye un método `uploadFile` para adjuntar archivos a los campos de tipo archivo:
|
||||
|
||||
```ts
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| ---------------------------------- | -------- | ----------------------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | El contenido sin procesar del archivo |
|
||||
| `filename` | `string` | El nombre del archivo (se utiliza para el almacenamiento y la visualización) |
|
||||
| `contentType` | `string` | Tipo MIME (de forma predeterminada es `application/octet-stream` si se omite) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | El `universalIdentifier` del campo de tipo de archivo de tu objeto |
|
||||
|
||||
Puntos clave:
|
||||
* Utiliza el `universalIdentifier` del campo (no su ID específico del espacio de trabajo), por lo que tu código de carga funciona en cualquier espacio de trabajo donde esté instalada tu aplicación.
|
||||
* La `url` devuelta es una URL firmada que puedes usar para acceder al archivo cargado.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
Cuando tu código se ejecuta en Twenty (funciones de lógica o componentes de frontend), la plataforma inyecta credenciales como variables de entorno:
|
||||
|
||||
* `TWENTY_API_URL` — URL base de la API de Twenty
|
||||
* `TWENTY_APP_ACCESS_TOKEN` — Token de corta duración con alcance al rol de función predeterminado de tu aplicación
|
||||
|
||||
No necesitas pasar estas credenciales a los clientes — leen de `process.env` automáticamente. Los permisos de la clave de API están determinados por el rol declarado con `defineApplicationRole()` (o referenciado mediante `defaultRoleUniversalIdentifier` en `application-config.ts`).
|
||||
</Note>
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Resumen
|
||||
description: TypeScript del lado del servidor que se ejecuta dentro de Twenty, activado por rutas HTTP, programaciones cron, eventos de base de datos, herramientas de IA o acciones de flujos de trabajo.
|
||||
icon: bolt
|
||||
---
|
||||
|
||||
La **capa de lógica** de una app de Twenty es el código que *se ejecuta*: controladores de TypeScript del lado del servidor que reaccionan a solicitudes HTTP, programaciones cron y cambios en registros; habilidades y agentes de IA que viven dentro del espacio de trabajo; y conexiones OAuth que permiten que tus funciones actúen en nombre de un usuario en servicios de terceros.
|
||||
|
||||
```text
|
||||
┌─ HTTP route ──┐
|
||||
│ Cron schedule │
|
||||
│ Database event │ ┌────────────────────┐
|
||||
triggers ─┤ AI tool call ├─────▶│ Logic function │
|
||||
│ Workflow action │ │ (your handler) │
|
||||
│ Manual exec │ └────────────────────┘
|
||||
└────────────────────┘ │
|
||||
▼
|
||||
┌────────────────────────────┐
|
||||
│ Twenty API (records) │
|
||||
│ Third-party API │
|
||||
│ (via Connection token) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## En esta sección
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Funciones de lógica" icon="bolt" href="/l/es/developers/extend/apps/logic/logic-functions">
|
||||
El bloque de construcción principal: tipos de disparadores, cargas útiles y el cliente de API tipado.
|
||||
</Card>
|
||||
<Card title="Habilidades y agentes" icon="robot" href="/l/es/developers/extend/apps/logic/skills-and-agents">
|
||||
Instrucciones reutilizables para agentes de IA y asistentes con mensajes de sistema personalizados.
|
||||
</Card>
|
||||
<Card title="Conexiones" icon="plug" href="/l/es/developers/extend/apps/logic/connections">
|
||||
Credenciales OAuth que tu app mantiene para servicios de terceros — Linear, GitHub, Slack y más.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Tipos de disparadores de un vistazo
|
||||
|
||||
Una función de lógica selecciona uno o más disparadores: cada entrada a continuación es un campo independiente en `defineLogicFunction()`:
|
||||
|
||||
| Disparador | Cuándo se ejecuta | Configuración |
|
||||
| ------------------------------- | --------------------------------------------------------------- | ------------------------------- |
|
||||
| **Ruta HTTP** | Una solicitud llega a tu endpoint `/s/\<path>` | `httpRouteTriggerSettings` |
|
||||
| **Cron** | Coincide una expresión CRON | `cronTriggerSettings` |
|
||||
| **Evento de base de datos** | Se crea, actualiza o elimina un registro del espacio de trabajo | `databaseEventTriggerSettings` |
|
||||
| **Herramienta de IA** | Una funcionalidad de IA de Twenty decide llamar a tu función | `toolTriggerSettings` |
|
||||
| **Acción del Flujo de Trabajo** | Un paso de flujo de trabajo invoca tu función | `workflowActionTriggerSettings` |
|
||||
|
||||
Las funciones se ejecutan en un entorno aislado en procesos independientes de Node.js y acceden al espacio de trabajo a través de un cliente de API tipado con un ámbito limitado al rol declarado en [`defineApplication()`](/l/es/developers/extend/apps/config/application).
|
||||
|
||||
<Note>
|
||||
**Hooks de instalación**: el código que se ejecuta antes o después de la instalación comparte este entorno de ejecución pero usa sus propias funciones define y se encuentra en [Config → Install Hooks](/l/es/developers/extend/apps/config/install-hooks).
|
||||
</Note>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Habilidades y agentes
|
||||
description: Define habilidades y agentes de IA para tu aplicación.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Las habilidades y los agentes están actualmente en pruebas alfa. La funcionalidad es operativa, pero sigue evolucionando.
|
||||
</Warning>
|
||||
|
||||
Las aplicaciones pueden definir capacidades de IA que residen dentro del espacio de trabajo — instrucciones de habilidades reutilizables y agentes con prompts de sistema personalizados.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineSkill" description="Define habilidades para agentes de IA">
|
||||
|
||||
Las habilidades definen instrucciones y capacidades reutilizables que los agentes de IA pueden usar dentro de tu espacio de trabajo. Usa `defineSkill()` para definir habilidades con validación incorporada:
|
||||
|
||||
```ts src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* `name` es una cadena identificadora única de la habilidad (se recomienda kebab-case).
|
||||
* `label` es el nombre para mostrar, legible para humanos, que aparece en la interfaz de usuario.
|
||||
* `content` contiene las instrucciones de la habilidad — este es el texto que usa el agente de IA.
|
||||
* `icon` (opcional) establece el icono mostrado en la interfaz de usuario.
|
||||
* `description` (opcional) proporciona contexto adicional sobre el propósito de la habilidad.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineAgent" description="Define agentes de IA con prompts personalizados">
|
||||
|
||||
Los agentes son asistentes de IA que viven dentro de tu espacio de trabajo. Usa `defineAgent()` para crear agentes con un prompt de sistema personalizado:
|
||||
|
||||
```ts src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* `name` es una cadena identificadora única del agente (se recomienda kebab-case).
|
||||
* `label` es el nombre para mostrar que aparece en la interfaz de usuario.
|
||||
* `prompt` es el mensaje del sistema que define el comportamiento del agente.
|
||||
* `description` (opcional) proporciona contexto sobre lo que hace el agente.
|
||||
* `icon` (opcional) establece el icono mostrado en la interfaz de usuario.
|
||||
* `modelId` (opcional) reemplaza el modelo de IA predeterminado usado por el agente.
|
||||
* `responseFormat` (opcional) controla la forma de la salida del agente. De forma predeterminada es `{ type: 'text' }` para texto de formato libre. Usa `{ type: 'json', schema }` para forzar una salida JSON estructurada.
|
||||
|
||||
De forma predeterminada, un agente devuelve texto de formato libre. Para obtener una salida estructurada, establece `responseFormat` en `{ type: 'json' }` y proporciona un `schema`:
|
||||
|
||||
```ts src/agents/structured-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
|
||||
name: 'lead-scorer',
|
||||
label: 'Lead Scorer',
|
||||
prompt: 'Score the lead and explain your reasoning.',
|
||||
responseFormat: {
|
||||
type: 'json',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'number', description: 'Lead score from 0 to 100' },
|
||||
summary: { type: 'string', description: 'Short reasoning for the score' },
|
||||
},
|
||||
required: ['score', 'summary'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Notas sobre el esquema:
|
||||
* El esquema es un objeto plano: el `type` de cada propiedad debe ser un tipo primitivo (`string`, `number` o `boolean`). Los objetos anidados y los arrays no son compatibles.
|
||||
* `description` (opcional) en cada propiedad guía al modelo sobre qué debe poner allí.
|
||||
* `required` (opcional) enumera las propiedades que el modelo siempre debe devolver.
|
||||
* `additionalProperties: false` (opcional) prohíbe cualquier propiedad que no esté declarada en `properties`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="runAgent" description="Ejecutar un agente desde una función de lógica">
|
||||
|
||||
`runAgent()` permite que una función de lógica ejecute uno de los agentes de tu app (con sus skills y tools). Identifica el agente mediante el `universalIdentifier` que pasaste a `defineAgent()`:
|
||||
|
||||
```ts src/logic-functions/run-enricher.ts
|
||||
import { runAgent } from 'twenty-sdk/logic-function';
|
||||
|
||||
const { result, error, success } = await runAgent({
|
||||
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
|
||||
});
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
* El agente se ejecuta **sincrónicamente** y puede leer/actualizar registros por sí mismo mediante sus propias tools; `runAgent()` se resuelve una vez que la ejecución finaliza.
|
||||
* Una app solo puede ejecutar sus propios agentes.
|
||||
* El [rol predeterminado](/l/es/developers/extend/apps/config/roles) de la app debe conceder el indicador de permiso `AI`; agrega `SystemPermissionFlag.AI` a sus `permissionFlagUniversalIdentifiers` (o establece `canAccessAllTools: true`).
|
||||
Sin esto, `runAgent()` falla con un error de permisos.
|
||||
* Establece un valor generoso de `timeoutSeconds` en la función de lógica: las ejecuciones de agentes pueden tardar varios segundos.
|
||||
* `success` es `true` y `result` es no nulo cuando la ejecución finaliza; en caso de fallo `success` es `false`, `result` es `null`, y `error` contiene el motivo (por ejemplo, cuando el espacio de trabajo se queda sin créditos de AI en mitad de la ejecución).
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
// runAgent() requires the AI permission flag on the app's default role.
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Evita los bucles:** si llamas a `runAgent()` desde un trigger de evento de base de datos `*.updated` y el agente actualiza el mismo registro, limita el alcance del trigger con `updatedFields` a un campo que el agente nunca escriba (por ejemplo, la URL de origen), o comprueba si algún campo de destino sigue vacío antes de llamar a `runAgent()`.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: CLI
|
||||
description: Comandos de `yarn twenty` para ejecutar funciones, transmitir registros en tiempo real, gestionar instalaciones de aplicaciones y cambiar entre remotos.
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
Más allá de `dev`, `dev:build`, `dev:add` y `dev:typecheck`, la CLI de `yarn twenty` proporciona comandos para ejecutar funciones, ver registros y gestionar instalaciones de aplicaciones.
|
||||
|
||||
## Ejecutar funciones (`yarn twenty dev:function:exec`)
|
||||
|
||||
Ejecuta manualmente una función de lógica sin activarla mediante HTTP, cron o evento de base de datos:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Execute by function name
|
||||
yarn twenty dev:function:exec -n create-new-post-card
|
||||
|
||||
# Execute by universalIdentifier
|
||||
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
|
||||
# Pass a JSON payload
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
```
|
||||
|
||||
## Ver registros de funciones (`yarn twenty dev:function:logs`)
|
||||
|
||||
Transmitir en tiempo real los registros de ejecución de las funciones de lógica de tu aplicación:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Stream all function logs
|
||||
yarn twenty dev:function:logs
|
||||
|
||||
# Filter by function name
|
||||
yarn twenty dev:function:logs -n create-new-post-card
|
||||
|
||||
# Filter by universalIdentifier
|
||||
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
<Note>
|
||||
Esto es diferente de `yarn twenty docker:logs`, que muestra los registros del contenedor de Docker. `yarn twenty dev:function:logs` muestra los registros de ejecución de funciones de tu aplicación desde el servidor de Twenty.
|
||||
</Note>
|
||||
|
||||
## Generando el cliente tipado (`yarn twenty dev:generate-client`)
|
||||
|
||||
Regenera el cliente de API tipado (`twenty-client-sdk`) a partir del esquema del remoto activo, sin compilar ni sincronizar una aplicación. Úsalo para obtener un cliente tipado en cualquier proyecto — como un servicio backend que vive en un repositorio separado — que se comunica con tu instancia de Twenty:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# In your project (no Twenty app definition required)
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
|
||||
# Connect to the Twenty instance to generate the client from
|
||||
yarn twenty remote:add
|
||||
|
||||
# Generate the typed client into node_modules/twenty-client-sdk
|
||||
yarn twenty dev:generate-client
|
||||
```
|
||||
|
||||
Luego importa el cliente en tu código:
|
||||
|
||||
```typescript
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
```
|
||||
|
||||
Vuelve a ejecutar el comando cada vez que cambie tu modelo de datos para actualizar los tipos generados.
|
||||
|
||||
<Note>
|
||||
El cliente se genera dentro de `node_modules`, por lo que no se incluye en tus commits de código. Ejecuta `yarn twenty dev:generate-client` después de cada instalación (por ejemplo, en un script de `postinstall` o en CI).
|
||||
</Note>
|
||||
|
||||
## Desinstalar una aplicación (`yarn twenty app:uninstall`)
|
||||
|
||||
Elimina tu aplicación del espacio de trabajo activo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty app:uninstall --yes
|
||||
```
|
||||
|
||||
## Gestión de remotos
|
||||
|
||||
Un **remoto** es un servidor de Twenty al que se conecta tu app. Durante la configuración, el generador crea uno automáticamente para ti. Puedes añadir más remotos o cambiar entre ellos en cualquier momento.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote:add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote:add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote:list
|
||||
|
||||
# Set the active remote
|
||||
yarn twenty remote:use <name>
|
||||
```
|
||||
|
||||
Tus credenciales se almacenan en `~/.twenty/config.json`.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Resumen
|
||||
description: "Compila, prueba y envía tu aplicación: comandos de CLI, pruebas de integración, CI y publicación en un servidor o en npm."
|
||||
icon: rocket
|
||||
---
|
||||
|
||||
La **capa de operaciones** es todo lo que haces *a* tu aplicación en lugar de *con* ella: invocar comandos de CLI, ejecutar pruebas de integración contra un servidor Twenty real, configurar CI y enviar versiones, ya sea como un tarball implementado en un único servidor o como un paquete de npm listado en el marketplace.
|
||||
|
||||
```text
|
||||
develop ─▶ test ─▶ build ─▶ deploy / publish
|
||||
─────── ──── ───── ─────────────────
|
||||
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
|
||||
twenty test twenty
|
||||
dev dev:build yarn twenty app:publish (npm → marketplace)
|
||||
```
|
||||
|
||||
## En esta sección
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CLI" icon="terminal" href="/l/es/developers/extend/apps/operations/cli">
|
||||
Referencia de `yarn twenty` — exec, logs, uninstall, remotes.
|
||||
</Card>
|
||||
<Card title="Sincronización y recuperación" icon="brújula" href="/l/es/developers/extend/apps/operations/sync-and-recovery">
|
||||
Qué comando usar y cuándo, cómo leer el diff de sincronización y una guía escalonada de recuperación.
|
||||
</Card>
|
||||
<Card title="Pruebas" icon="flask" href="/l/es/developers/extend/apps/operations/testing">
|
||||
Configuración de Vitest, pruebas de integración, comprobación de tipos, flujo de trabajo de CI.
|
||||
</Card>
|
||||
<Card title="Publicación" icon="subir" href="/l/es/developers/extend/apps/operations/publishing">
|
||||
Compilar, desplegar un tarball, publicar en npm, instalar.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
title: Publicación
|
||||
icon: subir
|
||||
description: Distribuye tu aplicación de Twenty en el marketplace o despliégala internamente.
|
||||
---
|
||||
|
||||
## Resumen
|
||||
|
||||
Una vez que tu aplicación esté [compilada y probada localmente](/l/es/developers/extend/apps/getting-started/concepts), tienes dos vías para distribuirla:
|
||||
|
||||
* **Desplegar un paquete tar** — sube tu aplicación directamente a un servidor Twenty específico para uso interno o privado.
|
||||
* **Publicar en npm** — incluye tu aplicación en el marketplace de Twenty para que cualquier espacio de trabajo la descubra e instale.
|
||||
|
||||
Ambas rutas comienzan en el mismo paso de **build**.
|
||||
|
||||
## Compilar tu aplicación
|
||||
|
||||
Ejecuta el comando `build` para compilar tu aplicación y generar un `manifest.json` listo para distribución:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:build
|
||||
```
|
||||
|
||||
Esto compila el código fuente de TypeScript, transpila las funciones de lógica y los componentes de frontend, y escribe todo en `.twenty/output/`. Agrega `--tarball` para generar también un paquete `.tgz` para la distribución manual o para el comando `publish`.
|
||||
|
||||
## Despliegue en un servidor (tarball)
|
||||
|
||||
Para aplicaciones que no quieres que estén disponibles públicamente — herramientas propietarias, integraciones solo para empresas o compilaciones experimentales — puedes desplegar un tarball directamente en un servidor de Twenty.
|
||||
|
||||
### Prerrequisitos
|
||||
|
||||
Antes de desplegar, necesitas un remoto configurado que apunte al servidor de destino. Los remotos almacenan la URL del servidor y las credenciales de autenticación localmente en `~/.twenty/config.json`.
|
||||
|
||||
Agrega un remoto:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty remote:add --url https://your-twenty-server.com --as production
|
||||
```
|
||||
|
||||
### Despliegue
|
||||
|
||||
Compila y sube tu aplicación al servidor en un solo paso:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --private
|
||||
# To deploy to a specific remote:
|
||||
# yarn twenty app:publish --private --remote production
|
||||
```
|
||||
|
||||
### Compartir una aplicación desplegada
|
||||
|
||||
<Warning>
|
||||
Compartir aplicaciones privadas (tarball) entre espacios de trabajo es una función de **Enterprise**. La pestaña **Distribución** mostrará un aviso de actualización en lugar de los controles para compartir hasta que tu espacio de trabajo tenga una clave de Enterprise válida. Ve a [Configuración > Panel de administración > Enterprise](/settings/admin-panel#enterprise) para habilitarla.
|
||||
</Warning>
|
||||
|
||||
Las aplicaciones en tarball no se listan en el marketplace público, por lo que otros espacios de trabajo en el mismo servidor no las descubrirán navegando. Una vez que tu espacio de trabajo esté en el plan Enterprise, puedes compartir una aplicación desplegada de esta manera:
|
||||
|
||||
1. Ve a **Configuración > Aplicaciones > Registros** y abre tu aplicación
|
||||
2. En la pestaña **Distribución**, haz clic en **Copiar enlace para compartir**
|
||||
3. Comparte este enlace con usuarios de otros espacios de trabajo — los llevará directamente a la página de instalación de la aplicación
|
||||
|
||||
El enlace para compartir usa la URL base del servidor (sin ningún subdominio de espacio de trabajo), por lo que funciona para cualquier espacio de trabajo en el servidor.
|
||||
|
||||
### Gestión de versiones
|
||||
|
||||
Al actualizar una aplicación tarball ya desplegada, el servidor requiere que la `version` en `package.json` sea **estrictamente mayor** (según el orden de [semver](https://semver.org)) que la versión actualmente desplegada. Volver a desplegar la misma versión, o subir una inferior, se rechaza antes de que se almacene el tarball — verás un error `VERSION_ALREADY_EXISTS` en la CLI.
|
||||
|
||||
Para publicar una actualización:
|
||||
|
||||
1. Incrementa el campo `version` en tu `package.json` (p. ej., `1.2.3` → `1.2.4`, `1.3.0` o `2.0.0`)
|
||||
2. Ejecuta `yarn twenty app:publish --private` (o `yarn twenty app:publish --private --remote production`)
|
||||
3. Los espacios de trabajo que tengan la aplicación instalada verán la actualización disponible en su configuración
|
||||
|
||||
<Note>
|
||||
Las etiquetas de prelanzamiento funcionan como se espera: incrementar `1.0.0-rc.1` → `1.0.0-rc.2` está permitido, y una versión final como `1.0.0` se reconoce correctamente como superior a `1.0.0-rc.5`. La versión en `package.json` debe ser en sí misma una cadena semver válida.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
### Compatibilidad de la versión del servidor
|
||||
|
||||
Si tu app usa una función introducida en una versión específica del servidor Twenty (por ejemplo, proveedores de OAuth agregados en v2.3.0), debes declarar la versión mínima del servidor que tu app requiere usando el campo `engines.twenty` en `package.json`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-my-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"twenty": ">=2.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
El valor es un [rango semver](https://github.com/npm/node-semver#ranges) estándar. Patrones comunes:
|
||||
|
||||
| Rango | Significado |
|
||||
| ---------------------------------- | -------------------------------------------------------------------- |
|
||||
| `>=2.3.0` | Cualquier servidor desde 2.3.0 en adelante |
|
||||
| `>=2.3.0 \<3.0.0` | 2.3.0 o posterior, pero por debajo de la siguiente versión principal |
|
||||
| `^2.3.0` | Igual que `>=2.3.0 \<3.0.0` |
|
||||
|
||||
**Qué sucede durante la implementación e instalación:**
|
||||
|
||||
* Si `engines.twenty` está configurado y la versión del servidor de destino no cumple el rango, la implementación (carga del tarball) o la instalación se rechaza con un error `SERVER_VERSION_INCOMPATIBLE` y un mensaje que indica tanto el rango requerido como la versión real del servidor.
|
||||
* Si `engines.twenty` **no está configurado**, la app se acepta en cualquier versión del servidor (retrocompatible con las apps existentes).
|
||||
* Si el servidor no tiene `APP_VERSION` configurado, se omite la comprobación.
|
||||
|
||||
<Note>
|
||||
El servidor es la autoridad en la comprobación — valida `engines.twenty` tanto en la carga del tarball como en la instalación en el espacio de trabajo. Si implementas un tarball fuera de banda o instalas desde el marketplace, el servidor sigue garantizando la compatibilidad.
|
||||
</Note>
|
||||
|
||||
## CI/CD automatizado (flujos de trabajo preconfigurados)
|
||||
|
||||
Las aplicaciones generadas con `create-twenty-app` incluyen de forma predeterminada dos flujos de trabajo de GitHub Actions, en `.github/workflows/`. Están listas para ejecutarse en cuanto hagas push del repositorio a GitHub — no se necesita configuración adicional para CI, y CD solo requiere un único secreto.
|
||||
|
||||
### CI — `ci.yml`
|
||||
|
||||
Ejecuta pruebas de integración en cada push a `main` y en cada pull request.
|
||||
|
||||
**Qué hace:**
|
||||
|
||||
1. Obtiene el código fuente de tu aplicación.
|
||||
2. Inicia una instancia de prueba aislada de Twenty usando la acción compuesta `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (el equivalente en CI de `yarn twenty docker:start --test`).
|
||||
3. Habilita Corepack, configura Node.js desde tu `.nvmrc` e instala las dependencias con `yarn install --immutable`.
|
||||
4. Ejecuta `yarn test`, pasando `TWENTY_API_URL` y `TWENTY_API_KEY` de la instancia iniciada para que tus pruebas puedan comunicarse con un servidor real.
|
||||
|
||||
**Ajustes de configuración:**
|
||||
|
||||
* `TWENTY_VERSION` (variable de entorno; por defecto `latest`) — fija la versión del servidor de Twenty usada en CI editando esto en `ci.yml`.
|
||||
* La concurrencia se agrupa por `github.ref` y cancela las ejecuciones en progreso cuando hay nuevos pushes.
|
||||
|
||||
No se requieren secretos — la instancia de prueba es efímera y existe solo durante la ejecución del trabajo.
|
||||
|
||||
### CD — `cd.yml`
|
||||
|
||||
Despliega tu aplicación en un servidor de Twenty configurado en cada push a `main` y, opcionalmente, desde un pull request cuando se aplica la etiqueta `deploy`.
|
||||
|
||||
**Qué hace:**
|
||||
|
||||
1. Obtiene el head del PR (para PR etiquetados) o el commit enviado.
|
||||
2. Ejecuta `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — el equivalente en CI de `yarn twenty app:publish --private`.
|
||||
3. Ejecuta `twentyhq/twenty/.github/actions/install-twenty-app@main` para que la versión recién desplegada se instale en el espacio de trabajo de destino.
|
||||
|
||||
**Configuración necesaria:**
|
||||
|
||||
| Configuración | Dónde | Propósito |
|
||||
| ----------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `TWENTY_DEPLOY_URL` | `env` en `cd.yml` (por defecto `http://localhost:3000`) | El servidor de Twenty al que se va a desplegar. Cámbialo por la URL real de tu servidor antes del primer uso. |
|
||||
| `TWENTY_DEPLOY_API_KEY` | Repositorio de GitHub **Settings → Secrets and variables → Actions** | Clave de API con permiso de despliegue en el servidor de destino. |
|
||||
|
||||
<Note>
|
||||
La `TWENTY_DEPLOY_URL` predeterminada de `http://localhost:3000` es un marcador de posición — no alcanzará nada desde un runner alojado por GitHub. Actualízala a la URL pública de tu servidor (o usa un runner autohospedado con acceso a la red) antes de habilitar CD.
|
||||
</Note>
|
||||
|
||||
**Activar un despliegue de vista previa desde un PR:**
|
||||
|
||||
Añade la etiqueta `deploy` a un pull request. La condición `if:` en `cd.yml` ejecutará el trabajo para ese PR usando el commit head del PR, lo que te permitirá validar un cambio en el servidor de destino antes de hacer merge.
|
||||
|
||||
### Fijar las acciones reutilizables
|
||||
|
||||
Ambos flujos de trabajo hacen referencia a acciones reutilizables en `@main`, por lo que las actualizaciones de acciones en el repositorio `twentyhq/twenty` se aplican automáticamente. Si quieres compilaciones deterministas, reemplaza `@main` por un SHA de commit o una etiqueta de versión en cada línea `uses:`.
|
||||
|
||||
## Publicación en npm
|
||||
|
||||
Publicarla en npm hace que tu aplicación sea visible en el marketplace de Twenty. Cualquier espacio de trabajo de Twenty puede explorar, instalar y actualizar aplicaciones del marketplace directamente desde la interfaz de usuario.
|
||||
|
||||
### Requisitos
|
||||
|
||||
* Una cuenta de [npm](https://www.npmjs.com)
|
||||
* La palabra clave `twenty-app` en la matriz `keywords` de tu `package.json` (agrégala manualmente — no se incluye de forma predeterminada en la plantilla `create-twenty-app`)
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"name": "twenty-app-postcard-sender",
|
||||
"version": "1.0.0",
|
||||
"keywords": ["twenty-app"]
|
||||
}
|
||||
```
|
||||
|
||||
### Metadatos del Marketplace
|
||||
|
||||
La configuración de `defineApplication()` admite campos opcionales que controlan cómo aparece tu aplicación en el marketplace. Usa `logoUrl` y `screenshots` para hacer referencia a imágenes de la carpeta `public/`:
|
||||
|
||||
```ts src/application-config.ts
|
||||
export default defineApplication({
|
||||
universalIdentifier: '...',
|
||||
displayName: 'My App',
|
||||
description: 'A great app',
|
||||
logoUrl: 'public/logo.png',
|
||||
screenshots: [
|
||||
'public/screenshot-1.png',
|
||||
'public/screenshot-2.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Consulta el [acordeón de defineApplication](/l/es/developers/extend/apps/config/application#marketplace-metadata) en la página Building Apps para ver la lista completa de campos del marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
|
||||
|
||||
#### Dimensiones recomendadas de las capturas de pantalla
|
||||
|
||||
El marketplace muestra `screenshots` en un contenedor fijo de `8:5` (por ejemplo, `1600×1000 px`).
|
||||
|
||||
<Note>
|
||||
Las capturas de pantalla de cualquier relación de aspecto se muestran completas y nunca se recortan, pero las que sean significativamente más altas o más estrechas que `8:5` mostrarán franjas vacías a los lados.
|
||||
</Note>
|
||||
|
||||
### Publicar
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish
|
||||
```
|
||||
|
||||
Para publicar con una dist-tag específica (p. ej., `beta` o `next`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
### Cómo funciona el descubrimiento en el marketplace
|
||||
|
||||
El servidor de Twenty sincroniza su catálogo del marketplace desde el registro de npm **cada hora**.
|
||||
|
||||
Puedes activar la sincronización de inmediato en lugar de esperar:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync
|
||||
# To target a specific remote:
|
||||
# yarn twenty dev:catalog-sync --remote production
|
||||
```
|
||||
|
||||
Los metadatos que se muestran en el marketplace provienen de tu configuración de `defineApplication()` — campos como `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` y `termsUrl`.
|
||||
|
||||
<Note>
|
||||
Si tu aplicación no define un `aboutDescription` en `defineApplication()`, el marketplace usará automáticamente el `README.md` de tu paquete en npm como el contenido de la página Acerca de. Esto significa que puedes mantener un único README tanto para npm como para el marketplace de Twenty. Si quieres una descripción diferente en el marketplace, establece explícitamente `aboutDescription`.
|
||||
</Note>
|
||||
|
||||
### Publicación en CI
|
||||
|
||||
Usa este flujo de trabajo de GitHub Actions para publicar automáticamente en cada versión (usa [OIDC](https://docs.npmjs.com/trusted-publishers)):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty dev:build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
```
|
||||
|
||||
Para otros sistemas de CI (GitLab CI, CircleCI, etc.), se aplican los mismos tres comandos: `yarn install`, `yarn twenty dev:build` y luego `npm publish` desde `.twenty/output`.
|
||||
|
||||
<Note>
|
||||
**npm provenance** es opcional pero recomendable. Publicar con `--provenance` añade una insignia de confianza a tu ficha de npm, permitiendo que los usuarios verifiquen que el paquete se compiló a partir de un commit específico en una canalización de CI pública. Consulta la [documentación de npm sobre provenance](https://docs.npmjs.com/generating-provenance-statements) para las instrucciones de configuración.
|
||||
</Note>
|
||||
|
||||
## Instalar aplicaciones
|
||||
|
||||
Una vez que una aplicación esté publicada (npm) o desplegada (tarball), los espacios de trabajo pueden instalarla a través de la interfaz de usuario.
|
||||
|
||||
Ve a la página **Configuración > Aplicaciones** en Twenty, donde se pueden explorar e instalar tanto las aplicaciones del marketplace como las desplegadas mediante tarball.
|
||||
|
||||
{/* TODO: add screenshot of the UI when the app is registered */}
|
||||
|
||||
También puedes instalar aplicaciones desde la línea de comandos:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty app:install
|
||||
```
|
||||
|
||||
<Note>
|
||||
El servidor aplica el versionado semver al instalar, reflejando las reglas del despliegue:
|
||||
|
||||
* Instalar la misma versión que ya está instalada en tu espacio de trabajo se rechaza con un error `APP_ALREADY_INSTALLED`.
|
||||
* Instalar una versión inferior a la que está instalada actualmente se rechaza con un error `CANNOT_DOWNGRADE_APPLICATION`.
|
||||
|
||||
Para instalar una versión más reciente, primero despliégala o publícala y luego vuelve a ejecutar `yarn twenty app:install`.
|
||||
</Note>
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Sincronización y recuperación
|
||||
description: Qué comando usar y cuándo, cómo leer la salida de sincronización y una escalera de recuperación para cuando los metadatos locales se desvían, antes de llegar a un restablecimiento completo.
|
||||
icon: brújula
|
||||
---
|
||||
|
||||
El desarrollo de aplicaciones locales gira en torno a la **sincronización**: la CLI recompila tu manifiesto y el servidor aplica solo la diferencia entre este y los metadatos que ya se encuentran en tu espacio de trabajo. Esta página explica qué comando usar, cómo leer qué cambió una sincronización y qué hacer, en orden, cuando el estado local parece inconsistente.
|
||||
|
||||
## Qué comando usar y cuándo
|
||||
|
||||
<Note>
|
||||
Para la iteración local del día a día casi siempre quieres `yarn twenty dev`. La implementación y la publicación son para enviar versiones, **no** para el ciclo local.
|
||||
</Note>
|
||||
|
||||
| Quieres… | Comando | Notas |
|
||||
| --------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Iterar localmente con sincronización en tiempo real | `yarn twenty dev` | Supervisa tus archivos y sincroniza en cada cambio. |
|
||||
| Sincronizar una vez y salir (CI, scripts, hooks) | `yarn twenty dev --once` | Una compilación + sincronización, luego sale. |
|
||||
| Previsualizar cambios **sin aplicarlos** | `yarn twenty dev --once --dry-run` | Calcula e imprime el diff; no escribe nada. |
|
||||
| Eliminar la aplicación del espacio de trabajo | `yarn twenty app:uninstall` | Agrega `--yes` para omitir la confirmación. |
|
||||
| Enviar un tarball a un servidor | `yarn twenty app:publish --private` | Requiere una versión de `package.json` **estrictamente superior**; consulta [Publicación](/l/es/developers/extend/apps/operations/publishing). |
|
||||
| Publicar en el marketplace (npm) | `yarn twenty app:publish` | — |
|
||||
| Instalar / actualizar una versión implementada | `yarn twenty app:install` | Instala la versión actualmente implementada. |
|
||||
| Borrar el servidor local y empezar desde cero | `yarn twenty docker:reset` | Elimina **todos** los datos locales: último recurso. |
|
||||
|
||||
### La sincronización local no necesita un aumento de versión
|
||||
|
||||
La regla de `version` estrictamente creciente (`VERSION_ALREADY_EXISTS` al implementar, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` al instalar) se aplica a **`app:publish` / `app:install`**: la ruta de publicación. `yarn twenty dev` sincroniza tu manifiesto en su lugar y nunca requiere un cambio de versión, por lo que no necesitas tocar `package.json` para iterar. Si te encuentras aumentando la versión para probar un cambio local, estás usando la ruta de publicación cuando lo que quieres es el ciclo de desarrollo.
|
||||
|
||||
## Leer la salida de la sincronización
|
||||
|
||||
Cada sincronización muestra los cambios de metadatos que aplicó (o aplicaría, con `--dry-run`):
|
||||
|
||||
```text filename="Terminal"
|
||||
Metadata changes: 2 created, 1 updated, 1 deleted
|
||||
created objectMetadata rocket
|
||||
created fieldMetadata timelineActivities
|
||||
updated fieldMetadata launchedAt
|
||||
deleted pageLayout legacyTab
|
||||
✓ Synced
|
||||
```
|
||||
|
||||
Este es tu primer diagnóstico: te indica exactamente qué objetos, campos y diseños cambiaron, para que puedas confirmar que una sincronización hizo lo que esperabas antes de revisar la interfaz de usuario.
|
||||
|
||||
Cuando una sincronización falla en una sola entidad, el error nombra la entidad implicada y su `universalIdentifier`, por ejemplo:
|
||||
|
||||
```text
|
||||
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
|
||||
```
|
||||
|
||||
Usa ese identificador para encontrar la entidad en tu manifiesto (y, si es necesario, en el espacio de trabajo) en lugar de adivinar cuál entra en conflicto.
|
||||
|
||||
## Previsualizar cambios (simulación)
|
||||
|
||||
`yarn twenty dev --once --dry-run` compila tu manifiesto, le pide al servidor el plan de migración y lo imprime, **sin aplicar nada**. Es la forma segura de responder "¿qué cambiaría esta sincronización?" antes de comprometerte a ella.
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once --dry-run
|
||||
```
|
||||
|
||||
```text filename="Terminal"
|
||||
Building manifest...
|
||||
Computing metadata diff (dry run, nothing will be applied)...
|
||||
Metadata changes: 1 created, 1 updated
|
||||
created fieldMetadata timelineActivities
|
||||
updated objectMetadata rocket
|
||||
✓ Dry run complete for My App — no changes were applied
|
||||
```
|
||||
|
||||
Una simulación:
|
||||
|
||||
* **No escribe nada**: sin migración de metadatos, sin actualización del registro de la aplicación, sin cambios de roles/pestañas predeterminados y sin generación del cliente de la API.
|
||||
* Devuelve el **mismo diff** que aplicaría una sincronización real, para que puedas revisar por adelantado las entidades creadas/actualizadas/eliminadas.
|
||||
* Es útil antes de un cambio arriesgado, al revisar un cambio generado por IA o en un script que deba fallar si está a punto de producirse un cambio inesperado.
|
||||
|
||||
<Note>
|
||||
Una simulación solo previsualiza cambios de **metadatos** y requiere que la aplicación se haya sincronizado al menos una vez (para que el espacio de trabajo la conozca). Si la ejecutas con una aplicación que nunca se sincronizó, el servidor indicará que la aplicación no está instalada; ejecuta `yarn twenty dev` una vez primero.
|
||||
</Note>
|
||||
|
||||
## Escalera de recuperación
|
||||
|
||||
Cuando los metadatos locales parezcan incorrectos, ve escalando en este orden y detente en cuanto te hayas desbloqueado. Cada paso es más disruptivo que el anterior.
|
||||
|
||||
1. **Volver a sincronizar.** Ejecuta `yarn twenty dev --once` de nuevo. Las sincronizaciones son idempotentes: volver a ejecutar un manifiesto limpio es seguro y suele resolver un problema transitorio.
|
||||
2. **Previsualizar el plan.** Ejecuta `yarn twenty dev --once --dry-run` para ver exactamente qué pretende cambiar la siguiente sincronización, sin aplicarlo.
|
||||
3. Lee el error identificado. Un conflicto suele señalar un identificador duplicado o reutilizado.
|
||||
4. **Desinstalar y volver a instalar.** `yarn twenty app:uninstall`, luego vuelve a sincronizar (`yarn twenty dev`). Esto reconstruye los metadatos de la aplicación desde cero manteniendo intacto el resto de tu espacio de trabajo.
|
||||
5. **Restablecimiento completo (último recurso).** `yarn twenty docker:reset`, luego vuelve a sembrar los datos y a sincronizar.
|
||||
|
||||
<Warning>
|
||||
`yarn twenty docker:reset` elimina **todos** los datos de tu instancia local: todos los espacios de trabajo, registros y aplicaciones. Úsalo solo cuando los pasos anteriores hayan fallado.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
¿Te has encontrado con un error de metadatos? Por favor, [abre una incidencia](https://github.com/twentyhq/twenty/issues/new/choose) e incluye el mensaje de migración con error (con su tipo de metadatos y `universalIdentifier`), la salida de `Metadata changes` de la sincronización y los comandos que ejecutaste.
|
||||
</Note>
|
||||
|
||||
## Evita sincronizaciones concurrentes en un mismo espacio de trabajo
|
||||
|
||||
La sincronización aplica migraciones de metadatos. Ejecutar varias operaciones de sincronización, implementación o instalación contra el **mismo espacio de trabajo al mismo tiempo** (por ejemplo, múltiples terminales o agentes de IA iterando en paralelo) puede entremezclar esas migraciones y dejar los metadatos en un estado parcialmente aplicado.
|
||||
|
||||
El servidor serializa las sincronizaciones por espacio de trabajo para evitar esto, pero aun así deberías canalizar las operaciones de metadatos sensibles a través de un proceso **único** en lugar de lanzarlas de forma concurrente. Si orquestas el desarrollo con varios agentes, enruta sus llamadas de sincronización/implementación/instalación a través de una sola cola de modo que solo una se ejecute a la vez.
|
||||
|
||||
## Distinguir los tipos de error
|
||||
|
||||
Cuando algo sale mal, el diff de metadatos y los errores con nombre te permiten situar el fallo:
|
||||
|
||||
* **Error de compilación del manifiesto**: la CLI falla antes de sincronizar (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); corrige el código fuente de tu aplicación.
|
||||
* **Error de sincronización / migración**: la compilación tiene éxito, pero aplicar el diff falla, nombrando la entidad y el `universalIdentifier`; corrige los metadatos en conflicto.
|
||||
* **Error de tiempo de ejecución del código de la aplicación**: la sincronización se completa correctamente, pero tus funciones lógicas o componentes se comportan de forma incorrecta en tiempo de ejecución; revisa los [registros de funciones](/l/es/developers/extend/apps/operations/cli).
|
||||
* **Estado de instancia local**: nada de lo anterior aplica y el espacio de trabajo sigue viéndose mal; desciende por la escalera de recuperación.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: Pruebas
|
||||
description: Configuración de Vitest, pruebas de integración contra un servidor real de Twenty, comprobación de tipos e integración continua (CI) con GitHub Actions.
|
||||
icon: flask
|
||||
---
|
||||
|
||||
El SDK proporciona APIs programáticas que te permiten compilar, desplegar, instalar y desinstalar tu aplicación desde código de pruebas. Combinado con [Vitest](https://vitest.dev/) y los clientes de API tipados, puedes escribir pruebas de integración que verifiquen que tu aplicación funciona de extremo a extremo contra un servidor real de Twenty.
|
||||
|
||||
## Uso de paquetes de npm
|
||||
|
||||
Puedes instalar y usar cualquier paquete de npm en tu aplicación. Tanto las funciones de lógica como los componentes de frontend se empaquetan con [esbuild](https://esbuild.github.io/), que incorpora todas las dependencias en la salida — no se necesitan `node_modules` en tiempo de ejecución.
|
||||
|
||||
### Instalar un paquete
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add axios
|
||||
```
|
||||
|
||||
Luego impórtalo en tu código:
|
||||
|
||||
```ts src/logic-functions/fetch-data.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import axios from 'axios';
|
||||
|
||||
const handler = async (): Promise<any> => {
|
||||
const { data } = await axios.get('https://api.example.com/data');
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '...',
|
||||
name: 'fetch-data',
|
||||
description: 'Fetches data from an external API',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Lo mismo funciona para los componentes de frontend:
|
||||
|
||||
```tsx src/front-components/chart.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DateWidget = () => {
|
||||
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'date-widget',
|
||||
component: DateWidget,
|
||||
});
|
||||
```
|
||||
|
||||
### Cómo funciona el empaquetado
|
||||
|
||||
El paso de compilación usa esbuild para producir un solo archivo autónomo por función de lógica y por componente de frontend. Todos los paquetes importados se insertan en el bundle.
|
||||
|
||||
**Las funciones de lógica** se ejecutan en un entorno Node.js. Los módulos integrados de Node (`fs`, `path`, `crypto`, `http`, etc.) están disponibles y no necesitan instalarse.
|
||||
|
||||
**Los componentes de frontend** se ejecutan en un Web Worker. Los módulos integrados de Node **no** están disponibles — solo las APIs del navegador y paquetes de npm que funcionen en un entorno de navegador.
|
||||
|
||||
Ambos entornos tienen `twenty-client-sdk/core` y `twenty-client-sdk/metadata` disponibles como módulos preproporcionados — estos no se incluyen en el bundle sino que se resuelven en tiempo de ejecución por el servidor.
|
||||
|
||||
## Configuración
|
||||
|
||||
La aplicación generada ya incluye Vitest. Si lo configuras manualmente, instala las dependencias:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D vitest vite-tsconfig-paths
|
||||
```
|
||||
|
||||
Crea un `vitest.config.ts` en la raíz de tu aplicación:
|
||||
|
||||
```ts vitest.config.ts
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Crea un archivo de configuración que verifique que el servidor es accesible antes de ejecutar las pruebas:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## APIs programáticas del SDK
|
||||
|
||||
La subruta `twenty-sdk/cli` exporta funciones que puedes invocar directamente desde el código de pruebas:
|
||||
|
||||
| Función | Descripción |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| `appBuild` | Compilar la aplicación y opcionalmente empaquetar un tarball |
|
||||
| `appDeploy` | Subir un tarball al servidor |
|
||||
| `appInstall` | Instalar la aplicación en el espacio de trabajo activo |
|
||||
| `appUninstall` | Desinstalar la aplicación del espacio de trabajo activo |
|
||||
|
||||
Cada función devuelve un objeto de resultado con `success: boolean` y `data` o `error`.
|
||||
|
||||
## Escribir una prueba de integración
|
||||
|
||||
Aquí tienes un ejemplo completo que compila, despliega e instala la aplicación, y luego verifica que aparezca en el espacio de trabajo:
|
||||
|
||||
```ts src/__tests__/app-install.integration-test.ts
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(`Install failed: ${installResult.error?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
});
|
||||
|
||||
it('should find the installed app in the workspace', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(app: { universalIdentifier: string }) =>
|
||||
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Ejecutar pruebas
|
||||
|
||||
Asegúrate de que tu servidor local de Twenty esté en ejecución y luego:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test
|
||||
```
|
||||
|
||||
O en modo watch durante el desarrollo:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
## Comprobación de tipos
|
||||
|
||||
También puedes ejecutar la comprobación de tipos en tu aplicación sin ejecutar pruebas:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:typecheck
|
||||
```
|
||||
|
||||
Esto ejecuta `tsc --noEmit` e informa cualquier error de tipo.
|
||||
|
||||
## CI con GitHub Actions
|
||||
|
||||
El generador crea un flujo de trabajo de GitHub Actions listo para usar en `.github/workflows/ci.yml`. Ejecuta tus pruebas de integración automáticamente en cada push a `main` y en los pull requests.
|
||||
|
||||
El flujo de trabajo:
|
||||
|
||||
1. Obtiene tu código
|
||||
2. Inicia un servidor temporal de Twenty usando la acción `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Instala las dependencias con `yarn install --immutable`
|
||||
4. Ejecuta `yarn test` con `TWENTY_API_URL` y `TWENTY_API_KEY` inyectados a partir de las salidas de la acción
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
No necesitas configurar secretos: la acción `spawn-twenty-docker-image` inicia un servidor efímero de Twenty directamente en el runner y devuelve los detalles de conexión. El secreto `GITHUB_TOKEN` lo proporciona GitHub automáticamente.
|
||||
|
||||
Para fijar una versión específica de Twenty en lugar de `latest`, cambia la variable de entorno `TWENTY_VERSION` al inicio del flujo de trabajo.
|
||||
Reference in New Issue
Block a user