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
212 lines
16 KiB
Plaintext
212 lines
16 KiB
Plaintext
---
|
||
title: Обращение к системным метаданным
|
||
description: Определяйте детерминированные универсальные идентификаторы метаданных, которые Twenty автоматически подготавливает для каждого объекта, чтобы ваше приложение могло ссылаться на них без жёсткого кодирования.
|
||
icon: gears
|
||
---
|
||
|
||
Каждый объект в Twenty содержит **системные метаданные**, которые вы сами никогда не объявляете, например набор полей и основное представление списка с его столбцами. Сервер создаёт всё это при подготовке объекта, и этот набор растёт по мере развития Twenty.
|
||
|
||
Поскольку вы это не объявляете, у вас нет константы `universalIdentifier`, которую можно импортировать. Вместо этого сервер **выводит** каждый идентификатор детерминированным образом, а `twenty-sdk` предоставляет тот же механизм вывода, чтобы ваш манифест мог получить точное значение, которое использует сервер.
|
||
|
||
## Системные поля
|
||
|
||
Скалярные поля, присутствующие в каждом объекте, ни одно из которых вы не объявляете с помощью [`defineField()`](/l/ru/developers/extend/apps/data/extending-objects):
|
||
|
||
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
|
||
|
||
Тогда как сослаться на `createdAt` как на столбец в [представлении](/l/ru/developers/extend/apps/layout/views)?
|
||
|
||
### Проблема
|
||
|
||
Начиная с Twenty 2.19, универсальный идентификатор системного поля **детерминированно выводится** сервером из трех входных данных: универсального идентификатора приложения, универсального идентификатора объекта и имени поля. Придумать id и захардкодить его не получится: он не соответствует ничему на сервере, и синхронизация отклоняет висящую ссылку:
|
||
|
||
```
|
||
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
|
||
```
|
||
|
||
### Решение
|
||
|
||
<Note>
|
||
`getFieldUniversalIdentifier` доступен, начиная с `twenty-sdk` версии 2.21.
|
||
</Note>
|
||
|
||
Используйте `getFieldUniversalIdentifier`, чтобы получить в точности то же значение, которое использует сервер. Он принимает три входных параметра и возвращает универсальный идентификатор поля:
|
||
|
||
```ts
|
||
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
|
||
|
||
const createdAtFieldId = getFieldUniversalIdentifier({
|
||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
name: 'createdAt',
|
||
});
|
||
```
|
||
|
||
* `applicationUniversalIdentifier` — это идентификатор вашего приложения, тот, который вы передаете в [`defineApplication()`](/l/ru/developers/extend/apps/config/application).
|
||
* `objectUniversalIdentifier` — это идентификатор объекта, к которому принадлежит поле.
|
||
* `name` — это имя системного поля, одно из значений, перечисленных выше.
|
||
|
||
### Пример: столбец createdAt в представлении
|
||
|
||
Типичный случай — добавление столбца `createdAt` в представление одного из ваших пользовательских объектов. Разрешите id поля и ссылайтесь на него так же, как на любой другой `fieldMetadataUniversalIdentifier`:
|
||
|
||
```ts src/views/example-view.ts
|
||
import {
|
||
defineView,
|
||
getFieldUniversalIdentifier,
|
||
} from 'twenty-sdk/define';
|
||
|
||
const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||
'0b04e15c-27b2-4741-9046-b32e07469072';
|
||
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
|
||
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
|
||
|
||
export default defineView({
|
||
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
|
||
name: 'All records',
|
||
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
icon: 'IconList',
|
||
position: 0,
|
||
fields: [
|
||
{
|
||
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
|
||
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
|
||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
name: 'createdAt',
|
||
}),
|
||
position: 0,
|
||
isVisible: true,
|
||
size: 200,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Тот же разрешенный id работает везде, где ожидается `fieldMetadataUniversalIdentifier`: поля представления, фильтры, сортировки, группировки и виджеты макета страницы.
|
||
|
||
<Note>
|
||
Разрешайте id, не хардкодьте его. Поскольку сервер выводит значение из
|
||
id приложения, id объекта и имени поля, вызов
|
||
`getFieldUniversalIdentifier` сохраняет вашу ссылку корректной, даже если эти
|
||
входные данные изменятся, и предотвращает расхождение, если способ вывода когда-нибудь изменится.
|
||
</Note>
|
||
|
||
### Системные поля связей
|
||
|
||
<Note>
|
||
`getSystemRelationFieldUniversalIdentifier` доступен в `twenty-sdk`, начиная с версии 2.23, и требует сервер Twenty версии 2.23 или новее.
|
||
</Note>
|
||
|
||
Помимо перечисленных выше скалярных системных полей, сервер также подготавливает четыре **системных поля связей** для каждого объекта: `timelineActivities`, `attachments`, `noteTargets` и `taskTargets`, каждое из которых указывает на соответствующий стандартный объект связи.
|
||
|
||
Таким образом, эти поля не разрешаются с помощью `getFieldUniversalIdentifier`: их идентификатор выводится **безотносительно имени** из объекта, содержащего поле, и объекта, на который поле указывает. Таким образом, переименование объекта никогда не изменяет идентификаторы его полей связей.
|
||
|
||
Используйте `getSystemRelationFieldUniversalIdentifier`, чтобы разрешить их:
|
||
|
||
```ts
|
||
import {
|
||
getSystemRelationFieldUniversalIdentifier,
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||
} from 'twenty-sdk/define';
|
||
|
||
// rocket.attachments — the relation field hosted on your custom object
|
||
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
|
||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
relationTargetObjectUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
|
||
});
|
||
```
|
||
|
||
* `objectUniversalIdentifier` — это объект, **содержащий** поле.
|
||
* `relationTargetObjectUniversalIdentifier` — это объект, на который **указывает** поле.
|
||
|
||
Направление кодируется порядком аргументов. Чтобы получить обратную сторону (например, `attachment.targetRocket`, morph‑поле, которое сервер создаёт на стандартном объекте связи), поменяйте их местами:
|
||
|
||
```ts
|
||
// attachment.targetRocket — the reverse morph field on Attachment
|
||
const attachmentTargetRocketFieldId =
|
||
getSystemRelationFieldUniversalIdentifier({
|
||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
objectUniversalIdentifier:
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
|
||
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
});
|
||
```
|
||
|
||
Как и в случае со скалярными системными полями, разрешённый id работает везде, где ожидается `fieldMetadataUniversalIdentifier`.
|
||
|
||
## Системные представления
|
||
|
||
<Note>
|
||
`getSystemViewUniversalIdentifier` и `getSystemViewFieldUniversalIdentifier`
|
||
доступны в `twenty-sdk`, начиная с версии 2.26, и требуют сервер Twenty
|
||
версии 2.26 или новее.
|
||
</Note>
|
||
|
||
Сервер также подготавливает **системное представление** для каждого объекта: основное представление списка (`All {objectLabelPlural}`, с ключом `ViewKey.INDEX` (полученным с помощью `SYSTEM_VIEW_KEYS.INDEX`)), с одним столбцом на каждое отображаемое поле. Как и у системных полей связей, их идентификаторы выводятся **без использования имён**, поэтому переименование объекта или поля никогда их не меняет.
|
||
|
||
Используйте `getSystemViewUniversalIdentifier`, чтобы получить идентификатор представления:
|
||
|
||
```ts
|
||
import {
|
||
getSystemViewUniversalIdentifier,
|
||
SYSTEM_VIEW_KEYS,
|
||
} from 'twenty-sdk/define';
|
||
|
||
const rocketIndexViewId = getSystemViewUniversalIdentifier({
|
||
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
|
||
viewKey: SYSTEM_VIEW_KEYS.INDEX,
|
||
});
|
||
```
|
||
|
||
* `objectMetadataApplicationUniversalIdentifier` — это приложение, которому принадлежит **объект**, и именно по нему задаётся пространство имён представления.
|
||
* `objectUniversalIdentifier` — это объект, который отображается в представлении.
|
||
* `viewKey` — это дискриминатор системного представления: `SYSTEM_VIEW_KEYS.INDEX` для основного представления списка, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` для представления виджета полей на странице записи. Он определяет способ вычисления представления; только `INDEX` также сохраняется в строке представления.
|
||
|
||
Полученный идентификатор подходит везде, где ожидается `viewUniversalIdentifier`, например для пункта боковой панели [`NavigationMenuItemType.VIEW`](/l/ru/developers/extend/apps/layout/navigation-menu-items). Чтобы просто открыть основной список объекта, предпочитайте `NavigationMenuItemType.OBJECT` с `targetObjectUniversalIdentifier`: здесь не нужен вывод идентификатора.
|
||
|
||
`getSystemViewFieldUniversalIdentifier` определяет идентификатор одного **столбца** в системном представлении по самому представлению и отображаемому в нём полю:
|
||
|
||
```ts
|
||
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
|
||
|
||
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
|
||
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||
viewUniversalIdentifier: rocketIndexViewId,
|
||
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||
});
|
||
```
|
||
|
||
Обратите внимание на первый аргумент: пространство имён столбца задаётся приложением, которому принадлежит **поле, отображаемое в нём**, а не приложением, которому принадлежит представление. Поле, которое ваше приложение добавляет к стандартному объекту, получает свой столбец, выводимый в пространстве имён вашего приложения, на представлении, принадлежащем Twenty.
|
||
|
||
<Warning>
|
||
Системные представления и их столбцы **принадлежат серверу**: разрешайте их идентификаторы, чтобы ссылаться на них, но никогда не объявляйте их сами. `key` в
|
||
[`defineView()`](/l/ru/developers/extend/apps/layout/views) устарел и
|
||
игнорируется, поэтому представление из манифеста никогда не может заявить ключ `INDEX`, а сервер уже подготавливает столбец для каждого добавленного вами поля, так что объявление собственного
|
||
`defineViewField()` для того же поля в системном представлении конфликтует с ним.
|
||
</Warning>
|
||
|
||
## Стандартные объекты Twenty
|
||
|
||
Для **стандартного** объекта Twenty (Person, Company, Opportunity, …) вам не нужно ничего выводить: идентификаторы как полей, так и представлений — это заранее вычисленные константы, которые вы можете импортировать напрямую.
|
||
|
||
```ts
|
||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||
|
||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
|
||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
|
||
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
|
||
```
|
||
|
||
Используйте указанные выше вспомогательные функции, когда объект — это объект, который **ваше приложение** определяет с помощью [`defineObject()`](/l/ru/developers/extend/apps/data/objects), где такой константы не существует.
|
||
|
||
<Note>
|
||
`name` — это **поле по умолчанию**, а не системное поле. У него есть собственный захардкоженный
|
||
универсальный идентификатор, и он не разрешается через
|
||
`getFieldUniversalIdentifier`. В определяемых вами объектах ссылайтесь на поле
|
||
`name` по идентификатору, который вы задали ему в `defineObject()`.
|
||
</Note>
|