snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
title: Конфигурация приложения
description: Определите идентификацию вашего приложения, роль по умолчанию, переменные и метаданные маркетплейса с помощью defineApplication.
icon: rocket
---
В каждом приложении должен быть ровно один вызов `defineApplication`. Он объявляет:
* **Идентификация** — универсальный идентификатор, отображаемое имя, описание.
* **Разрешения** — от имени какой роли выполняются его логические функции и фронтенд-компоненты.
* **Переменные** *(необязательно)* — пары ключ–значение, доступные вашему коду как переменные окружения.
* **Хуки предустановки / постустановки / удаления** *(необязательно)* — см. [Логические функции](/l/ru/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
Заметки:
* Поля `universalIdentifier` — это детерминированные идентификаторы, которые принадлежат вам. Сгенерируйте их один раз и сохраняйте неизменными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций и фронтенд-компонентов. В логических функциях (на стороне сервера) они доступны как `process.env.VARIABLE_NAME`. Во фронтенд-компонентах используйте `getApplicationVariable('VARIABLE_NAME')` из `twenty-sdk/front-component`. Переменные, помеченные как `isSecret: true`, внедряются только в логические функции. Фронтенд-компоненты получают только несекретные переменные.
* Роль по умолчанию автоматически определяется из файла роли, помеченного с помощью [`defineApplicationRole()`](/l/ru/developers/extend/apps/config/roles) — вам не нужно ссылаться на неё из `defineApplication()`.
* Функции предустановки, постустановки и удаления обнаруживаются автоматически во время сборки манифеста — вам не нужно указывать их в `defineApplication()`.
* Явная передача `defaultRoleUniversalIdentifier` по-прежнему поддерживается для обратной совместимости, но считается устаревшей и вместо неё рекомендуется использовать `defineApplicationRole()`.
* `serverVariables` — это параметры конфигурации и секреты в области экземпляра (например, ключи API). В отличие от `applicationVariables`, значения для них не указываются в манифесте — оператор рабочего пространства заполняет их в настройках приложения, и они внедряются в логические функции только после того, как будут заданы.
* Оба типа переменных принимают `isDeprecated: true`. Используйте это, чтобы вывести переменную из обращения вместо её удаления: сохранение объявленного ключа сохраняет хранимое значение (удаление уничтожает значение, введённое пользователем), а переменная по-прежнему внедряется, поэтому код сможет обратиться к ней — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Устаревшая переменная исчезает из настроек приложения, как только у неё больше нет значения, и никогда не учитывается при проверке конфигурации приложения, поэтому `isDeprecated` имеет приоритет над `isRequired`.
* Чтобы отобразить на вкладке **Settings** приложения пользовательский интерфейс настройки (вместо стандартного раздела настройки переменных), объявите фронтенд‑компонент с помощью [`defineSettingsFrontComponent()`](/l/ru/developers/extend/apps/layout/front-components#custom-settings-component) в отдельном файле. Для каждого приложения допускается только один такой компонент. Разделы, управляемые системой (автообновление, App URL, соединения), всегда остаются видимыми.
## Типы переменных
И `applicationVariables`, и `serverVariables` принимают необязательный `type` (а для `SELECT` / `MULTI_SELECT` — список `options`). Поддерживаемые типы: `TEXT` (по умолчанию), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
Свойство `type` влияет только на **отображение и валидацию** — оно выбирает соответствующий ввод в UI настроек рабочего пространства (переключатель, числовое поле, раскрывающийся список, выбор даты, JSON‑редактор, …) и позволяет сборке проверить вашу конфигурацию (например, `SELECT` / `MULTI_SELECT` должны объявить непустой список `options`). Оно **не** меняет то, как значение попадает в ваш код.
Значения **всегда внедряются как строки** — это заложено в природу переменных окружения (`process.env.*` содержит только строки). Когда выполняется ваша логическая функция, исполнитель сериализует каждое значение в соответствии с объявленным `type` при построении `process.env`, так что строковый формат остается единым, независимо от того, как было задано значение (значение по умолчанию в манифесте, UI настроек или предыдущая версия):
| Тип | строка в `process.env` |
| ------------------------------------- | --------------------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | непреобразованное значение (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | десятичная строка (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | JSON‑массив (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | JSON‑объект (`'{"retries":3}'`) |
Преобразуйте строку обратно в ожидаемый вами тип:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
Это же относится к фронтенд‑компонентам, читающим значения через `getApplicationVariable('VARIABLE_NAME')` — возвращаемое значение является строкой; при необходимости преобразуйте его.
## Роль функции по умолчанию
Роль, объявленная с помощью [`defineApplicationRole()`](/l/ru/developers/extend/apps/config/roles), определяет, к чему могут получать доступ логические функции и фронтенд‑компоненты приложения:
* Токены времени выполнения, подставляемые в ваши функции логики, формируются из этой роли. Вызов, выполняемый от имени пользователя, дополнительно ограничен тем, что может делать этот пользователь, поэтому он никогда не может превышать ни один из них. См. [Чей доступ использует вызов](/l/ru/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Типизированный клиент API ограничен правами, предоставленными этой роли.
* Следуйте принципу наименьших привилегий: объявляйте только те разрешения, которые действительно нужны вашим функциям.
Когда вы создаёте новое приложение с помощью шаблона, CLI создаёт стартовый файл роли по адресу `src/roles/default-role.ts`. Полную справочную информацию см. в разделе [Роли и разрешения](/l/ru/developers/extend/apps/config/roles).
## Метаданные маркетплейса
Если вы планируете [опубликовать приложение](/l/ru/developers/extend/apps/operations/publishing), эти необязательные поля определяют, как оно отображается в маркетплейсе:
| Поле | Описание |
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `author` | Имя автора или название компании |
| `category` | Категория приложения для фильтрации в маркетплейсе |
| `logo` | Путь к вашему логотипу приложения в `public/` (например, `public/logo.png`) |
| `galleryImages` | Массив путей галереи изображений в комплекте `public/` (например, `public/screenshot-1.png`) |
| `aboutDescription` | Расширенное описание в Markdown для вкладки "About". Если опущено, маркетплейс использует `README.md` пакета из npm |
| `websiteUrl` | Ссылка на ваш сайт |
| `termsUrl` | Ссылка на условия предоставления услуг |
| `emailSupport` | Адрес электронной почты поддержки |
| `issueReportUrl` | Ссылка на систему отслеживания проблем |
<Note>
«logoUrl» и «screenshots» являются устаревшими псевдонимами «logo» и «galleryImages». Внешние абсолютные URL-адреса (`http://` или `https://`) не поддерживаются для этих полей: они удаляются с предупреждением на время сборки. Вместо этого объедините изображения в папку `public/` вашего приложения.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Установочные хуки
description: Выполняйте логику во время жизненного цикла установки, обновления или удаления — выполняйте начальное заполнение данными, создавайте резервные копии записей, проверяйте корректность обновления, очищайте внешние ресурсы.
icon: wrench
---
Установочные хуки — это специальные логические функции, которые выполняются во время жизненного цикла установки, обновления или удаления. Они используют то же окружение выполнения обработчика, что и обычные [logic functions](/l/ru/developers/extend/apps/logic/logic-functions), но объявляются с помощью собственных функций определения и существуют вне обычной модели триггеров (HTTP, cron, события базы данных). Установочные хуки получают `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` имеет значение `undefined` при новой установке); хук удаления получает `UninstallPayload` (`{ version?: string }` — версия, которая удаляется).
Каждое приложение может определить **не более одного** хука каждого типа (предустановочный, постустановочный, хук удаления). Сборка манифеста завершится ошибкой, если будет обнаружено более одного хука любого типа.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## Краткий обзор
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Запуски | До миграции метаданных — **предыдущая** схема и данные остаются нетронутыми | После миграции и генерации SDK — **новая** схема уже применяется |
| Выполнение | Всегда синхронное; блокирует установку | Асинхронно по умолчанию (постановка в очередь, 3 повторные попытки); синхронный режим по желанию через `shouldRunSynchronously: true` |
| При ошибке | Установка **прерывается** до любых изменений схемы | Асинхронный режим: до 3 повторных попыток. Синхронный режим: вызывающая сторона получает `POST_INSTALL_ERROR` (изменения схемы **не** откатываются) |
| Типичное использование | Создать резервную копию или исправить данные, которые были бы потеряны при миграции; отклонить рискованное обновление, выбросив исключение | Наполнить данными по умолчанию, настроить рабочее пространство, зарегистрировать внешние ресурсы |
**Общее практическое правило:** по умолчанию используйте post-install. Обращайтесь к pre-install только тогда, когда сама миграция разрушительна и вам нужно перехватить предыдущее состояние, прежде чем оно исчезнет.
| Вы хотите... | Использовать |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Инициализировать данные, настроить рабочее пространство, зарегистрировать внешние ресурсы | `post-install` |
| Длительные операции, которые не должны блокировать ответ установки | `post-install` (асинхронный режим по умолчанию, с повторными попытками воркера) |
| Быстрая настройка, на которую вызывающая сторона полагается сразу после завершения установки | `post-install` с `shouldRunSynchronously: true` |
| Прочитать или сохранить данные, которые предстоящая миграция может потерять | `pre-install` |
| Отклонить обновление, которое повредит существующие данные | `pre-install` (бросьте исключение из обработчика) |
| Согласование при каждом обновлении | Любой хук с `shouldRunOnVersionUpgrade: true` |
## Общее поведение для обоих хуков
* Конфиг — это конфиг `defineLogicFunction` без настроек триггера, но с `shouldRunOnVersionUpgrade`.
* **Когда запускается**: по умолчанию только при чистой установке. Установите `shouldRunOnVersionUpgrade: true`, чтобы также запускать его при обновлениях. Используйте `previousVersion` / `newVersion`, чтобы разветвлять логику в зависимости от пути обновления.
* **Идемпотентность имеет значение**: асинхронный post-install может быть повторно выполнен, и любой хук будет запускаться повторно при обновлениях, если включён `shouldRunOnVersionUpgrade`.
* Обычное окружение logic-функций (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) инъецируется, поэтому вы можете вызывать Twenty API с токеном своего приложения.
* Хук автоматически прикрепляется к манифесту приложения на этапе сборки (`preInstallLogicFunction` / `postInstallLogicFunction`) — ничего не нужно указывать в [`defineApplication()`](/l/ru/developers/extend/apps/config/application).
* Значение `timeoutSeconds` по умолчанию — 300, чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
* **Не выполняется в dev-режиме**: `yarn twenty dev` пропускает процесс установки и синхронизирует файлы напрямую, поэтому хуки там никогда не запускаются. Вместо этого запускайте их вручную:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="Выполняется после применения миграции метаданных рабочего пространства">
Запускается после завершения установки приложения: метаданные синхронизированы, SDK-клиент сгенерирован, новая схема доступна для запросов. Пример — инициализировать запись по умолчанию при новой установке:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Флаг `shouldRunSynchronously` управляет моделью выполнения:
* `false` *(по умолчанию)* — ставится в очередь сообщений (`retryLimit: 3`) и выполняется воркером. Ответ установки возвращается, как только задача поставлена в очередь. **Используйте для длительных операций** — инициализация больших наборов данных, медленные сторонние API.
* `true` — выполняется непосредственно в процессе установки. Запрос на установку блокируется до завершения обработчика; выброшенная ошибка возвращается вызывающей стороне как `POST_INSTALL_ERROR` (без повторных попыток). **Используйте для быстрых операций, которые обязательно должны завершиться до ответа.** На этом этапе миграция уже применена, поэтому сбой не откатывает изменения схемы — он только сообщает об ошибке.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Выполняется до применения миграции метаданных рабочего пространства">
Запускается до миграции метаданных, в контексте **предыдущей** схемы — подходящее место, чтобы создать резервную копию данных, которые были бы потеряны при миграции, или отклонить рискованное обновление. Перед выполнением сервер запускает чисто добавочную «урезанную синхронизацию», которая регистрирует только pre-install функцию новой версии; всё остальное — объекты, поля и данные предыдущей версии — остаётся нетронутым во время выполнения вашего обработчика.
Pre-install всегда **синхронный** и блокирует установку. Если обработчик генерирует исключение, установка прерывается до применения каких-либо изменений схемы — рабочее пространство остаётся на предыдущей версии в согласованном состоянии. Это сделано намеренно: pre-install — ваш последний шанс отказать в рискованном обновлении.
Пример — скопировать значения устаревшего поля перед тем, как миграция его удалит:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
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 = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
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,
});
```
</Accordion>
</AccordionGroup>
## Хук удаления
`defineUninstallLogicFunction` объявляет хук, который выполняется, когда пользователь удаляет ваше приложение. Он выполняется **до того**, как метаданные, данные и код приложения будут удалены — после запуска миграции удаления уже нечему будет выполняться — поэтому ваш обработчик все еще может запрашивать объекты и записи приложения. Используйте его для очистки внешних ресурсов: отмены выделения ресурсов API, удаления оставшихся ботов, отзыва вебхуков.
Заметки:
* Хук работает по принципу «по возможности»: он выполняется синхронно, но сбой только логируется и **никогда не блокирует удаление** — очистка не должна делать удаление приложения невозможным.
* Он получает `UninstallPayload` (`{ version?: string }` — версия, которая удаляется).
* Он **не** выполняется при откате неуспешной новой установки — приложение так и не было полностью установлено.
* Хук не может выполниться после того, как приложение удалено, поэтому внешняя очистка, которая зависит от данных приложения (например, ID ботов, сохраненные в записях), должна выполняться здесь, а не во внешнем запланированном задании.
* Как и установочные хуки, он **не выполняется в режиме разработки** — вместо этого запустите его вручную:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Обзор
description: Настройте само приложение — его идентичность, разрешения по умолчанию и то, что выполняется во время установки.
icon: screwdriver-wrench
---
**Конфигурационный слой** приложения Twenty описывает приложение *для платформы* — его идентичность, разрешения, которыми оно обладает, и код, который выполняется при установке или обновлении. Эти декларации не добавляют новых структур данных или поведения во время выполнения; они сообщают Twenty, *что это за приложение* и *как его настроить*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## В этом разделе
<CardGroup cols={2}>
<Card title="Конфигурация приложения" icon="rocket" href="/l/ru/developers/extend/apps/config/application">
`defineApplication` — идентичность, роль по умолчанию, переменные, метаданные маркетплейса.
</Card>
<Card title="Роли и разрешения" icon="shield-halved" href="/l/ru/developers/extend/apps/config/roles">
`defineRole` — определите, что логические функции вашего приложения могут читать и записывать.
</Card>
<Card title="Установочные хуки" icon="wrench" href="/l/ru/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` и `defineUninstallLogicFunction` — создавайте резервные копии данных, заполняйте значения по умолчанию, проверяйте обновления, выполняйте очистку при удалении.
</Card>
</CardGroup>
## Связь между частями
* **Приложение** — это точка входа. У каждого приложения есть ровно один вызов `defineApplication()`, и он указывает на одну **роль** по умолчанию.
* **Роль** управляет тем, что логические функции и фронтенд‑компоненты приложения могут читать и записывать. Следуйте принципу наименьших привилегий: выдавайте только те разрешения, которые вашему коду действительно нужны.
* **Установочные хуки** запускаются при установке или обновлении — предустановочный до миграции метаданных (чтобы можно было отклонить рискованное обновление), постустановочный после миграции (чтобы можно было заполнить данные по умолчанию в соответствии с новой схемой). Хук удаления запускается непосредственно перед тем, как приложение будет удалено, поэтому он может очистить внешние ресурсы, пока данные приложения ещё доступны для чтения.
<Note>
Установочные хуки используют то же окружение выполнения, что и [логическая функция](/l/ru/developers/extend/apps/logic/logic-functions) — тот же формат обработчика, те же переменные окружения, тот же типизированный клиент API, — но объявляются через собственные функции `define` и находятся вне обычной модели триггеров (HTTP, cron, события базы данных).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Публичные ресурсы
description: Поставляйте статические файлы — изображения, значки, шрифты — вместе с приложением через папку `public/`.
icon: folder-open
---
Папка `public/` в корне вашего приложения содержит статические файлы — изображения, значки, шрифты и любые другие ресурсы, необходимые вашему приложению во время выполнения. Эти файлы автоматически включаются в сборки, синхронизируются в режиме разработки и загружаются на сервер.
Файлы, размещённые в `public/`, являются:
* **Публично доступными** — после синхронизации с сервером ресурсы доступны по публичному URL. Для доступа к ним аутентификация не требуется.
* **Доступными в компонентах фронтенда** — используйте URL ресурсов для отображения изображений, значков или любого медиа внутри ваших компонентов React.
* **Доступными в логических функциях** — используйте URL ресурсов в письмах, ответах API или любой серверной логике.
* \*\*Используется для метаданных на marketplace — поля `logo` и `galleryImages` в файле `defineApplication()` из этой папки (например, `public/logo.png`). Они отображаются в маркетплейсе при публикации вашего приложения. Внешние абсолютные URL-адреса игнорируются для этих полей — вместо этого объединяйте изображения в `public/`. Каждое изображение не должно превышать 10 МБ.
* **Автосинхронизация в режиме разработки** — когда вы добавляете, обновляете или удаляете файл в `public/`, он автоматически синхронизируется с сервером. Перезапуск не требуется.
* **Включены в сборки** — `yarn twenty dev:build` упаковывает все публичные ресурсы в выходной дистрибутив.
## Доступ к публичным ресурсам с помощью `getPublicAssetUrl`
Используйте хелпер `getPublicAssetUrl` из `twenty-sdk`, чтобы получить полный URL файла в каталоге `public/` вашего приложения. Он работает как в **логических функциях**, так и в **компонентах фронтенда**.
**В логической функции:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**В компоненте фронтенда:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
Аргумент `path` задаётся относительно папки `public/` вашего приложения. И `getPublicAssetUrl('logo.png')`, и `getPublicAssetUrl('public/logo.png')` приводят к одному и тому же URL — префикс `public/`, если он есть, удаляется автоматически.

View File

@@ -0,0 +1,205 @@
---
title: Роли и разрешения
description: Укажите, к каким объектам и полям функции логики и фронтенд‑компоненты вашего приложения имеют доступ на чтение и запись.
icon: shield-halved
---
**Роль** — это набор разрешений: какие объекты приложение может читать или изменять, какие поля оно может видеть и какие возможности платформенного уровня оно может использовать. Функции логики и фронтенд‑компоненты каждого приложения наследуют разрешения роли, помеченной с помощью `defineApplicationRole()` (см. раздел [Роль функции по умолчанию](#the-default-function-role) ниже).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Безопасность на уровне строк
Разрешения на объекты и поля определяют, *к каким объектам и полям* у роли есть доступ. **Предикаты прав доступа на уровне строк** заходят дальше и определяют, *какие записи* роль может видеть и с которыми может работать — например, для роли самообслуживания, где каждый внешний пользователь видит только свои собственные записи.
Объявляйте предикаты с помощью `rowLevelPermissionPredicates` в роли. Как и остальная часть манифеста, каждый предикат имеет свой собственный `universalIdentifier` и ссылается на объект и поле по их `universalIdentifier`, `operand`, а также (необязательно) поле workspaceMember, значение которого подставляется во время выполнения запроса — чтобы вы могли выразить: "отношение владельца записи **является** текущим участником рабочей области":
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Поскольку предикаты поставляются в манифесте, они создаются, обновляются и удаляются вместе с ролью при каждой установке и обновлении — не требуется отдельный шаг после установки для поддержания синхронизации.
### Комбинирование предикатов с группами
По умолчанию предикаты роли комбинируются с помощью `AND`. Чтобы комбинировать некоторые из них с помощью `OR` (или
вкладывать логику), объявите запись `rowLevelPermissionPredicateGroups` и укажите на неё каждый предикат через
`predicateGroupUniversalIdentifier`. Эта роль позволяет партнёру видеть сделку, которая либо **принадлежит** ему,
либо по которой он является **контактным лицом**:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Заметки:
* Присвойте каждому предикату и группе стабильный `universalIdentifier` (любой uuid) — он служит ключом сущности между обновлениями, а предикаты ссылаются на группы по нему.
* Предикаты могут ссылаться на объекты и поля, принадлежащие вашему приложению, или на стандартные объекты и поля Twenty.
* Безопасность на уровне строк (row-level security) применяется для рабочих пространств в тарифах, которые её включают; предикаты по-прежнему синхронизируются
в других тарифах, они просто не применяются.
## Роль функции по умолчанию
Когда вы создаёте новое приложение с помощью шаблона, CLI создаёт файл роли по умолчанию, объявленный с помощью `defineApplicationRole()`:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` — это тонкая обёртка вокруг `defineRole()`, которая помечает **ту** роль, которая используется как роль приложения по умолчанию во время установки. Проверка идентична `defineRole`, но конвейер сборки автоматически подключает его `universalIdentifier` к полю `defaultRoleUniversalIdentifier` манифеста приложения — поэтому вам не нужно ссылаться на него из [`defineApplication`](/l/ru/developers/extend/apps/config/application) самостоятельно.
Заметки:
* Допускается ровно **один** вызов `defineApplicationRole(...)` на приложение — сборка манифеста завершится с ошибкой, если будет найдено более одного.
* Используйте `defineRole()` (а не `defineApplicationRole()`) для любых **дополнительных** ролей, которые ставятся вместе с вашим приложением.
* Явная установка `defaultRoleUniversalIdentifier` в `defineApplication()` всё ещё поддерживается для обратной совместимости, но считается устаревшей по сравнению с `defineApplicationRole()`.
## Лучшие практики
* Начните с сгенерированной роли и постепенно ограничивайте её — роль по умолчанию предоставляет широкий доступ на чтение, что редко подходит для продакшена.
* Замените `objectPermissions` и `fieldPermissions` на точные объекты и поля, которые действительно нужны вашим функциям.
* `permissionFlagUniversalIdentifiers` управляют доступом к возможностям на уровне платформы. Сведите их к минимуму.
* Смотрите рабочий пример: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).