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
206 lines
10 KiB
Plaintext
206 lines
10 KiB
Plaintext
---
|
||
title: 定位系统元数据
|
||
description: 解析 Twenty 在每个对象上自动预配置的元数据的确定性通用标识符,这样你的应用无需硬编码就能引用它。
|
||
icon: gears
|
||
---
|
||
|
||
Twenty 中的每个对象都带有你无需自行声明的**系统元数据**,例如一组字段以及带有列的主列表视图。 当对象被预配置时,服务器会创建所有这些内容,并且随着 Twenty 的发展,这个集合也会随之增长。
|
||
|
||
由于你不会声明它,因此也就没有可供你导入的 `universalIdentifier` 常量。 相反,服务器会以确定性方式**派生**每个标识符,而 `twenty-sdk` 暴露了相同的派生逻辑,这样你的 manifest 就能解析出服务器实际使用的精确值。
|
||
|
||
## 系统字段
|
||
|
||
存在于每个对象上的标量字段,你不会使用 [`defineField()`](/l/zh/developers/extend/apps/data/extending-objects) 来声明其中任何一个:
|
||
|
||
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
|
||
|
||
那么,如何在[视图](/l/zh/developers/extend/apps/layout/views)中将 `createdAt` 作为一列来引用呢?
|
||
|
||
### 问题
|
||
|
||
从 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/zh/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` 同时会存储在视图行中。
|
||
|
||
解析得到的 id 可用于任何需要 `viewUniversalIdentifier` 的地方,例如侧边栏中的 [`NavigationMenuItemType.VIEW`](/l/zh/developers/extend/apps/layout/navigation-menu-items) 条目。 如果只是要打开某个对象的主列表,优先使用带有 `targetObjectUniversalIdentifier` 的 `NavigationMenuItemType.OBJECT`:它不需要派生。
|
||
|
||
`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>
|
||
系统视图及其列是由**服务器所有**的:解析它们的标识符是为了引用它们,而不是为了声明它们。 [`defineView()`](/l/zh/developers/extend/apps/layout/views) 上的 `key` 已被弃用且会被忽略,因此 manifest 视图永远无法占用 `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/zh/developers/extend/apps/data/objects) 定义、且不存在此类常量时,再使用上面的辅助方法。
|
||
|
||
<Note>
|
||
`name` 是一个**默认**字段,而不是系统字段。 它保留自己的硬编码
|
||
通用标识符,而不是通过
|
||
`getFieldUniversalIdentifier` 解析。 在你定义的对象上,请使用你在 `defineObject()` 中赋予它的标识符来引用
|
||
`name` 字段。
|
||
</Note>
|