--- title: Vytváření aplikací description: Definujte objekty, logické funkce, frontendové komponenty a další pomocí Twenty SDK. --- Aplikace jsou aktuálně v alfa fázi. Funkce funguje, ale stále se vyvíjí. Balíček `twenty-sdk` poskytuje typované stavební bloky pro vytváření vaší aplikace. Tato stránka pokrývá všechny typy entit a klienty API dostupné v SDK. ## Funkce DefineEntity SDK poskytuje funkce pro definování entit vaší aplikace. Abyste umožnili SDK detekovat vaše entity, musíte použít `export default defineEntity({...})`. Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost. **Uspořádání souborů je na vás.** Detekce entit je založená na AST — SDK najde volání `export default defineEntity(...)` bez ohledu na to, kde se soubor nachází. Seskupování souborů podle typu (např. `logic-functions/`, `roles/`) je pouze konvence, nikoli požadavek. Role zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. ```ts restricted-company-role.ts import { defineRole, PermissionFlag, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk'; 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, }, ], permissionFlags: [PermissionFlag.APPLICATIONS], }); ``` Každá aplikace musí mít právě jedno volání `defineApplication`, které popisuje: * **Identita**: identifikátory, zobrazovaný název a popis. * **Oprávnění**: jakou roli používají její funkce a frontendové komponenty. * **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí. * **(Volitelné) předinstalační / postinstalační funkce**: logické funkce, které se spouštějí před nebo po instalaci. ```ts src/application-config.ts import { defineApplication } from 'twenty-sdk'; import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; export default defineApplication({ universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', displayName: 'My Twenty App', description: 'My first Twenty app', icon: 'IconWorld', applicationVariables: { DEFAULT_RECIPIENT_NAME: { universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', description: 'Default recipient name for postcards', value: 'Jane Doe', isSecret: false, }, }, defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` Poznámky: * Pole `universalIdentifier` jsou deterministické identifikátory, které vlastníte. Vygenerujte je jednou a zachovejte je stabilní napříč synchronizacemi. * `applicationVariables` se stanou proměnnými prostředí pro vaše funkce a frontendové komponenty (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`). * `defaultRoleUniversalIdentifier` musí odkazovat na roli definovanou pomocí `defineRole()` (viz výše). * Předinstalační a postinstalační funkce jsou při sestavení manifestu detekovány automaticky — není třeba na ně odkazovat v `defineApplication()`. #### Metadata Marketplace Pokud plánujete [zveřejnit svou aplikaci](/l/cs/developers/extend/apps/publishing), tato volitelná pole určují, jak se vaše aplikace zobrazuje na Marketplace: | Pole | Popis | | ------------------ | ------------------------------------------------------------------------------------------------------------- | | `author` | Jméno autora nebo název společnosti | | `category` | Kategorie aplikace pro filtrování na Marketplace | | `logoUrl` | Cesta k logu vaší aplikace (např. `public/logo.png`) | | `screenshots` | Pole cest ke snímkům obrazovky (např. `public/screenshot-1.png`) | | `aboutDescription` | Delší popis v Markdownu pro kartu "O aplikaci". Pokud je vynecháno, tržiště použije `README.md` balíčku z npm | | `websiteUrl` | Odkaz na váš web | | `termsUrl` | Odkaz na Podmínky služby | | `emailSupport` | E-mailová adresa podpory | | `issueReportUrl` | Odkaz na nástroj pro sledování problémů | #### Role a oprávnění Pole `defaultRoleUniversalIdentifier` v `application-config.ts` určuje výchozí roli používanou logickými funkcemi a frontendovými komponentami vaší aplikace. Podrobnosti viz výše u `defineRole`. * Běhový token vložený jako `TWENTY_APP_ACCESS_TOKEN` je odvozen z této role. * Typovaný klient bude omezen oprávněními udělenými této roli. * Dodržujte princip nejmenších oprávnění: vytvořte vyhrazenou roli pouze s oprávněními, která vaše funkce potřebují. ##### Výchozí role funkce Když vygenerujete novou aplikaci, CLI vytvoří výchozí soubor role: ```ts src/roles/default-role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ 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: [], permissionFlags: [], }); ``` Na `universalIdentifier` této role se v `application-config.ts` odkazuje jako na `defaultRoleUniversalIdentifier`: * **\*.role.ts** definuje, co daná role může dělat. * **application-config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění. Poznámky: * Začněte vygenerovanou rolí a postupně ji omezujte podle principu nejmenších oprávnění. * Nahraďte `objectPermissions` a `fieldPermissions` objekty a poli, které vaše funkce skutečně potřebují. * `permissionFlags` řídí přístup k schopnostem na úrovni platformy. Udržujte je co nejmenší. * Podívejte se na funkční příklad: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts). Vlastní objekty popisují jak schéma, tak chování záznamů ve vašem pracovním prostoru. K definování objektů s vestavěnou validací použijte `defineObject()`: ```ts postCard.object.ts import { defineObject, FieldType } from 'twenty-sdk'; 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, }, ], }); ``` Hlavní body: * Použijte `defineObject()` pro vestavěnou validaci a lepší podporu v IDE. * Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními. * Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`. * Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí. * Nové objekty můžete vygenerovat pomocí `yarn twenty add`, který vás provede pojmenováním, poli a vztahy. **Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole jako `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` a `deletedAt`. Nemusíte je definovat v poli `fields` — přidejte pouze svá vlastní pole. Výchozí pole můžete přepsat definováním pole se stejným názvem v poli `fields`, ale to se nedoporučuje. Pomocí `defineField()` přidejte pole k objektům, které nevlastníte — například ke standardním objektům Twenty (Person, Company atd.). nebo k objektům z jiných aplikací. Na rozdíl od inline polí v `defineObject()` vyžadují samostatná pole `objectUniversalIdentifier` k určení, který objekt rozšiřují: ```ts src/fields/company-loyalty-tier.field.ts import { defineField, FieldType } from 'twenty-sdk'; 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' }, ], }); ``` Hlavní body: * `objectUniversalIdentifier` identifikuje cílový objekt. Pro standardní objekty použijte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportovaný z `twenty-sdk`. * Při definování polí inline v `defineObject()` `objectUniversalIdentifier` nepotřebujete — dědí se z nadřazeného objektu. * `defineField()` je jediný způsob, jak přidat pole k objektům, které jste nevytvořili pomocí `defineObject()`. Relace propojují objekty. Ve Twenty jsou relace vždy obousměrné — definujete obě strany a každá strana odkazuje na tu druhou. Existují dva typy relací: | Typ vztahu | Popis | Má cizí klíč? | | ------------- | --------------------------------------------------------------------- | ---------------------- | | `MANY_TO_ONE` | Mnoho záznamů tohoto objektu ukazuje na jeden záznam cílového objektu | Ano (`joinColumnName`) | | `ONE_TO_MANY` | Jeden záznam tohoto objektu má mnoho záznamů cílového objektu | Ne (inverzní strana) | #### Jak fungují relace Každá relace vyžaduje dvě pole, která na sebe vzájemně odkazují: 1. Strana MANY_TO_ONE — je na objektu, který drží cizí klíč 2. Strana ONE_TO_MANY — je na objektu, který vlastní kolekci Obě pole používají `FieldType.RELATION` a vzájemně se odkazují prostřednictvím `relationTargetFieldMetadataUniversalIdentifier`. #### Příklad: Pohlednice má mnoho příjemců Předpokládejme, že `PostCard` lze odeslat mnoha záznamům `PostCardRecipient`. Každý příjemce náleží přesně jedné pohlednici. **Krok 1: Definujte stranu ONE_TO_MANY na PostCard** (strana "one"): ```ts src/fields/post-card-recipients-on-post-card.field.ts import { defineField, FieldType, RelationType } from 'twenty-sdk'; 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, }, }); ``` **Krok 2: Definujte stranu MANY_TO_ONE na PostCardRecipient** (strana "many" — drží cizí klíč): ```ts src/fields/post-card-on-post-card-recipient.field.ts import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk'; 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', }, }); ``` **Cyklické importy:** Obě relační pole odkazují na `universalIdentifier` toho druhého. Abyste předešli problémům s cyklickými importy, exportujte ID polí jako pojmenované konstanty z každého souboru a v druhém souboru je importujte. Build systém je vyřeší v době kompilace. #### Vazby na standardní objekty Chcete-li vytvořit relaci s vestavěným objektem Twenty (Person, Company atd.), použijte `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'; 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', }, }); ``` #### Vlastnosti relačních polí | Vlastnost | Povinné | Popis | | ------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------- | | `type` | Ano | Musí být `FieldType.RELATION` | | `relationTargetObjectMetadataUniversalIdentifier` | Ano | `universalIdentifier` cílového objektu | | `relationTargetFieldMetadataUniversalIdentifier` | Ano | `universalIdentifier` odpovídajícího pole na cílovém objektu | | `universalSettings.relationType` | Ano | `RelationType.MANY_TO_ONE` nebo `RelationType.ONE_TO_MANY` | | `universalSettings.onDelete` | Pouze MANY_TO_ONE | Co se stane, když je smazán odkazovaný záznam: `CASCADE`, `SET_NULL`, `RESTRICT` nebo `NO_ACTION` | | `universalSettings.joinColumnName` | Pouze MANY_TO_ONE | Název databázového sloupce pro cizí klíč (např. `postCardId`) | #### Vložená relační pole v defineObject Relační pole můžete také definovat přímo uvnitř `defineObject()`. V takovém případě vynechejte `objectUniversalIdentifier` — dědí se z nadřazeného objektu: ```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 ], }); ``` Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči. ```ts src/logic-functions/createPostCard.logic-function.ts import { defineLogicFunction } from 'twenty-sdk'; import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk'; import { CoreApiClient, type Person } from 'twenty-client-sdk/core'; const handler = async (params: RoutePayload) => { const client = new CoreApiClient(); const name = 'name' in params.queryStringParameters ? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world' : '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: 'GET', isAuthRequired: true, }, /*databaseEventTriggerSettings: { eventName: 'people.created', },*/ /*cronTriggerSettings: { pattern: '0 0 1 1 *', },*/ }); ``` Dostupné typy spouštěčů: * **httpRoute**: Zpřístupní vaši funkci na HTTP cestě a metodě **pod koncovým bodem `/s/`**: > např. `path: '/post-card/create'` je volatelné na `https://your-twenty-server.com/s/post-card/create` * **cron**: Spouští vaši funkci podle plánu pomocí výrazu CRON. * **databaseEvent**: Spouští se při událostech životního cyklu objektů v pracovním prostoru. Když je operace události `updated`, lze konkrétní sledovaná pole určit v poli `updatedFields`. Pokud zůstane nedefinované nebo prázdné, spustí funkci jakákoli aktualizace. > např. `person.updated`, `*.created`, `company.*` Funkci můžete také spustit ručně pomocí CLI: ```bash filename="Terminal" yarn twenty exec -n create-new-post-card -p '{"key": "value"}' ``` ```bash filename="Terminal" yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf ``` Logy můžete sledovat pomocí: ```bash filename="Terminal" yarn twenty logs ``` #### Payload spouštěče trasy Když spouštěč typu route vyvolá vaši logickou funkci, ta obdrží objekt `RoutePayload`, který odpovídá [AWS HTTP API v2 formátu](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). Importujte typ `RoutePayload` z `twenty-sdk`: ```ts import { defineLogicFunction, type RoutePayload } from 'twenty-sdk'; const handler = async (event: RoutePayload) => { const { headers, queryStringParameters, pathParameters, body } = event; const { method, path } = event.requestContext.http; return { message: 'Success' }; }; ``` Typ `RoutePayload` má následující strukturu: | Vlastnost | Typ | Popis | Příklad | | ---------------------------- | ------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------- | | `headers` | `Record` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekce níže | | `queryStringParameters` | `Record` | Parametry query stringu (více hodnot spojených čárkami) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` | | `pathParameters` | `Record` | Parametry cesty extrahované ze vzoru trasy | `/users/:id`, `/users/123` -> `{ id: '123' }` | | `body` | `object \| null` | Parsované tělo požadavku (JSON) | `{ id: 1 }` -> `{ id: 1 }` | | `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 | | | `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | | | `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku | | #### forwardedRequestHeaders Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší logické funkce **ne** předávají. Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `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'], }, }); ``` Ve vašem handleru k přeposlaným záhlavím přistupujte takto: ```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 }; }; ``` Názvy záhlaví jsou normalizovány na malá písmena. Přistupujte k nim pomocí klíčů s malými písmeny (například `event.headers['content-type']`). #### Zpřístupnění funkce jako nástroje Logické funkce lze zpřístupnit jako **nástroje** pro agenty AI a pracovní postupy. Když je funkce označena jako nástroj, stane se dohledatelnou funkcemi AI produktu Twenty a lze ji použít v automatizacích pracovních postupů. Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true`: ```ts src/logic-functions/enrich-company.logic-function.ts import { defineLogicFunction } from 'twenty-sdk'; 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, isTool: true, }); ``` Hlavní body: * Můžete kombinovat `isTool` se spouštěči — funkce může být zároveň nástrojem (volatelným agenty AI) a současně se spouštět událostmi. * **`toolInputSchema`** (volitelné): Objekt JSON Schema, který popisuje parametry, jež vaše funkce přijímá. Schéma se určuje automaticky ze statické analýzy zdrojového kódu, ale můžete ho nastavit i explicitně: ```ts export default defineLogicFunction({ ..., toolInputSchema: { 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'], }, }); ``` **Napište kvalitní `description`.** Agenti AI se spoléhají na pole funkce `description` při rozhodování, kdy nástroj použít. Buďte konkrétní ohledně toho, co nástroj dělá a kdy se má volat. Postinstalační funkce je logická funkce, která se spustí automaticky, jakmile je instalace vaší aplikace v pracovním prostoru dokončena. Server ji provede **poté**, co byla synchronizována metadata aplikace a vygenerován klient SDK, takže je pracovní prostor plně připraven k použití a nové schéma je zavedeno. Mezi typické případy použití patří naplnění výchozími daty, vytvoření počátečních záznamů, konfigurace nastavení pracovního prostoru nebo zřizování prostředků ve službách třetích stran. ```ts src/logic-functions/post-install.ts import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk'; const handler = async (payload: InstallPayload): Promise => { 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, }); ``` Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI: ```bash filename="Terminal" yarn twenty exec --postInstall ``` Hlavní body: * Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`). * Obslužná funkce obdrží `InstallPayload` s `{ previousVersion?: string; newVersion: string }` — `newVersion` je verze, která se instaluje, a `previousVersion` je verze, která byla nainstalována dříve (nebo `undefined` při čisté instalaci). Tyto hodnoty použijte k rozlišení čistých instalací od aktualizací a ke spuštění migrační logiky specifické pro verzi. * **Kdy se hook spouští**: ve výchozím nastavení pouze při čistých instalacích. Předejte `shouldRunOnVersionUpgrade: true`, pokud chcete, aby se spouštěl i při aktualizaci aplikace z předchozí verze. Pokud je vynechán, příznak má výchozí hodnotu `false` a při aktualizacích se hook přeskočí. * **Model provádění — ve výchozím nastavení asynchronní, synchronní volitelně**: příznak `shouldRunSynchronously` určuje *jak* se spouští post-install. * `shouldRunSynchronously: false` *(výchozí)* — hook je **zařazen do fronty zpráv** s `retryLimit: 3` a běží asynchronně ve workeru. Odezva instalace se vrátí hned po zařazení úlohy do fronty, takže pomalá nebo chybující obslužná funkce neblokuje volajícího. Worker se pokusí o opakování až třikrát. **Použijte pro dlouho běžící úlohy** — plnění velkých datových sad, volání pomalých externích API, zřizování externích prostředků, cokoli, co by mohlo přesáhnout rozumné časové okno HTTP odezvy. * `shouldRunSynchronously: true` — hook se provádí **inline během instalačního procesu** (stejný vykonavatel jako pre-install). Instalační požadavek blokuje, dokud obslužná funkce nedokončí, a pokud vyvolá výjimku, volající instalace obdrží `POST_INSTALL_ERROR`. Žádné automatické opakování. **Použijte pro rychlé úlohy, které se musí dokončit před odpovědí** — například vrácení validační chyby uživateli nebo rychlé nastavení, na kterém bude klient záviset ihned po návratu volání instalace. Mějte na paměti, že v době, kdy se spustí post-install, už byla migrace metadat aplikována, takže selhání v synchronním režimu změny schématu **ne**vrací zpět — pouze odhalí chybu. * Ujistěte se, že vaše obslužná funkce je idempotentní. V asynchronním režimu se může fronta pokusit až třikrát; v obou režimech se může hook znovu spustit při aktualizacích, pokud je `shouldRunOnVersionUpgrade: true`. * Proměnné prostředí `APPLICATION_ID`, `APP_ACCESS_TOKEN` a `API_URL` jsou dostupné uvnitř obslužné funkce (stejně jako u jakékoli jiné logické funkce), takže můžete volat Twenty API s aplikačním přístupovým tokenem omezeným na vaši aplikaci. * Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna. * Atributy funkce `universalIdentifier`, `shouldRunOnVersionUpgrade` a `shouldRunSynchronously` jsou během buildu automaticky připojeny k manifestu aplikace do pole `postInstallLogicFunction` — není potřeba je uvádět v `defineApplication()`. * Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty. * **Nespouští se v režimu dev**: když je aplikace registrována lokálně (pomocí `yarn twenty dev`), server zcela přeskočí instalační tok a synchronizuje soubory přímo prostřednictvím sledovače CLI — takže se post-install v režimu dev nikdy nespustí bez ohledu na `shouldRunSynchronously`. Použijte `yarn twenty exec --postInstall` k ručnímu spuštění nad běžícím pracovním prostorem. Funkce pre-install je logická funkce, která se během instalace spouští automaticky, **před aplikováním migrace metadat pracovního prostoru**. Má stejný tvar payloadu jako post-install (`InstallPayload`), ale je zařazena dříve v instalačním toku, aby mohla připravit stav, na němž nadcházející migrace závisí — typické použití zahrnuje zálohování dat, ověření kompatibility s novým schématem nebo archivaci záznamů, které se chystají přeuspořádat nebo odstranit. ```ts src/logic-functions/pre-install.ts import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk'; const handler = async (payload: InstallPayload): Promise => { 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, }); ``` Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI: ```bash filename="Terminal" yarn twenty exec --preInstall ``` Hlavní body: * Funkce pre-install používají `definePreInstallLogicFunction()` — stejné specializované nastavení jako u post-install, pouze připojené k jiné fázi životního cyklu. * Obě obslužné funkce pre- i post-install přijímají stejný typ `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importujte jej jednou a znovu použijte pro oba hooky. * **Kdy se hook spouští**: umístěn těsně před migrací metadat pracovního prostoru (`synchronizeFromManifest`). Před spuštěním server provede čistě aditivní "zjednodušenou synchronizaci", která v metadatech pracovního prostoru zaregistruje pre-install funkci **nové** verze — ničeho dalšího se nedotkne — a poté ji spustí. Protože tato synchronizace je pouze aditivní, objekty, pole a data předchozí verze zůstávají při spuštění vaší obslužné funkce zachována: můžete bezpečně číst a zálohovat stav před migrací. * **Model provádění**: pre-install se provádí **synchronně** a **blokuje instalaci**. Pokud obslužná funkce vyvolá výjimku, instalace se přeruší ještě před aplikováním jakýchkoli změn schématu — pracovní prostor zůstane na předchozí verzi v konzistentním stavu. Je to záměrné: pre-install je vaše poslední šance odmítnout rizikovou aktualizaci. * Stejně jako u post-install je na jednu aplikaci povolena pouze jedna funkce pre-install. Během buildu je automaticky připojena k manifestu aplikace pod `preInstallLogicFunction`. * **Nespouští se v režimu dev**: stejně jako u post-install — u lokálně registrovaných aplikací je instalační tok zcela přeskočen, takže se pre-install pod `yarn twenty dev` nikdy nespustí. Použijte `yarn twenty exec --preInstall` k ručnímu spuštění. Oba hooky jsou součástí téhož instalačního toku a přijímají stejný `InstallPayload`. Rozdíl je v tom, **kdy** se spouštějí vzhledem k migraci metadat pracovního prostoru, a to určuje, jakých dat se mohou bezpečně dotýkat. ``` ┌─────────────────────────────────────────────────────────────┐ │ install flow │ │ │ │ upload package → [pre-install] → metadata migration → │ │ generate SDK → [post-install] │ │ │ │ old schema visible new schema visible │ └─────────────────────────────────────────────────────────────┘ ``` Pre-install je vždy **synchronní** (blokuje instalaci a může ji přerušit). Post-install je **ve výchozím nastavení asynchronní** — zařazen do workeru s automatickými pokusy o opakování — ale může přejít na synchronní provádění pomocí `shouldRunSynchronously: true`. Viz accordion `definePostInstallLogicFunction` výše, kdy použít jednotlivé režimy. **Použijte `post-install` pro cokoli, co vyžaduje existenci nového schématu.** To je běžný případ: * Plnění výchozími daty (vytváření počátečních záznamů, výchozích pohledů, demo obsahu) vůči nově přidaným objektům a polím. * Registrace webhooků u služeb třetích stran poté, co má aplikace své přihlašovací údaje. * Volání vlastního API k dokončení nastavení, které závisí na synchronizovaných metadatech. * Idempotentní logika "zajisti, že to existuje", která má při každé aktualizaci uvést stav do souladu — kombinujte s `shouldRunOnVersionUpgrade: true`. Příklad — po instalaci naplňte výchozí záznam `PostCard`: ```ts src/logic-functions/post-install.ts import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk'; import { createClient } from './generated/client'; const handler = async ({ previousVersion }: InstallPayload): Promise => { 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, }); ``` **Použijte `pre-install`, pokud by migrace jinak zničila nebo poškodila existující data.** Protože pre-install běží proti *předchozímu* schématu a jeho selhání vrací aktualizaci zpět, je to správné místo pro cokoli rizikového: * **Zálohování dat, která se chystají odstranit nebo přeuspořádat** — např. odstraňujete pole ve verzi v2 a potřebujete jeho hodnoty zkopírovat do jiného pole nebo je před spuštěním migrace exportovat do úložiště. * **Archivace záznamů, které by nové omezení zneplatnilo** — např. pole se stává `NOT NULL` a je třeba nejprve smazat nebo opravit řádky s hodnotami null. * **Ověření kompatibility a odmítnutí aktualizace, pokud nelze aktuální data čistě migrovat** — vyhoďte výjimku z obslužné funkce a instalace se ukončí bez provedených změn. Je to bezpečnější, než zjistit nekompatibilitu uprostřed migrace. * **Přejmenování nebo změna klíčů dat** před změnou schématu, která by ztratila vazby. Příklad — archivujte záznamy před destruktivní migrací: ```ts src/logic-functions/pre-install.ts import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk'; import { createClient } from './generated/client'; const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { // 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, }); ``` **Zlaté pravidlo:** | Chcete… | Použít | | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Naplňte výchozí data, nakonfigurujte pracovní prostor, zaregistrujte externí prostředky | `post-install` | | Spusťte dlouho běžící plnění nebo volání třetích stran, která by neměla blokovat odezvu instalace | `post-install` (výchozí — `shouldRunSynchronously: false`, s opakovanými pokusy workeru) | | Spusťte rychlé nastavení, na které bude volající spoléhat ihned po návratu volání instalace | `post-install` s `shouldRunSynchronously: true` | | Čtěte nebo zálohujte data, která by nadcházející migrace ztratila | `pre-install` | | Odmítněte aktualizaci, která by poškodila existující data | `pre-install` (vyhoďte výjimku z obslužné funkce) | | Spouštějte srovnání stavu při každé aktualizaci | `post-install` s `shouldRunOnVersionUpgrade: true` | | Proveďte jednorázové nastavení pouze při první instalaci | `post-install` s `shouldRunOnVersionUpgrade: false` (výchozí) | Pokud si nejste jisti, výchozí volbou je **post-install**. Po pre-install sáhněte pouze tehdy, když je samotná migrace destruktivní a potřebujete zachytit předchozí stav, než zmizí. Frontendové komponenty jsou React komponenty, které se vykreslují přímo v uživatelském rozhraní Twenty. Běží v **izolovaném Web Workeru** s využitím Remote DOM — váš kód je sandboxovaný, ale vykresluje se nativně na stránce, nikoli v iframu. #### Kde lze použít front komponenty Front komponenty se mohou vykreslovat na dvou místech v rámci Twenty: * **Postranní panel** — Ne-headless front komponenty se otevírají v pravém postranním panelu. Toto je výchozí chování, když je front komponenta vyvolána z menu příkazů. * **Widgety (nástěnky a stránky záznamů)** — Front komponenty lze vkládat jako widgety do rozložení stránek. Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget front komponenty. #### Základní příklad Nejrychlejší způsob, jak vidět frontendovou komponentu v akci, je zaregistrovat ji jako **příkaz**. Přidáním pole `command` s `isPinned: true` se zobrazí jako tlačítko rychlé akce v pravém horním rohu stránky — není potřeba žádné rozvržení stránky: ```tsx src/front-components/hello-world.tsx import { defineFrontComponent } from 'twenty-sdk'; const HelloWorld = () => { return (

Hello from my app!

This component renders inside Twenty.

); }; export default defineFrontComponent({ universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948', name: 'hello-world', description: 'A simple front component', component: HelloWorld, command: { universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345', shortLabel: 'Hello', label: 'Hello World', icon: 'IconBolt', isPinned: true, availabilityType: 'GLOBAL', }, }); ``` Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
Tlačítko rychlé akce v pravém horním rohu
Kliknutím na něj vykreslíte komponentu přímo ve stránce. {/* TODO: add screenshot of the rendered front component */} #### Konfigurační pole | Pole | Povinné | Popis | | --------------------- | ------- | ------------------------------------------------------------------------------------ | | `universalIdentifier` | Ano | Stabilní jedinečné ID pro tuto komponentu | | `component` | Ano | Funkce komponenty React | | `name` | Ne | Zobrazovaný název | | `description` | Ne | Popis toho, co komponenta dělá | | `isHeadless` | Ne | Nastavte na `true`, pokud komponenta nemá viditelné UI (viz níže) | | `command` | Ne | Zaregistrujte komponentu jako příkaz (viz [možnosti příkazu](#command-options) níže) | #### Umístění frontendové komponenty na stránku Mimo příkazy můžete frontendovou komponentu vložit přímo na stránku záznamu přidáním jako widget v **rozvržení stránky**. Podrobnosti viz sekce [definePageLayout](#definepagelayout). #### Headless vs. ne-headless Front komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`: **Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena. **Headless (`isHeadless: true`)** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže. ```tsx src/front-components/sync-tracker.tsx import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk'; 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, }); ``` Protože komponenta vrací `null`, Twenty přeskočí vykreslení kontejneru — v rozvržení se neobjeví žádné prázdné místo. Komponenta má však stále přístup ke všem hookům a API komunikace s hostitelem. #### Komponenty SDK Command Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front komponentu. Importujte je z `twenty-sdk/command`: * **`Command`** — Spustí asynchronní callback přes prop `execute`. * **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`. * **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`. * **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`. Zde je kompletní příklad headless front komponenty, která pomocí `Command` spouští akci z menu příkazů: ```tsx src/front-components/run-action.tsx import { defineFrontComponent } from 'twenty-sdk'; 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 ; }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234', name: 'run-action', description: 'Creates a task from the command menu', component: RunAction, isHeadless: true, command: { universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345', label: 'Run my action', icon: 'IconPlayerPlay', }, }); ``` A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením: ```tsx src/front-components/delete-draft.tsx import { defineFrontComponent } from 'twenty-sdk'; import { CommandModal } from 'twenty-sdk/command'; const DeleteDraft = () => { const execute = async () => { // perform the deletion }; return ( ); }; export default defineFrontComponent({ universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456', name: 'delete-draft', description: 'Deletes a draft with confirmation', component: DeleteDraft, isHeadless: true, command: { universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567', label: 'Delete draft', icon: 'IconTrash', }, }); ``` #### Přístup k běhovému kontextu Uvnitř komponenty použijte hooky SDK pro přístup k aktuálnímu uživateli, záznamu a instanci komponenty: ```tsx src/front-components/record-info.tsx import { defineFrontComponent, useUserId, useRecordId, useFrontComponentId, } from 'twenty-sdk'; const RecordInfo = () => { const userId = useUserId(); const recordId = useRecordId(); const componentId = useFrontComponentId(); return (

User: {userId}

Record: {recordId ?? 'No record context'}

Component: {componentId}

); }; export default defineFrontComponent({ universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012', name: 'record-info', component: RecordInfo, }); ``` Dostupné hooky: | Hook | Vrací | Popis | | --------------------------------------------- | -------------------- | ------------------------------------------------------------ | | `useUserId()` | `string` nebo `null` | ID aktuálního uživatele | | `useRecordId()` | `string` nebo `null` | ID aktuálního záznamu (pokud je umístěna na stránce záznamu) | | `useFrontComponentId()` | `string` | ID této instance komponenty | | `useFrontComponentExecutionContext(selector)` | různé | Přístup k úplnému kontextu běhu pomocí selektorové funkce | #### API komunikace s hostitelem Frontendové komponenty mohou pomocí funkcí z `twenty-sdk` vyvolávat navigaci, modály a oznámení: | Funkce | Popis | | ----------------------------------------------- | ------------------------------ | | `navigate(to, params?, queryParams?, options?)` | Přejít na stránku v aplikaci | | `openSidePanelPage(params)` | Otevřít postranní panel | | `closeSidePanel()` | Zavře postranní panel | | `openCommandConfirmationModal(params)` | Zobrazit potvrzovací dialog | | `enqueueSnackbar(params)` | Zobrazit oznámení typu toast | | `unmountFrontComponent()` | Odmontovat komponentu | | `updateProgress(progress)` | Aktualizovat indikátor průběhu | Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce: ```tsx src/front-components/archive-record.tsx import { defineFrontComponent, useRecordId } from 'twenty-sdk'; import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk'; 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 (

Archive this record?

); }; export default defineFrontComponent({ universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678', name: 'archive-record', description: 'Archives the current record', component: ArchiveRecord, }); ``` #### Možnosti příkazu Přidání pole `command` do `defineFrontComponent` zaregistruje komponentu v příkazovém menu (Cmd+K). Pokud je `isPinned` nastaveno na `true`, zobrazí se také jako tlačítko rychlé akce v pravém horním rohu stránky. | Pole | Povinné | Popis | | --------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `universalIdentifier` | Ano | Stabilní jedinečné ID pro příkaz | | `label` | Ano | Plný popisek zobrazený v příkazovém menu (Cmd+K) | | `shortLabel` | Ne | Kratší popisek zobrazený na připnutém tlačítku rychlé akce | | `icon` | Ne | Název ikony zobrazený vedle popisku (např. `'IconBolt'`, `'IconSend'`) | | `isPinned` | Ne | Pokud je `true`, zobrazí příkaz jako tlačítko rychlé akce v pravém horním rohu stránky | | `availabilityType` | Ne | Určuje, kde se příkaz zobrazuje: `'GLOBAL'` (vždy dostupné), `'RECORD_SELECTION'` (pouze když jsou vybrány záznamy) nebo `'FALLBACK'` (zobrazeno, když neodpovídají žádné jiné příkazy) | | `availabilityObjectUniversalIdentifier` | Ne | Omezí příkaz na stránky konkrétního typu objektu (např. pouze u záznamů Company) | | `conditionalAvailabilityExpression` | Ne | Logický výraz pro dynamické řízení, zda je příkaz viditelný (viz níže) | #### Výrazy podmíněné dostupnosti Pole `conditionalAvailabilityExpression` vám umožní řídit viditelnost příkazu na základě aktuálního kontextu stránky. Pro sestavení výrazů importujte typované proměnné a operátory z `twenty-sdk`: ```tsx import { defineFrontComponent, pageType, numberOfSelectedRecords, objectPermissions, everyEquals, isDefined, } from 'twenty-sdk'; export default defineFrontComponent({ universalIdentifier: '...', name: 'bulk-action', component: BulkAction, command: { universalIdentifier: '...', label: 'Bulk Update', availabilityType: 'RECORD_SELECTION', conditionalAvailabilityExpression: everyEquals( objectPermissions, 'canUpdateObjectRecords', true, ), }, }); ``` **Kontextové proměnné** — reprezentují aktuální stav stránky: | Proměnná | Typ | Popis | | ------------------------------ | --------- | -------------------------------------------------------------------- | | `pageType` | `string` | Aktuální typ stránky (např. `'RecordIndexPage'`, `'RecordShowPage'`) | | `isInSidePanel` | `boolean` | Zda je komponenta vykreslena v postranním panelu | | `numberOfSelectedRecords` | `number` | Počet aktuálně vybraných záznamů | | `isSelectAll` | `boolean` | Zda je aktivní "vybrat vše" | | `selectedRecords` | `array` | Vybrané objekty záznamů | | `favoriteRecordIds` | `array` | ID oblíbených záznamů | | `objectPermissions` | `object` | Oprávnění pro aktuální typ objektu | | `targetObjectReadPermissions` | `object` | Oprávnění ke čtení pro cílový objekt | | `targetObjectWritePermissions` | `object` | Oprávnění k zápisu pro cílový objekt | | `featureFlags` | `object` | Aktivní příznaky funkcí | | `objectMetadataItem` | `object` | Metadata aktuálního typu objektu | | `hasAnySoftDeleteFilterOnView` | `boolean` | Zda má aktuální zobrazení filtr soft-delete | **Operátory** — kombinují proměnné do logických výrazů: | Operátor | Popis | | ----------------------------------- | ------------------------------------------------------------------------------ | | `isDefined(value)` | `true`, pokud hodnota není null/undefined | | `isNonEmptyString(value)` | `true`, pokud je hodnota neprázdný řetězec | | `includes(array, value)` | `true`, pokud pole obsahuje danou hodnotu | | `includesEvery(array, prop, value)` | `true`, pokud vlastnost každé položky zahrnuje danou hodnotu | | `every(array, prop)` | `true`, pokud je vlastnost u každé položky pravdivá (truthy) | | `everyDefined(array, prop)` | `true`, pokud je vlastnost definována u každé položky | | `everyEquals(array, prop, value)` | `true`, pokud se vlastnost rovná hodnotě u každé položky | | `some(array, prop)` | `true`, pokud je vlastnost pravdivá (truthy) alespoň u jedné položky | | `someDefined(array, prop)` | `true`, pokud je vlastnost definována alespoň u jedné položky | | `someEquals(array, prop, value)` | `true`, pokud se vlastnost rovná hodnotě alespoň u jedné položky | | `someNonEmptyString(array, prop)` | `true`, pokud má vlastnost alespoň u jedné položky hodnotu neprázdného řetězce | | `none(array, prop)` | `true`, pokud je vlastnost u všech položek nepravdivá (falsy) | | `noneDefined(array, prop)` | `true`, pokud je vlastnost u všech položek nedefinovaná | | `noneEquals(array, prop, value)` | `true`, pokud se vlastnost nerovná hodnotě u žádné položky | #### Veřejné soubory Frontendové komponenty mohou přistupovat k souborům ze složky aplikace `public/` pomocí `getPublicAssetUrl`: ```tsx import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk'; const Logo = () => Logo; export default defineFrontComponent({ universalIdentifier: '...', name: 'logo', component: Logo, }); ``` Podrobnosti viz [sekci veřejných souborů](#accessing-public-assets-with-getpublicasseturl). #### Styling Frontendové komponenty podporují více přístupů ke stylování. Můžete použít: * **Inline styly** — `style={{ color: 'red' }}` * **Komponenty Twenty UI** — import z `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar a další) * **Emotion** — CSS-in-JS s `@emotion/react` * **Styled-components** — vzory `styled.div` * **Tailwind CSS** — utilitní třídy * **Jakákoli CSS-in-JS knihovna** kompatibilní s Reactem ```tsx import { defineFrontComponent } from 'twenty-sdk'; import { Button, Tag, Status } from 'twenty-sdk/ui'; const StyledWidget = () => { return (
); }; export default defineFrontComponent({ universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456', name: 'styled-widget', component: StyledWidget, }); ```
Dovednosti definují znovupoužitelné pokyny a schopnosti, které mohou agenti AI používat ve vašem pracovním prostoru. K definování dovedností s vestavěnou validací použijte `defineSkill()`: ```ts src/skills/example-skill.ts import { defineSkill } from 'twenty-sdk'; 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`, }); ``` Hlavní body: * `name` je jedinečný identifikátor dovednosti (doporučuje se kebab-case). * `label` je uživatelsky čitelný název zobrazovaný v UI. * `content` obsahuje pokyny dovednosti — je to text, který agent AI používá. * `icon` (volitelné) nastavuje ikonu zobrazovanou v UI. * `description` (volitelné) poskytuje doplňující kontext o účelu dovednosti. Agenti jsou asistenti AI, kteří běží ve vašem pracovním prostoru. K vytvoření agentů s vlastním systémovým promptem použijte `defineAgent()`: ```ts src/agents/example-agent.ts import { defineAgent } from 'twenty-sdk'; 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.', }); ``` Hlavní body: * `name` je jedinečný identifikátor agenta (doporučuje se kebab-case). * `label` je zobrazovaný název v UI. * `prompt` je systémový prompt, který definuje chování agenta. * `description` (volitelné) poskytuje kontext o tom, co agent dělá. * `icon` (volitelné) nastavuje ikonu zobrazovanou v UI. * `modelId` (volitelné) přepíše výchozí model AI používaný agentem. Zobrazení jsou uložené konfigurace toho, jak se zobrazují záznamy objektu — včetně toho, která pole jsou viditelná, jejich pořadí a jaké filtry či seskupení jsou použity. Pomocí `defineView()` můžete k aplikaci přidat předkonfigurovaná zobrazení: ```ts src/views/example-view.ts import { defineView, ViewKey } from 'twenty-sdk'; 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, }, ], }); ``` Hlavní body: * `objectUniversalIdentifier` určuje, na který objekt se toto zobrazení vztahuje. * `key` určuje typ zobrazení (např. `ViewKey.INDEX` pro hlavní seznam). * `fields` určuje, které sloupce se zobrazí a v jakém pořadí. Každé pole odkazuje na `fieldMetadataUniversalIdentifier`. * Pro pokročilejší konfigurace můžete definovat také `filters`, `filterGroups`, `groups` a `fieldGroups`. * `position` určuje pořadí, pokud pro stejný objekt existuje více zobrazení. Položky navigační nabídky přidávají vlastní položky do postranního panelu pracovního prostoru. Použijte `defineNavigationMenuItem()` k odkazování na zobrazení, externí URL nebo objekty: ```ts src/navigation-menu-items/example-navigation-menu-item.ts import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk'; 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, }); ``` Hlavní body: * `type` určuje, na co položka menu odkazuje: `NavigationMenuItemType.VIEW` pro uložené zobrazení nebo `NavigationMenuItemType.LINK` pro externí URL. * Pro odkazy na zobrazení nastavte `viewUniversalIdentifier`. Pro externí odkazy nastavte `link`. * `position` určuje pořadí v postranním panelu. * `icon` a `color` (volitelné) upravují vzhled. Rozvržení stránek vám umožní přizpůsobit vzhled stránky s detailem záznamu — které karty se zobrazí, jaké widgety jsou uvnitř každé karty a jak jsou uspořádány. Pomocí `definePageLayout()` můžete k aplikaci přidat vlastní rozvržení: ```ts src/page-layouts/example-record-page-layout.ts import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk'; 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, }, }, ], }, ], }); ``` Hlavní body: * `type` je obvykle `'RECORD_PAGE'` pro úpravu detailního zobrazení konkrétního objektu. * `objectUniversalIdentifier` určuje, na který objekt se toto rozvržení vztahuje. * Každá `tab` definuje sekci stránky s `title`, `position` a `layoutMode` (`CANVAS` pro volné rozvržení). * Každý `widget` uvnitř karty může vykreslit frontendovou komponentu, seznam relací nebo jiné vestavěné typy widgetů. * `position` na kartách určuje jejich pořadí. Použijte vyšší hodnoty (např. 50) pro umístění vlastních karet za vestavěné.
## Veřejné prostředky (složka `public/`) Složka `public/` v kořenu vaší aplikace obsahuje statické soubory — obrázky, ikony, písma a další prostředky, které vaše aplikace potřebuje za běhu. Tyto soubory jsou automaticky zahrnuty do buildů, synchronizovány během vývojového režimu a nahrávány na server. Soubory umístěné v `public/` jsou: * **Veřejně přístupné** — po synchronizaci na server jsou prostředky dostupné na veřejné URL. K přístupu k nim není potřeba žádná autentizace. * **Dostupné ve frontendových komponentách** — použijte URL prostředků k zobrazení obrázků, ikon či jiných médií uvnitř komponent Reactu. * **Dostupné v logických funkcích** — odkazujte na URL prostředků v e-mailech, odpovědích API či jiné serverové logice. * **Používány pro metadata Marketplace** — pole `logoUrl` a `screenshots` v `defineApplication()` odkazují na soubory z této složky (např. `public/logo.png`). Tyto se zobrazují v Marketplace, když je vaše aplikace zveřejněna. * **Automaticky synchronizované ve vývojovém režimu** — když v `public/` přidáte, aktualizujete nebo smažete soubor, je automaticky synchronizován na server. Není potřeba restart. * **Zahrnuté do buildů** — `yarn twenty build` zabalí všechny veřejné prostředky do distribučního výstupu. ### Přístup k veřejným prostředkům pomocí `getPublicAssetUrl` K získání plné URL souboru ve vaší složce `public/` použijte pomocnou funkci `getPublicAssetUrl` z `twenty-sdk`. Funguje jak v logických funkcích, tak ve frontendových komponentách. **V logické funkci:** ```ts src/logic-functions/send-invoice.ts import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk'; const handler = async (): Promise => { 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, }); ``` **Ve frontendové komponentě:** ```tsx src/front-components/company-card.tsx import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk'; export default defineFrontComponent(() => { const logoUrl = getPublicAssetUrl('logo.png'); return App logo; }); ``` Argument `path` je relativní ke složce `public/` vaší aplikace. Jak `getPublicAssetUrl('logo.png')`, tak `getPublicAssetUrl('public/logo.png')` se vyhodnotí na stejnou URL — předpona `public/` je, je-li přítomna, automaticky odstraněna. ## Používání balíčků npm Ve své aplikaci můžete nainstalovat a používat libovolný balíček npm. Logické funkce i frontendové komponenty se bundlují pomocí [esbuild](https://esbuild.github.io/), který vloží všechny závislosti přímo do výstupu — za běhu nejsou potřeba žádné `node_modules`. ### Instalace balíčku ```bash filename="Terminal" yarn add axios ``` Poté jej importujte ve svém kódu: ```ts src/logic-functions/fetch-data.ts import { defineLogicFunction } from 'twenty-sdk'; import axios from 'axios'; const handler = async (): Promise => { 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, }); ``` Stejně to funguje i pro frontendové komponenty: ```tsx src/front-components/chart.tsx import { defineFrontComponent } from 'twenty-sdk'; import { format } from 'date-fns'; const DateWidget = () => { return

Today is {format(new Date(), 'MMMM do, yyyy')}

; }; export default defineFrontComponent({ universalIdentifier: '...', name: 'date-widget', component: DateWidget, }); ``` ### Jak funguje bundlování Krok sestavení používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu. **Logické funkce** běží v prostředí Node.js. Vestavěné moduly Node (`fs`, `path`, `crypto`, `http` atd.) jsou k dispozici a není je třeba instalovat. **Frontendové komponenty** běží ve Web Workeru. Vestavěné moduly Node nejsou k dispozici — pouze prohlížečová API a balíčky npm, které fungují v prohlížečovém prostředí. V obou prostředích jsou jako předpřipravené moduly k dispozici `twenty-client-sdk/core` a `twenty-client-sdk/metadata` — nejsou součástí bundlu, ale server je za běhu načítá. ## Generování entit pomocí `yarn twenty add` Místo ručního vytváření souborů entit můžete použít interaktivní generátor: ```bash filename="Terminal" yarn twenty add ``` Požádá vás o výběr typu entity a provede vás požadovanými poli. Vygeneruje soubor připravený k použití se stabilním `universalIdentifier` a správným voláním `defineEntity()`. Můžete také předat typ entity přímo a přeskočit první dotaz: ```bash filename="Terminal" yarn twenty add object yarn twenty add logicFunction yarn twenty add frontComponent ``` ### Dostupné typy entit | Typ entity | Příkaz | Vygenerovaný soubor | | ------------------------- | ------------------------------------ | ------------------------------------- | | Objekt | `yarn twenty add object` | `src/objects/.ts` | | Pole | `yarn twenty add field` | `src/fields/.ts` | | Logická funkce | `yarn twenty add logicFunction` | `src/logic-functions/.ts` | | Frontendová komponenta | `yarn twenty add frontComponent` | `src/front-components/.tsx` | | Role | `yarn twenty add role` | `src/roles/.ts` | | Dovednost | `yarn twenty add skill` | `src/skills/.ts` | | Agent | `yarn twenty add agent` | `src/agents/.ts` | | Pohled | `yarn twenty add view` | `src/views/.ts` | | Položka navigační nabídky | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/.ts` | | Rozvržení stránky | `yarn twenty add pageLayout` | `src/page-layouts/.ts` | ### Co generátor vytváří Každý typ entity má vlastní šablonu. Například `yarn twenty add object` se zeptá na: 1. **Název (jednotné číslo)** — např. `invoice` 2. **Název (množné číslo)** — např. `invoices` 3. **Štítek (jednotné číslo)** — automaticky doplněn z názvu (např. `Invoice`) 4. **Štítek (množné číslo)** — automaticky doplněn (např. `Invoices`) 5. **Vytvořit zobrazení a položku navigace?** — pokud odpovíte ano, generátor také vytvoří odpovídající zobrazení a odkaz v postranním panelu pro nový objekt. Ostatní typy entit mají jednodušší dotazy — většinou se ptají pouze na název. Typ entity `field` je podrobnější: ptá se na název pole, štítek, typ (ze seznamu všech dostupných typů polí jako `TEXT`, `NUMBER`, `SELECT`, `RELATION` atd.) a `universalIdentifier` cílového objektu. ### Vlastní výstupní cesta Pomocí příznaku `--path` umístíte vygenerovaný soubor do vlastního umístění: ```bash filename="Terminal" yarn twenty add logicFunction --path src/custom-folder ``` ## Typovaní klienti API (twenty-client-sdk) Balíček `twenty-client-sdk` poskytuje dva typované klienty GraphQL pro práci s Twenty API z vašich logických funkcí a frontendových komponent. | Klient | Importovat | Koncový bod | Generováno? | | ------------------- | ---------------------------- | ---------------------------------------------------------------- | ------------------------------ | | `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — data pracovního prostoru (záznamy, objekty) | Ano, při vývoji/sestavení | | `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — konfigurace pracovního prostoru, nahrávání souborů | Ne, dodává se předem sestavený | `CoreApiClient` je hlavní klient pro dotazování a mutace dat pracovního prostoru. Generuje se z vašeho schématu pracovního prostoru během `yarn twenty dev` nebo `yarn twenty build`, takže je plně typovaný tak, aby odpovídal vašim objektům a polím. ```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, }, }); ``` Klient používá syntaxi výběrové sady (selection-set): předáním `true` zahrnete pole, pro argumenty použijte `__args` a pro relace vnořujte objekty. Získáte plné automatické doplňování a kontrolu typů založené na schématu vašeho pracovního prostoru. **CoreApiClient je generován při vývoji/sestavení.** Pokud jej použijete bez předchozího spuštění `yarn twenty dev` nebo `yarn twenty build`, vyvolá chybu. Generování probíhá automaticky — CLI prozkoumá GraphQL schéma vašeho pracovního prostoru a vygeneruje typovaného klienta pomocí `@genql/cli`. #### Použití CoreSchema pro anotace typů `CoreSchema` poskytuje typy TypeScriptu odpovídající objektům vašeho pracovního prostoru — hodí se pro typování stavu komponent nebo parametrů funkcí: ```ts import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core'; import { useState } from 'react'; const [company, setCompany] = useState< Pick | undefined >(undefined); const client = new CoreApiClient(); const result = await client.query({ company: { __args: { filter: { position: { eq: 1 } } }, id: true, name: true, }, }); setCompany(result.company); ``` `MetadataApiClient` je součástí SDK již předem sestavený (není vyžadována žádná generace). Odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru, aplikace a nahrávání souborů. ```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 }, }, }, }); ``` #### Nahrávání souborů `MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru: ```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://...' } ``` | Parametr | Typ | Popis | | ---------------------------------- | -------- | ------------------------------------------------------------------- | | `fileBuffer` | `Buffer` | Surový obsah souboru | | `filename` | `string` | Název souboru (používá se pro ukládání a zobrazení) | | `contentType` | `string` | Typ MIME (pokud je vynechán, výchozí je `application/octet-stream`) | | `fieldMetadataUniversalIdentifier` | `string` | `universalIdentifier` pole typu souboru ve vašem objektu | Hlavní body: * Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje v jakémkoli pracovním prostoru, kde je vaše aplikace nainstalována. * Vrácená hodnota `url` je podepsaná adresa URL, kterou můžete použít k přístupu k nahranému souboru. Když váš kód běží na Twenty (logické funkce nebo frontendové komponenty), platforma vloží přihlašovací údaje jako proměnné prostředí: * `TWENTY_API_URL` — Základní URL Twenty API * `TWENTY_APP_ACCESS_TOKEN` — krátkodobý klíč s rozsahem omezeným na výchozí roli funkce vaší aplikace Není nutné je předávat klientům — čtou je automaticky z `process.env`. Oprávnění API klíče jsou určena rolí uvedenou v `defaultRoleUniversalIdentifier` ve vašem `application-config.ts`. ## Testování vaší aplikace SDK poskytuje programová rozhraní, která vám umožní z testovacího kódu aplikaci sestavit, nasadit, nainstalovat a odinstalovat. V kombinaci s [Vitest](https://vitest.dev/) a typovanými klienty API můžete psát integrační testy, které ověří, že vaše aplikace funguje end-to-end proti reálnému serveru Twenty. ### Nastavení Vygenerovaná aplikace již obsahuje Vitest. Pokud to nastavujete ručně, nainstalujte závislosti: ```bash filename="Terminal" yarn add -D vitest vite-tsconfig-paths ``` Vytvořte `vitest.config.ts` v kořeni vaší aplikace: ```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', }, }, }); ``` Vytvořte soubor nastavení, který před spuštěním testů ověří dostupnost serveru: ```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), ); }); ``` ### Programová rozhraní SDK Subcesta `twenty-sdk/cli` exportuje funkce, které můžete volat přímo z testovacího kódu: | Funkce | Popis | | -------------- | ----------------------------------------------------- | | `appBuild` | Sestaví aplikaci a volitelně zabalí tarball | | `appDeploy` | Nahraje tarball na server | | `appInstall` | Nainstaluje aplikaci do aktivního pracovního prostoru | | `appUninstall` | Odinstaluje aplikaci z aktivního pracovního prostoru | Každá funkce vrací objekt výsledku se `success: boolean` a buď `data`, nebo `error`. ### Psání integračního testu Zde je kompletní příklad, který aplikaci sestaví, nasadí a nainstaluje a poté ověří, že se objeví v pracovním prostoru: ```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(); }); }); ``` ### Spuštění testů Ujistěte se, že běží váš lokální server Twenty, a poté: ```bash filename="Terminal" yarn test ``` Nebo v režimu watch během vývoje: ```bash filename="Terminal" yarn test:watch ``` ### Kontrola typů Kontrolu typů můžete spustit i na vaší aplikaci bez spuštění testů: ```bash filename="Terminal" yarn twenty typecheck ``` Spustí se `tsc --noEmit` a nahlásí se případné chyby typů. ## Referenční dokumentace CLI Kromě `dev`, `build`, `add` a `typecheck` poskytuje CLI příkazy pro spouštění funkcí, zobrazení logů a správu instalací aplikací. ### Spouštění funkcí (`yarn twenty exec`) Spusťte logickou funkci ručně bez vyvolání přes HTTP, cron nebo databázovou událost: ```bash filename="Terminal" # Execute by function name yarn twenty exec -n create-new-post-card # Execute by universalIdentifier yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf # Pass a JSON payload yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}' # Execute the post-install function yarn twenty exec --postInstall ``` ### Zobrazení logů funkcí (`yarn twenty logs`) Streamujte výstupní logy běhu logických funkcí vaší aplikace: ```bash filename="Terminal" # Stream all function logs yarn twenty logs # Filter by function name yarn twenty logs -n create-new-post-card # Filter by universalIdentifier yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf ``` To je jiné než `yarn twenty server logs`, které zobrazují logy kontejneru Docker. `yarn twenty logs` zobrazuje logy spuštění funkcí vaší aplikace ze serveru Twenty. ### Odinstalace aplikace (`yarn twenty uninstall`) Odeberte svou aplikaci z aktivního pracovního prostoru: ```bash filename="Terminal" yarn twenty uninstall # Skip the confirmation prompt yarn twenty uninstall --yes ``` ## Správa vzdálených serverů **Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat. ```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 --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote # List all configured remotes yarn twenty remote list # Switch the active remote yarn twenty remote switch ``` Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`. ## CI s GitHub Actions Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů. Workflow: 1. Načte váš kód (checkout). 2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` 3. Nainstaluje závislosti pomocí `yarn install --immutable` 4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce ```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 }} ``` Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí efemérní server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky. Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.