mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / docs-lint (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
2063 lines
94 KiB
Text
2063 lines
94 KiB
Text
---
|
||
title: Vytváření aplikací
|
||
description: Definujte objekty, logické funkce, frontendové komponenty a další pomocí Twenty SDK.
|
||
---
|
||
|
||
<Warning>
|
||
Aplikace jsou aktuálně v alfa fázi. Funkce funguje, ale stále se vyvíjí.
|
||
</Warning>
|
||
|
||
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.
|
||
|
||
<Note>
|
||
**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.
|
||
</Note>
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="defineRole" description="Konfigurace oprávnění rolí a přístupu k objektům">
|
||
|
||
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/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,
|
||
},
|
||
],
|
||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||
});
|
||
```
|
||
|
||
</Accordion>
|
||
<Accordion title="defineApplication" description="Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci)">
|
||
|
||
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/define';
|
||
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/define';
|
||
|
||
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).
|
||
|
||
</Accordion>
|
||
<Accordion title="defineObject" description="Definice vlastních objektů s poli">
|
||
|
||
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/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,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
<Note>
|
||
**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.
|
||
</Note>
|
||
|
||
</Accordion>
|
||
<Accordion title="defineField — Standardní pole" description="Rozšiřte existující objekty o další pole">
|
||
|
||
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/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' },
|
||
],
|
||
});
|
||
```
|
||
|
||
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()`.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineField — Relační pole" description="Propojte objekty obousměrnými relacemi">
|
||
|
||
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/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,
|
||
},
|
||
});
|
||
```
|
||
|
||
**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/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>
|
||
**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.
|
||
</Note>
|
||
|
||
#### 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/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',
|
||
},
|
||
});
|
||
```
|
||
|
||
#### 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
|
||
],
|
||
});
|
||
```
|
||
</Accordion>
|
||
<Accordion title="defineLogicFunction" description="Definujte logické funkce a jejich spouštěče">
|
||
|
||
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/define';
|
||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
|
||
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.*`
|
||
|
||
<Note>
|
||
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
|
||
```
|
||
</Note>
|
||
|
||
#### 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/define';
|
||
|
||
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\<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) | viz sekce níže |
|
||
| `queryStringParameters` | `Record\<string, string \| undefined>` | 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\<string, string \| undefined>` | 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 };
|
||
};
|
||
```
|
||
|
||
<Note>
|
||
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']`).
|
||
</Note>
|
||
|
||
#### 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/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,
|
||
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'],
|
||
},
|
||
});
|
||
```
|
||
|
||
<Note>
|
||
**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.
|
||
</Note>
|
||
|
||
</Accordion>
|
||
<Accordion title="definePostInstallLogicFunction" description="Definujte postinstalační logickou funkci (jedna na aplikaci)">
|
||
|
||
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/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,
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
</Accordion>
|
||
<Accordion title="definePreInstallLogicFunction" description="Definujte předinstalační logickou funkci (jedna na aplikaci)">
|
||
|
||
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/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,
|
||
});
|
||
```
|
||
|
||
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í.
|
||
|
||
</Accordion>
|
||
<Accordion title="Pre-install vs post-install: kdy použít který" description="Výběr správného instalačního hooku">
|
||
|
||
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/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,
|
||
});
|
||
```
|
||
|
||
**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/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,
|
||
});
|
||
```
|
||
|
||
**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í) |
|
||
|
||
<Note>
|
||
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í.
|
||
</Note>
|
||
|
||
</Accordion>
|
||
<Accordion title="defineFrontComponent" description="Definujte frontendové komponenty pro vlastní uživatelské rozhraní">
|
||
|
||
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/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,
|
||
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:
|
||
|
||
<div style={{textAlign: 'center'}}>
|
||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Tlačítko rychlé akce v pravém horním rohu" />
|
||
</div>
|
||
|
||
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 } 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,
|
||
});
|
||
```
|
||
|
||
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/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,
|
||
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/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,
|
||
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 } 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,
|
||
});
|
||
```
|
||
|
||
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 } 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,
|
||
});
|
||
```
|
||
|
||
#### 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 } from 'twenty-sdk/define';
|
||
import {
|
||
pageType,
|
||
numberOfSelectedRecords,
|
||
objectPermissions,
|
||
everyEquals,
|
||
isDefined,
|
||
} from 'twenty-sdk/front-component';
|
||
|
||
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/define';
|
||
|
||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="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/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,
|
||
});
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="defineSkill" description="Definujte dovednosti agenta AI">
|
||
|
||
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/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`,
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineAgent" description="Definujte AI agenty s vlastními prompty">
|
||
|
||
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/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.',
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineView" description="Definujte uložená zobrazení pro objekty">
|
||
|
||
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/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,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
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í.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineNavigationMenuItem" description="Definujte odkazy postranní navigace">
|
||
|
||
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/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,
|
||
});
|
||
```
|
||
|
||
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.
|
||
|
||
</Accordion>
|
||
<Accordion title="definePageLayout" description="Definujte vlastní rozvržení stránek pro zobrazení záznamů">
|
||
|
||
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/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,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
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é.
|
||
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
## 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/define';
|
||
|
||
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,
|
||
});
|
||
```
|
||
|
||
**Ve frontendové komponentě:**
|
||
|
||
```tsx src/front-components/company-card.tsx
|
||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
|
||
|
||
export default defineFrontComponent(() => {
|
||
const logoUrl = getPublicAssetUrl('logo.png');
|
||
|
||
return <img src={logoUrl} alt="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/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,
|
||
});
|
||
```
|
||
|
||
Stejně to funguje i pro frontendové komponenty:
|
||
|
||
```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,
|
||
});
|
||
```
|
||
|
||
### 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/\<name>.ts` |
|
||
| Pole | `yarn twenty add field` | `src/fields/\<name>.ts` |
|
||
| Logická funkce | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||
| Frontendová komponenta | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
|
||
| Role | `yarn twenty add role` | `src/roles/\<name>.ts` |
|
||
| Dovednost | `yarn twenty add skill` | `src/skills/\<name>.ts` |
|
||
| Agent | `yarn twenty add agent` | `src/agents/\<name>.ts` |
|
||
| Pohled | `yarn twenty add view` | `src/views/\<name>.ts` |
|
||
| Položka navigační nabídky | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||
| Rozvržení stránky | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.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ý |
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="CoreApiClient" description="Dotazování a změny dat pracovního prostoru (záznamy, objekty)">
|
||
|
||
`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.
|
||
|
||
<Note>
|
||
**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`.
|
||
</Note>
|
||
|
||
#### 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<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="Konfigurace pracovního prostoru, aplikace a nahrávání souborů">
|
||
|
||
`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.
|
||
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
<Note>
|
||
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`.
|
||
</Note>
|
||
|
||
## 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
|
||
```
|
||
|
||
<Note>
|
||
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.
|
||
</Note>
|
||
|
||
### 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 <name>
|
||
```
|
||
|
||
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.
|