ae202a1b59
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
1716 lines
73 KiB
Plaintext
1716 lines
73 KiB
Plaintext
---
|
||
title: Uygulama Geliştirme
|
||
description: Nesneleri, mantık fonksiyonlarını, ön uç bileşenlerini ve daha fazlasını Twenty SDK ile tanımlayın.
|
||
---
|
||
|
||
<Warning>
|
||
Apps are currently in alpha. The feature works but is still evolving.
|
||
</Warning>
|
||
|
||
The `twenty-sdk` package provides typed building blocks to create your app. This page covers every entity type and API client available in the SDK.
|
||
|
||
## DefineEntity functions
|
||
|
||
The SDK provides functions to define your app entities. You must use `export default defineEntity({...})` for the SDK to detect your entities. Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
|
||
|
||
<Note>
|
||
**File organization is up to you.**
|
||
Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. Grouping files by type (e.g., `logic-functions/`, `roles/`) is just a convention, not a requirement.
|
||
</Note>
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="defineRole" description="Rol izinlerini ve nesne erişimini yapılandırın">
|
||
|
||
Roles encapsulate permissions on your workspace's objects and actions.
|
||
|
||
```ts restricted-company-role.ts
|
||
import {
|
||
defineRole,
|
||
PermissionFlag,
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||
} from 'twenty-sdk';
|
||
|
||
export default defineRole({
|
||
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
||
label: 'My new role',
|
||
description: 'A role that can be used in your workspace',
|
||
canReadAllObjectRecords: false,
|
||
canUpdateAllObjectRecords: false,
|
||
canSoftDeleteAllObjectRecords: false,
|
||
canDestroyAllObjectRecords: false,
|
||
canUpdateAllSettings: false,
|
||
canBeAssignedToAgents: false,
|
||
canBeAssignedToUsers: false,
|
||
canBeAssignedToApiKeys: false,
|
||
objectPermissions: [
|
||
{
|
||
objectUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||
canReadObjectRecords: true,
|
||
canUpdateObjectRecords: true,
|
||
canSoftDeleteObjectRecords: false,
|
||
canDestroyObjectRecords: false,
|
||
},
|
||
],
|
||
fieldPermissions: [
|
||
{
|
||
objectUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||
fieldUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||
canReadFieldValue: false,
|
||
canUpdateFieldValue: false,
|
||
},
|
||
],
|
||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||
});
|
||
```
|
||
|
||
</Accordion>
|
||
<Accordion title="defineApplication" description="Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet)">
|
||
|
||
Every app must have exactly one `defineApplication` call that describes:
|
||
|
||
* **Identity**: identifiers, display name, and description.
|
||
* **Permissions**: which role its functions and front components use.
|
||
* **(Optional) Variables**: key–value pairs exposed to your functions as environment variables.
|
||
* **(Optional) Pre-install / post-install functions**: logic functions that run before or after installation.
|
||
|
||
```ts src/application-config.ts
|
||
import { defineApplication } from 'twenty-sdk';
|
||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||
|
||
export default defineApplication({
|
||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||
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,
|
||
});
|
||
```
|
||
|
||
Notlar:
|
||
* `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
|
||
* `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||
* `defaultRoleUniversalIdentifier` must reference a role defined with `defineRole()` (see above).
|
||
* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||
|
||
#### Pazaryeri meta verileri
|
||
|
||
If you plan to [publish your app](/l/tr/developers/extend/apps/publishing), these optional fields control how it appears in the marketplace:
|
||
|
||
| Alan | Açıklama |
|
||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `author` | Yazar veya şirket adı |
|
||
| `category` | Pazaryerinde filtreleme için uygulama kategorisi |
|
||
| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) |
|
||
| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) |
|
||
| `aboutDescription` | "Hakkında" sekmesi için daha uzun bir markdown açıklaması. Belirtilmezse, pazaryeri npm'deki paketin `README.md` dosyasını kullanır |
|
||
| `websiteUrl` | Web sitenize bağlantı |
|
||
| `termsUrl` | Hizmet Koşulları'na bağlantı |
|
||
| `emailSupport` | Destek e-posta adresi |
|
||
| `issueReportUrl` | Sorun izleyicisine bağlantı |
|
||
|
||
#### Roller ve izinler
|
||
|
||
The `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions and front components. See `defineRole` above for details.
|
||
|
||
* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role.
|
||
* The typed client is restricted to the permissions granted to that role.
|
||
* Follow least-privilege: create a dedicated role with only the permissions your functions need.
|
||
|
||
##### Default function role
|
||
|
||
When you scaffold a new app, the CLI creates a default role file:
|
||
|
||
```ts src/roles/default-role.ts
|
||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||
|
||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||
|
||
export default defineRole({
|
||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||
label: 'Default function role',
|
||
description: 'Default role for function Twenty client',
|
||
canReadAllObjectRecords: true,
|
||
canUpdateAllObjectRecords: false,
|
||
canSoftDeleteAllObjectRecords: false,
|
||
canDestroyAllObjectRecords: false,
|
||
canUpdateAllSettings: false,
|
||
canBeAssignedToAgents: false,
|
||
canBeAssignedToUsers: false,
|
||
canBeAssignedToApiKeys: false,
|
||
objectPermissions: [],
|
||
fieldPermissions: [],
|
||
permissionFlags: [],
|
||
});
|
||
```
|
||
|
||
This role's `universalIdentifier` is referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`:
|
||
|
||
* **\*.role.ts** defines what the role can do.
|
||
* **application-config.ts**, fonksiyonlarınızın izinlerini devralması için bu role işaret eder.
|
||
|
||
Notlar:
|
||
* Oluşturulan rolden başlayın ve en az ayrıcalık ilkesini izleyerek bunu aşamalı olarak kısıtlayın.
|
||
* Replace `objectPermissions` and `fieldPermissions` with the objects and fields your functions actually need.
|
||
* `permissionFlags`, platform düzeyindeki yeteneklere erişimi kontrol eder. Keep them minimal.
|
||
* See a working example: [`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="Alanlara sahip özel nesneler tanımlayın">
|
||
|
||
Özel nesneler, çalışma alanınızdaki kayıtlar için hem şemayı hem de davranışı tanımlar. Yerleşik doğrulamayla nesneler tanımlamak için `defineObject()` kullanın:
|
||
|
||
```ts postCard.object.ts
|
||
import { defineObject, FieldType } from 'twenty-sdk';
|
||
|
||
enum PostCardStatus {
|
||
DRAFT = 'DRAFT',
|
||
SENT = 'SENT',
|
||
DELIVERED = 'DELIVERED',
|
||
RETURNED = 'RETURNED',
|
||
}
|
||
|
||
export default defineObject({
|
||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||
nameSingular: 'postCard',
|
||
namePlural: 'postCards',
|
||
labelSingular: 'Post Card',
|
||
labelPlural: 'Post Cards',
|
||
description: 'A post card object',
|
||
icon: 'IconMail',
|
||
fields: [
|
||
{
|
||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||
name: 'content',
|
||
type: FieldType.TEXT,
|
||
label: 'Content',
|
||
description: "Postcard's content",
|
||
icon: 'IconAbc',
|
||
},
|
||
{
|
||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||
name: 'recipientName',
|
||
type: FieldType.FULL_NAME,
|
||
label: 'Recipient name',
|
||
icon: 'IconUser',
|
||
},
|
||
{
|
||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||
name: 'recipientAddress',
|
||
type: FieldType.ADDRESS,
|
||
label: 'Recipient address',
|
||
icon: 'IconHome',
|
||
},
|
||
{
|
||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||
name: 'status',
|
||
type: FieldType.SELECT,
|
||
label: 'Status',
|
||
icon: 'IconSend',
|
||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||
options: [
|
||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||
],
|
||
},
|
||
{
|
||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||
name: 'deliveredAt',
|
||
type: FieldType.DATE_TIME,
|
||
label: 'Delivered at',
|
||
icon: 'IconCheck',
|
||
isNullable: true,
|
||
defaultValue: null,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
|
||
* Yerleşik doğrulama ve daha iyi IDE desteği için `defineObject()` kullanın.
|
||
* `universalIdentifier` dağıtımlar arasında benzersiz ve kararlı olmalıdır.
|
||
* Her alan bir `name`, `type`, `label` ve kendi kararlı `universalIdentifier` değerini gerektirir.
|
||
* `fields` dizisi isteğe bağlıdır — özel alanlar olmadan da nesneler tanımlayabilirsiniz.
|
||
* `yarn twenty add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
|
||
|
||
<Note>
|
||
**Temel alanlar otomatik olarak oluşturulur.** Özel bir nesne tanımladığınızda Twenty, standart alanları otomatik olarak ekler
|
||
örneğin `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` ve `deletedAt`.
|
||
Bunları `fields` dizinizde tanımlamanız gerekmez — yalnızca özel alanlarınızı ekleyin.
|
||
`fields` dizinizde aynı ada sahip bir alan tanımlayarak varsayılan alanları geçersiz kılabilirsiniz,
|
||
ancak bu önerilmez.
|
||
</Note>
|
||
|
||
</Accordion>
|
||
<Accordion title="defineField — Standard fields" description="Mevcut nesneleri ek alanlarla genişletin">
|
||
|
||
Sahibi olmadığınız nesnelere alan eklemek için `defineField()` kullanın — standart Twenty nesneleri (Person, Company, vb.) gibi. veya diğer uygulamalardaki nesneler. `defineObject()` içindeki satır içi alanların aksine, bağımsız alanlar hangi nesneyi genişlettiklerini belirtmek için bir `objectUniversalIdentifier` gerektirir:
|
||
|
||
```ts src/fields/company-loyalty-tier.field.ts
|
||
import { defineField, FieldType } from 'twenty-sdk';
|
||
|
||
export default defineField({
|
||
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
|
||
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
|
||
name: 'loyaltyTier',
|
||
type: FieldType.SELECT,
|
||
label: 'Loyalty Tier',
|
||
icon: 'IconStar',
|
||
options: [
|
||
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
|
||
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
|
||
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
|
||
],
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `objectUniversalIdentifier` hedef nesneyi tanımlar. Standart nesneler için, `twenty-sdk`'den dışa aktarılan `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`'ı kullanın.
|
||
* Alanları `defineObject()` içinde satır içi tanımlarken, `objectUniversalIdentifier`'a ihtiyacınız yoktur — üst nesneden devralınır.
|
||
* `defineField()`, `defineObject()` ile oluşturmadığınız nesnelere alan eklemenin tek yoludur.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineField — Relation fields" description="Connect objects together with bidirectional relations">
|
||
|
||
İlişkiler nesneleri birbirine bağlar. Twenty'de ilişkiler her zaman **çift yönlüdür** — her iki tarafı da tanımlarsınız ve her taraf diğerine başvurur.
|
||
|
||
İki ilişki türü vardır:
|
||
|
||
| İlişki türü | Açıklama | Yabancı anahtar var mı? |
|
||
| ------------- | --------------------------------------------------------- | ----------------------- |
|
||
| `MANY_TO_ONE` | Bu nesnenin birçok kaydı, hedefin bir kaydını işaret eder | Evet (`joinColumnName`) |
|
||
| `ONE_TO_MANY` | Bu nesnenin bir kaydı, hedefin birçok kaydına sahiptir | Hayır (ters taraf) |
|
||
|
||
#### İlişkiler nasıl çalışır
|
||
|
||
Her ilişki, birbirine referans veren iki alan gerektirir:
|
||
|
||
1. **MANY_TO_ONE** tarafı — yabancı anahtarı tutan nesne üzerinde bulunur
|
||
2. **ONE_TO_MANY** tarafı — koleksiyona sahip olan nesne üzerinde bulunur
|
||
|
||
Her iki alan da `FieldType.RELATION` kullanır ve `relationTargetFieldMetadataUniversalIdentifier` aracılığıyla birbirine karşılıklı referans verir.
|
||
|
||
#### Örnek: Posta Kartı'nın birçok Alıcısı vardır
|
||
|
||
Bir `PostCard`'ın birçok `PostCardRecipient` kaydına gönderilebildiğini varsayalım. Her alıcı tam olarak bir posta kartına aittir.
|
||
|
||
**Adım 1: PostCard üzerinde ONE_TO_MANY tarafını tanımlayın** ("bir" taraf):
|
||
|
||
```ts src/fields/post-card-recipients-on-post-card.field.ts
|
||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||
|
||
// Export so the other side can reference it
|
||
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
|
||
// Import from the other side
|
||
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
|
||
|
||
export default defineField({
|
||
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||
type: FieldType.RELATION,
|
||
name: 'postCardRecipients',
|
||
label: 'Post Card Recipients',
|
||
icon: 'IconUsers',
|
||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
|
||
universalSettings: {
|
||
relationType: RelationType.ONE_TO_MANY,
|
||
},
|
||
});
|
||
```
|
||
|
||
**Adım 2: PostCardRecipient üzerinde MANY_TO_ONE tarafını tanımlayın** ("çok" taraf — yabancı anahtarı tutar):
|
||
|
||
```ts src/fields/post-card-on-post-card-recipient.field.ts
|
||
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
|
||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||
|
||
// Export so the other side can reference it
|
||
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
|
||
// Import from the other side
|
||
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
|
||
|
||
export default defineField({
|
||
universalIdentifier: POST_CARD_FIELD_ID,
|
||
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||
type: FieldType.RELATION,
|
||
name: 'postCard',
|
||
label: 'Post Card',
|
||
icon: 'IconMail',
|
||
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
|
||
universalSettings: {
|
||
relationType: RelationType.MANY_TO_ONE,
|
||
onDelete: OnDeleteAction.CASCADE,
|
||
joinColumnName: 'postCardId',
|
||
},
|
||
});
|
||
```
|
||
|
||
<Note>
|
||
**Döngüsel içe aktarmalar:** Her iki ilişki alanı da birbirlerinin `universalIdentifier` değerine referans verir. Döngüsel içe aktarma sorunlarından kaçınmak için, alan kimliklerinizi her dosyadan adlandırılmış sabitler olarak dışa aktarın ve diğer dosyada içe aktarın. Derleme sistemi bunları derleme zamanında çözer.
|
||
</Note>
|
||
|
||
#### Standart nesnelerle ilişkilendirme
|
||
|
||
Yerleşik bir Twenty nesnesiyle (Person, Company, vb.) ilişki oluşturmak için `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` kullanın:
|
||
|
||
```ts src/fields/person-on-self-hosting-user.field.ts
|
||
import {
|
||
defineField,
|
||
FieldType,
|
||
RelationType,
|
||
OnDeleteAction,
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||
} from 'twenty-sdk';
|
||
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
|
||
|
||
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
|
||
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
|
||
|
||
export default defineField({
|
||
universalIdentifier: PERSON_FIELD_ID,
|
||
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
|
||
type: FieldType.RELATION,
|
||
name: 'person',
|
||
label: 'Person',
|
||
description: 'Person matching with the self hosting user',
|
||
isNullable: true,
|
||
relationTargetObjectMetadataUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
|
||
universalSettings: {
|
||
relationType: RelationType.MANY_TO_ONE,
|
||
onDelete: OnDeleteAction.SET_NULL,
|
||
joinColumnName: 'personId',
|
||
},
|
||
});
|
||
```
|
||
|
||
#### İlişki alanı özellikleri
|
||
|
||
| Özellik | Zorunlu | Açıklama |
|
||
| ------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
|
||
| `type` | Evet | `FieldType.RELATION` olmalıdır |
|
||
| `relationTargetObjectMetadataUniversalIdentifier` | Evet | Hedef nesnenin `universalIdentifier` değeri |
|
||
| `relationTargetFieldMetadataUniversalIdentifier` | Evet | Hedef nesnedeki eşleşen alanın `universalIdentifier` değeri |
|
||
| `universalSettings.relationType` | Evet | `RelationType.MANY_TO_ONE` veya `RelationType.ONE_TO_MANY` |
|
||
| `universalSettings.onDelete` | Yalnızca MANY_TO_ONE | Başvurulan kayıt silindiğinde ne olacağı: `CASCADE`, `SET_NULL`, `RESTRICT` veya `NO_ACTION` |
|
||
| `universalSettings.joinColumnName` | Yalnızca MANY_TO_ONE | Yabancı anahtar için veritabanı sütun adı (örn. `postCardId`) |
|
||
|
||
#### defineObject içinde satır içi ilişki alanları
|
||
|
||
İlişki alanlarını doğrudan `defineObject()` içinde de tanımlayabilirsiniz. Bu durumda, `objectUniversalIdentifier`'ı atlayın — üst nesneden devralınır:
|
||
|
||
```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="Define logic functions and their triggers">
|
||
|
||
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineLogicFunction()` kullanır.
|
||
|
||
```ts src/logic-functions/createPostCard.logic-function.ts
|
||
import { defineLogicFunction } from 'twenty-sdk';
|
||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||
|
||
const handler = async (params: RoutePayload) => {
|
||
const client = new CoreApiClient();
|
||
const name = 'name' in params.queryStringParameters
|
||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||
: 'Hello world';
|
||
|
||
const result = await client.mutation({
|
||
createPostCard: {
|
||
__args: { data: { name } },
|
||
id: true,
|
||
name: true,
|
||
},
|
||
});
|
||
return result;
|
||
};
|
||
|
||
export default defineLogicFunction({
|
||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||
name: 'create-new-post-card',
|
||
timeoutSeconds: 2,
|
||
handler,
|
||
httpRouteTriggerSettings: {
|
||
path: '/post-card/create',
|
||
httpMethod: 'GET',
|
||
isAuthRequired: false,
|
||
},
|
||
/*databaseEventTriggerSettings: {
|
||
eventName: 'people.created',
|
||
},*/
|
||
/*cronTriggerSettings: {
|
||
pattern: '0 0 1 1 *',
|
||
},*/
|
||
});
|
||
```
|
||
|
||
Available trigger types:
|
||
* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
|
||
* **cron**: Bir CRON ifadesi kullanarak fonksiyonunuzu bir zamanlamayla çalıştırır.
|
||
* **databaseEvent**: Çalışma alanı nesnesi yaşam döngüsü olaylarında çalışır. Olay işlemi `updated` olduğunda, dinlenecek belirli alanlar `updatedFields` dizisinde belirtilebilir. Tanımsız veya boş bırakılırsa, herhangi bir güncelleme fonksiyonu tetikler.
|
||
> e.g. `person.updated`, `*.created`, `company.*`
|
||
|
||
<Note>
|
||
You can also manually execute a function using the 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
|
||
```
|
||
|
||
You can watch logs with:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty logs
|
||
```
|
||
</Note>
|
||
|
||
#### Rota tetikleyicisi yükü
|
||
|
||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
|
||
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||
Import the `RoutePayload` type from `twenty-sdk`:
|
||
|
||
```ts
|
||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||
|
||
const handler = async (event: RoutePayload) => {
|
||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||
const { method, path } = event.requestContext.http;
|
||
|
||
return { message: 'Success' };
|
||
};
|
||
```
|
||
|
||
`RoutePayload` türünün yapısı şu şekildedir:
|
||
|
||
| Özellik | Tür | Açıklama | Örnek |
|
||
| ---------------------------- | ------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||
| `headers` | `Record<string, string \| undefined>` | HTTP başlıkları (`forwardedRequestHeaders` içinde listelenenlerle sınırlı) | see section below |
|
||
| `queryStringParameters` | `Record<string, string \| undefined>` | Sorgu dizesi parametreleri (birden çok değer virgülle birleştirilir) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||
| `body` | `object \| null` | Ayrıştırılmış istek gövdesi (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||
| `isBase64Encoded` | `boolean` | Gövdenin base64 ile kodlanıp kodlanmadığı | |
|
||
| `requestContext.http.method` | `string` | HTTP yöntemi (GET, POST, PUT, PATCH, DELETE) | |
|
||
| `requestContext.http.path` | `string` | Ham istek yolu | |
|
||
|
||
|
||
#### forwardedRequestHeaders
|
||
|
||
Varsayılan olarak, güvenlik nedenleriyle gelen isteklerden HTTP başlıkları mantık fonksiyonunuza **aktarılmaz**.
|
||
To access specific headers, list them in the `forwardedRequestHeaders` array:
|
||
|
||
```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'],
|
||
},
|
||
});
|
||
```
|
||
|
||
In your handler, access the forwarded headers like this:
|
||
|
||
```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>
|
||
Başlık adları küçük harfe normalize edilir. Access them using lowercase keys (e.g., `event.headers['content-type']`).
|
||
</Note>
|
||
|
||
#### Exposing a function as a tool
|
||
|
||
Mantık işlevleri, yapay zeka ajanları ve iş akışları için **araçlar** olarak sunulabilir. When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
|
||
|
||
To mark a logic function as a tool, set `isTool: true`:
|
||
|
||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||
import { defineLogicFunction } from 'twenty-sdk';
|
||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||
|
||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||
const client = new CoreApiClient();
|
||
|
||
const result = await client.mutation({
|
||
createTask: {
|
||
__args: {
|
||
data: {
|
||
title: `Enrich data for ${params.companyName}`,
|
||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||
},
|
||
},
|
||
id: true,
|
||
},
|
||
});
|
||
|
||
return { taskId: result.createTask.id };
|
||
};
|
||
|
||
export default defineLogicFunction({
|
||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||
name: 'enrich-company',
|
||
description: 'Enrich a company record with external data',
|
||
timeoutSeconds: 10,
|
||
handler,
|
||
isTool: true,
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
|
||
* You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
|
||
* **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
|
||
|
||
```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>
|
||
**İyi bir `description` yazın.** AI ajanları, aracı ne zaman kullanacaklarına karar vermek için işlevin `description` alanına güvenir. Aracın ne yaptığını ve ne zaman çağrılması gerektiğini açıkça belirtin.
|
||
</Note>
|
||
|
||
</Accordion>
|
||
<Accordion title="definePreInstallLogicFunction" description="Bir kurulum öncesi mantık işlevi tanımlayın (uygulama başına bir adet)">
|
||
|
||
Kurulum öncesi işlev, uygulamanız bir çalışma alanına yüklenmeden önce otomatik olarak çalışan bir mantık işlevidir. Bu, doğrulama görevleri, önkoşul kontrolleri veya ana kurulum başlamadan önce çalışma alanı durumunun hazırlanması için yararlıdır.
|
||
|
||
```ts src/logic-functions/pre-install.ts
|
||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||
|
||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||
};
|
||
|
||
export default definePreInstallLogicFunction({
|
||
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
||
name: 'pre-install',
|
||
description: 'Runs before installation to prepare the application.',
|
||
timeoutSeconds: 300,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
Ayrıca kurulum öncesi işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty exec --preInstall
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* Kurulum öncesi işlevler `definePreInstallLogicFunction()` kullanır — tetikleyici ayarlarını atlayan (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) özel bir varyanttır.
|
||
* İşleyici, `{ previousVersion: string }` içeren bir `InstallLogicFunctionPayload` alır — daha önce yüklü olan uygulamanın sürümü (veya yeni kurulumlar için boş bir dize).
|
||
* Uygulama başına yalnızca bir kurulum öncesi işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
|
||
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `preInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
|
||
* Varsayılan zaman aşımı, daha uzun hazırlık görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
|
||
|
||
</Accordion>
|
||
<Accordion title="definePostInstallLogicFunction" description="Bir kurulum sonrası mantık işlevi tanımlayın (uygulama başına bir adet)">
|
||
|
||
Kurulum sonrası işlev, uygulamanız bir çalışma alanına yüklendikten sonra otomatik olarak çalışan bir mantık işlevidir. Bu, varsayılan verileri tohumlama, ilk kayıtları oluşturma veya çalışma alanı ayarlarını yapılandırma gibi tek seferlik kurulum görevleri için yararlıdır.
|
||
|
||
```ts src/logic-functions/post-install.ts
|
||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||
|
||
const handler = async (payload: InstallLogicFunctionPayload): 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,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
Ayrıca kurulum sonrası işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty exec --postInstall
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* Kurulum sonrası işlevler `definePostInstallLogicFunction()` kullanır — tetikleyici ayarlarını atlayan (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) özel bir varyanttır.
|
||
* İşleyici, `{ previousVersion: string }` içeren bir `InstallLogicFunctionPayload` alır — daha önce yüklü olan uygulamanın sürümü (veya yeni kurulumlar için boş bir dize).
|
||
* Uygulama başına yalnızca bir kurulum sonrası işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
|
||
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `postInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
|
||
* Varsayılan zaman aşımı, veri tohumlama gibi daha uzun kurulum görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineFrontComponent" description="Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın">
|
||
|
||
Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe.
|
||
|
||
#### Basic example
|
||
|
||
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
|
||
|
||
```tsx src/front-components/hello-world.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk';
|
||
|
||
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',
|
||
},
|
||
});
|
||
```
|
||
|
||
After syncing with `yarn twenty dev`, the quick action appears in the top-right corner of the page:
|
||
|
||
<div style={{textAlign: 'center'}}>
|
||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
|
||
</div>
|
||
|
||
Click it to render the component inline.
|
||
|
||
{/* TODO: add screenshot of the rendered front component */}
|
||
|
||
#### Configuration fields
|
||
|
||
| Alan | Zorunlu | Açıklama |
|
||
| --------------------- | ------- | ----------------------------------------------------------------------------------- |
|
||
| `universalIdentifier` | Evet | Stable unique ID for this component |
|
||
| `component` | Evet | A React component function |
|
||
| `name` | Hayır | Display name |
|
||
| `description` | Hayır | Description of what the component does |
|
||
| `isHeadless` | Hayır | Set to `true` if the component has no visible UI (see below) |
|
||
| `command` | Hayır | Register the component as a command (see [command options](#command-options) below) |
|
||
|
||
#### Placing a front component on a page
|
||
|
||
Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See the [definePageLayout](#definepagelayout) section for details.
|
||
|
||
#### Headless components (`isHeadless: true`)
|
||
|
||
Headless components render no visible UI but still run React logic. This is useful for **effect components** — components that perform side effects when mounted, such as syncing data, starting a timer, listening to events, or triggering a notification.
|
||
|
||
```tsx src/front-components/sync-tracker.tsx
|
||
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
|
||
import { useEffect } from 'react';
|
||
|
||
const SyncTracker = () => {
|
||
const recordId = useRecordId();
|
||
|
||
useEffect(() => {
|
||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||
}, [recordId]);
|
||
|
||
return null;
|
||
};
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'sync-tracker',
|
||
description: 'Tracks record views silently',
|
||
isHeadless: true,
|
||
component: SyncTracker,
|
||
});
|
||
```
|
||
|
||
Because the component returns `null`, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API.
|
||
|
||
#### Accessing runtime context
|
||
|
||
Inside your component, use SDK hooks to access the current user, record, and component instance:
|
||
|
||
```tsx src/front-components/record-info.tsx
|
||
import {
|
||
defineFrontComponent,
|
||
useUserId,
|
||
useRecordId,
|
||
useFrontComponentId,
|
||
} from 'twenty-sdk';
|
||
|
||
const RecordInfo = () => {
|
||
const userId = useUserId();
|
||
const recordId = useRecordId();
|
||
const componentId = useFrontComponentId();
|
||
|
||
return (
|
||
<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,
|
||
});
|
||
```
|
||
|
||
Available hooks:
|
||
|
||
| Hook | Returns | Açıklama |
|
||
| --------------------------------------------- | ------------------ | ---------------------------------------------------------- |
|
||
| `useUserId()` | `string` or `null` | The current user's ID |
|
||
| `useRecordId()` | `string` or `null` | The current record's ID (when placed on a record page) |
|
||
| `useFrontComponentId()` | `string` | This component instance's ID |
|
||
| `useFrontComponentExecutionContext(selector)` | değişir | Access the full execution context with a selector function |
|
||
|
||
#### Host communication API
|
||
|
||
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
|
||
|
||
| Fonksiyon | Açıklama |
|
||
| ----------------------------------------------- | ----------------------------- |
|
||
| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app |
|
||
| `openSidePanelPage(params)` | Open a side panel |
|
||
| `closeSidePanel()` | Yan paneli kapat |
|
||
| `openCommandConfirmationModal(params)` | Show a confirmation dialog |
|
||
| `enqueueSnackbar(params)` | Show a toast notification |
|
||
| `unmountFrontComponent()` | Unmount the component |
|
||
| `updateProgress(progress)` | Update a progress indicator |
|
||
|
||
#### Command options
|
||
|
||
Adding a `command` field to `defineFrontComponent` registers the component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
|
||
|
||
| Alan | Zorunlu | Açıklama |
|
||
| --------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `universalIdentifier` | Evet | Stable unique ID for the command |
|
||
| `label` | Evet | Full label shown in the command menu (Cmd+K) |
|
||
| `shortLabel` | Hayır | Shorter label displayed on the pinned quick-action button |
|
||
| `icon` | Hayır | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
|
||
| `isPinned` | Hayır | When `true`, shows the command as a quick-action button in the top-right corner of the page |
|
||
| `availabilityType` | Hayır | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) |
|
||
| `availabilityObjectUniversalIdentifier` | Hayır | Restrict the command to pages of a specific object type (e.g. only on Company records) |
|
||
| `conditionalAvailabilityExpression` | Hayır | A boolean expression to dynamically control whether the command is visible (see below) |
|
||
|
||
#### Conditional availability expressions
|
||
|
||
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
|
||
|
||
```tsx
|
||
import {
|
||
defineFrontComponent,
|
||
pageType,
|
||
numberOfSelectedRecords,
|
||
objectPermissions,
|
||
everyEquals,
|
||
isDefined,
|
||
} from 'twenty-sdk';
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'bulk-action',
|
||
component: BulkAction,
|
||
command: {
|
||
universalIdentifier: '...',
|
||
label: 'Bulk Update',
|
||
availabilityType: 'RECORD_SELECTION',
|
||
conditionalAvailabilityExpression: everyEquals(
|
||
objectPermissions,
|
||
'canUpdateObjectRecords',
|
||
true,
|
||
),
|
||
},
|
||
});
|
||
```
|
||
|
||
**Context variables** — these represent the current state of the page:
|
||
|
||
| Değişken | Tür | Açıklama |
|
||
| ------------------------------ | --------- | ---------------------------------------------------------------- |
|
||
| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) |
|
||
| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel |
|
||
| `numberOfSelectedRecords` | `number` | Number of currently selected records |
|
||
| `isSelectAll` | `boolean` | Whether "select all" is active |
|
||
| `selectedRecords` | `array` | The selected record objects |
|
||
| `favoriteRecordIds` | `array` | IDs of favorited records |
|
||
| `objectPermissions` | `object` | Permissions for the current object type |
|
||
| `targetObjectReadPermissions` | `object` | Read permissions for the target object |
|
||
| `targetObjectWritePermissions` | `object` | Write permissions for the target object |
|
||
| `featureFlags` | `object` | Active feature flags |
|
||
| `objectMetadataItem` | `object` | Metadata of the current object type |
|
||
| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter |
|
||
|
||
**Operators** — combine variables into boolean expressions:
|
||
|
||
| Operator | Açıklama |
|
||
| ----------------------------------- | ----------------------------------------------------------------- |
|
||
| `isDefined(value)` | `true` if the value is not null/undefined |
|
||
| `isNonEmptyString(value)` | `true` if the value is a non-empty string |
|
||
| `includes(array, value)` | `true` if the array contains the value |
|
||
| `includesEvery(array, prop, value)` | `true` if every item's property includes the value |
|
||
| `every(array, prop)` | `true` if the property is truthy on every item |
|
||
| `everyDefined(array, prop)` | `true` if the property is defined on every item |
|
||
| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item |
|
||
| `some(array, prop)` | `true` if the property is truthy on at least one item |
|
||
| `someDefined(array, prop)` | `true` if the property is defined on at least one item |
|
||
| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item |
|
||
| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item |
|
||
| `none(array, prop)` | `true` if the property is falsy on every item |
|
||
| `noneDefined(array, prop)` | `true` if the property is undefined on every item |
|
||
| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item |
|
||
|
||
#### Public assets
|
||
|
||
Front components can access files from the app's `public/` directory using `getPublicAssetUrl`:
|
||
|
||
```tsx
|
||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';
|
||
|
||
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
|
||
|
||
export default defineFrontComponent({
|
||
universalIdentifier: '...',
|
||
name: 'logo',
|
||
component: Logo,
|
||
});
|
||
```
|
||
|
||
See the [public assets section](#accessing-public-assets-with-getpublicasseturl) for details.
|
||
|
||
#### Stil
|
||
|
||
Front components support multiple styling approaches. You can use:
|
||
|
||
* **Inline styles** — `style={{ color: 'red' }}`
|
||
* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
|
||
* **Emotion** — CSS-in-JS with `@emotion/react`
|
||
* **Styled-components** — `styled.div` patterns
|
||
* **Tailwind CSS** — utility classes
|
||
* **Any CSS-in-JS library** compatible with React
|
||
|
||
```tsx
|
||
import { defineFrontComponent } from 'twenty-sdk';
|
||
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="Yapay zekâ ajanı yeteneklerini tanımlayın">
|
||
|
||
Yetenekler, yapay zekâ ajanlarının çalışma alanınızda kullanabileceği yeniden kullanılabilir yönergeleri ve kabiliyetleri tanımlar. Yerleşik doğrulamayla yetenekleri tanımlamak için `defineSkill()` kullanın:
|
||
|
||
```ts src/skills/example-skill.ts
|
||
import { defineSkill } from 'twenty-sdk';
|
||
|
||
export default defineSkill({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'sales-outreach',
|
||
label: 'Sales Outreach',
|
||
description: 'Guides the AI agent through a structured sales outreach process',
|
||
icon: 'IconBrain',
|
||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||
1. Research the company and recent news
|
||
2. Identify the prospect's role and likely pain points
|
||
3. Draft a personalized message referencing specific details
|
||
4. Keep the tone professional but conversational`,
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `name`, yetenek için benzersiz bir tanımlayıcı dizedir (kebab-case önerilir).
|
||
* `label`, UI'de gösterilen, insan tarafından okunabilir addır.
|
||
* `content`, yetenek yönergelerini içerir — bu, yapay zekâ ajanının kullandığı metindir.
|
||
* `icon` (isteğe bağlı), UI'de gösterilen simgeyi ayarlar.
|
||
* `description` (isteğe bağlı), yeteneğin amacı hakkında ek bağlam sağlar.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineAgent" description="Define AI agents with custom prompts">
|
||
|
||
Agents are AI assistants that live inside your workspace. Use `defineAgent()` to create agents with a custom system prompt:
|
||
|
||
```ts src/agents/example-agent.ts
|
||
import { defineAgent } from 'twenty-sdk';
|
||
|
||
export default defineAgent({
|
||
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||
name: 'sales-assistant',
|
||
label: 'Sales Assistant',
|
||
description: 'Helps the sales team draft outreach emails and research prospects',
|
||
icon: 'IconRobot',
|
||
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `name` is the unique identifier string for the agent (kebab-case recommended).
|
||
* `label` is the display name shown in the UI.
|
||
* `prompt` is the system prompt that defines the agent's behavior.
|
||
* `description` (optional) provides context about what the agent does.
|
||
* `icon` (isteğe bağlı), UI'de gösterilen simgeyi ayarlar.
|
||
* `modelId` (optional) overrides the default AI model used by the agent.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineView" description="Nesneler için kaydedilmiş görünümler tanımlayın">
|
||
|
||
Views are saved configurations for how records of an object are displayed — including which fields are visible, their order, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app:
|
||
|
||
```ts src/views/example-view.ts
|
||
import { defineView, ViewKey } from 'twenty-sdk';
|
||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||
|
||
export default defineView({
|
||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||
name: 'All example items',
|
||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
icon: 'IconList',
|
||
key: ViewKey.INDEX,
|
||
position: 0,
|
||
fields: [
|
||
{
|
||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||
position: 0,
|
||
isVisible: true,
|
||
size: 200,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `objectUniversalIdentifier` specifies which object this view applies to.
|
||
* `key` determines the view type (e.g., `ViewKey.INDEX` for the main list view).
|
||
* `fields` controls which columns appear and their order. Each field references a `fieldMetadataUniversalIdentifier`.
|
||
* You can also define `filters`, `filterGroups`, `groups`, and `fieldGroups` for more advanced configurations.
|
||
* `position` controls the ordering when multiple views exist for the same object.
|
||
|
||
</Accordion>
|
||
<Accordion title="defineNavigationMenuItem" description="Kenar çubuğu gezinme bağlantılarını tanımlayın">
|
||
|
||
Navigation menu items add custom entries to the workspace sidebar. Use `defineNavigationMenuItem()` to link to views, external URLs, or objects:
|
||
|
||
```ts src/navigation-menu-items/example-navigation-menu-item.ts
|
||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk';
|
||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
|
||
|
||
export default defineNavigationMenuItem({
|
||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||
name: 'example-navigation-menu-item',
|
||
icon: 'IconList',
|
||
color: 'blue',
|
||
position: 0,
|
||
type: NavigationMenuItemType.VIEW,
|
||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `type` determines what the menu item links to: `NavigationMenuItemType.VIEW` for a saved view, or `NavigationMenuItemType.LINK` for an external URL.
|
||
* For view links, set `viewUniversalIdentifier`. For external links, set `link`.
|
||
* `position` controls the ordering in the sidebar.
|
||
* `icon` and `color` (optional) customize the appearance.
|
||
|
||
</Accordion>
|
||
<Accordion title="definePageLayout" description="Define custom page layouts for record views">
|
||
|
||
Page layouts let you customize how a record detail page looks — which tabs appear, what widgets are inside each tab, and how they are arranged. Use `definePageLayout()` to ship custom layouts with your app:
|
||
|
||
```ts src/page-layouts/example-record-page-layout.ts
|
||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
|
||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
|
||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
|
||
|
||
export default definePageLayout({
|
||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||
name: 'Example Record Page',
|
||
type: 'RECORD_PAGE',
|
||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
tabs: [
|
||
{
|
||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||
title: 'Hello World',
|
||
position: 50,
|
||
icon: 'IconWorld',
|
||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||
widgets: [
|
||
{
|
||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||
title: 'Hello World',
|
||
type: 'FRONT_COMPONENT',
|
||
configuration: {
|
||
configurationType: 'FRONT_COMPONENT',
|
||
frontComponentUniversalIdentifier:
|
||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Önemli noktalar:
|
||
* `type` is typically `'RECORD_PAGE'` to customize the detail view of a specific object.
|
||
* `objectUniversalIdentifier` specifies which object this layout applies to.
|
||
* Each `tab` defines a section of the page with a `title`, `position`, and `layoutMode` (`CANVAS` for free-form layout).
|
||
* Each `widget` inside a tab can render a front component, a relation list, or other built-in widget types.
|
||
* `position` on tabs controls their order. Use higher values (e.g., 50) to place custom tabs after built-in ones.
|
||
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
## Public assets (`public/` folder)
|
||
|
||
The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server.
|
||
|
||
Files placed in `public/` are:
|
||
|
||
* **Publicly accessible** — once synced to the server, assets are served at a public URL. No authentication is needed to access them.
|
||
* **Available in front components** — use asset URLs to display images, icons, or any media inside your React components.
|
||
* **Available in logic functions** — reference asset URLs in emails, API responses, or any server-side logic.
|
||
* **Used for marketplace metadata** — the `logoUrl` and `screenshots` fields in `defineApplication()` reference files from this folder (e.g., `public/logo.png`). These are displayed in the marketplace when your app is published.
|
||
* **Auto-synced in dev mode** — when you add, update, or delete a file in `public/`, it is synced to the server automatically. No restart needed.
|
||
* **Included in builds** — `yarn twenty build` bundles all public assets into the distribution output.
|
||
|
||
### Accessing public assets with `getPublicAssetUrl`
|
||
|
||
Use the `getPublicAssetUrl` helper from `twenty-sdk` to get the full URL of a file in your `public/` directory. It works in both **logic functions** and **front components**.
|
||
|
||
**In a logic function:**
|
||
|
||
```ts src/logic-functions/send-invoice.ts
|
||
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk';
|
||
|
||
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,
|
||
});
|
||
```
|
||
|
||
**In a front component:**
|
||
|
||
```tsx src/front-components/company-card.tsx
|
||
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';
|
||
|
||
export default defineFrontComponent(() => {
|
||
const logoUrl = getPublicAssetUrl('logo.png');
|
||
|
||
return <img src={logoUrl} alt="App logo" />;
|
||
});
|
||
```
|
||
|
||
The `path` argument is relative to your app's `public/` folder. Both `getPublicAssetUrl('logo.png')` and `getPublicAssetUrl('public/logo.png')` resolve to the same URL — the `public/` prefix is stripped automatically if present.
|
||
|
||
## Using npm packages
|
||
|
||
You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime.
|
||
|
||
### Installing a package
|
||
|
||
```bash filename="Terminal"
|
||
yarn add axios
|
||
```
|
||
|
||
Then import it in your code:
|
||
|
||
```ts src/logic-functions/fetch-data.ts
|
||
import { defineLogicFunction } from 'twenty-sdk';
|
||
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,
|
||
});
|
||
```
|
||
|
||
The same works for front components:
|
||
|
||
```tsx src/front-components/chart.tsx
|
||
import { defineFrontComponent } from 'twenty-sdk';
|
||
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,
|
||
});
|
||
```
|
||
|
||
### How bundling works
|
||
|
||
The build step (`yarn twenty dev` or `yarn twenty build`) uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
|
||
|
||
**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed.
|
||
|
||
**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment.
|
||
|
||
Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server.
|
||
|
||
## Scaffolding entities with `yarn twenty add`
|
||
|
||
Instead of creating entity files by hand, you can use the interactive scaffolder:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty add
|
||
```
|
||
|
||
This prompts you to pick an entity type and walks you through the required fields. It generates a ready-to-use file with a stable `universalIdentifier` and the correct `defineEntity()` call.
|
||
|
||
You can also pass the entity type directly to skip the first prompt:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty add object
|
||
yarn twenty add logicFunction
|
||
yarn twenty add frontComponent
|
||
```
|
||
|
||
### Available entity types
|
||
|
||
| Varlık türü | Komut | Generated file |
|
||
| -------------------- | ------------------------------------ | ------------------------------------- |
|
||
| Nesne | `yarn twenty add object` | `src/objects/<name>.ts` |
|
||
| Alan | `yarn twenty add field` | `src/fields/<name>.ts` |
|
||
| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
|
||
| Front component | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
|
||
| Rol | `yarn twenty add role` | `src/roles/<name>.ts` |
|
||
| Beceri | `yarn twenty add skill` | `src/skills/<name>.ts` |
|
||
| Temsilci | `yarn twenty add agent` | `src/agents/<name>.ts` |
|
||
| Görünüm | `yarn twenty add view` | `src/views/<name>.ts` |
|
||
| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
|
||
| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
|
||
|
||
### What the scaffolder generates
|
||
|
||
Each entity type has its own template. For example, `yarn twenty add object` asks for:
|
||
|
||
1. **Name (singular)** — e.g., `invoice`
|
||
2. **Name (plural)** — e.g., `invoices`
|
||
3. **Label (singular)** — auto-populated from the name (e.g., `Invoice`)
|
||
4. **Label (plural)** — auto-populated (e.g., `Invoices`)
|
||
5. **Create a view and navigation item?** — if you answer yes, the scaffolder also generates a matching view and sidebar link for the new object.
|
||
|
||
Other entity types have simpler prompts — most only ask for a name.
|
||
|
||
The `field` entity type is more detailed: it asks for the field name, label, type (from a list of all available field types like `TEXT`, `NUMBER`, `SELECT`, `RELATION`, etc.), and the target object's `universalIdentifier`.
|
||
|
||
### Custom output path
|
||
|
||
Use the `--path` flag to place the generated file in a custom location:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty add logicFunction --path src/custom-folder
|
||
```
|
||
|
||
## Typed API clients (twenty-client-sdk)
|
||
|
||
The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components.
|
||
|
||
| İstemci | İçe Aktar | Uç nokta | Oluşturuldu mu? |
|
||
| ------------------- | ---------------------------- | ------------------------------------------------------------- | --------------------------------------- |
|
||
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — çalışma alanı verileri (kayıtlar, nesneler) | Evet, geliştirme/derleme zamanında |
|
||
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — çalışma alanı yapılandırması, dosya yüklemeleri | Hayır, önceden hazırlanmış olarak gelir |
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="CoreApiClient" description="Query and mutate workspace data (records, objects)">
|
||
|
||
`CoreApiClient`, çalışma alanı verilerini sorgulamak ve değiştirmek için ana istemcidir. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it is fully typed to match your objects and fields.
|
||
|
||
```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,
|
||
},
|
||
});
|
||
```
|
||
|
||
İstemci bir seçim kümesi sözdizimi kullanır: Bir alanı dahil etmek için `true` geçin, bağımsız değişkenler için `__args` kullanın ve ilişkiler için nesneleri iç içe yerleştirin. Çalışma alanı şemanıza göre tam otomatik tamamlama ve tip denetimi elde edersiniz.
|
||
|
||
<Note>
|
||
**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`.
|
||
</Note>
|
||
|
||
#### Tür açıklamaları için CoreSchema'yı kullanma
|
||
|
||
`CoreSchema` provides TypeScript types matching your workspace objects — useful for typing component state or function parameters:
|
||
|
||
```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="Workspace config, applications, and file uploads">
|
||
|
||
`MetadataApiClient`, SDK ile birlikte önceden hazırlanmış olarak gelir (oluşturma gerektirmez). Çalışma alanı yapılandırması, uygulamalar ve dosya yüklemeleri için `/metadata` uç noktasını sorgular.
|
||
|
||
```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 },
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
#### Dosya yükleme
|
||
|
||
`MetadataApiClient`, dosya türü alanlara dosya eklemek için bir `uploadFile` yöntemi içerir:
|
||
|
||
```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://...' }
|
||
```
|
||
|
||
| Parametre | Tür | Açıklama |
|
||
| ---------------------------------- | -------- | ------------------------------------------------------------- |
|
||
| `fileBuffer` | `Buffer` | Dosyanın ham içeriği |
|
||
| `filename` | `string` | Dosyanın adı (depolama ve görüntüleme için kullanılır) |
|
||
| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) |
|
||
| `fieldMetadataUniversalIdentifier` | `string` | Nesnenizdeki dosya türü alanının `universalIdentifier` değeri |
|
||
|
||
Önemli noktalar:
|
||
* Alan için `universalIdentifier` kullanır (çalışma alanına özgü kimliği değil), böylece yükleme kodunuz uygulamanızın yüklü olduğu herhangi bir çalışma alanında çalışır.
|
||
* Döndürülen `url`, yüklenen dosyaya erişmek için kullanabileceğiniz imzalı bir URL'dir.
|
||
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
<Note>
|
||
Kodunuz Twenty üzerinde çalıştığında (mantık işlevleri veya ön uç bileşenleri), platform kimlik bilgilerini ortam değişkenleri olarak enjekte eder:
|
||
|
||
* `TWENTY_API_URL` — Twenty API'nin temel URL'si
|
||
* `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role
|
||
|
||
Bunları istemcilere iletmeniz gerekmez — otomatik olarak `process.env`'den okurlar. API anahtarının izinleri, `application-config.ts` içinde `defaultRoleUniversalIdentifier` ile referans verilen role göre belirlenir.
|
||
</Note>
|
||
|
||
## Testing your app
|
||
|
||
The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server.
|
||
|
||
### Kurulum
|
||
|
||
The scaffolded app already includes Vitest. If you set it up manually, install the dependencies:
|
||
|
||
```bash filename="Terminal"
|
||
yarn add -D vitest vite-tsconfig-paths
|
||
```
|
||
|
||
Create a `vitest.config.ts` at the root of your app:
|
||
|
||
```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',
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
Create a setup file that verifies the server is reachable before tests run:
|
||
|
||
```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),
|
||
);
|
||
});
|
||
```
|
||
|
||
### Programmatic SDK APIs
|
||
|
||
The `twenty-sdk/cli` subpath exports functions you can call directly from test code:
|
||
|
||
| Fonksiyon | Açıklama |
|
||
| -------------- | ------------------------------------------- |
|
||
| `appBuild` | Build the app and optionally pack a tarball |
|
||
| `appDeploy` | Upload a tarball to the server |
|
||
| `appInstall` | Install the app on the active workspace |
|
||
| `appUninstall` | Uninstall the app from the active workspace |
|
||
|
||
Each function returns a result object with `success: boolean` and either `data` or `error`.
|
||
|
||
### Writing an integration test
|
||
|
||
Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace:
|
||
|
||
```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();
|
||
});
|
||
});
|
||
```
|
||
|
||
### Running tests
|
||
|
||
Make sure your local Twenty server is running, then:
|
||
|
||
```bash filename="Terminal"
|
||
yarn test
|
||
```
|
||
|
||
Or in watch mode during development:
|
||
|
||
```bash filename="Terminal"
|
||
yarn test:watch
|
||
```
|
||
|
||
### Type checking
|
||
|
||
You can also run type checking on your app without running tests:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty typecheck
|
||
```
|
||
|
||
This runs `tsc --noEmit` and reports any type errors.
|
||
|
||
## CLI başvurusu
|
||
|
||
Beyond `dev`, `build`, `add`, and `typecheck`, the CLI provides commands for executing functions, viewing logs, and managing app installations.
|
||
|
||
### Executing functions (`yarn twenty exec`)
|
||
|
||
Run a logic function manually without triggering it via HTTP, cron, or database event:
|
||
|
||
```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 pre-install or post-install functions
|
||
yarn twenty exec --preInstall
|
||
yarn twenty exec --postInstall
|
||
```
|
||
|
||
### Viewing function logs (`yarn twenty logs`)
|
||
|
||
Stream execution logs for your app's logic functions:
|
||
|
||
```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>
|
||
This is different from `yarn twenty server logs`, which shows the Docker container logs. `yarn twenty logs` shows your app's function execution logs from the Twenty server.
|
||
</Note>
|
||
|
||
### Uninstalling an app (`yarn twenty uninstall`)
|
||
|
||
Remove your app from the active workspace:
|
||
|
||
```bash filename="Terminal"
|
||
yarn twenty uninstall
|
||
|
||
# Skip the confirmation prompt
|
||
yarn twenty uninstall --yes
|
||
```
|