[BREAKING-CHANGE] Centralize system View viewField side effect (#23081)

# Introduction

Closes https://github.com/twentyhq/core-team-issues/issues/2669

Part of the `isSystemSideEffect` engine-ownership effort. Until now, a
custom object's default **INDEX** table view (`All {objectLabelPlural}`)
and its view fields were built imperatively in `ObjectMetadataService`
with random `v4()` identifiers, while `twenty-standard` authored its own
copies with hardcoded literals. The two never converged, an object
rename could drift the view, and nothing marked these rows as
engine-owned.

This PR makes the metadata side-effect engine the **single owner** of
the INDEX view and its view fields, on name-free deterministic
identifiers, for custom and standard objects alike.

## Core design

- **Name-free deterministic identity.** The INDEX view identifier
derives from `object identifier + ViewKey.INDEX`
(`getSystemViewUniversalIdentifier`); each view-field identifier derives
from `view identifier + field identifier`
(`getViewFieldUniversalIdentifier`). An object rename (with a pinned
object identifier) keeps the same view, losslessly.
- **`isSystemSideEffect: true` is provenance.** Every INDEX view / view
field the engine emits is flagged system-owned, so manifest deletion
inference never drops it. The flag follows the view: a view field
inherits its parent view's flag.
- **The engine is the sole owner of the INDEX view.** It always emits
it; a caller providing one with the same derived identifier is a genuine
conflict surfaced by the engine's reserved-identifier collision, not
silently deferred.

## Changes

### Shared (`twenty-shared`)

- `getIndexViewUniversalIdentifier` →
`getSystemViewUniversalIdentifier`, now taking a `viewKey` (generalizes
to any singleton engine-owned view).
- Standard field identifiers extracted into a new
`STANDARD_OBJECT_FIELDS` constant, so both an object's `fields` and its
INDEX view read the same field identifiers.
- `buildStandardObjectIndexView` derives the standard INDEX view +
view-field identifiers from `STANDARD_OBJECT_FIELDS`, replacing the
hardcoded literals in `standard-object.constant.ts`.

### Metadata side-effect engine (custom objects)

- **`objectSystemFieldsAndIndexViewOnCreate`** (replaces
`objectSystemFieldsOnCreate`): on object creation, provisions the 7
reserved system fields **and** the INDEX view with one view field per
displayable system field, all `isSystemSideEffect: true`.
- **`fieldIndexViewFieldOnCreate`** (new): on field creation, provisions
the field's INDEX view field. Object created in the same batch →
visible, positioned before the system view fields; pre-existing object →
hidden, appended (preserving the historical `createOneField` behavior).
Both branches resolve the INDEX view by its derived identifier (single
map access, never a scan).
- **`fieldSystemViewFieldsOnDelete`** (new): on field deletion,
cascade-deletes every engine-owned view field displaying it.
- **`objectSystemSideEffectsOnDelete`** (extended): now also
cascade-deletes the object's engine-owned views and their view fields
(in addition to system fields, indexes, searchFieldMetadata). Every
lookup walks a foreign-key aggregator down from the deleted object, so
the work is proportional to what the object owns, never to workspace
size.
- Object-create and field-create positions are derived from the same
caller-input field list, so the INDEX view layout is contiguous with no
handler-ordering dependency.
- `view` / `viewField` added to the side-effect companion metadata names
for `fieldMetadata` and `objectMetadata`.

### Reserved-identifier invariant

A caller can never define an entity whose identifier collides with one a
system side effect produces: caller inputs are forced
`isSystemSideEffect: false` at every entry point (API and app-manifest
transpilers), and the engine raises
`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`, aborting the operation, when a
system emission lands on a caller-claimed identifier. Covered by a new
engine-level test.

### Caller-side provisioning removed

The imperative INDEX view + view-field provisioning is removed from
`ObjectMetadataService.createOneObject`. The record-page `FIELDS_WIDGET`
view is intentionally left caller-side and deferred to the follow-up
(see below).

### `twenty-standard` convergence

Standard INDEX views and their view fields converge on the same
derived-identifier + `isSystemSideEffect: true` scheme as the engine.
`twenty-standard` syncs through the from/to migration path (which never
runs the side-effect engine), so it authors this INDEX surface itself,
matching what the engine produces for custom objects.

## Rollout

Two `2.26.0` workspace commands, running after the `2.25`
messageCampaign commands:

- `upgrade:2-26:reconcile-index-view-universal-identifier` re-owns the
INDEX views of the **twenty-standard and workspace-custom applications**
and all their view fields to the derived identifiers with
`isSystemSideEffect: true`, in a single per-workspace transaction. Each
view field identifier is keyed on the application of the **displayed
field** (an app or user column on a standard INDEX view converges too).
Soft-deleted views and view fields are skipped: one can coexist with an
active successor on the same derivation inputs and both would derive the
same identifier. Children reference the view by primary key, so the
re-own is lossless.
- `upgrade:2-26:demote-and-backfill-application-index-view` handles
**manifest-installed applications**, which never had their INDEX view
auto-provisioned: every caller-authored INDEX view of another
application is demoted to `key: null` (a plain additional view under its
manifest identifier), then every application object gets the
engine-owned INDEX view and its full view-field layout backfilled
through the migration pipeline's legacy path (no side-effect expansion),
views committed before view fields across applications since a view
field belongs to the application owning its field. Idempotent and
retry-safe: engine-owned INDEX views are neither demoted nor
re-backfilled, and view creation and view-field creation are gated
independently, so a retry after a partial failure still backfills the
missing view fields of an already-committed view.

Both support `--dry-run` and invalidate the full flat-maps closure
(parents aggregate the re-owned identifiers, children resolve them as
universal foreign keys, and page-layout widget universal configurations
resolve view PKs at cache-build time).

The `2.25` `upgrade:2-25:add-message-campaign-name-field` command is
adapted to resolve the campaign INDEX view by its INDEX key on the
object instead of by universal identifier: it now runs before the
reconcile, on workspaces still holding legacy identifiers.

## ⚠️ Breaking change

This PR **mutates 187 previously hardcoded universal identifiers** — the
standard objects' INDEX views and their view fields (the literals
removed from `standard-object.constant.ts`), now derived.

- **Handled by the `2.26` commands above** for all existing workspaces.
- **The INDEX key is now engine-reserved.** The flat view validator
rejects caller-created INDEX views (API and manifest inputs are forced
`isSystemSideEffect: false`) and enforces a single non-deleted INDEX
view per object; `view.key` is no longer a comparable/updatable
property, so no writer can promote or demote a view after creation.
`ViewManifest.key` is deprecated and ignored (manifest views are always
additional views, so old apps keep syncing and demoted views are not
promoted back); the REST/GraphQL create path now rejects `key: INDEX`.
In-repo example apps (`hello-world`, `document-generator`) no longer
declare it.
- **12 declared-but-never-seeded standard INDEX view field identifiers
deleted** (the former `preservedViewFields` on `timelineActivity`,
`workflowRun` and `workspaceMember`): after the reconcile, no workspace
row references them.
- **`computeFlatViewFieldsToCreate` now derives view field identifiers**
instead of drawing `v4()` ones, which also changes what the committed
`1-23` record-page backfill produces going forward (deliberate,
documented in-code).
- **Record-page views and view fields are not affected** (identifiers
unchanged).
- **In-repo apps: `twenty-last-contact` updated.** It was the only app
declaring explicit INDEX view fields (10 columns across `allPeople` /
`allCompanies` / `allOpportunities`) through manifest `viewFields`.
Those target identifiers are now engine-owned and derived, so the
manifest inputs no longer resolve and install failed with `View not
found`. The app now declares only its fields; the engine's
`fieldIndexViewFieldOnCreate` provisions the matching INDEX view field
automatically. No other app under `packages/twenty-apps` references any
of the 187 mutated identifiers, and apps that target standard views
point at record-page views (e.g. `real-estate` →
`opportunityRecordPageFields`) or their own objects (`twenty-partners`),
all unchanged.

### Loss of granularity for app maintainers

The engine now owns the INDEX view field of every field a caller adds to
an object, so app maintainers lose direct control over those columns.
Previously an app could target the engine-owned INDEX view with an
explicit manifest `viewField` and set its `position` and `isVisible`.
Now `fieldIndexViewFieldOnCreate` appends a **hidden** view field in
caller-input order on field creation, so:

- Columns an app previously showed at a **dedicated position** and
**visible** (e.g. `twenty-last-contact`'s last-contact columns) become
**hidden** and **appended in input order** after install.
- There is currently **no manifest way to override** the
engine-provisioned INDEX view field's position, visibility, or size.

This is a deliberate regression accepted for the sake of
single-ownership, and app maintainers should expect their INDEX columns
to move/hide after upgrading. A follow-up override API will let
maintainers reclaim per-field control over the engine-provisioned INDEX
view field.

## Testing

- Unit specs for each handler: object create (system fields + INDEX
view/view fields, override, position offset), field create (same-batch
vs existing-object, non-displayable noop, no-INDEX-view noop), field
delete, object delete (fields/indexes/searchFieldMetadata/views/view
fields cascade, reverse-relation view field on another object).
- Engine-level test for the reserved-identifier collision.
- `twenty-standard` guard test that its INDEX views/view fields stay on
the derived scheme and stay system-owned.
- Integration test: full engine provisioning of the INDEX view/view
fields on object creation, same view id preserved across an object
rename, and cascade delete on object deletion.

## Follow-up

The full record-page stack (record-page view, its view fields, view
field groups, page layout / tab / widget) is still built imperatively
and moves into the engine in
https://github.com/twentyhq/core-team-issues/issues/2721.
This commit is contained in:
Paul Rastoin
2026-07-29 15:32:21 +02:00
committed by GitHub
parent a0281f635b
commit 0c545bcdeb
92 changed files with 7794 additions and 3428 deletions

View File

@@ -1,4 +1,4 @@
import { defineView, ViewKey } from 'twenty-sdk/define';
import { defineView } from 'twenty-sdk/define';
import {
DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
@@ -16,7 +16,6 @@ export default defineView({
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
key: ViewKey.INDEX,
position: 0,
fields: [
{

View File

@@ -1,4 +1,4 @@
import { defineView, ViewKey } from 'twenty-sdk/define';
import { defineView } from 'twenty-sdk/define';
import {
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
@@ -14,7 +14,6 @@ export default defineView({
name: 'All templates',
objectUniversalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFileText',
key: ViewKey.INDEX,
position: 0,
fields: [
{

View File

@@ -1,4 +1,4 @@
import { defineView, ViewKey } from 'twenty-sdk/define';
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = '965e3776-b966-4be8-83f7-6cd3bce5e1bd';
@@ -8,7 +8,6 @@ export default defineView({
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{

View File

@@ -1,5 +1,9 @@
# Changelog
## 1.1.3
- Stop declaring INDEX view fields explicitly: the server now provisions the INDEX view column for each app field automatically, so the manifest no longer targets the engine-owned standard INDEX views.
## 1.1.1
- Throttle backfill updates and retry rate-limited or transient API failures with exponential backoff, so install/upgrade no longer fails behind Cloudflare rate limiting.

View File

@@ -1,6 +1,6 @@
{
"name": "@twentyhq/last-contact",
"version": "1.1.2",
"version": "1.1.3",
"license": "MIT",
"engines": {
"node": "^24.5.0",

View File

@@ -5,8 +5,6 @@ export const APPLICATION_UNIVERSAL_IDENTIFIER = '66a504cc-0a75-410e-a43f-cdeae1d
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = '34187abe-1b98-4153-85cd-4808e0aebf30';
export const LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER =
'4e0fd7ff-0bbc-47b2-baab-5fe2c0d12557';
export const LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'8fc4d931-426c-413b-bd7e-0c10d1ad8d0a';
export const EMAIL_INTERACTION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
'87c8926b-15e3-43ed-a303-60f6d072f351';
export const CALENDAR_INTERACTION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
@@ -20,8 +18,6 @@ export const LAST_CONTACT_BY_FIELD_UNIVERSAL_IDENTIFIER =
'cfdee7bd-8d41-41e6-a888-512705e75d7b';
export const LAST_CONTACT_FOR_PEOPLE_ON_WORKSPACE_MEMBER_FIELD_UNIVERSAL_IDENTIFIER =
'bec41e57-6b6f-4bb2-90cc-4030384dd8f6';
export const LAST_CONTACT_BY_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'd68d71f7-10f3-42a2-ab81-a709eee83ca7';
export const LAST_CONTACT_ITEM_MORPH_ID =
'97eafe97-0ee1-4857-b233-fec855f6bf1a';
@@ -33,10 +29,6 @@ export const LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
'cceb21fe-1422-40d4-bc8b-44c12c79c452';
export const LAST_CONTACT_FOR_PEOPLE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
'f49ce9ca-5aab-445d-9749-db256aaf74b7';
export const LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'88ab5daf-0650-4813-b9ca-f413d7c9f611';
export const LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'9c264385-e833-4d88-8149-e392651eb5dd';
export const LAST_OUTBOUND_AT_FIELD_UNIVERSAL_IDENTIFIER =
'f4bbac22-8a3d-44c2-bbf2-7e28237252e1';
@@ -63,12 +55,6 @@ export const LAST_CONTACT_FOR_COMPANIES_ON_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER =
'9f08c2e0-23ba-413c-94c8-04ba828586a3';
export const LAST_CONTACT_FOR_COMPANIES_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
'79103905-cfdb-413a-b964-8f347332ef3c';
export const COMPANY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'08244258-dc60-475f-a2c7-3122f8b658fb';
export const COMPANY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'7adc5b23-1a39-4bb6-8cff-ac4e979d38f3';
export const COMPANY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'61a3b683-3129-4265-889e-309d7ec57403';
export const OPPORTUNITY_LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER =
'69225d61-0efb-4959-af6d-566caba58412';
@@ -82,9 +68,3 @@ export const LAST_CONTACT_FOR_OPPORTUNITIES_ON_MESSAGE_FIELD_UNIVERSAL_IDENTIFIE
'1ccff5c2-44c3-4a7d-847f-c74440fe56a9';
export const LAST_CONTACT_FOR_OPPORTUNITIES_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
'db36a44e-a6dc-4bec-a23e-3b27f63d1629';
export const OPPORTUNITY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'8b447030-5124-4cfa-bd96-9836c25d4fc9';
export const OPPORTUNITY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'cac2ec01-6c8b-4d50-b897-7717b2679842';
export const OPPORTUNITY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'ab720bee-a0f3-41b9-9c94-8beb50e0b525';

View File

@@ -1,21 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
COMPANY_LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
COMPANY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier: COMPANY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.views.allCompanies
.universalIdentifier,
fieldMetadataUniversalIdentifier:
COMPANY_LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
position: 8,
isVisible: true,
size: 150,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
COMPANY_LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
COMPANY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
COMPANY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.views.allCompanies
.universalIdentifier,
fieldMetadataUniversalIdentifier:
COMPANY_LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
position: 10,
isVisible: true,
size: 180,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
COMPANY_LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
COMPANY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
COMPANY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.views.allCompanies
.universalIdentifier,
fieldMetadataUniversalIdentifier:
COMPANY_LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
position: 9,
isVisible: true,
size: 180,
});

View File

@@ -1,20 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier: LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople
.universalIdentifier,
fieldMetadataUniversalIdentifier: LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
position: 8,
isVisible: true,
size: 150,
});

View File

@@ -1,20 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
LAST_CONTACT_BY_FIELD_UNIVERSAL_IDENTIFIER,
LAST_CONTACT_BY_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier: LAST_CONTACT_BY_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople
.universalIdentifier,
fieldMetadataUniversalIdentifier: LAST_CONTACT_BY_FIELD_UNIVERSAL_IDENTIFIER,
position: 7,
isVisible: true,
size: 150,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople
.universalIdentifier,
fieldMetadataUniversalIdentifier:
LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
position: 10,
isVisible: true,
size: 180,
});

View File

@@ -1,21 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier: LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople
.universalIdentifier,
fieldMetadataUniversalIdentifier:
LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
position: 9,
isVisible: true,
size: 180,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
OPPORTUNITY_LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
OPPORTUNITY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
OPPORTUNITY_LAST_CONTACT_AT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.allOpportunities
.universalIdentifier,
fieldMetadataUniversalIdentifier:
OPPORTUNITY_LAST_CONTACT_AT_FIELD_UNIVERSAL_IDENTIFIER,
position: 7,
isVisible: true,
size: 150,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
OPPORTUNITY_LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
OPPORTUNITY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
OPPORTUNITY_LAST_CONTACT_ITEM_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.allOpportunities
.universalIdentifier,
fieldMetadataUniversalIdentifier:
OPPORTUNITY_LAST_CONTACT_ITEM_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
position: 9,
isVisible: true,
size: 180,
});

View File

@@ -1,22 +0,0 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
OPPORTUNITY_LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
OPPORTUNITY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineViewField({
universalIdentifier:
OPPORTUNITY_LAST_CONTACT_ITEM_MESSAGE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.allOpportunities
.universalIdentifier,
fieldMetadataUniversalIdentifier:
OPPORTUNITY_LAST_CONTACT_ITEM_MESSAGE_FIELD_UNIVERSAL_IDENTIFIER,
position: 8,
isVisible: true,
size: 180,
});

View File

@@ -4,10 +4,12 @@ description: Ship pre-configured saved views — column order, filters, groups
icon: 'list'
---
A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create.
A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app.
Every object automatically gets its main list view (the "All ..." table an `OBJECT` navigation item opens) provisioned by the server — including the objects your app creates, with one column per field your app declares. Views you define with `defineView()` are always **additional** views on top of that default one.
```ts src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk/define';
import { defineView } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
@@ -16,7 +18,6 @@ export default defineView({
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
@@ -33,7 +34,7 @@ export default defineView({
## Key points
- `objectUniversalIdentifier` specifies which object this view applies to. It can be a custom object you defined or a standard Twenty object.
- `key: ViewKey.INDEX` marks the view as the object's main list view (the one an `OBJECT` navigation item opens).
- The object's main list view is server-owned: `key` is deprecated and ignored, so a manifest view can never claim it. Ship a `VIEW` navigation item if you want your view one click away in the sidebar.
- `fields` controls which columns appear and in what order. Each field references a `fieldMetadataUniversalIdentifier`. To reference an auto-created system field such as `createdAt`, see [Targeting System Fields](/developers/extend/apps/data/system-fields).
- You can also declare `filters`, `filterGroups`, `sorts`, `groups`, and `fieldGroups` for advanced configurations.
- `position` controls ordering when multiple views exist for the same object.

View File

@@ -13,17 +13,18 @@ document, and a native rich-text **editor** tab for templates.
A [view](/developers/extend/apps/layout/views) is a saved list of a given object.
A [navigation menu item](/developers/extend/apps/layout/navigation-menu-items)
puts that view in the sidebar.
puts that view in the sidebar. The server already provisions each object's
default "All ..." view automatically, so the views an app defines are
additional, curated ones.
```ts filename="src/views/documents.view.ts"
import { defineView, ViewKey } from 'twenty-sdk/define';
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
key: ViewKey.INDEX,
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,

View File

@@ -3,7 +3,7 @@ import { type StandaloneViewFieldManifest } from 'twenty-shared/application';
const validConfig: StandaloneViewFieldManifest = {
universalIdentifier: '40b17076-ea50-4e42-968e-8989a95f2b5d',
viewUniversalIdentifier: '20202020-a002-4a02-8a02-ae0a1ea11a00',
viewUniversalIdentifier: '0cda0ad4-74da-5ccd-975c-a0e4db43a05a',
fieldMetadataUniversalIdentifier: '4e0fd7ff-0bbc-47b2-baab-5fe2c0d12557',
position: 10,
isVisible: true,

View File

@@ -522,7 +522,8 @@ export class BackfillRecordPageLayoutsCommand extends ProvisionedWorkspaceComman
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: fieldsView.universalIdentifier,
flatApplication: twentyStandardFlatApplication,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier: true,

View File

@@ -1,6 +1,7 @@
import { Command } from 'nest-commander';
import { getSearchFieldUniversalIdentifier } from 'twenty-shared/application';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
@@ -13,8 +14,8 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -159,15 +160,34 @@ export class AddMessageCampaignNameFieldCommand extends ProvisionedWorkspaceComm
);
}
viewFieldsToCreate.push({
...standardNameViewField,
position: this.computeNameViewFieldPosition({
flatViewMaps,
flatViewFieldMaps,
standardNameViewField,
isNameLabelIdentifier,
}),
});
const campaignIndexFlatView =
campaignObjectMetadata.viewUniversalIdentifiers
.map(
(viewUniversalIdentifier) =>
flatViewMaps.byUniversalIdentifier[viewUniversalIdentifier],
)
.filter(isDefined)
.find(
(flatView) =>
flatView.key === ViewKey.INDEX && !isDefined(flatView.deletedAt),
);
if (isDefined(campaignIndexFlatView)) {
viewFieldsToCreate.push({
...standardNameViewField,
viewUniversalIdentifier: campaignIndexFlatView.universalIdentifier,
position: this.computeNameViewFieldPosition({
campaignIndexFlatView,
flatViewFieldMaps,
standardNameViewField,
isNameLabelIdentifier,
}),
});
} else {
this.logger.warn(
`No INDEX view found for messageCampaign in workspace ${workspaceId}, skipping the name view column`,
);
}
}
const searchFieldMetadatasToCreate =
@@ -255,32 +275,26 @@ export class AddMessageCampaignNameFieldCommand extends ProvisionedWorkspaceComm
// it), above all of them otherwise (the validator forbids anything below the
// label identifier).
private computeNameViewFieldPosition({
flatViewMaps,
campaignIndexFlatView,
flatViewFieldMaps,
standardNameViewField,
isNameLabelIdentifier,
}: {
flatViewMaps: FlatEntityMaps<FlatView>;
campaignIndexFlatView: FlatView;
flatViewFieldMaps: FlatEntityMaps<FlatViewField>;
standardNameViewField: FlatViewField;
isNameLabelIdentifier: boolean;
}): number {
const flatView = findFlatEntityByUniversalIdentifier<FlatView>({
flatEntityMaps: flatViewMaps,
universalIdentifier: standardNameViewField.viewUniversalIdentifier,
});
if (!isDefined(flatView)) {
return standardNameViewField.position;
}
const otherViewFieldPositions = flatView.viewFieldUniversalIdentifiers
.map(
(viewFieldUniversalIdentifier) =>
flatViewFieldMaps.byUniversalIdentifier[viewFieldUniversalIdentifier],
)
.filter(isDefined)
.map(({ position }) => position);
const otherViewFieldPositions =
campaignIndexFlatView.viewFieldUniversalIdentifiers
.map(
(viewFieldUniversalIdentifier) =>
flatViewFieldMaps.byUniversalIdentifier[
viewFieldUniversalIdentifier
],
)
.filter(isDefined)
.map(({ position }) => position);
if (otherViewFieldPositions.length === 0) {
return standardNameViewField.position;

View File

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { ReconcileIndexViewUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255689000-reconcile-index-view-universal-identifier.command';
import { DemoteAndBackfillApplicationIndexViewCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255690000-demote-and-backfill-application-index-view.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature([ViewEntity]),
ApplicationModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
WorkspaceIteratorModule,
],
providers: [
ReconcileIndexViewUniversalIdentifierCommand,
DemoteAndBackfillApplicationIndexViewCommand,
],
})
export class V2_26_UpgradeVersionCommandModule {}

View File

@@ -0,0 +1,291 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { invalidateIndexViewReconcileCache } from 'src/database/commands/upgrade-version-command/2-26/utils/invalidate-index-view-reconcile-cache.util';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps.util';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
type ReownUpdate = {
id: string;
update: {
universalIdentifier?: string;
isSystemSideEffect?: boolean;
};
};
@RegisteredWorkspaceCommand('2.26.0', 1785255689000)
@Command({
name: 'upgrade:2-26:reconcile-index-view-universal-identifier',
description:
'Re-own the INDEX table views ("All {objectLabelPlural}", keyed on ViewKey.INDEX) of the twenty-standard and workspace-custom applications, and all their view fields, onto the engine convention: the view gets the name-free deterministic universal identifier (getSystemViewUniversalIdentifier, object identifier + INDEX key), each view field gets the derived getViewFieldUniversalIdentifier keyed on the application of the field it DISPLAYS — not the row attribution, which diverges when a user shows a hidden standard column and mints a workspace-custom view field on a standard field — so an app or user column on a standard INDEX view converges too, and both get isSystemSideEffect: true, as if provisioned by the metadata side-effect engine. INDEX views of other applications are handled by the demote-and-backfill command. Children reference the view by primary key, so the re-own is a lossless update.',
})
export class ReconcileIndexViewUniversalIdentifierCommand extends ProvisionedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly applicationService: ApplicationService,
// eslint-disable-next-line twenty/prefer-workspace-scoped-repository
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const {
flatViewMaps,
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatViewMaps',
'flatViewFieldMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const engineOwnedApplicationUniversalIdentifiers = new Set([
twentyStandardFlatApplication.universalIdentifier,
workspaceCustomFlatApplication.universalIdentifier,
]);
const flatIndexViews = Object.values(flatViewMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(flatView) =>
flatView.key === ViewKey.INDEX &&
!isDefined(flatView.deletedAt) &&
engineOwnedApplicationUniversalIdentifiers.has(
flatView.applicationUniversalIdentifier,
),
);
const { viewUpdates, viewFieldUpdates } = this.computeReownUpdates({
workspaceId,
flatIndexViews,
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
if (viewUpdates.length === 0 && viewFieldUpdates.length === 0) {
this.logger.log(
`No INDEX view to reconcile for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Reconciling ${viewUpdates.length} INDEX view(s) and ${viewFieldUpdates.length} view field(s) for workspace ${workspaceId}`,
);
if (isDryRun) {
return;
}
await this.viewRepository.manager.transaction(async (entityManager) => {
const transactionalViewRepository =
entityManager.getRepository(ViewEntity);
const transactionalViewFieldRepository =
entityManager.getRepository(ViewFieldEntity);
for (const { id, update } of viewUpdates) {
await transactionalViewRepository.update({ id, workspaceId }, update);
}
for (const { id, update } of viewFieldUpdates) {
await transactionalViewFieldRepository.update(
{ id, workspaceId },
update,
);
}
});
await invalidateIndexViewReconcileCache({
workspaceId,
workspaceMigrationRunnerService: this.workspaceMigrationRunnerService,
});
this.logger.log(
`Reconciled ${viewUpdates.length} INDEX view(s) and ${viewFieldUpdates.length} view field(s) for workspace ${workspaceId}`,
);
}
private computeReownUpdates({
workspaceId,
flatIndexViews,
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
workspaceId: string;
flatIndexViews: NonNullable<
AllFlatEntityMaps['flatViewMaps']['byUniversalIdentifier'][string]
>[];
flatViewFieldMaps: AllFlatEntityMaps['flatViewFieldMaps'];
flatObjectMetadataMaps: AllFlatEntityMaps['flatObjectMetadataMaps'];
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
}): { viewUpdates: ReownUpdate[]; viewFieldUpdates: ReownUpdate[] } {
const viewUpdates: ReownUpdate[] = [];
const viewFieldUpdates: ReownUpdate[] = [];
for (const flatView of flatIndexViews) {
const flatObjectMetadata =
flatObjectMetadataMaps.byUniversalIdentifier[
flatView.objectMetadataUniversalIdentifier
];
if (!isDefined(flatObjectMetadata)) {
this.logger.warn(
`Missing object for INDEX view ${flatView.id} in workspace ${workspaceId}, skipping`,
);
continue;
}
const derivedViewUniversalIdentifier = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier:
flatObjectMetadata.applicationUniversalIdentifier,
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
viewKey: ViewKey.INDEX,
});
const viewUpdate = this.computeViewReownUpdate({
flatView,
derivedViewUniversalIdentifier,
});
if (isDefined(viewUpdate)) {
viewUpdates.push(viewUpdate);
}
viewFieldUpdates.push(
...findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps({
flatEntityMaps: flatViewFieldMaps,
universalIdentifiers: flatView.viewFieldUniversalIdentifiers,
})
.map((flatViewField) =>
this.computeViewFieldReownUpdate({
workspaceId,
flatViewField,
derivedViewUniversalIdentifier,
flatFieldMetadataMaps,
}),
)
.filter(isDefined),
);
}
return { viewUpdates, viewFieldUpdates };
}
private computeViewReownUpdate({
flatView,
derivedViewUniversalIdentifier,
}: {
flatView: NonNullable<
AllFlatEntityMaps['flatViewMaps']['byUniversalIdentifier'][string]
>;
derivedViewUniversalIdentifier: string;
}): ReownUpdate | undefined {
const update: ReownUpdate['update'] = {};
if (flatView.universalIdentifier !== derivedViewUniversalIdentifier) {
update.universalIdentifier = derivedViewUniversalIdentifier;
}
if (!flatView.isSystemSideEffect) {
update.isSystemSideEffect = true;
}
if (Object.keys(update).length === 0) {
return undefined;
}
return { id: flatView.id, update };
}
private computeViewFieldReownUpdate({
workspaceId,
flatViewField,
derivedViewUniversalIdentifier,
flatFieldMetadataMaps,
}: {
workspaceId: string;
flatViewField: NonNullable<
AllFlatEntityMaps['flatViewFieldMaps']['byUniversalIdentifier'][string]
>;
derivedViewUniversalIdentifier: string;
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
}): ReownUpdate | undefined {
if (isDefined(flatViewField.deletedAt)) {
return undefined;
}
const flatFieldMetadata =
flatFieldMetadataMaps.byUniversalIdentifier[
flatViewField.fieldMetadataUniversalIdentifier
];
if (!isDefined(flatFieldMetadata)) {
this.logger.warn(
`Missing field for INDEX view field ${flatViewField.id} in workspace ${workspaceId}, skipping`,
);
return undefined;
}
const derivedViewFieldUniversalIdentifier = getViewFieldUniversalIdentifier(
{
applicationUniversalIdentifier:
flatFieldMetadata.applicationUniversalIdentifier,
viewUniversalIdentifier: derivedViewUniversalIdentifier,
fieldMetadataUniversalIdentifier:
flatViewField.fieldMetadataUniversalIdentifier,
},
);
const update: ReownUpdate['update'] = {};
if (
flatViewField.universalIdentifier !== derivedViewFieldUniversalIdentifier
) {
update.universalIdentifier = derivedViewFieldUniversalIdentifier;
}
if (!flatViewField.isSystemSideEffect) {
update.isSystemSideEffect = true;
}
if (Object.keys(update).length === 0) {
return undefined;
}
return { id: flatViewField.id, update };
}
}

View File

@@ -0,0 +1,411 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { invalidateIndexViewReconcileCache } from 'src/database/commands/upgrade-version-command/2-26/utils/invalidate-index-view-reconcile-cache.util';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
import { computeFlatIndexViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-index-view-to-create.util';
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
type BackfillOperationsByApplication = Map<
string,
{
viewsToCreate: UniversalFlatView[];
viewFieldsToCreate: UniversalFlatViewField[];
}
>;
@RegisteredWorkspaceCommand('2.26.0', 1785255690000)
@Command({
name: 'upgrade:2-26:demote-and-backfill-application-index-view',
description:
'Manifest-installed applications never had their INDEX view auto-provisioned: the ones that declared a view with ViewKey.INDEX authored it themselves under a manifest identifier. The engine is now the sole owner of the INDEX key, so every caller-authored (isSystemSideEffect: false) INDEX view owned by an application other than twenty-standard and workspace-custom is demoted to key: null — it becomes a plain additional view under its manifest identifier, and the flat view validator rejects caller-provided INDEX keys so a later app sync cannot promote it back. Every application object left without an engine-owned INDEX view then gets one backfilled through the workspace migration pipeline with its full view-field layout, converging upgraded installs with fresh installs. Views are committed before view fields across all applications because a view field belongs to the application owning its field, which can differ from the application owning the object (fields can be created on another application object). The backfill runs through the legacy pipeline path (no side-effect expansion: it replays a state the engine convention already defines) and is idempotent and retry-safe: engine-owned INDEX views are neither demoted nor re-backfilled, and view creation and view-field creation are gated independently, so a retry after a partial failure still backfills the missing view fields of an already-committed view.',
})
export class DemoteAndBackfillApplicationIndexViewCommand extends ProvisionedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
// eslint-disable-next-line twenty/prefer-workspace-scoped-repository
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const {
flatViewMaps,
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatViewMaps',
'flatViewFieldMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const engineOwnedApplicationUniversalIdentifiers = new Set([
twentyStandardFlatApplication.universalIdentifier,
workspaceCustomFlatApplication.universalIdentifier,
]);
const viewIdsToDemote: string[] = [];
const objectUniversalIdentifiersWithEngineIndexView = new Set<string>();
for (const flatView of Object.values(flatViewMaps.byUniversalIdentifier)) {
if (
!isDefined(flatView) ||
flatView.key !== ViewKey.INDEX ||
isDefined(flatView.deletedAt) ||
engineOwnedApplicationUniversalIdentifiers.has(
flatView.applicationUniversalIdentifier,
)
) {
continue;
}
if (flatView.isSystemSideEffect === true) {
objectUniversalIdentifiersWithEngineIndexView.add(
flatView.objectMetadataUniversalIdentifier,
);
continue;
}
viewIdsToDemote.push(flatView.id);
}
const backfillOperationsByApplication =
this.computeBackfillOperationsByApplication({
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
engineOwnedApplicationUniversalIdentifiers,
objectUniversalIdentifiersWithEngineIndexView,
});
const { backfillViewCount, backfillViewFieldCount } = [
...backfillOperationsByApplication.values(),
].reduce(
(counts, { viewsToCreate, viewFieldsToCreate }) => ({
backfillViewCount: counts.backfillViewCount + viewsToCreate.length,
backfillViewFieldCount:
counts.backfillViewFieldCount + viewFieldsToCreate.length,
}),
{ backfillViewCount: 0, backfillViewFieldCount: 0 },
);
if (
viewIdsToDemote.length === 0 &&
backfillViewCount === 0 &&
backfillViewFieldCount === 0
) {
this.logger.log(
`No application INDEX view to demote or backfill for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Demoting ${viewIdsToDemote.length} application INDEX view(s) and backfilling ${backfillViewCount} engine INDEX view(s) with ${backfillViewFieldCount} view field(s) for workspace ${workspaceId}`,
);
if (isDryRun) {
return;
}
if (viewIdsToDemote.length > 0) {
// Single bulk statement, atomic on its own: every row gets the same
// patch.
await this.viewRepository.update(
{ id: In(viewIdsToDemote), workspaceId },
{ key: null },
);
await invalidateIndexViewReconcileCache({
workspaceId,
workspaceMigrationRunnerService: this.workspaceMigrationRunnerService,
});
}
await this.runBackfillMigrations({
workspaceId,
backfillOperationsByApplication,
});
this.logger.log(
`Demoted ${viewIdsToDemote.length} application INDEX view(s) and backfilled ${backfillViewCount} engine INDEX view(s) with ${backfillViewFieldCount} view field(s) for workspace ${workspaceId}`,
);
}
private computeBackfillOperationsByApplication({
flatViewFieldMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
engineOwnedApplicationUniversalIdentifiers,
objectUniversalIdentifiersWithEngineIndexView,
}: {
flatViewFieldMaps: AllFlatEntityMaps['flatViewFieldMaps'];
flatObjectMetadataMaps: AllFlatEntityMaps['flatObjectMetadataMaps'];
flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps'];
engineOwnedApplicationUniversalIdentifiers: Set<string>;
objectUniversalIdentifiersWithEngineIndexView: Set<string>;
}): BackfillOperationsByApplication {
const backfillOperationsByApplication: BackfillOperationsByApplication =
new Map();
const getApplicationBucket = (applicationUniversalIdentifier: string) => {
const existingBucket = backfillOperationsByApplication.get(
applicationUniversalIdentifier,
);
if (isDefined(existingBucket)) {
return existingBucket;
}
const newBucket = { viewsToCreate: [], viewFieldsToCreate: [] };
backfillOperationsByApplication.set(
applicationUniversalIdentifier,
newBucket,
);
return newBucket;
};
for (const flatObjectMetadata of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (
!isDefined(flatObjectMetadata) ||
engineOwnedApplicationUniversalIdentifiers.has(
flatObjectMetadata.applicationUniversalIdentifier,
)
) {
continue;
}
const indexViewUniversalIdentifier = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier:
flatObjectMetadata.applicationUniversalIdentifier,
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
viewKey: ViewKey.INDEX,
});
if (
!objectUniversalIdentifiersWithEngineIndexView.has(
flatObjectMetadata.universalIdentifier,
)
) {
// computeFlatIndexViewToCreate derives the same universal identifier.
getApplicationBucket(
flatObjectMetadata.applicationUniversalIdentifier,
).viewsToCreate.push(
computeFlatIndexViewToCreate({
objectMetadata: flatObjectMetadata,
applicationUniversalIdentifier:
flatObjectMetadata.applicationUniversalIdentifier,
}),
);
}
const { labelIdentifierFieldMetadataUniversalIdentifier } =
flatObjectMetadata;
const displayableFlatFieldMetadatas =
flatObjectMetadata.fieldUniversalIdentifiers
.map(
(fieldUniversalIdentifier) =>
flatFieldMetadataMaps.byUniversalIdentifier[
fieldUniversalIdentifier
],
)
.filter(isDefined)
.filter((flatFieldMetadata) =>
isFlatFieldMetadataDisplayableInDefaultView({
flatFieldMetadata,
labelIdentifierFieldMetadataUniversalIdentifier,
}),
);
// The label identifier view field must be strictly lowest and visible.
const orderedDisplayableFlatFieldMetadatas =
orderFlatFieldMetadatasForSystemIndexView({
labelIdentifierFieldMetadataUniversalIdentifier,
flatFieldMetadatas: displayableFlatFieldMetadatas,
});
const createdAt = new Date().toISOString();
orderedDisplayableFlatFieldMetadatas.forEach(
(flatFieldMetadata, position) => {
const fieldApplicationUniversalIdentifier =
flatFieldMetadata.applicationUniversalIdentifier;
const viewFieldUniversalIdentifier = getViewFieldUniversalIdentifier({
applicationUniversalIdentifier: fieldApplicationUniversalIdentifier,
viewUniversalIdentifier: indexViewUniversalIdentifier,
fieldMetadataUniversalIdentifier:
flatFieldMetadata.universalIdentifier,
});
// Already backfilled by a previous (partially failed) run.
if (
isDefined(
flatViewFieldMaps.byUniversalIdentifier[
viewFieldUniversalIdentifier
],
)
) {
return;
}
getApplicationBucket(
fieldApplicationUniversalIdentifier,
).viewFieldsToCreate.push({
universalIdentifier: viewFieldUniversalIdentifier,
applicationUniversalIdentifier: fieldApplicationUniversalIdentifier,
fieldMetadataUniversalIdentifier:
flatFieldMetadata.universalIdentifier,
viewUniversalIdentifier: indexViewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
isVisible: true,
size: DEFAULT_VIEW_FIELD_SIZE,
position,
aggregateOperation: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
});
},
);
}
return backfillOperationsByApplication;
}
private async runBackfillMigrations({
workspaceId,
backfillOperationsByApplication,
}: {
workspaceId: string;
backfillOperationsByApplication: BackfillOperationsByApplication;
}): Promise<void> {
for (const [
applicationUniversalIdentifier,
{ viewsToCreate },
] of backfillOperationsByApplication.entries()) {
if (viewsToCreate.length === 0) {
continue;
}
await this.runBackfillMigration({
workspaceId,
applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
});
}
for (const [
applicationUniversalIdentifier,
{ viewFieldsToCreate },
] of backfillOperationsByApplication.entries()) {
if (viewFieldsToCreate.length === 0) {
continue;
}
await this.runBackfillMigration({
workspaceId,
applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName: {
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
});
}
}
private async runBackfillMigration({
workspaceId,
applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
allFlatEntityOperationByMetadataName: Record<
string,
{
flatEntityToCreate: unknown[];
flatEntityToDelete: never[];
flatEntityToUpdate: never[];
}
>;
}): Promise<void> {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName:
allFlatEntityOperationByMetadataName as never,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to backfill engine INDEX view(s) for application ${applicationUniversalIdentifier} in workspace ${workspaceId}:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to backfill engine INDEX view(s) for workspace ${workspaceId}`,
);
}
}
}

View File

@@ -0,0 +1,490 @@
import {
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { type WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { ReconcileIndexViewUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255689000-reconcile-index-view-universal-identifier.command';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
const WORKSPACE_ID = '20202020-0000-4000-8000-000000000001';
const STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000ad';
const CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000aa';
const EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000ae';
const STANDARD_OBJECT_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000bb';
const CUSTOM_OBJECT_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000bc';
const FIELD_UNIVERSAL_IDENTIFIER = '20202020-0000-4000-8000-0000000000cc';
const DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER =
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
const DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
getViewFieldUniversalIdentifier({
applicationUniversalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
});
const STANDARD_OBJECT_METADATA = {
universalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
};
// A custom object: its INDEX view was historically created with a random v4
// universal identifier by ObjectMetadataService.createOneObject.
const CUSTOM_OBJECT_METADATA = {
universalIdentifier: CUSTOM_OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
};
type WorkspaceView = {
id: string;
universalIdentifier: string;
key: ViewKey | null;
applicationUniversalIdentifier?: string;
deletedAt?: string | null;
isSystemSideEffect?: boolean;
objectMetadataUniversalIdentifier?: string;
viewFieldUniversalIdentifiers?: string[];
};
type WorkspaceViewField = {
id: string;
universalIdentifier: string;
applicationUniversalIdentifier?: string;
fieldMetadataUniversalIdentifier?: string;
deletedAt?: string | null;
isSystemSideEffect?: boolean;
};
const buildFlatView = (view: WorkspaceView) => ({
applicationUniversalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
deletedAt: null,
isSystemSideEffect: false,
objectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIER,
viewFieldUniversalIdentifiers: [],
...view,
});
const buildFlatViewField = (viewField: WorkspaceViewField) => ({
applicationUniversalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
deletedAt: null,
isSystemSideEffect: false,
...viewField,
});
const buildFlatFieldMetadata = ({
applicationUniversalIdentifier = STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
}: {
applicationUniversalIdentifier?: string;
} = {}) => ({
universalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier,
});
const buildByUniversalIdentifierMap = <
T extends { universalIdentifier: string },
>(
flatEntities: T[],
) => ({
byUniversalIdentifier: Object.fromEntries(
flatEntities.map((flatEntity) => [
flatEntity.universalIdentifier,
flatEntity,
]),
),
});
describe('ReconcileIndexViewUniversalIdentifierCommand', () => {
let command: ReconcileIndexViewUniversalIdentifierCommand;
let getOrRecomputeMock: jest.Mock;
let invalidateCacheMock: jest.Mock;
let viewUpdateMock: jest.Mock;
let viewFieldUpdateMock: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
getOrRecomputeMock = jest.fn();
invalidateCacheMock = jest.fn().mockResolvedValue(undefined);
viewUpdateMock = jest.fn().mockResolvedValue(undefined);
viewFieldUpdateMock = jest.fn().mockResolvedValue(undefined);
const entityManagerMock = {
getRepository: (entity: unknown) => {
if (entity === ViewEntity) {
return { update: viewUpdateMock };
}
if (entity === ViewFieldEntity) {
return { update: viewFieldUpdateMock };
}
throw new Error('Unexpected repository');
},
};
const viewRepositoryMock = {
manager: {
transaction: jest.fn(
async (callback: (entityManager: unknown) => Promise<void>) =>
callback(entityManagerMock),
),
},
};
command = new ReconcileIndexViewUniversalIdentifierCommand(
{} as WorkspaceIteratorService,
{
getOrRecompute: getOrRecomputeMock,
} as unknown as WorkspaceCacheService,
{
invalidateCache: invalidateCacheMock,
} as unknown as WorkspaceMigrationRunnerService,
{
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
.fn()
.mockResolvedValue({
twentyStandardFlatApplication: {
universalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
},
workspaceCustomFlatApplication: {
universalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
},
}),
} as unknown as ApplicationService,
viewRepositoryMock as never,
);
});
const mockWorkspaceCache = ({
views,
viewFields = [],
fieldMetadatas = [buildFlatFieldMetadata()],
}: {
views: ReturnType<typeof buildFlatView>[];
viewFields?: ReturnType<typeof buildFlatViewField>[];
fieldMetadatas?: ReturnType<typeof buildFlatFieldMetadata>[];
}) => {
getOrRecomputeMock.mockResolvedValue({
flatViewMaps: buildByUniversalIdentifierMap(views),
flatViewFieldMaps: buildByUniversalIdentifierMap(viewFields),
flatObjectMetadataMaps: buildByUniversalIdentifierMap([
STANDARD_OBJECT_METADATA,
CUSTOM_OBJECT_METADATA,
]),
flatFieldMetadataMaps: buildByUniversalIdentifierMap(fieldMetadatas),
});
};
const runOnWorkspace = (dryRun = false) =>
command.runOnWorkspace({
workspaceId: WORKSPACE_ID,
options: { dryRun },
index: 0,
total: 1,
});
it('re-owns an underived standard INDEX view and its view fields', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: 'legacy-view-uid',
key: ViewKey.INDEX,
viewFieldUniversalIdentifiers: ['legacy-view-field-uid'],
}),
],
viewFields: [
buildFlatViewField({
id: 'view-field-id',
universalIdentifier: 'legacy-view-field-uid',
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).toHaveBeenCalledTimes(1);
expect(viewUpdateMock).toHaveBeenCalledWith(
{ id: 'view-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
);
expect(viewFieldUpdateMock).toHaveBeenCalledTimes(1);
expect(viewFieldUpdateMock).toHaveBeenCalledWith(
{ id: 'view-field-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
);
expect(invalidateCacheMock).toHaveBeenCalledTimes(1);
expect(invalidateCacheMock.mock.calls[0][0].allFlatEntityMapsKeys).toEqual(
expect.arrayContaining([
'flatViewMaps',
'flatViewFieldMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutWidgetMaps',
]),
);
});
it('re-owns a custom object INDEX view onto the workspace-custom derivation', async () => {
const derivedCustomViewUniversalIdentifier =
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: CUSTOM_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
mockWorkspaceCache({
views: [
buildFlatView({
id: 'custom-view-id',
universalIdentifier: 'legacy-v4-view-uid',
key: ViewKey.INDEX,
applicationUniversalIdentifier:
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
objectMetadataUniversalIdentifier: CUSTOM_OBJECT_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).toHaveBeenCalledTimes(1);
expect(viewUpdateMock).toHaveBeenCalledWith(
{ id: 'custom-view-id', workspaceId: WORKSPACE_ID },
{ universalIdentifier: derivedCustomViewUniversalIdentifier },
);
});
it('ignores INDEX views owned by other applications', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'external-view-id',
universalIdentifier: 'external-view-uid',
key: ViewKey.INDEX,
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
});
it('skips a soft-deleted INDEX view coexisting with an active one on the same object', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'soft-deleted-view-id',
universalIdentifier: 'soft-deleted-view-uid',
key: ViewKey.INDEX,
deletedAt: '2024-01-01T00:00:00.000Z',
}),
buildFlatView({
id: 'active-view-id',
universalIdentifier: 'active-view-uid',
key: ViewKey.INDEX,
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).toHaveBeenCalledTimes(1);
expect(viewUpdateMock).toHaveBeenCalledWith(
{ id: 'active-view-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
);
});
it('re-owns only the active row of a soft-deleted + re-created view field pair', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: 'legacy-view-uid',
key: ViewKey.INDEX,
viewFieldUniversalIdentifiers: [
'soft-deleted-view-field-uid',
'active-view-field-uid',
],
}),
],
viewFields: [
buildFlatViewField({
id: 'soft-deleted-view-field-id',
universalIdentifier: 'soft-deleted-view-field-uid',
deletedAt: '2024-01-01T00:00:00.000Z',
}),
buildFlatViewField({
id: 'active-view-field-id',
universalIdentifier: 'active-view-field-uid',
}),
],
});
await runOnWorkspace();
expect(viewFieldUpdateMock).toHaveBeenCalledTimes(1);
expect(viewFieldUpdateMock).toHaveBeenCalledWith(
{ id: 'active-view-field-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
);
});
it('re-owns an external application view field living on a standard INDEX view', async () => {
// e.g. an app that declared explicit columns on a standard INDEX view:
// those must converge on the derived scheme so manifest deletion inference
// never drops them once the app stops declaring them.
const derivedExternalViewFieldUniversalIdentifier =
getViewFieldUniversalIdentifier({
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
});
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: 'legacy-view-uid',
key: ViewKey.INDEX,
viewFieldUniversalIdentifiers: ['external-app-view-field-uid'],
}),
],
viewFields: [
buildFlatViewField({
id: 'external-app-view-field-id',
universalIdentifier: 'external-app-view-field-uid',
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
}),
],
fieldMetadatas: [
buildFlatFieldMetadata({
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
}),
],
});
await runOnWorkspace();
expect(viewFieldUpdateMock).toHaveBeenCalledWith(
{ id: 'external-app-view-field-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: derivedExternalViewFieldUniversalIdentifier,
isSystemSideEffect: true,
},
);
});
it('derives a view field from the displayed field application, not the row attribution', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: 'legacy-view-uid',
key: ViewKey.INDEX,
viewFieldUniversalIdentifiers: ['user-shown-view-field-uid'],
}),
],
viewFields: [
buildFlatViewField({
id: 'user-shown-view-field-id',
universalIdentifier: 'user-shown-view-field-uid',
applicationUniversalIdentifier:
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
}),
],
});
await runOnWorkspace();
expect(viewFieldUpdateMock).toHaveBeenCalledWith(
{ id: 'user-shown-view-field-id', workspaceId: WORKSPACE_ID },
{
universalIdentifier: DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
);
});
it('does nothing when the INDEX view and its view fields are already derived and system-owned', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: DERIVED_STANDARD_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
viewFieldUniversalIdentifiers: [
DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
],
}),
],
viewFields: [
buildFlatViewField({
id: 'view-field-id',
universalIdentifier: DERIVED_STANDARD_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(viewFieldUpdateMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
});
it('does not write in dry-run mode', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'view-id',
universalIdentifier: 'legacy-view-uid',
key: ViewKey.INDEX,
}),
],
});
await runOnWorkspace(true);
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(viewFieldUpdateMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,314 @@
import {
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
import { In } from 'typeorm';
import { type WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { DemoteAndBackfillApplicationIndexViewCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255690000-demote-and-backfill-application-index-view.command';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
const WORKSPACE_ID = '20202020-0000-4000-8000-000000000001';
const STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000ad';
const CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000aa';
const EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-0000-4000-8000-0000000000ae';
const OBJECT_UNIVERSAL_IDENTIFIER = '20202020-0000-4000-8000-0000000000bb';
const FIELD_UNIVERSAL_IDENTIFIER = '20202020-0000-4000-8000-0000000000cc';
const DERIVED_VIEW_UNIVERSAL_IDENTIFIER = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
// An external application object with one displayable field.
const OBJECT_METADATA = {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifiers: [FIELD_UNIVERSAL_IDENTIFIER],
};
const FIELD_METADATA = {
universalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
name: 'name',
type: FieldMetadataType.TEXT,
};
const buildByUniversalIdentifierMap = <
T extends { universalIdentifier: string },
>(
flatEntities: T[],
) => ({
byUniversalIdentifier: Object.fromEntries(
flatEntities.map((flatEntity) => [
flatEntity.universalIdentifier,
flatEntity,
]),
),
});
type WorkspaceView = {
id: string;
universalIdentifier: string;
key: ViewKey | null;
applicationUniversalIdentifier?: string;
deletedAt?: string | null;
isSystemSideEffect?: boolean;
objectMetadataUniversalIdentifier?: string;
};
const buildFlatView = (view: WorkspaceView) => ({
applicationUniversalIdentifier: EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
deletedAt: null,
isSystemSideEffect: false,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
...view,
});
describe('DemoteAndBackfillApplicationIndexViewCommand', () => {
let command: DemoteAndBackfillApplicationIndexViewCommand;
let getOrRecomputeMock: jest.Mock;
let invalidateCacheMock: jest.Mock;
let viewUpdateMock: jest.Mock;
let validateBuildAndRunWorkspaceMigrationMock: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
getOrRecomputeMock = jest.fn();
invalidateCacheMock = jest.fn().mockResolvedValue(undefined);
viewUpdateMock = jest.fn().mockResolvedValue(undefined);
validateBuildAndRunWorkspaceMigrationMock = jest
.fn()
.mockResolvedValue({ status: 'success' });
const viewRepositoryMock = {
update: viewUpdateMock,
};
command = new DemoteAndBackfillApplicationIndexViewCommand(
{} as WorkspaceIteratorService,
{
getOrRecompute: getOrRecomputeMock,
} as unknown as WorkspaceCacheService,
{
invalidateCache: invalidateCacheMock,
} as unknown as WorkspaceMigrationRunnerService,
{
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
.fn()
.mockResolvedValue({
twentyStandardFlatApplication: {
universalIdentifier: STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
},
workspaceCustomFlatApplication: {
universalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
},
}),
} as unknown as ApplicationService,
{
validateBuildAndRunLegacyWorkspaceMigration:
validateBuildAndRunWorkspaceMigrationMock,
} as unknown as WorkspaceMigrationValidateBuildAndRunService,
viewRepositoryMock as never,
);
});
const mockWorkspaceCache = ({
views = [],
viewFields = [],
}: {
views?: ReturnType<typeof buildFlatView>[];
viewFields?: { universalIdentifier: string }[];
}) => {
getOrRecomputeMock.mockResolvedValue({
flatViewMaps: buildByUniversalIdentifierMap(views),
flatViewFieldMaps: buildByUniversalIdentifierMap(viewFields),
flatObjectMetadataMaps: buildByUniversalIdentifierMap([OBJECT_METADATA]),
flatFieldMetadataMaps: buildByUniversalIdentifierMap([FIELD_METADATA]),
});
};
const runOnWorkspace = (dryRun = false) =>
command.runOnWorkspace({
workspaceId: WORKSPACE_ID,
options: { dryRun },
index: 0,
total: 1,
});
it('demotes the application INDEX view and backfills the engine-owned one', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'app-view-id',
universalIdentifier: 'app-view-uid',
key: ViewKey.INDEX,
}),
],
});
await runOnWorkspace();
// The app view becomes a plain additional view under its manifest
// identifier.
expect(viewUpdateMock).toHaveBeenCalledTimes(1);
expect(viewUpdateMock).toHaveBeenCalledWith(
{ id: In(['app-view-id']), workspaceId: WORKSPACE_ID },
{ key: null },
);
// Demotions are visible to the backfill pipeline validators.
expect(invalidateCacheMock).toHaveBeenCalledTimes(1);
// Two-phase: every application's views are committed before any view
// field, so cross-application view fields always find their parent view.
expect(validateBuildAndRunWorkspaceMigrationMock).toHaveBeenCalledTimes(2);
const [viewPayload, viewFieldPayload] =
validateBuildAndRunWorkspaceMigrationMock.mock.calls.map(
([payload]) => payload,
);
expect(viewPayload.applicationUniversalIdentifier).toBe(
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(
viewPayload.allFlatEntityOperationByMetadataName.view.flatEntityToCreate,
).toEqual([
expect.objectContaining({
universalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
]);
expect(
viewPayload.allFlatEntityOperationByMetadataName.viewField,
).toBeUndefined();
expect(viewFieldPayload.applicationUniversalIdentifier).toBe(
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(
viewFieldPayload.allFlatEntityOperationByMetadataName.view,
).toBeUndefined();
expect(
viewFieldPayload.allFlatEntityOperationByMetadataName.viewField
.flatEntityToCreate,
).toEqual([
expect.objectContaining({
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
}),
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
isVisible: true,
position: 0,
isSystemSideEffect: true,
}),
]);
});
it('backfills an application object that never had an INDEX view', async () => {
mockWorkspaceCache({ views: [] });
await runOnWorkspace();
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).toHaveBeenCalledTimes(2);
});
it('is idempotent: a fully backfilled object is neither demoted nor re-backfilled', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'engine-view-id',
universalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
],
viewFields: [
{
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
}),
},
],
});
await runOnWorkspace();
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
});
it('backfills the missing view fields of an already-committed engine INDEX view', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'engine-view-id',
universalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
],
});
await runOnWorkspace();
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).toHaveBeenCalledTimes(1);
const [payload] = validateBuildAndRunWorkspaceMigrationMock.mock.calls[0];
expect(payload.allFlatEntityOperationByMetadataName.view).toBeUndefined();
expect(
payload.allFlatEntityOperationByMetadataName.viewField.flatEntityToCreate,
).toEqual([
expect.objectContaining({
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier:
EXTERNAL_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: FIELD_UNIVERSAL_IDENTIFIER,
}),
viewUniversalIdentifier: DERIVED_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
}),
]);
});
it('does not write in dry-run mode', async () => {
mockWorkspaceCache({
views: [
buildFlatView({
id: 'app-view-id',
universalIdentifier: 'app-view-uid',
key: ViewKey.INDEX,
}),
],
});
await runOnWorkspace(true);
expect(viewUpdateMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
expect(invalidateCacheMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,28 @@
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { type WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
export const invalidateIndexViewReconcileCache = async ({
workspaceId,
workspaceMigrationRunnerService,
}: {
workspaceId: string;
workspaceMigrationRunnerService: WorkspaceMigrationRunnerService;
}): Promise<void> => {
const reconciledMetadataRelatedNames = [
'view',
...getMetadataRelatedMetadataNames('view'),
'viewField',
...getMetadataRelatedMetadataNames('viewField'),
'pageLayoutWidget',
] as const;
await workspaceMigrationRunnerService.invalidateCache({
allFlatEntityMapsKeys: [
...new Set(
reconciledMetadataRelatedNames.map(getMetadataFlatEntityMapsKey),
),
],
workspaceId,
});
};

View File

@@ -25,6 +25,7 @@ import { V2_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
import { V2_22_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-22/2-22-upgrade-version-command.module';
import { V2_23_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-23/2-23-upgrade-version-command.module';
import { V2_25_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-25/2-25-upgrade-version-command.module';
import { V2_26_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-26/2-26-upgrade-version-command.module';
@Module({
imports: [
@@ -53,6 +54,7 @@ import { V2_25_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
V2_22_UpgradeVersionCommandModule,
V2_23_UpgradeVersionCommandModule,
V2_25_UpgradeVersionCommandModule,
V2_26_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}

View File

@@ -1,10 +1,10 @@
import {
AggregateOperations,
ViewCalendarLayout,
ViewKey,
ViewOpenRecordIn,
ViewType,
ViewVisibility,
ViewKey,
} from 'twenty-shared/types';
import { fromViewManifestToUniversalFlatView } from 'src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util';
@@ -39,7 +39,7 @@ describe('fromViewManifestToUniversalFlatView', () => {
expect(result.isCustom).toBe(true);
expect(result.visibility).toBe(ViewVisibility.WORKSPACE);
expect(result.openRecordIn).toBe(ViewOpenRecordIn.SIDE_PANEL);
expect(result.key).toBe(ViewKey.INDEX);
expect(result.key).toBeNull();
expect(result.createdAt).toBe(now);
expect(result.updatedAt).toBe(now);
});

View File

@@ -28,7 +28,7 @@ export const fromViewManifestToUniversalFlatView = ({
isCustom: true,
visibility: viewManifest.visibility ?? ViewVisibility.WORKSPACE,
openRecordIn: viewManifest.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL,
key: viewManifest.key ?? null,
key: null,
kanbanAggregateOperation: viewManifest.kanbanAggregateOperation ?? null,
kanbanAggregateOperationFieldMetadataUniversalIdentifier:
viewManifest.kanbanAggregateOperationFieldMetadataUniversalIdentifier ??

View File

@@ -330,7 +330,6 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
},
"view": {
"propertiesToCompare": [
"key",
"deletedAt",
"createdByUserWorkspaceId",
"name",

View File

@@ -303,7 +303,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
universalProperty: undefined,
},
key: { toCompare: true, toStringify: false, universalProperty: undefined },
key: { toCompare: false, toStringify: false, universalProperty: undefined },
deletedAt: {
toCompare: true,
toStringify: false,

View File

@@ -1,8 +1,14 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
export const ALL_METADATA_SIDE_EFFECT_COMPANION_METADATA_NAMES = {
fieldMetadata: ['index', 'searchFieldMetadata'],
objectMetadata: ['fieldMetadata', 'index', 'searchFieldMetadata'],
fieldMetadata: ['index', 'searchFieldMetadata', 'view', 'viewField'],
objectMetadata: [
'fieldMetadata',
'index',
'searchFieldMetadata',
'view',
'viewField',
],
} as const satisfies Partial<
Record<AllMetadataName, readonly AllMetadataName[]>
>;

View File

@@ -790,8 +790,8 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
});
});
describe('INDEX view propagation', () => {
it('should add a hidden, flagged view field to the object INDEX view even without a fields widget', () => {
describe('INDEX view non-propagation', () => {
it('should not target the object INDEX view without a fields widget', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
@@ -817,81 +817,10 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
viewUniversalIdentifier: 'index-view-uid',
fieldMetadataUniversalIdentifier: 'field-uid-1',
isVisible: false,
isSystemSideEffect: true,
});
});
it('should not target a standalone non-INDEX view without a widget', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: 'field-uid-1',
},
],
flatPageLayoutWidgetMaps:
buildEmptyFlatEntityMaps() as unknown as FlatPageLayoutWidgetMaps,
flatViewFieldMaps: buildFlatViewFieldMaps(),
flatViewMaps: buildFlatViewMaps([
{
id: 'plain-view-db-id',
universalIdentifier: 'plain-view-uid',
key: null,
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toEqual([]);
});
it('should skip an inactive or soft-deleted INDEX view', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: 'field-uid-1',
},
],
flatPageLayoutWidgetMaps:
buildEmptyFlatEntityMaps() as unknown as FlatPageLayoutWidgetMaps,
flatViewFieldMaps: buildFlatViewFieldMaps(),
flatViewMaps: buildFlatViewMaps([
{
id: 'inactive-index-view-db-id',
universalIdentifier: 'inactive-index-view-uid',
key: ViewKey.INDEX,
isActive: false,
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
},
{
id: 'deleted-index-view-db-id',
universalIdentifier: 'deleted-index-view-uid',
key: ViewKey.INDEX,
deletedAt: '2024-01-01T00:00:00.000Z',
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toEqual([]);
});
it('should not duplicate a field when a fields widget and the INDEX view share the same view', () => {
it('should still honor a fields widget explicitly targeting the INDEX view', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
@@ -919,6 +848,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
expect(result).toHaveLength(1);
expect(result[0].isVisible).toBe(true);
expect(result[0].viewUniversalIdentifier).toBe('index-view-uid');
});
});

View File

@@ -1,4 +1,3 @@
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -33,14 +32,13 @@ const isFieldsWidgetConfiguration = (
);
};
// TODO remove when implementing https://github.com/twentyhq/core-team-issues/issues/2721
const getFieldViewTargets = ({
objectMetadataUniversalIdentifier,
flatPageLayoutWidgetMaps,
flatViewMaps,
}: {
objectMetadataUniversalIdentifier: string;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
flatViewMaps: FlatViewMaps;
}): FieldViewTarget[] => {
const targets: FieldViewTarget[] = [];
const seenViewIds = new Set<string>();
@@ -72,24 +70,6 @@ const getFieldViewTargets = ({
targets.push({ viewId, isVisible: newFieldDefaultVisibility });
}
for (const view of Object.values(flatViewMaps.byUniversalIdentifier).filter(
isDefined,
)) {
if (
view.key !== ViewKey.INDEX ||
!view.isActive ||
isDefined(view.deletedAt) ||
view.objectMetadataUniversalIdentifier !==
objectMetadataUniversalIdentifier ||
seenViewIds.has(view.id)
) {
continue;
}
seenViewIds.add(view.id);
targets.push({ viewId: view.id, isVisible: false });
}
return targets;
};
@@ -181,7 +161,6 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
const targets = getFieldViewTargets({
objectMetadataUniversalIdentifier,
flatPageLayoutWidgetMaps,
flatViewMaps,
});
const fieldsForObject = fieldsToCreate.filter(

View File

@@ -0,0 +1,680 @@
import {
getFieldUniversalIdentifier,
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
import { FieldIndexViewFieldOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const computeFieldUniversalIdentifier = (name: string) =>
getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name,
});
const NAME_FIELD_UNIVERSAL_IDENTIFIER = computeFieldUniversalIdentifier('name');
const PRIORITY_FIELD_UNIVERSAL_IDENTIFIER =
computeFieldUniversalIdentifier('priority');
const DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER =
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
const computeViewFieldUniversalIdentifier = ({
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier,
}: {
viewUniversalIdentifier: string;
fieldMetadataUniversalIdentifier: string;
}) =>
getViewFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier,
});
type PendingFieldMetadata = {
universalIdentifier: string;
objectMetadataUniversalIdentifier: string;
applicationUniversalIdentifier: string;
name: string;
type: FieldMetadataType;
isSystemSideEffect: boolean;
};
const buildPendingFieldMetadata = (
name: string,
type: FieldMetadataType = FieldMetadataType.TEXT,
isSystemSideEffect = false,
): PendingFieldMetadata => ({
universalIdentifier: computeFieldUniversalIdentifier(name),
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
name,
type,
isSystemSideEffect,
});
const NAME_FIELD = buildPendingFieldMetadata('name');
const PRIORITY_FIELD = buildPendingFieldMetadata(
'priority',
FieldMetadataType.NUMBER,
);
const OBJECT_METADATA = {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
nameSingular: 'ticket',
labelIdentifierFieldMetadataUniversalIdentifier:
NAME_FIELD_UNIVERSAL_IDENTIFIER,
};
type WorkspaceView = {
id: string;
universalIdentifier: string;
key: ViewKey | null;
isActive?: boolean;
deletedAt?: string | null;
isSystemSideEffect?: boolean;
objectMetadataUniversalIdentifier?: string;
};
type WorkspaceViewField = {
universalIdentifier: string;
viewId: string;
viewUniversalIdentifier?: string;
fieldMetadataUniversalIdentifier?: string;
position: number;
isActive: boolean;
deletedAt?: string | null;
};
const buildArgs = ({
triggerFieldMetadata,
pendingFieldMetadatas = [],
objectMetadataCreatedInBatch = false,
pendingViews = [],
pendingViewFields = [],
objectMetadataInWorkspace = false,
viewsInWorkspace = [],
viewFieldsInWorkspace = [],
}: {
triggerFieldMetadata: PendingFieldMetadata;
pendingFieldMetadatas?: PendingFieldMetadata[];
objectMetadataCreatedInBatch?: boolean;
pendingViews?: { universalIdentifier: string; isSystemSideEffect: boolean }[];
pendingViewFields?: {
universalIdentifier: string;
viewUniversalIdentifier: string;
fieldMetadataUniversalIdentifier: string;
}[];
objectMetadataInWorkspace?: boolean;
viewsInWorkspace?: WorkspaceView[];
viewFieldsInWorkspace?: WorkspaceViewField[];
}): BuildSideEffectsArgs<'fieldMetadata'> =>
({
flatEntity: triggerFieldMetadata,
allFlatEntityOperationRecordByMetadataName: {
fieldMetadata: {
flatEntityToCreate: Object.fromEntries(
pendingFieldMetadatas.map((pendingFieldMetadata) => [
pendingFieldMetadata.universalIdentifier,
pendingFieldMetadata,
]),
),
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
objectMetadata: {
flatEntityToCreate: objectMetadataCreatedInBatch
? { [OBJECT_UNIVERSAL_IDENTIFIER]: OBJECT_METADATA }
: {},
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
view: {
flatEntityToCreate: Object.fromEntries(
pendingViews.map((pendingView) => [
pendingView.universalIdentifier,
pendingView,
]),
),
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
viewField: {
flatEntityToCreate: Object.fromEntries(
pendingViewFields.map((pendingViewField) => [
pendingViewField.universalIdentifier,
pendingViewField,
]),
),
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
} as unknown as AllFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps: {
flatObjectMetadataMaps: {
byUniversalIdentifier: objectMetadataInWorkspace
? { [OBJECT_UNIVERSAL_IDENTIFIER]: OBJECT_METADATA }
: {},
},
flatViewMaps: {
byUniversalIdentifier: Object.fromEntries(
viewsInWorkspace.map((view) => [
view.universalIdentifier,
{
isActive: true,
deletedAt: null,
isSystemSideEffect: false,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
...view,
// Mirrors fromViewEntityToFlatView: the view aggregates the
// view fields pointing at it.
viewFieldUniversalIdentifiers: viewFieldsInWorkspace
.filter((viewField) => viewField.viewId === view.id)
.map((viewField) => viewField.universalIdentifier),
},
]),
),
},
flatViewFieldMaps: {
byUniversalIdentifier: Object.fromEntries(
viewFieldsInWorkspace.map((viewField) => [
viewField.universalIdentifier,
viewField,
]),
),
},
},
context: {},
}) as unknown as BuildSideEffectsArgs<'fieldMetadata'>;
// Synced INDEX view: the 2-26 reconcile command re-owns every INDEX view to the
// derived identifier, so the handler resolves it by that identifier alone.
const SYNCED_INDEX_VIEW: WorkspaceView = {
id: 'view-db-id-1',
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
};
describe('FieldIndexViewFieldOnCreateSideEffectHandlerService', () => {
const handler =
new (FieldIndexViewFieldOnCreateSideEffectHandlerService as unknown as new () => FieldIndexViewFieldOnCreateSideEffectHandlerService)();
describe('object created in the same batch (default view assembly)', () => {
it('should emit a visible view field at position 0 for the label identifier', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD, PRIORITY_FIELD],
objectMetadataCreatedInBatch: true,
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].universalIdentifier).toBe(
computeViewFieldUniversalIdentifier({
viewUniversalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
}),
);
expect(viewFields[0].viewUniversalIdentifier).toBe(
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
);
expect(viewFields[0].position).toBe(0);
expect(viewFields[0].isVisible).toBe(true);
expect(viewFields[0].isSystemSideEffect).toBe(true);
});
it('should position a non-label caller field after the label identifier', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [NAME_FIELD, PRIORITY_FIELD],
objectMetadataCreatedInBatch: true,
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].position).toBe(1);
});
it('should still emit when the pending INDEX view is a system side effect (object handler emission)', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD],
objectMetadataCreatedInBatch: true,
pendingViews: [
{
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
],
}),
);
expect(result.status).toBe('success');
});
// The engine owns the INDEX view, so the handler no longer defers to a
// caller-provided one: it emits and lets the engine collision on the view
// surface the conflict.
it('should still emit when the caller provides the pending INDEX view (no override)', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD],
objectMetadataCreatedInBatch: true,
pendingViews: [
{
universalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
],
}),
);
expect(result.status).toBe('success');
});
it('should noop for a non-displayable field (relation) at object creation', () => {
const relationField = buildPendingFieldMetadata(
'assignee',
FieldMetadataType.RELATION,
);
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: relationField,
pendingFieldMetadatas: [NAME_FIELD, relationField],
objectMetadataCreatedInBatch: true,
}),
);
expect(result.status).toBe('noop');
});
});
describe('field created on an existing object (historical createOneField behavior)', () => {
it('should append a hidden view field to the INDEX view resolved by its derived identifier', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
viewFieldsInWorkspace: [
{
universalIdentifier: 'existing-vf-1',
viewId: SYNCED_INDEX_VIEW.id,
position: 0,
isActive: true,
},
{
universalIdentifier: 'existing-vf-2',
viewId: SYNCED_INDEX_VIEW.id,
position: 4,
isActive: true,
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].universalIdentifier).toBe(
computeViewFieldUniversalIdentifier({
viewUniversalIdentifier: SYNCED_INDEX_VIEW.universalIdentifier,
fieldMetadataUniversalIdentifier: PRIORITY_FIELD_UNIVERSAL_IDENTIFIER,
}),
);
expect(viewFields[0].viewUniversalIdentifier).toBe(
SYNCED_INDEX_VIEW.universalIdentifier,
);
expect(viewFields[0].position).toBe(5);
expect(viewFields[0].isVisible).toBe(false);
expect(viewFields[0].isSystemSideEffect).toBe(true);
});
it('should ignore soft-deleted view fields when computing the append position', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
viewFieldsInWorkspace: [
{
universalIdentifier: 'existing-vf-1',
viewId: SYNCED_INDEX_VIEW.id,
position: 0,
isActive: true,
},
// A removed column: still in the flat maps but soft-deleted, its
// position must not inflate the append position.
{
universalIdentifier: 'soft-deleted-vf',
viewId: SYNCED_INDEX_VIEW.id,
position: 9,
isActive: true,
deletedAt: '2024-01-01T00:00:00.000Z',
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].position).toBe(1);
});
it('should place the label identifier view field strictly below every existing one', () => {
// Relabeling onto a field created in the same sync: existing positions
// start above zero, the label lands one below the lowest.
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
viewFieldsInWorkspace: [
{
universalIdentifier: 'existing-vf-1',
viewId: SYNCED_INDEX_VIEW.id,
position: 2,
isActive: true,
},
{
universalIdentifier: 'existing-vf-2',
viewId: SYNCED_INDEX_VIEW.id,
position: 5,
isActive: true,
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].isVisible).toBe(true);
expect(viewFields[0].position).toBe(1);
});
it('should place the label identifier view field at position 0 on an empty INDEX view', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].isVisible).toBe(true);
expect(viewFields[0].position).toBe(0);
});
it('should emit a hidden view field for a relation field', () => {
const relationField = buildPendingFieldMetadata(
'assignee',
FieldMetadataType.RELATION,
);
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: relationField,
pendingFieldMetadatas: [relationField],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields).toHaveLength(1);
expect(viewFields[0].isVisible).toBe(false);
});
// The emitted view field is engine-owned whatever the view provenance, so
// manifest sync deletion inference never drops it.
it('should emit an isSystemSideEffect view field on a manifest-owned INDEX view', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [
{ ...SYNCED_INDEX_VIEW, isSystemSideEffect: false },
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(viewFields[0].isSystemSideEffect).toBe(true);
});
// View field groups only ever exist on record-page views, never on an INDEX
// view, so the appended view field is always ungrouped.
it('should emit an ungrouped view field', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
.viewFieldGroupUniversalIdentifier,
).toBeNull();
});
it('should noop when an INDEX view exists but not under its derived identifier (unreconciled workspace)', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [
{
...SYNCED_INDEX_VIEW,
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000042',
},
],
}),
);
expect(result.status).toBe('noop');
});
it('should noop when the object has no active INDEX view', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [
{ ...SYNCED_INDEX_VIEW, isActive: false },
{
id: 'other-view-db-id',
universalIdentifier: 'other-view-uid',
key: null,
},
],
}),
);
expect(result.status).toBe('noop');
});
});
it('should not count engine-owned fields when positioning caller view fields', () => {
const searchVectorField = buildPendingFieldMetadata(
'searchVector',
FieldMetadataType.TS_VECTOR,
true,
);
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
// searchVector sits in the expanded matrix (emitted by a peer handler)
// but must not shift the caller field positions.
pendingFieldMetadatas: [NAME_FIELD, searchVectorField, PRIORITY_FIELD],
objectMetadataCreatedInBatch: true,
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
.position,
).toBe(1);
});
// A second writer claiming the same (view, field) pair is not deduped here:
// it is a genuine conflict left to surface downstream (engine collision, then
// the flat view field validator on the pair).
it('should still emit when a pending view field already covers the same (view, field) pair', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: PRIORITY_FIELD,
pendingFieldMetadatas: [PRIORITY_FIELD],
objectMetadataInWorkspace: true,
viewsInWorkspace: [SYNCED_INDEX_VIEW],
pendingViewFields: [
{
// e.g. emitted caller-side for a FIELDS widget targeting the view
universalIdentifier: 'random-caller-vf-uid',
viewUniversalIdentifier: SYNCED_INDEX_VIEW.universalIdentifier,
fieldMetadataUniversalIdentifier:
PRIORITY_FIELD_UNIVERSAL_IDENTIFIER,
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})[0]
.fieldMetadataUniversalIdentifier,
).toBe(PRIORITY_FIELD_UNIVERSAL_IDENTIFIER);
});
it('should fail when the parent object cannot be resolved', () => {
const result = handler.buildSideEffects(
buildArgs({
triggerFieldMetadata: NAME_FIELD,
pendingFieldMetadatas: [NAME_FIELD],
}),
);
expect(result.status).toBe('fail');
});
});

View File

@@ -0,0 +1,102 @@
import { FieldSystemViewFieldsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-system-view-fields-on-delete-side-effect-handler.service';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
const DELETED_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000001';
const SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000001';
const CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000002';
const buildArgs = ({
viewFieldUniversalIdentifiers,
viewFieldsInWorkspace = [],
}: {
viewFieldUniversalIdentifiers: string[];
viewFieldsInWorkspace?: {
universalIdentifier: string;
isSystemSideEffect: boolean;
}[];
}): BuildSideEffectsArgs<'fieldMetadata'> =>
({
flatEntity: {
universalIdentifier: DELETED_FIELD_UNIVERSAL_IDENTIFIER,
viewFieldUniversalIdentifiers,
},
allFlatEntityOperationRecordByMetadataName: {},
relatedFlatEntityMaps: {
flatViewFieldMaps: {
byUniversalIdentifier: Object.fromEntries(
viewFieldsInWorkspace.map((viewField) => [
viewField.universalIdentifier,
{
...viewField,
fieldMetadataUniversalIdentifier:
DELETED_FIELD_UNIVERSAL_IDENTIFIER,
},
]),
),
},
},
context: {},
}) as unknown as BuildSideEffectsArgs<'fieldMetadata'>;
describe('FieldSystemViewFieldsOnDeleteSideEffectHandlerService', () => {
const handler =
new (FieldSystemViewFieldsOnDeleteSideEffectHandlerService as unknown as new () => FieldSystemViewFieldsOnDeleteSideEffectHandlerService)();
it('should cascade-delete every engine-owned view field displaying the deleted field', () => {
const result = handler.buildSideEffects(
buildArgs({
viewFieldUniversalIdentifiers: [
SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
],
viewFieldsInWorkspace: [
{
universalIdentifier: SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
{
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
).toEqual([SYSTEM_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
});
it('should noop when the field only has caller-owned view fields', () => {
const result = handler.buildSideEffects(
buildArgs({
viewFieldUniversalIdentifiers: [CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER],
viewFieldsInWorkspace: [
{
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
],
}),
);
expect(result.status).toBe('noop');
});
it('should noop when the field has no view field', () => {
const result = handler.buildSideEffects(
buildArgs({ viewFieldUniversalIdentifiers: [] }),
);
expect(result.status).toBe('noop');
});
});

View File

@@ -0,0 +1,281 @@
import { Injectable } from '@nestjs/common';
import {
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
import { buildFieldSideEffectParentNotFoundFailure } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/utils/build-field-side-effect-parent-not-found-failure.util';
import { resolveParentFlatObjectMetadataAfterStateForFieldSideEffect } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/utils/resolve-parent-flat-object-metadata-after-state-for-field-side-effect.util';
import { computeCallerFlatFieldMetadatasForObject } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util';
import { computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
@Injectable()
export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
{
operation: 'create',
metadataName: 'fieldMetadata',
name: 'fieldIndexViewFieldOnCreate',
description:
'When a field is created, provision its view field on the parent object INDEX view. This handler owns the view fields of every caller-provided field. When the parent object is created in the same batch, the default view assembly applies: only displayable fields (no relations) get a visible view field, positioned before the system-field view fields emitted by objectSystemFieldsAndIndexViewOnCreate; both handlers derive positions from the same caller-input list so no ordering dependency exists. When the parent object pre-exists, every created field (relations included) gets a HIDDEN view field appended to the INDEX view, preserving the historical createOneField behavior previously implemented caller-side. Emitted view fields are always isSystemSideEffect, whatever the provenance of the view they land on: the engine authored them, so manifest sync deletion inference must never drop them. Both branches resolve the INDEX view through its name-free deterministic identifier (object identifier + INDEX key), so the lookup is a single map access and never a scan; workspaces synced before the 2-26 reconcile command hold underived INDEX view identifiers and are simply skipped until it runs. Engine-owned fields (the reserved system fields, the searchVector) never reach this handler: they are emitted as side effects and the engine only triggers handlers on the original input, so their view fields belong to the handler that emits them. Noop when the created field is not displayable in the default view (object created in the same batch) or when the object has no active INDEX view (pre-existing object). The engine owns the INDEX view, so this handler never defers to a caller-provided one: the flat view validator rejects caller-created INDEX views outright, and a second writer claiming the same INDEX view field is a genuine conflict left to surface downstream (engine universal-identifier collision, then the flat view field validator on the (view, field) pair).',
},
) {
buildSideEffects({
flatEntity: flatFieldMetadata,
allFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps,
}: BuildSideEffectsArgs<'fieldMetadata'>): MetadataSideEffectResult {
const sourceFlatFieldMetadata =
flatFieldMetadata as UniversalFlatFieldMetadata;
const { objectMetadataUniversalIdentifier } = sourceFlatFieldMetadata;
const parentFlatObjectMetadata =
resolveParentFlatObjectMetadataAfterStateForFieldSideEffect({
objectMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps,
});
if (!isDefined(parentFlatObjectMetadata)) {
return buildFieldSideEffectParentNotFoundFailure({
flatFieldMetadata: sourceFlatFieldMetadata,
operation: 'create',
});
}
const indexViewUniversalIdentifier = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier:
parentFlatObjectMetadata.applicationUniversalIdentifier,
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
viewKey: ViewKey.INDEX,
});
const parentObjectCreatedInSameBatch = isDefined(
allFlatEntityOperationRecordByMetadataName.objectMetadata
?.flatEntityToCreate[objectMetadataUniversalIdentifier],
);
const flatViewFieldToCreate = parentObjectCreatedInSameBatch
? this.buildViewFieldForObjectCreatedInSameBatch({
sourceFlatFieldMetadata,
parentFlatObjectMetadata,
indexViewUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
})
: this.buildViewFieldForExistingObject({
sourceFlatFieldMetadata,
parentFlatObjectMetadata,
indexViewUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps,
});
if (!isDefined(flatViewFieldToCreate)) {
return { status: 'noop' };
}
return {
status: 'success',
operations: {
viewField: {
flatEntityToCreate: {
[flatViewFieldToCreate.universalIdentifier]: flatViewFieldToCreate,
},
},
},
};
}
private buildViewFieldForObjectCreatedInSameBatch({
sourceFlatFieldMetadata,
parentFlatObjectMetadata,
indexViewUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
}: {
sourceFlatFieldMetadata: UniversalFlatFieldMetadata;
parentFlatObjectMetadata: {
applicationUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
};
indexViewUniversalIdentifier: string;
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'fieldMetadata'>['allFlatEntityOperationRecordByMetadataName'];
}): UniversalFlatViewField | undefined {
const { labelIdentifierFieldMetadataUniversalIdentifier } =
parentFlatObjectMetadata;
if (
!isFlatFieldMetadataDisplayableInDefaultView({
flatFieldMetadata: sourceFlatFieldMetadata,
labelIdentifierFieldMetadataUniversalIdentifier,
})
) {
return undefined;
}
const displayableCallerFlatFieldMetadatas =
computeCallerFlatFieldMetadatasForObject({
objectMetadataUniversalIdentifier:
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
displayableOnly: true,
});
const positionByFieldUniversalIdentifier =
computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({
applicationUniversalIdentifier:
parentFlatObjectMetadata.applicationUniversalIdentifier,
objectMetadataUniversalIdentifier:
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
displayableCallerFlatFieldMetadatas,
});
const position =
positionByFieldUniversalIdentifier.get(
sourceFlatFieldMetadata.universalIdentifier,
) ?? 0;
const [flatViewFieldToCreate] = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [sourceFlatFieldMetadata],
viewUniversalIdentifier: indexViewUniversalIdentifier,
applicationUniversalIdentifier:
sourceFlatFieldMetadata.applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
startPosition: position,
});
return flatViewFieldToCreate;
}
private buildViewFieldForExistingObject({
sourceFlatFieldMetadata,
parentFlatObjectMetadata,
indexViewUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps,
}: {
sourceFlatFieldMetadata: UniversalFlatFieldMetadata;
parentFlatObjectMetadata: {
applicationUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
};
indexViewUniversalIdentifier: string;
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'fieldMetadata'>['allFlatEntityOperationRecordByMetadataName'];
relatedFlatEntityMaps: BuildSideEffectsArgs<'fieldMetadata'>['relatedFlatEntityMaps'];
}): UniversalFlatViewField | undefined {
const existingIndexFlatView =
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
indexViewUniversalIdentifier
];
if (
!isDefined(existingIndexFlatView) ||
!existingIndexFlatView.isActive ||
isDefined(existingIndexFlatView.deletedAt)
) {
return undefined;
}
const existingActivePositions =
existingIndexFlatView.viewFieldUniversalIdentifiers
.map(
(viewFieldUniversalIdentifier) =>
relatedFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier[
viewFieldUniversalIdentifier
],
)
.filter(isDefined)
.filter(
(existingFlatViewField) =>
existingFlatViewField.isActive &&
!isDefined(existingFlatViewField.deletedAt),
)
.map((existingFlatViewField) => existingFlatViewField.position);
const isLabelIdentifierField =
sourceFlatFieldMetadata.universalIdentifier ===
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
if (isLabelIdentifierField) {
const lowestExistingPosition =
existingActivePositions.length > 0
? Math.min(...existingActivePositions)
: 1;
const [flatLabelViewFieldToCreate] = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [sourceFlatFieldMetadata],
viewUniversalIdentifier: indexViewUniversalIdentifier,
applicationUniversalIdentifier:
sourceFlatFieldMetadata.applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
startPosition: lowestExistingPosition - 1,
});
return flatLabelViewFieldToCreate;
}
const appendBasePosition =
existingActivePositions.reduce(
(maxPosition, position) => Math.max(maxPosition, position),
-1,
) + 1;
const callerFlatFieldMetadatas = computeCallerFlatFieldMetadatasForObject({
objectMetadataUniversalIdentifier:
sourceFlatFieldMetadata.objectMetadataUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
parentFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
displayableOnly: false,
});
const indexAmongCallerFlatFieldMetadatas = Math.max(
callerFlatFieldMetadatas.findIndex(
(callerFlatFieldMetadata) =>
callerFlatFieldMetadata.universalIdentifier ===
sourceFlatFieldMetadata.universalIdentifier,
),
0,
);
const createdAt = new Date().toISOString();
const { applicationUniversalIdentifier } = sourceFlatFieldMetadata;
return {
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier,
viewUniversalIdentifier: indexViewUniversalIdentifier,
fieldMetadataUniversalIdentifier:
sourceFlatFieldMetadata.universalIdentifier,
}),
applicationUniversalIdentifier,
fieldMetadataUniversalIdentifier:
sourceFlatFieldMetadata.universalIdentifier,
viewUniversalIdentifier: indexViewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
isVisible: false,
size: DEFAULT_VIEW_FIELD_SIZE,
position: appendBasePosition + indexAmongCallerFlatFieldMetadatas,
aggregateOperation: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
};
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { filterSystemSideEffectFlatViewFieldsToDelete } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/filter-system-side-effect-flat-view-fields-to-delete.util';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
@Injectable()
export class FieldSystemViewFieldsOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
{
operation: 'delete',
metadataName: 'fieldMetadata',
name: 'fieldSystemViewFieldsOnDelete',
description:
'When a field is deleted, cascade-delete every engine-owned view field displaying it, wherever it lives: the INDEX view fields emitted by fieldIndexViewFieldOnCreate, and the record-page ones. Counterpart of fieldIndexViewFieldOnCreate: the engine authored those view fields, so it owns their deletion. Manifest deletion inference excludes isSystemSideEffect entities, so without this cascade they would only ever disappear through the viewField -> fieldMetadata foreign key, behind the engine back. Caller-authored view fields are NOT touched: they are deleted through normal deletion inference / the field delete transpiler.',
},
) {
buildSideEffects({
flatEntity: flatFieldMetadata,
relatedFlatEntityMaps,
}: BuildSideEffectsArgs<'fieldMetadata'>): MetadataSideEffectResult {
const viewFieldToDelete = filterSystemSideEffectFlatViewFieldsToDelete({
viewFieldUniversalIdentifiers:
flatFieldMetadata.viewFieldUniversalIdentifiers,
flatViewFieldMaps: relatedFlatEntityMaps.flatViewFieldMaps,
});
if (Object.keys(viewFieldToDelete).length === 0) {
return { status: 'noop' };
}
return {
status: 'success',
operations: {
viewField: {
flatEntityToDelete: viewFieldToDelete,
},
},
};
}
}

View File

@@ -1,12 +1,15 @@
import { Module } from '@nestjs/common';
import { FieldIndexViewFieldOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service';
import { FieldSearchFieldMetadataOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-search-field-metadata-on-delete-side-effect-handler.service';
import { FieldSystemViewFieldsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-system-view-fields-on-delete-side-effect-handler.service';
import { FieldUniqueBackingIndexOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-create-side-effect-handler.service';
import { FieldUniqueBackingIndexOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-delete-side-effect-handler.service';
import { FieldUniqueBackingIndexOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-update-side-effect-handler.service';
import { ObjectSearchVectorOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-create-side-effect-handler.service';
import { ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service';
import { ObjectSearchVectorOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-update-side-effect-handler.service';
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
import { ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service';
import { ObjectSystemRelationsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-relations-on-create-side-effect-handler.service';
import { ObjectSystemRelationsOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-relations-on-update-side-effect-handler.service';
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
@@ -17,11 +20,14 @@ import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/eng
FieldUniqueBackingIndexOnUpdateSideEffectHandlerService,
FieldUniqueBackingIndexOnDeleteSideEffectHandlerService,
FieldSearchFieldMetadataOnDeleteSideEffectHandlerService,
ObjectSystemFieldsOnCreateSideEffectHandlerService,
FieldIndexViewFieldOnCreateSideEffectHandlerService,
FieldSystemViewFieldsOnDeleteSideEffectHandlerService,
ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService,
ObjectSystemRelationsOnCreateSideEffectHandlerService,
ObjectSystemRelationsOnUpdateSideEffectHandlerService,
ObjectSearchVectorOnCreateSideEffectHandlerService,
ObjectSearchVectorOnUpdateSideEffectHandlerService,
ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService,
ObjectSystemSideEffectsOnDeleteSideEffectHandlerService,
],
})

View File

@@ -0,0 +1,160 @@
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const NAME_FIELD_UNIVERSAL_IDENTIFIER = 'd1d2d3d4-d5d6-4000-8000-000000000001';
const CODE_FIELD_UNIVERSAL_IDENTIFIER = 'd1d2d3d4-d5d6-4000-8000-000000000002';
const INDEX_VIEW_UNIVERSAL_IDENTIFIER = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
const NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'e1e2e3e4-e5e6-4000-8000-000000000001';
const CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'e1e2e3e4-e5e6-4000-8000-000000000002';
const buildArgs = ({
fromLabelIdentifier,
toLabelIdentifier,
viewFieldsInWorkspace = [],
}: {
fromLabelIdentifier: string | null;
toLabelIdentifier: string | null;
viewFieldsInWorkspace?: {
universalIdentifier: string;
fieldMetadataUniversalIdentifier: string;
position: number;
isVisible: boolean;
}[];
}): BuildSideEffectsArgs<'objectMetadata'> =>
({
flatEntity: {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier: toLabelIdentifier,
},
allFlatEntityOperationRecordByMetadataName: {},
relatedFlatEntityMaps: {
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[OBJECT_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier:
fromLabelIdentifier,
},
},
},
flatViewMaps: {
byUniversalIdentifier: {
[INDEX_VIEW_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: INDEX_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
isActive: true,
deletedAt: null,
viewFieldUniversalIdentifiers: viewFieldsInWorkspace.map(
(viewField) => viewField.universalIdentifier,
),
},
},
},
flatViewFieldMaps: {
byUniversalIdentifier: Object.fromEntries(
viewFieldsInWorkspace.map((viewField) => [
viewField.universalIdentifier,
{
isActive: true,
deletedAt: null,
isSystemSideEffect: true,
...viewField,
},
]),
),
},
},
context: {},
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
const SYNCED_VIEW_FIELDS = [
{
universalIdentifier: NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
},
{
universalIdentifier: CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
position: 5,
isVisible: false,
},
];
describe('ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService', () => {
const handler =
new (ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService as unknown as new () => ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService)();
it('should move the new label identifier view field below the lowest and make it visible', () => {
const result = handler.buildSideEffects(
buildArgs({
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
toLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const updatedCodeViewField = (result.operations.viewField
?.flatEntityToUpdate ?? {})[CODE_VIEW_FIELD_UNIVERSAL_IDENTIFIER];
expect(updatedCodeViewField?.position).toBe(-1);
expect(updatedCodeViewField?.isVisible).toBe(true);
});
it('should noop when the label identifier is unchanged', () => {
const result = handler.buildSideEffects(
buildArgs({
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
toLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
}),
);
expect(result.status).toBe('noop');
});
it('should noop when the new label identifier view field is already visible and strictly lowest', () => {
const result = handler.buildSideEffects(
buildArgs({
fromLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
toLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
viewFieldsInWorkspace: SYNCED_VIEW_FIELDS,
}),
);
expect(result.status).toBe('noop');
});
it('should noop when the new label identifier has no synced INDEX view field (created in the same sync)', () => {
const result = handler.buildSideEffects(
buildArgs({
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
toLabelIdentifier: CODE_FIELD_UNIVERSAL_IDENTIFIER,
viewFieldsInWorkspace: [SYNCED_VIEW_FIELDS[0]],
}),
);
expect(result.status).toBe('noop');
});
});

View File

@@ -0,0 +1,338 @@
import {
getFieldUniversalIdentifier,
getSystemViewUniversalIdentifier,
getViewFieldUniversalIdentifier,
} from 'twenty-shared/application';
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
import { ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-and-index-view-on-create-side-effect-handler.service';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const OTHER_OBJECT_UNIVERSAL_IDENTIFIER =
'b1b2b3b4-b5b6-4000-8000-000000000002';
const computeFieldUniversalIdentifier = (name: string) =>
getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name,
});
const NAME_FIELD_UNIVERSAL_IDENTIFIER = computeFieldUniversalIdentifier('name');
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER =
computeFieldUniversalIdentifier('searchVector');
const ALL_SYSTEM_FIELD_NAMES = [
'id',
'createdAt',
'updatedAt',
'deletedAt',
'createdBy',
'updatedBy',
'position',
] as const;
// The subset of system fields the INDEX view displays: id/deletedAt/position are
// filtered out by the view-field builder.
const DISPLAYABLE_SYSTEM_FIELD_NAMES = [
'createdAt',
'updatedAt',
'createdBy',
'updatedBy',
] as const;
const DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER =
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: ViewKey.INDEX,
});
type PendingFieldMetadata = {
universalIdentifier: string;
objectMetadataUniversalIdentifier: string;
name: string;
type: FieldMetadataType;
};
const NAME_FIELD: PendingFieldMetadata = {
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name: 'name',
type: FieldMetadataType.TEXT,
};
const buildArgs = ({
labelIdentifierFieldMetadataUniversalIdentifier = NAME_FIELD_UNIVERSAL_IDENTIFIER,
pendingFieldMetadatas = [],
pendingViewUniversalIdentifiers = [],
}: {
labelIdentifierFieldMetadataUniversalIdentifier?: string | null;
pendingFieldMetadatas?: PendingFieldMetadata[];
pendingViewUniversalIdentifiers?: string[];
}): BuildSideEffectsArgs<'objectMetadata'> =>
({
flatEntity: {
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'ticket',
labelIdentifierFieldMetadataUniversalIdentifier,
},
allFlatEntityOperationRecordByMetadataName: {
fieldMetadata: {
flatEntityToCreate: Object.fromEntries(
pendingFieldMetadatas.map((pendingFieldMetadata) => [
pendingFieldMetadata.universalIdentifier,
pendingFieldMetadata,
]),
),
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
...(pendingViewUniversalIdentifiers.length > 0 && {
view: {
flatEntityToCreate: Object.fromEntries(
pendingViewUniversalIdentifiers.map((universalIdentifier) => [
universalIdentifier,
{ universalIdentifier },
]),
),
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
}),
} as unknown as AllFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps: {},
context: {},
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
describe('ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService', () => {
const handler =
new (ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService as unknown as new () => ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService)();
it('should synthesize exactly the 7 reserved system fields, never the caller name nor searchVector', () => {
const result = handler.buildSideEffects(
buildArgs({ pendingFieldMetadatas: [NAME_FIELD] }),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const createdUniversalIdentifiers = Object.keys(
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
);
expect(createdUniversalIdentifiers).toHaveLength(7);
expect(createdUniversalIdentifiers).not.toContain(
NAME_FIELD_UNIVERSAL_IDENTIFIER,
);
expect(createdUniversalIdentifiers).not.toContain(
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
);
for (const name of ALL_SYSTEM_FIELD_NAMES) {
expect(createdUniversalIdentifiers).toContain(
computeFieldUniversalIdentifier(name),
);
}
});
it('should provision the INDEX view and the system-field view fields only, positioned after the displayable caller fields', () => {
const result = handler.buildSideEffects(
buildArgs({ pendingFieldMetadatas: [NAME_FIELD] }),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewUniversalIdentifiers = Object.keys(
result.operations.view?.flatEntityToCreate ?? {},
);
expect(viewUniversalIdentifiers).toEqual([
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
]);
expect(
(result.operations.view?.flatEntityToCreate ?? {})[
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER
]?.isSystemSideEffect,
).toBe(true);
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
// Only the 4 displayable system fields (id, deletedAt and position are
// filtered out); the caller name view field is owned by the field creation
// side effect handler.
const expectedFieldUniversalIdentifiers =
DISPLAYABLE_SYSTEM_FIELD_NAMES.map(computeFieldUniversalIdentifier);
expect(
viewFields
.map((viewField) => viewField.fieldMetadataUniversalIdentifier)
.sort(),
).toEqual([...expectedFieldUniversalIdentifiers].sort());
// Positions are offset past the single displayable caller field (name),
// whose view field the field handler emits at position 0.
expect(
viewFields.map((viewField) => viewField.position).sort((a, b) => a - b),
).toEqual([1, 2, 3, 4]);
for (const viewField of viewFields) {
expect(viewField.isSystemSideEffect).toBe(true);
expect(viewField.universalIdentifier).toBe(
getViewFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier:
viewField.fieldMetadataUniversalIdentifier,
}),
);
}
});
it('should start system view field positions at 0 when there is no displayable caller field', () => {
const result = handler.buildSideEffects(
buildArgs({
labelIdentifierFieldMetadataUniversalIdentifier: null,
pendingFieldMetadatas: [],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.values(result.operations.viewField?.flatEntityToCreate ?? {})
.map((viewField) => viewField.position)
.sort((a, b) => a - b),
).toEqual([0, 1, 2, 3]);
});
// The engine is the sole owner of the INDEX view and always emits it; a
// caller providing one with the same identifier is a conflict left to the
// engine collision downstream, not silently deferred here.
it('should still emit its own INDEX view even when the caller provides one with the same identifier', () => {
const result = handler.buildSideEffects(
buildArgs({
pendingFieldMetadatas: [NAME_FIELD],
pendingViewUniversalIdentifiers: [
DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER,
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
).toHaveLength(7);
expect(
Object.keys(result.operations.view?.flatEntityToCreate ?? {}),
).toEqual([DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER]);
expect(
Object.values(result.operations.viewField?.flatEntityToCreate ?? {}),
).toHaveLength(4);
});
it('should ignore caller fields belonging to other objects when computing the position offset', () => {
const result = handler.buildSideEffects(
buildArgs({
pendingFieldMetadatas: [
NAME_FIELD,
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000001',
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'otherObjectField',
type: FieldMetadataType.TEXT,
},
],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
expect(
viewFields.map((viewField) => viewField.fieldMetadataUniversalIdentifier),
).not.toContain('c1c2c3c4-c5c6-4000-8000-000000000001');
// Offset is 1 (only the name field belongs to the created object), not 2.
expect(
viewFields.map((viewField) => viewField.position).sort((a, b) => a - b),
).toEqual([1, 2, 3, 4]);
});
// An object with no name field (e.g. a manifest object) has its label
// identifier backed by the id system field; its view field must still be
// strictly lowest, ahead of the displayable caller fields.
it('should place the id label-identifier system view field at the strictly lowest position', () => {
const idFieldUniversalIdentifier = computeFieldUniversalIdentifier('id');
const callerField: PendingFieldMetadata = {
universalIdentifier: computeFieldUniversalIdentifier('firstKey'),
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name: 'firstKey',
type: FieldMetadataType.TEXT,
};
const result = handler.buildSideEffects(
buildArgs({
labelIdentifierFieldMetadataUniversalIdentifier:
idFieldUniversalIdentifier,
pendingFieldMetadatas: [callerField],
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const viewFields = Object.values(
result.operations.viewField?.flatEntityToCreate ?? {},
);
const idViewField = viewFields.find(
(viewField) =>
viewField.fieldMetadataUniversalIdentifier ===
idFieldUniversalIdentifier,
);
expect(idViewField?.position).toBe(0);
for (const viewField of viewFields) {
if (
viewField.fieldMetadataUniversalIdentifier !==
idFieldUniversalIdentifier
) {
expect(viewField.position).toBeGreaterThan(0);
}
}
});
});

View File

@@ -1,130 +0,0 @@
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const SYSTEM_FIELD_NAMES = [
'id',
'createdAt',
'updatedAt',
'deletedAt',
'createdBy',
'updatedBy',
'position',
] as const;
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name: 'name',
});
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name: 'searchVector',
});
const buildArgs = ({
labelIdentifierFieldMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName = {} as unknown as AllFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps = {},
}: {
labelIdentifierFieldMetadataUniversalIdentifier: string;
allFlatEntityOperationRecordByMetadataName?: AllFlatEntityOperationRecordByMetadataName;
relatedFlatEntityMaps?: object;
}): BuildSideEffectsArgs<'objectMetadata'> =>
({
flatEntity: {
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier,
},
allFlatEntityOperationRecordByMetadataName,
relatedFlatEntityMaps,
context: {},
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
describe('ObjectSystemFieldsOnCreateSideEffectHandlerService', () => {
const handler =
new (ObjectSystemFieldsOnCreateSideEffectHandlerService as unknown as new () => ObjectSystemFieldsOnCreateSideEffectHandlerService)();
it('should synthesize exactly the 7 reserved system fields and never the caller-provided name field nor the searchVector field', () => {
const result = handler.buildSideEffects(
buildArgs({
labelIdentifierFieldMetadataUniversalIdentifier:
NAME_FIELD_UNIVERSAL_IDENTIFIER,
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
const createdUniversalIdentifiers = Object.keys(
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
);
expect(createdUniversalIdentifiers).toHaveLength(7);
expect(createdUniversalIdentifiers).not.toContain(
NAME_FIELD_UNIVERSAL_IDENTIFIER,
);
expect(createdUniversalIdentifiers).not.toContain(
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
);
for (const name of SYSTEM_FIELD_NAMES) {
expect(createdUniversalIdentifiers).toContain(
getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name,
}),
);
}
});
it('should still emit all system fields even when they already exist in the from-state (dedup delegated to the engine merge)', () => {
const alreadyPresentByUniversalIdentifier: Record<string, unknown> = {};
for (const name of SYSTEM_FIELD_NAMES) {
const universalIdentifier = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
name,
});
alreadyPresentByUniversalIdentifier[universalIdentifier] = {
universalIdentifier,
};
}
const result = handler.buildSideEffects(
buildArgs({
labelIdentifierFieldMetadataUniversalIdentifier:
NAME_FIELD_UNIVERSAL_IDENTIFIER,
relatedFlatEntityMaps: {
flatFieldMetadataMaps: {
byUniversalIdentifier: alreadyPresentByUniversalIdentifier,
},
},
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
).toHaveLength(7);
});
});

View File

@@ -1,5 +1,5 @@
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const OTHER_OBJECT_UNIVERSAL_IDENTIFIER =
@@ -9,21 +9,81 @@ const SYSTEM_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000001';
const AUTHOR_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000002';
const FORWARD_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000003';
const REVERSE_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000004';
const GIN_INDEX_UNIVERSAL_IDENTIFIER = 'e1e2e3e4-e5e6-4000-8000-000000000001';
const REVERSE_INDEX_UNIVERSAL_IDENTIFIER =
'e1e2e3e4-e5e6-4000-8000-000000000002';
const SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
'f1f2f3f4-f5f6-4000-8000-000000000001';
const INDEX_VIEW_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000002';
const CALLER_VIEW_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000003';
const CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000004';
type FieldFixture = {
universalIdentifier: string;
isSystemSideEffect: boolean;
objectMetadataUniversalIdentifier?: string;
relationTargetFieldMetadataUniversalIdentifier?: string | null;
viewFieldUniversalIdentifiers?: string[];
};
type ViewFixture = {
universalIdentifier: string;
isSystemSideEffect: boolean;
viewFieldUniversalIdentifiers?: string[];
};
const buildFieldMetadataMaps = (fields: FieldFixture[]) => ({
byUniversalIdentifier: Object.fromEntries(
fields.map((field) => [
field.universalIdentifier,
{
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: null,
viewFieldUniversalIdentifiers: [],
...field,
},
]),
),
});
const buildArgs = ({
relatedFlatEntityMaps,
fieldUniversalIdentifiers = [],
indexMetadataUniversalIdentifiers = [],
searchFieldMetadataUniversalIdentifiers = [],
viewUniversalIdentifiers = [],
relatedFlatEntityMaps = {},
}: {
relatedFlatEntityMaps: object;
fieldUniversalIdentifiers?: string[];
indexMetadataUniversalIdentifiers?: string[];
searchFieldMetadataUniversalIdentifiers?: string[];
viewUniversalIdentifiers?: string[];
relatedFlatEntityMaps?: object;
}): BuildSideEffectsArgs<'objectMetadata'> =>
({
flatEntity: {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifiers,
indexMetadataUniversalIdentifiers,
searchFieldMetadataUniversalIdentifiers,
viewUniversalIdentifiers,
},
allFlatEntityOperationRecordByMetadataName: {},
relatedFlatEntityMaps,
relatedFlatEntityMaps: {
flatObjectMetadataMaps: { byUniversalIdentifier: {} },
flatFieldMetadataMaps: { byUniversalIdentifier: {} },
flatIndexMaps: { byUniversalIdentifier: {} },
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
flatViewMaps: { byUniversalIdentifier: {} },
flatViewFieldMaps: { byUniversalIdentifier: {} },
...relatedFlatEntityMaps,
},
context: {},
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
@@ -34,23 +94,25 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
it('should cascade-delete engine-owned fields, indexes and searchFieldMetadata while leaving author fields untouched', () => {
const result = handler.buildSideEffects(
buildArgs({
fieldUniversalIdentifiers: [
SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
],
indexMetadataUniversalIdentifiers: [GIN_INDEX_UNIVERSAL_IDENTIFIER],
searchFieldMetadataUniversalIdentifiers: [
SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
],
relatedFlatEntityMaps: {
flatFieldMetadataMaps: {
byUniversalIdentifier: {
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: null,
},
[AUTHOR_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: null,
},
flatFieldMetadataMaps: buildFieldMetadataMaps([
{
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
},
{
universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
]),
flatIndexMaps: {
byUniversalIdentifier: {
[GIN_INDEX_UNIVERSAL_IDENTIFIER]: {
@@ -65,7 +127,6 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
byUniversalIdentifier: {
[SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
},
},
},
@@ -100,35 +161,34 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
});
it('should cascade-delete the default-relation reverse morph field on a standard object and its join-column index', () => {
const FORWARD_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000003';
const REVERSE_FIELD_UNIVERSAL_IDENTIFIER =
'd1d2d3d4-d5d6-4000-8000-000000000004';
const REVERSE_INDEX_UNIVERSAL_IDENTIFIER =
'e1e2e3e4-e5e6-4000-8000-000000000002';
const result = handler.buildSideEffects(
buildArgs({
fieldUniversalIdentifiers: [FORWARD_FIELD_UNIVERSAL_IDENTIFIER],
relatedFlatEntityMaps: {
flatFieldMetadataMaps: {
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[FORWARD_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
},
[REVERSE_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier:
OBJECT_UNIVERSAL_IDENTIFIER,
[OTHER_OBJECT_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
indexMetadataUniversalIdentifiers: [
REVERSE_INDEX_UNIVERSAL_IDENTIFIER,
],
},
},
},
flatFieldMetadataMaps: buildFieldMetadataMaps([
{
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
relationTargetFieldMetadataUniversalIdentifier:
REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
},
{
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
},
]),
flatIndexMaps: {
byUniversalIdentifier: {
[REVERSE_INDEX_UNIVERSAL_IDENTIFIER]: {
@@ -145,7 +205,6 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
},
},
},
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
},
}),
);
@@ -167,27 +226,132 @@ describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
).toEqual([REVERSE_INDEX_UNIVERSAL_IDENTIFIER]);
});
it('should not delete entities belonging to a different object', () => {
it('should not delete entities the object aggregators do not reference', () => {
const result = handler.buildSideEffects(
buildArgs({
relatedFlatEntityMaps: {
flatFieldMetadataMaps: {
byUniversalIdentifier: {
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: null,
},
flatFieldMetadataMaps: buildFieldMetadataMaps([
{
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
},
},
flatIndexMaps: { byUniversalIdentifier: {} },
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
]),
},
}),
);
expect(result.status).toBe('noop');
});
it('should cascade-delete the engine-owned INDEX view and its view fields while leaving a caller-owned view untouched', () => {
const result = handler.buildSideEffects(
buildArgs({
viewUniversalIdentifiers: [
INDEX_VIEW_UNIVERSAL_IDENTIFIER,
CALLER_VIEW_UNIVERSAL_IDENTIFIER,
],
relatedFlatEntityMaps: {
flatViewMaps: {
byUniversalIdentifier: Object.fromEntries(
(
[
{
universalIdentifier: INDEX_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
viewFieldUniversalIdentifiers: [
INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
],
},
{
universalIdentifier: CALLER_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
viewFieldUniversalIdentifiers: [
CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
],
},
] satisfies ViewFixture[]
).map((view) => [view.universalIdentifier, view]),
),
},
flatViewFieldMaps: {
byUniversalIdentifier: {
[INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
[CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: CALLER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
},
},
},
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(
Object.keys(result.operations.view?.flatEntityToDelete ?? {}),
).toEqual([INDEX_VIEW_UNIVERSAL_IDENTIFIER]);
expect(
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
).toEqual([INDEX_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
});
it('should cascade-delete a system view field of a deleted field even when it lives on another object view', () => {
const OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000006';
const result = handler.buildSideEffects(
buildArgs({
fieldUniversalIdentifiers: [FORWARD_FIELD_UNIVERSAL_IDENTIFIER],
relatedFlatEntityMaps: {
flatFieldMetadataMaps: buildFieldMetadataMaps([
{
universalIdentifier: FORWARD_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
relationTargetFieldMetadataUniversalIdentifier:
REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
},
{
universalIdentifier: REVERSE_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
viewFieldUniversalIdentifiers: [
OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
],
},
]),
flatViewFieldMaps: {
byUniversalIdentifier: {
[OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER]: {
universalIdentifier:
OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
},
},
},
}),
);
expect(result.status).toBe('success');
if (result.status !== 'success') {
throw new Error('expected success');
}
expect(result.operations.view).toBeUndefined();
expect(
Object.keys(result.operations.viewField?.flatEntityToDelete ?? {}),
).toEqual([OTHER_OBJECT_VIEW_FIELD_UNIVERSAL_IDENTIFIER]);
});
});

View File

@@ -0,0 +1,147 @@
import { msg, t } from '@lingui/core/macro';
import { Injectable } from '@nestjs/common';
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { MetadataSideEffectExceptionCode } from 'src/engine/metadata-modules/metadata-side-effect/exceptions/metadata-side-effect-exception-code';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
@Injectable()
export class ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService extends MetadataSideEffectHandler(
{
operation: 'update',
metadataName: 'objectMetadata',
name: 'objectIndexViewLabelIdentifierOnUpdate',
description:
'When an object label identifier changes onto a pre-existing field, keep that field INDEX view field strictly lowest and visible, as the flat view field validator requires. The engine owns the INDEX view, so this is a system side effect on both the API and manifest-sync paths, and only the engine-owned view field is touched. Relabeling onto a field created in the same operation is handled by fieldIndexViewFieldOnCreate instead (its view field does not exist yet to be updated), so this handler considers only synced view fields. Noop when the label identifier is unchanged, when the object has no engine-owned INDEX view, when the new label identifier has no synced INDEX view field, or when that view field is already visible and strictly lowest.',
},
) {
buildSideEffects({
flatEntity,
relatedFlatEntityMaps,
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
const updatedFlatObjectMetadata = flatEntity as UniversalFlatObjectMetadata;
const existingFlatObjectMetadata =
relatedFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
updatedFlatObjectMetadata.universalIdentifier
];
if (!isDefined(existingFlatObjectMetadata)) {
return {
status: 'fail',
type: 'update',
metadataName: 'objectMetadata',
flatEntityMinimalInformation: {
universalIdentifier: updatedFlatObjectMetadata.universalIdentifier,
} as Partial<MetadataFlatEntity<'objectMetadata'>>,
errors: [
{
code: MetadataSideEffectExceptionCode.SIDE_EFFECT_PARENT_METADATA_NOT_FOUND,
message: t`Could not resolve the existing object to reconcile its INDEX label identifier view field`,
userFriendlyMessage: msg`The object to update could not be found to reconcile its INDEX view`,
},
],
};
}
const newLabelIdentifierFieldMetadataUniversalIdentifier =
updatedFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
if (
existingFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
newLabelIdentifierFieldMetadataUniversalIdentifier ||
!isDefined(newLabelIdentifierFieldMetadataUniversalIdentifier)
) {
return { status: 'noop' };
}
const indexViewUniversalIdentifier = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier:
updatedFlatObjectMetadata.applicationUniversalIdentifier,
objectUniversalIdentifier: updatedFlatObjectMetadata.universalIdentifier,
viewKey: ViewKey.INDEX,
});
const indexFlatView =
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
indexViewUniversalIdentifier
];
if (
!isDefined(indexFlatView) ||
indexFlatView.isSystemSideEffect !== true ||
!indexFlatView.isActive ||
isDefined(indexFlatView.deletedAt)
) {
return { status: 'noop' };
}
const indexFlatViewFields = indexFlatView.viewFieldUniversalIdentifiers
.map(
(viewFieldUniversalIdentifier) =>
relatedFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier[
viewFieldUniversalIdentifier
],
)
.filter(isDefined)
.filter(
(flatViewField) =>
flatViewField.isActive && !isDefined(flatViewField.deletedAt),
) as UniversalFlatViewField[];
const labelIdentifierFlatViewField = indexFlatViewFields.find(
(flatViewField) =>
flatViewField.fieldMetadataUniversalIdentifier ===
newLabelIdentifierFieldMetadataUniversalIdentifier,
);
if (!isDefined(labelIdentifierFlatViewField)) {
return { status: 'noop' };
}
const otherPositions = indexFlatViewFields
.filter(
(flatViewField) =>
flatViewField.universalIdentifier !==
labelIdentifierFlatViewField.universalIdentifier,
)
.map((flatViewField) => flatViewField.position);
const isAlreadyLowestAndVisible =
labelIdentifierFlatViewField.isVisible &&
(otherPositions.length === 0 ||
labelIdentifierFlatViewField.position < Math.min(...otherPositions));
if (isAlreadyLowestAndVisible) {
return { status: 'noop' };
}
const targetPosition =
otherPositions.length === 0 ? 0 : Math.min(...otherPositions) - 1;
return {
status: 'success',
operations: {
viewField: {
flatEntityToUpdate: {
[labelIdentifierFlatViewField.universalIdentifier]: {
...labelIdentifierFlatViewField,
position: targetPosition,
isVisible: true,
},
},
},
},
};
}
}

View File

@@ -0,0 +1,111 @@
import { Injectable } from '@nestjs/common';
import { fromArrayToUniqueKeyRecord } from 'twenty-shared/utils';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { computeCallerFlatFieldMetadatasForObject } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-caller-flat-field-metadatas-for-object.util';
import { computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/compute-default-index-view-field-position-by-field-universal-identifier.util';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
import { computeFlatIndexViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-index-view-to-create.util';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
@Injectable()
export class ObjectSystemFieldsAndIndexViewOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
{
operation: 'create',
metadataName: 'objectMetadata',
name: 'objectSystemFieldsAndIndexViewOnCreate',
description:
'When an object is created, provision (1) its 7 reserved system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position; searchVector is handled by objectSearchVectorOnCreate, and the name field is caller-provided) and (2) its default INDEX table view ("All {objectLabelPlural}") with one view field per displayable SYSTEM field only, all isSystemSideEffect so the engine owns their lifecycle. View fields for caller-provided fields are owned by fieldIndexViewFieldOnCreate (the field creation side effect), which positions them before the system view fields; both handlers derive positions from the same caller-input list so the layout is contiguous without any ordering dependency. The view identifier is name-free (object identifier + INDEX view key), so an object rename keeps the same view. The engine is the sole owner of the INDEX view and always emits it: the flat view validator rejects caller-created INDEX views (caller inputs are forced isSystemSideEffect: false) and enforces a single non-deleted INDEX view per object. twenty-standard is not concerned: it synchronizes through the from/to migration path, which never runs the side-effect engine, and authors its own curated INDEX view/fields.',
},
) {
buildSideEffects({
flatEntity: flatObjectMetadata,
allFlatEntityOperationRecordByMetadataName,
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
const sourceFlatObjectMetadata =
flatObjectMetadata as UniversalFlatObjectMetadata;
const { applicationUniversalIdentifier, universalIdentifier } =
sourceFlatObjectMetadata;
const systemFlatFieldMetadatas = Object.values(
buildReservedSystemFlatFieldMetadatasForCustomObject({
flatObjectMetadata: {
applicationUniversalIdentifier,
universalIdentifier,
},
}),
);
const systemFieldMetadataToCreate: Record<
string,
MetadataUniversalFlatEntity<'fieldMetadata'>
> = {};
for (const systemFlatFieldMetadata of systemFlatFieldMetadatas) {
systemFieldMetadataToCreate[systemFlatFieldMetadata.universalIdentifier] =
systemFlatFieldMetadata;
}
const flatIndexViewToCreate = computeFlatIndexViewToCreate({
objectMetadata: sourceFlatObjectMetadata,
applicationUniversalIdentifier,
});
const displayableCallerFlatFieldMetadatas =
computeCallerFlatFieldMetadatasForObject({
objectMetadataUniversalIdentifier: universalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
displayableOnly: true,
});
const positionByFieldUniversalIdentifier =
computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier({
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier: universalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
displayableCallerFlatFieldMetadatas,
});
const flatViewFieldsToCreate = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: systemFlatFieldMetadatas,
viewUniversalIdentifier: flatIndexViewToCreate.universalIdentifier,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
sourceFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
}).map((flatViewFieldToCreate) => ({
...flatViewFieldToCreate,
position:
positionByFieldUniversalIdentifier.get(
flatViewFieldToCreate.fieldMetadataUniversalIdentifier,
) ?? flatViewFieldToCreate.position,
}));
return {
status: 'success',
operations: {
fieldMetadata: { flatEntityToCreate: systemFieldMetadataToCreate },
view: {
flatEntityToCreate: {
[flatIndexViewToCreate.universalIdentifier]: flatIndexViewToCreate,
},
},
viewField: {
flatEntityToCreate: fromArrayToUniqueKeyRecord({
array: flatViewFieldsToCreate,
uniqueKey: 'universalIdentifier',
}),
},
},
};
}
}

View File

@@ -1,56 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
@Injectable()
export class ObjectSystemFieldsOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
{
operation: 'create',
metadataName: 'objectMetadata',
name: 'objectSystemFieldsOnCreate',
description:
'When an object is created, generate its 7 reserved system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position). The searchVector field is provisioned by the self-contained objectSearchVectorOnCreate handler alongside its GIN index and searchFieldMetadata. The default name field is NOT synthesized here: it is a caller-provided default field (SDK auto-complete on the manifest path, input transpiler on the API path).',
},
) {
buildSideEffects({
flatEntity: flatObjectMetadata,
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
const { applicationUniversalIdentifier, universalIdentifier } =
flatObjectMetadata;
const reservedSystemFlatFieldMetadatas =
buildReservedSystemFlatFieldMetadatasForCustomObject({
flatObjectMetadata: {
applicationUniversalIdentifier,
universalIdentifier,
},
});
const flatEntityToCreate: Record<
string,
MetadataUniversalFlatEntity<'fieldMetadata'>
> = {};
for (const flatFieldMetadata of Object.values(
reservedSystemFlatFieldMetadatas,
)) {
flatEntityToCreate[flatFieldMetadata.universalIdentifier] =
flatFieldMetadata;
}
return {
status: 'success',
operations: {
fieldMetadata: {
flatEntityToCreate,
},
},
};
}
}

View File

@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { filterSystemSideEffectFlatViewFieldsToDelete } from 'src/engine/metadata-modules/metadata-side-effect/handlers/utils/filter-system-side-effect-flat-view-fields-to-delete.util';
import {
type BuildSideEffectsArgs,
MetadataSideEffectHandler,
@@ -10,6 +11,18 @@ import {
import { type MetadataSideEffectOperationsByMetadataName } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-operations-by-metadata-name.type';
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
type RelatedFlatEntityMaps =
BuildSideEffectsArgs<'objectMetadata'>['relatedFlatEntityMaps'];
type FlatEntityToDelete<
TMetadataName extends
| 'fieldMetadata'
| 'index'
| 'searchFieldMetadata'
| 'view'
| 'viewField',
> = Record<string, MetadataUniversalFlatEntity<TMetadataName>>;
@Injectable()
export class ObjectSystemSideEffectsOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
{
@@ -17,139 +30,261 @@ export class ObjectSystemSideEffectsOnDeleteSideEffectHandlerService extends Met
metadataName: 'objectMetadata',
name: 'objectSystemSideEffectsOnDelete',
description:
'When an object is deleted, cascade-delete its engine-owned side effects: the reserved system fields, the default relation fields (forward field on the deleted object and reverse morph field on the standard object), every system index (reverse join-column indexes, the GIN searchVector index), and its searchFieldMetadata rows. The engine is the sole authority for isSystemSideEffect entities on delete: the API object delete transpiler cascades only user-authored fields and indexes, and manifest deletion inference excludes these entities entirely. Caller-provided defaults (e.g. the name field) are NOT engine-owned and are deleted through normal deletion inference / the object delete transpiler.',
'When an object is deleted, cascade-delete its engine-owned side effects: the reserved system fields, the default relation fields (forward field on the deleted object and reverse morph field on the standard object), every system index (reverse join-column indexes, the GIN searchVector index), its searchFieldMetadata rows, and its engine-owned views (the INDEX table view provisioned by objectSystemFieldsAndIndexViewOnCreate) with their view fields. View fields of the deleted system fields are cascaded too even when they live on another object view, which happens for the reverse relation fields. Every lookup walks a foreign key aggregator down from the deleted object (its fields, indexes, searchFieldMetadatas, views, then their view fields) and indexes into the flat entity maps, so the work is proportional to what the object owns and never to the size of the workspace. The engine is the sole authority for isSystemSideEffect entities on delete: the API object delete transpiler cascades only user-authored fields and indexes, and manifest deletion inference excludes these entities entirely. Caller-provided defaults (e.g. the name field) are NOT engine-owned and are deleted through normal deletion inference / the object delete transpiler.',
},
) {
buildSideEffects({
flatEntity: flatObjectMetadata,
relatedFlatEntityMaps,
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
const objectMetadataUniversalIdentifier =
flatObjectMetadata.universalIdentifier;
const fieldMetadataToDelete = this.computeFieldMetadataToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
});
const flatFieldMetadatasToDelete = Object.values(fieldMetadataToDelete);
const fieldMetadataToDelete: Record<
string,
MetadataUniversalFlatEntity<'fieldMetadata'>
> = {};
const deletedFieldUniversalIdentifiers = new Set<string>();
const viewToDelete = this.computeViewToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
});
for (const flatFieldMetadata of Object.values(
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatFieldMetadata)) {
continue;
}
const flatEntityToDeleteByMetadataName = {
fieldMetadata: fieldMetadataToDelete,
index: this.computeIndexToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
flatFieldMetadatasToDelete,
}),
searchFieldMetadata: this.computeSearchFieldMetadataToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
}),
view: viewToDelete,
viewField: this.computeViewFieldToDelete({
relatedFlatEntityMaps,
flatViewsToDelete: Object.values(viewToDelete),
flatFieldMetadatasToDelete,
}),
};
if (flatFieldMetadata.isSystemSideEffect !== true) {
continue;
}
const operations = Object.fromEntries(
Object.entries(flatEntityToDeleteByMetadataName)
.filter(
([, flatEntityToDelete]) =>
Object.keys(flatEntityToDelete).length > 0,
)
.map(([metadataName, flatEntityToDelete]) => [
metadataName,
{ flatEntityToDelete },
]),
) as MetadataSideEffectOperationsByMetadataName;
const belongsToObject =
flatFieldMetadata.objectMetadataUniversalIdentifier ===
objectMetadataUniversalIdentifier;
const targetsObject =
flatFieldMetadata.relationTargetObjectMetadataUniversalIdentifier ===
objectMetadataUniversalIdentifier;
if (Object.keys(operations).length === 0) {
return { status: 'noop' };
}
if (!belongsToObject && !targetsObject) {
return {
status: 'success',
operations,
};
}
private computeFieldMetadataToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
}: {
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
relatedFlatEntityMaps: RelatedFlatEntityMaps;
}): FlatEntityToDelete<'fieldMetadata'> {
const fieldMetadataToDelete: FlatEntityToDelete<'fieldMetadata'> = {};
for (const fieldUniversalIdentifier of flatObjectMetadata.fieldUniversalIdentifiers) {
const flatFieldMetadata =
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
fieldUniversalIdentifier
];
if (
!isDefined(flatFieldMetadata) ||
flatFieldMetadata.isSystemSideEffect !== true
) {
continue;
}
fieldMetadataToDelete[flatFieldMetadata.universalIdentifier] =
flatFieldMetadata;
deletedFieldUniversalIdentifiers.add(
flatFieldMetadata.universalIdentifier,
);
}
const indexToDelete: Record<
string,
MetadataUniversalFlatEntity<'index'>
> = {};
const { relationTargetFieldMetadataUniversalIdentifier } =
flatFieldMetadata;
for (const flatIndexMetadata of Object.values(
relatedFlatEntityMaps.flatIndexMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatIndexMetadata)) {
if (!isDefined(relationTargetFieldMetadataUniversalIdentifier)) {
continue;
}
if (flatIndexMetadata.isSystemSideEffect !== true) {
continue;
}
const belongsToObject =
flatIndexMetadata.objectMetadataUniversalIdentifier ===
objectMetadataUniversalIdentifier;
const referencesDeletedField =
flatIndexMetadata.universalFlatIndexFieldMetadatas.some(
(universalFlatIndexFieldMetadata) =>
deletedFieldUniversalIdentifiers.has(
universalFlatIndexFieldMetadata.fieldMetadataUniversalIdentifier,
),
);
if (!belongsToObject && !referencesDeletedField) {
continue;
}
indexToDelete[flatIndexMetadata.universalIdentifier] = flatIndexMetadata;
}
const searchFieldMetadataToDelete: Record<
string,
MetadataUniversalFlatEntity<'searchFieldMetadata'>
> = {};
for (const flatSearchFieldMetadata of Object.values(
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatSearchFieldMetadata)) {
continue;
}
const reverseFlatFieldMetadata =
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
relationTargetFieldMetadataUniversalIdentifier
];
if (
flatSearchFieldMetadata.objectMetadataUniversalIdentifier !==
objectMetadataUniversalIdentifier
!isDefined(reverseFlatFieldMetadata) ||
reverseFlatFieldMetadata.isSystemSideEffect !== true
) {
continue;
}
fieldMetadataToDelete[reverseFlatFieldMetadata.universalIdentifier] =
reverseFlatFieldMetadata;
}
return fieldMetadataToDelete;
}
private computeIndexToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
flatFieldMetadatasToDelete,
}: {
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
relatedFlatEntityMaps: RelatedFlatEntityMaps;
flatFieldMetadatasToDelete: MetadataUniversalFlatEntity<'fieldMetadata'>[];
}): FlatEntityToDelete<'index'> {
const deletedFieldUniversalIdentifiers = new Set(
flatFieldMetadatasToDelete.map(
(flatFieldMetadata) => flatFieldMetadata.universalIdentifier,
),
);
const indexOwnerFlatObjectMetadataByUniversalIdentifier = new Map([
[flatObjectMetadata.universalIdentifier, flatObjectMetadata],
]);
for (const flatFieldMetadata of flatFieldMetadatasToDelete) {
const ownerFlatObjectMetadata =
relatedFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
flatFieldMetadata.objectMetadataUniversalIdentifier
];
if (isDefined(ownerFlatObjectMetadata)) {
indexOwnerFlatObjectMetadataByUniversalIdentifier.set(
ownerFlatObjectMetadata.universalIdentifier,
ownerFlatObjectMetadata,
);
}
}
const indexToDelete: FlatEntityToDelete<'index'> = {};
for (const indexOwnerFlatObjectMetadata of indexOwnerFlatObjectMetadataByUniversalIdentifier.values()) {
for (const indexUniversalIdentifier of indexOwnerFlatObjectMetadata.indexMetadataUniversalIdentifiers) {
const flatIndexMetadata =
relatedFlatEntityMaps.flatIndexMaps.byUniversalIdentifier[
indexUniversalIdentifier
];
if (
!isDefined(flatIndexMetadata) ||
flatIndexMetadata.isSystemSideEffect !== true
) {
continue;
}
const belongsToObject =
flatIndexMetadata.objectMetadataUniversalIdentifier ===
flatObjectMetadata.universalIdentifier;
const referencesDeletedField =
flatIndexMetadata.universalFlatIndexFieldMetadatas.some(
(universalFlatIndexFieldMetadata) =>
deletedFieldUniversalIdentifiers.has(
universalFlatIndexFieldMetadata.fieldMetadataUniversalIdentifier,
),
);
if (!belongsToObject && !referencesDeletedField) {
continue;
}
indexToDelete[flatIndexMetadata.universalIdentifier] =
flatIndexMetadata;
}
}
return indexToDelete;
}
private computeSearchFieldMetadataToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
}: {
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
relatedFlatEntityMaps: RelatedFlatEntityMaps;
}): FlatEntityToDelete<'searchFieldMetadata'> {
const searchFieldMetadataToDelete: FlatEntityToDelete<'searchFieldMetadata'> =
{};
for (const searchFieldMetadataUniversalIdentifier of flatObjectMetadata.searchFieldMetadataUniversalIdentifiers) {
const flatSearchFieldMetadata =
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier[
searchFieldMetadataUniversalIdentifier
];
if (!isDefined(flatSearchFieldMetadata)) {
continue;
}
searchFieldMetadataToDelete[flatSearchFieldMetadata.universalIdentifier] =
flatSearchFieldMetadata;
}
const hasFieldMetadataToDelete =
Object.keys(fieldMetadataToDelete).length > 0;
const hasIndexToDelete = Object.keys(indexToDelete).length > 0;
const hasSearchFieldMetadataToDelete =
Object.keys(searchFieldMetadataToDelete).length > 0;
return searchFieldMetadataToDelete;
}
if (
!hasFieldMetadataToDelete &&
!hasIndexToDelete &&
!hasSearchFieldMetadataToDelete
) {
return { status: 'noop' };
private computeViewToDelete({
flatObjectMetadata,
relatedFlatEntityMaps,
}: {
flatObjectMetadata: MetadataUniversalFlatEntity<'objectMetadata'>;
relatedFlatEntityMaps: RelatedFlatEntityMaps;
}): FlatEntityToDelete<'view'> {
const viewToDelete: FlatEntityToDelete<'view'> = {};
for (const viewUniversalIdentifier of flatObjectMetadata.viewUniversalIdentifiers) {
const flatView =
relatedFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
viewUniversalIdentifier
];
if (!isDefined(flatView) || flatView.isSystemSideEffect !== true) {
continue;
}
viewToDelete[flatView.universalIdentifier] = flatView;
}
const operations: MetadataSideEffectOperationsByMetadataName = {
...(hasFieldMetadataToDelete && {
fieldMetadata: { flatEntityToDelete: fieldMetadataToDelete },
}),
...(hasIndexToDelete && {
index: { flatEntityToDelete: indexToDelete },
}),
...(hasSearchFieldMetadataToDelete && {
searchFieldMetadata: {
flatEntityToDelete: searchFieldMetadataToDelete,
},
}),
};
return viewToDelete;
}
return {
status: 'success',
operations,
};
private computeViewFieldToDelete({
relatedFlatEntityMaps,
flatViewsToDelete,
flatFieldMetadatasToDelete,
}: {
relatedFlatEntityMaps: RelatedFlatEntityMaps;
flatViewsToDelete: MetadataUniversalFlatEntity<'view'>[];
flatFieldMetadatasToDelete: MetadataUniversalFlatEntity<'fieldMetadata'>[];
}): FlatEntityToDelete<'viewField'> {
const viewFieldUniversalIdentifiers = [
...flatViewsToDelete.flatMap(
(flatView) => flatView.viewFieldUniversalIdentifiers,
),
...flatFieldMetadatasToDelete.flatMap(
(flatFieldMetadata) => flatFieldMetadata.viewFieldUniversalIdentifiers,
),
];
return filterSystemSideEffectFlatViewFieldsToDelete({
viewFieldUniversalIdentifiers,
flatViewFieldMaps: relatedFlatEntityMaps.flatViewFieldMaps,
});
}
}

View File

@@ -0,0 +1,35 @@
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
export const computeCallerFlatFieldMetadatasForObject = ({
objectMetadataUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
allFlatEntityOperationRecordByMetadataName,
displayableOnly,
}: {
objectMetadataUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
allFlatEntityOperationRecordByMetadataName: AllFlatEntityOperationRecordByMetadataName;
displayableOnly: boolean;
}): UniversalFlatFieldMetadata[] =>
orderFlatFieldMetadatasForSystemIndexView({
labelIdentifierFieldMetadataUniversalIdentifier,
flatFieldMetadatas: (
Object.values(
allFlatEntityOperationRecordByMetadataName.fieldMetadata
?.flatEntityToCreate ?? {},
) as UniversalFlatFieldMetadata[]
).filter(
(flatFieldMetadata) =>
flatFieldMetadata.objectMetadataUniversalIdentifier ===
objectMetadataUniversalIdentifier &&
!flatFieldMetadata.isSystemSideEffect &&
(!displayableOnly ||
isFlatFieldMetadataDisplayableInDefaultView({
flatFieldMetadata,
labelIdentifierFieldMetadataUniversalIdentifier,
})),
),
});

View File

@@ -0,0 +1,46 @@
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
export const computeDefaultIndexViewFieldPositionByFieldUniversalIdentifier = ({
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
displayableCallerFlatFieldMetadatas,
}: {
applicationUniversalIdentifier: string;
objectMetadataUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
displayableCallerFlatFieldMetadatas: UniversalFlatFieldMetadata[];
}): Map<string, number> => {
const displayableSystemFlatFieldMetadatas = Object.values(
buildReservedSystemFlatFieldMetadatasForCustomObject({
flatObjectMetadata: {
applicationUniversalIdentifier,
universalIdentifier: objectMetadataUniversalIdentifier,
},
}),
).filter((flatFieldMetadata) =>
isFlatFieldMetadataDisplayableInDefaultView({
flatFieldMetadata,
labelIdentifierFieldMetadataUniversalIdentifier,
}),
);
const orderedDisplayableFlatFieldMetadatas =
orderFlatFieldMetadatasForSystemIndexView({
labelIdentifierFieldMetadataUniversalIdentifier,
flatFieldMetadatas: [
...displayableCallerFlatFieldMetadatas,
...displayableSystemFlatFieldMetadatas,
],
});
return new Map(
orderedDisplayableFlatFieldMetadatas.map((flatFieldMetadata, position) => [
flatFieldMetadata.universalIdentifier,
position,
]),
);
};

View File

@@ -0,0 +1,35 @@
import { isDefined } from 'twenty-shared/utils';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
export const filterSystemSideEffectFlatViewFieldsToDelete = ({
viewFieldUniversalIdentifiers,
flatViewFieldMaps,
}: {
viewFieldUniversalIdentifiers: string[];
flatViewFieldMaps: UniversalFlatEntityMaps<
MetadataUniversalFlatEntity<'viewField'>
>;
}): Record<string, MetadataUniversalFlatEntity<'viewField'>> => {
const viewFieldToDelete: Record<
string,
MetadataUniversalFlatEntity<'viewField'>
> = {};
for (const viewFieldUniversalIdentifier of viewFieldUniversalIdentifiers) {
const flatViewField =
flatViewFieldMaps.byUniversalIdentifier[viewFieldUniversalIdentifier];
if (
!isDefined(flatViewField) ||
flatViewField.isSystemSideEffect !== true
) {
continue;
}
viewFieldToDelete[flatViewField.universalIdentifier] = flatViewField;
}
return viewFieldToDelete;
};

View File

@@ -0,0 +1,110 @@
import { MetadataSideEffectExceptionCode } from 'src/engine/metadata-modules/metadata-side-effect/exceptions/metadata-side-effect-exception-code';
import { MetadataSideEffectEngineService } from 'src/engine/metadata-modules/metadata-side-effect/services/metadata-side-effect-engine.service';
const RESERVED_VIEW_UNIVERSAL_IDENTIFIER =
'a1a2a3a4-a5a6-4000-8000-000000000001';
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
const systemViewEmittingHandler = {
operation: 'create',
metadataName: 'objectMetadata',
name: 'fakeSystemViewEmitter',
buildSideEffects: () => ({
status: 'success',
operations: {
view: {
flatEntityToCreate: {
[RESERVED_VIEW_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: RESERVED_VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
},
},
},
}),
};
const buildEngine = () => {
const registry = {
getRegisteredHandlerKeys: () => [
{ operation: 'create', metadataName: 'objectMetadata' },
],
getHandlers: (operation: string, metadataName: string) =>
operation === 'create' && metadataName === 'objectMetadata'
? [systemViewEmittingHandler]
: [],
};
return new MetadataSideEffectEngineService(registry as never);
};
const buildInput = ({
callerViewUniversalIdentifier,
}: {
callerViewUniversalIdentifier?: string;
}) =>
({
objectMetadata: {
flatEntityToCreate: {
[OBJECT_UNIVERSAL_IDENTIFIER]: {
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
},
},
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
view: {
flatEntityToCreate:
callerViewUniversalIdentifier === undefined
? {}
: {
[callerViewUniversalIdentifier]: {
universalIdentifier: callerViewUniversalIdentifier,
isSystemSideEffect: false,
},
},
flatEntityToUpdate: {},
flatEntityToDelete: {},
},
}) as never;
describe('MetadataSideEffectEngineService reserved identifier collision', () => {
it('should reject a caller-defined entity whose universal identifier collides with a system side effect one', () => {
const engine = buildEngine();
const result = engine.expandWithSideEffects({
allFlatEntityOperationRecordByMetadataName: buildInput({
callerViewUniversalIdentifier: RESERVED_VIEW_UNIVERSAL_IDENTIFIER,
}),
sideEffectRelatedFlatEntityMaps: {},
context: {} as never,
});
expect(result.status).toBe('fail');
if (result.status !== 'fail') {
throw new Error('expected fail');
}
const viewFailures = result.report.view;
expect(viewFailures).toHaveLength(1);
expect(viewFailures[0].errors[0].code).toBe(
MetadataSideEffectExceptionCode.RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER,
);
});
it('should let a caller-defined entity through when its universal identifier does not collide', () => {
const engine = buildEngine();
const result = engine.expandWithSideEffects({
allFlatEntityOperationRecordByMetadataName: buildInput({
callerViewUniversalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000001',
}),
sideEffectRelatedFlatEntityMaps: {},
context: {} as never,
});
expect(result.status).toBe('success');
});
});

View File

@@ -2,12 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import {
ViewKey,
ViewOpenRecordIn,
ViewType,
ViewVisibility,
} from 'twenty-shared/types';
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
import { v4, v5 } from 'uuid';
@@ -489,10 +483,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatApplication: resolvedOwnerFlatApplication,
});
// Default view fields reference the caller-provided fields (reused as-is from
// the transpiler output) plus the engine-owned reserved system fields, whose
// deterministic identifiers we re-derive here (searchVector is never shown).
// TODO: remove once default view fields move to the metadata side effect engine.
// The INDEX table view and its view fields are provisioned by the metadata
// side-effect engine (objectSystemFieldsAndIndexViewOnCreate). The FIELDS_WIDGET
// record-page view below still references the caller-provided fields plus
// the engine-owned reserved system fields, whose deterministic identifiers
// we re-derive here (searchVector is never shown).
// TODO: remove once the record-page view fields move to the side-effect engine.
const defaultFlatFieldMetadatasForViewFields: UniversalFlatFieldMetadata[] =
[
...flatFieldMetadataToCreateOnObject,
@@ -508,19 +504,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
),
];
const flatDefaultViewToCreate = this.computeFlatViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: resolvedOwnerFlatApplication,
});
const flatDefaultViewFieldsToCreate = computeFlatViewFieldsToCreate({
flatApplication: resolvedOwnerFlatApplication,
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
});
const flatNavigationMenuItemToCreate =
await this.computeFlatNavigationMenuItemToCreate({
objectMetadata: flatObjectMetadataToCreate,
@@ -557,7 +540,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const flatRecordPageFieldsViewFieldsToCreate =
computeFlatViewFieldsToCreate({
flatApplication: resolvedOwnerFlatApplication,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
@@ -594,18 +578,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: [
flatDefaultViewToCreate,
flatRecordPageFieldsViewToCreate,
],
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: [
...flatDefaultViewFieldsToCreate,
...flatRecordPageFieldsViewFieldsToCreate,
],
flatEntityToCreate: [...flatRecordPageFieldsViewFieldsToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
@@ -679,54 +657,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
return createdFlatObjectMetadata;
}
private computeFlatViewToCreate({
objectMetadata,
flatApplication,
}: {
flatApplication: FlatApplication;
objectMetadata: UniversalFlatObjectMetadata & { id: string };
}): UniversalFlatView & { id: string } {
const createdAt = new Date().toISOString();
return {
id: v4(),
objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier,
name: `All {objectLabelPlural}`,
key: ViewKey.INDEX,
icon: 'IconList',
type: ViewType.TABLE,
createdAt,
updatedAt: createdAt,
deletedAt: null,
isCustom: true,
anyFieldFilterValue: null,
calendarFieldMetadataUniversalIdentifier: null,
calendarEndFieldMetadataUniversalIdentifier: null,
calendarLayout: null,
isCompact: false,
shouldHideEmptyGroups: false,
kanbanColumnWidth: null,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
mainGroupByFieldMetadataUniversalIdentifier: null,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
position: 0,
universalIdentifier: v4(),
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
applicationUniversalIdentifier: flatApplication.universalIdentifier,
};
}
private computeFlatRecordPageFieldsViewToCreate({
objectMetadata,
flatApplication,

View File

@@ -0,0 +1,56 @@
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
import { ViewKey, ViewType } from 'twenty-shared/types';
import { computeFlatIndexViewToCreate } from '../compute-flat-index-view-to-create.util';
const applicationUniversalIdentifier = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const objectUniversalIdentifier = 'b1b2b3b4-b5b6-4000-8000-000000000001';
describe('computeFlatIndexViewToCreate', () => {
it('should derive the INDEX view universal identifier from the object', () => {
const result = computeFlatIndexViewToCreate({
applicationUniversalIdentifier,
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
});
expect(result.universalIdentifier).toBe(
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier,
objectUniversalIdentifier,
viewKey: ViewKey.INDEX,
}),
);
});
it('should build a system-owned INDEX table view keyed on ViewKey.INDEX', () => {
const result = computeFlatIndexViewToCreate({
applicationUniversalIdentifier,
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
});
expect(result.key).toBe(ViewKey.INDEX);
expect(result.type).toBe(ViewType.TABLE);
expect(result.name).toBe('All {objectLabelPlural}');
expect(result.isSystemSideEffect).toBe(true);
expect(result.objectMetadataUniversalIdentifier).toBe(
objectUniversalIdentifier,
);
expect(result.applicationUniversalIdentifier).toBe(
applicationUniversalIdentifier,
);
});
it('should be deterministic and independent from the view primary key', () => {
const first = computeFlatIndexViewToCreate({
applicationUniversalIdentifier,
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
});
const second = computeFlatIndexViewToCreate({
applicationUniversalIdentifier,
objectMetadata: { universalIdentifier: objectUniversalIdentifier },
});
expect(first.universalIdentifier).toBe(second.universalIdentifier);
expect(first.id).not.toBe(second.id);
});
});

View File

@@ -1,3 +1,4 @@
import { getViewFieldUniversalIdentifier } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
@@ -39,10 +40,7 @@ const makeFieldMetadata = (
} as unknown as UniversalFlatFieldMetadata;
};
const flatApplication = {
universalIdentifier: 'app-uid',
id: 'app-id',
} as never;
const applicationUniversalIdentifier = 'a1a2a3a4-a5a6-4000-8000-000000000001';
const viewUniversalIdentifier = 'view-uid';
@@ -62,7 +60,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
@@ -87,7 +85,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
@@ -116,7 +114,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
@@ -141,7 +139,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
@@ -165,7 +163,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
});
@@ -194,7 +192,7 @@ describe('computeFlatViewFieldsToCreate', () => {
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [labelField, otherField],
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
excludeLabelIdentifier: true,
@@ -205,4 +203,27 @@ describe('computeFlatViewFieldsToCreate', () => {
otherField.universalIdentifier,
);
});
it('should derive the view field universal identifier deterministically', () => {
const field = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [field],
viewUniversalIdentifier,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result[0].universalIdentifier).toBe(
getViewFieldUniversalIdentifier({
applicationUniversalIdentifier,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier: field.universalIdentifier,
}),
);
expect(result[0].isSystemSideEffect).toBe(true);
});
});

View File

@@ -0,0 +1,54 @@
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
const NAME = { universalIdentifier: 'name-uid' };
const CREATED_AT = { universalIdentifier: 'created-at-uid' };
const CREATED_BY = { universalIdentifier: 'created-by-uid' };
describe('orderFlatFieldMetadatasForSystemIndexView', () => {
it('should move the label identifier first when it is not already', () => {
expect(
orderFlatFieldMetadatasForSystemIndexView({
flatFieldMetadatas: [CREATED_AT, NAME, CREATED_BY],
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
}),
).toEqual([NAME, CREATED_AT, CREATED_BY]);
});
it('should preserve the input order of every other field', () => {
expect(
orderFlatFieldMetadatasForSystemIndexView({
flatFieldMetadatas: [NAME, CREATED_BY, CREATED_AT],
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
}),
).toEqual([NAME, CREATED_BY, CREATED_AT]);
});
it('should keep the input order when the object has no label identifier', () => {
expect(
orderFlatFieldMetadatasForSystemIndexView({
flatFieldMetadatas: [CREATED_AT, NAME],
labelIdentifierFieldMetadataUniversalIdentifier: null,
}),
).toEqual([CREATED_AT, NAME]);
});
it('should keep the input order when the label identifier is not in the list', () => {
expect(
orderFlatFieldMetadatasForSystemIndexView({
flatFieldMetadatas: [CREATED_AT, CREATED_BY],
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
}),
).toEqual([CREATED_AT, CREATED_BY]);
});
it('should not mutate the input array', () => {
const flatFieldMetadatas = [CREATED_AT, NAME];
orderFlatFieldMetadatasForSystemIndexView({
flatFieldMetadatas,
labelIdentifierFieldMetadataUniversalIdentifier: 'name-uid',
});
expect(flatFieldMetadatas).toEqual([CREATED_AT, NAME]);
});
});

View File

@@ -0,0 +1,63 @@
import { getSystemViewUniversalIdentifier } from 'twenty-shared/application';
import {
ViewKey,
ViewOpenRecordIn,
ViewType,
ViewVisibility,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
export const computeFlatIndexViewToCreate = ({
objectMetadata,
applicationUniversalIdentifier,
}: {
applicationUniversalIdentifier: string;
objectMetadata: Pick<UniversalFlatObjectMetadata, 'universalIdentifier'>;
}): UniversalFlatView & { id: string } => {
const createdAt = new Date().toISOString();
return {
id: v4(),
objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier,
name: `All {objectLabelPlural}`,
key: ViewKey.INDEX,
icon: 'IconList',
type: ViewType.TABLE,
createdAt,
updatedAt: createdAt,
deletedAt: null,
isCustom: true,
anyFieldFilterValue: null,
calendarFieldMetadataUniversalIdentifier: null,
calendarEndFieldMetadataUniversalIdentifier: null,
calendarLayout: null,
isCompact: false,
shouldHideEmptyGroups: false,
kanbanColumnWidth: null,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
mainGroupByFieldMetadataUniversalIdentifier: null,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
position: 0,
universalIdentifier: getSystemViewUniversalIdentifier({
applicationUniversalIdentifier,
objectUniversalIdentifier: objectMetadata.universalIdentifier,
viewKey: ViewKey.INDEX,
}),
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
applicationUniversalIdentifier,
};
};

View File

@@ -1,74 +1,61 @@
import { v4 } from 'uuid';
import { getViewFieldUniversalIdentifier } from 'twenty-shared/application';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
import { isFlatFieldMetadataDisplayableInDefaultView } from 'src/engine/metadata-modules/object-metadata/utils/is-flat-field-metadata-displayable-in-default-view.util';
import { orderFlatFieldMetadatasForSystemIndexView } from 'src/engine/metadata-modules/object-metadata/utils/order-flat-field-metadatas-for-system-index-view.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { FieldMetadataType } from 'twenty-shared/types';
export const computeFlatViewFieldsToCreate = ({
objectFlatFieldMetadatas,
viewUniversalIdentifier,
flatApplication,
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier = false,
startPosition = 0,
}: {
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
excludeLabelIdentifier?: boolean;
startPosition?: number;
}): UniversalFlatViewField[] => {
const createdAt = new Date().toISOString();
const defaultViewFields = objectFlatFieldMetadatas
.filter(
const defaultViewFields = orderFlatFieldMetadatasForSystemIndexView({
labelIdentifierFieldMetadataUniversalIdentifier,
flatFieldMetadatas: objectFlatFieldMetadatas.filter(
(field) =>
field.name !== 'deletedAt' &&
field.type !== FieldMetadataType.TS_VECTOR &&
field.type !== FieldMetadataType.POSITION &&
field.type !== FieldMetadataType.MORPH_RELATION &&
field.type !== FieldMetadataType.RELATION &&
// Include 'id' only if it's the label identifier (e.g., for junction tables)
(field.name !== 'id' ||
field.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier) &&
isFlatFieldMetadataDisplayableInDefaultView({
flatFieldMetadata: field,
labelIdentifierFieldMetadataUniversalIdentifier,
}) &&
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
(!excludeLabelIdentifier ||
field.universalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier),
)
.sort((a, b) => {
const aIsLabelIdentifierFieldMetadata =
a.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier;
const bIsLabelIdentifierFieldMetadata =
b.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier;
if (aIsLabelIdentifierFieldMetadata && !bIsLabelIdentifierFieldMetadata)
return -1;
if (!aIsLabelIdentifierFieldMetadata && bIsLabelIdentifierFieldMetadata)
return 1;
return 0;
})
.map<UniversalFlatViewField>((field, index) => ({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
),
}).map<UniversalFlatViewField>((field, index) => ({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier,
viewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: v4(),
isVisible: true,
size: DEFAULT_VIEW_FIELD_SIZE,
position: index,
aggregateOperation: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
}));
fieldMetadataUniversalIdentifier: field.universalIdentifier,
}),
isVisible: true,
size: DEFAULT_VIEW_FIELD_SIZE,
position: startPosition + index,
aggregateOperation: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
applicationUniversalIdentifier,
}));
return defaultViewFields;
};

View File

@@ -0,0 +1,22 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
export const isFlatFieldMetadataDisplayableInDefaultView = ({
flatFieldMetadata,
labelIdentifierFieldMetadataUniversalIdentifier,
}: {
flatFieldMetadata: Pick<
UniversalFlatFieldMetadata,
'name' | 'type' | 'universalIdentifier'
>;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
}): boolean =>
flatFieldMetadata.name !== 'deletedAt' &&
flatFieldMetadata.type !== FieldMetadataType.TS_VECTOR &&
flatFieldMetadata.type !== FieldMetadataType.POSITION &&
flatFieldMetadata.type !== FieldMetadataType.MORPH_RELATION &&
flatFieldMetadata.type !== FieldMetadataType.RELATION &&
(flatFieldMetadata.name !== 'id' ||
flatFieldMetadata.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier);

View File

@@ -0,0 +1,20 @@
export const orderFlatFieldMetadatasForSystemIndexView = <
TFlatFieldMetadata extends { universalIdentifier: string },
>({
flatFieldMetadatas,
labelIdentifierFieldMetadataUniversalIdentifier,
}: {
flatFieldMetadatas: TFlatFieldMetadata[];
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
}): TFlatFieldMetadata[] => [
...flatFieldMetadatas.filter(
(flatFieldMetadata) =>
flatFieldMetadata.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier,
),
...flatFieldMetadatas.filter(
(flatFieldMetadata) =>
flatFieldMetadata.universalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier,
),
];

View File

@@ -2493,54 +2493,48 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"id": "00000000-0000-0000-0000-000000000730",
},
},
"id": "00000000-0000-0000-0000-000000000760",
"id": "00000000-0000-0000-0000-000000000758",
"views": {
"allTimelineActivities": {
"id": "00000000-0000-0000-0000-000000000759",
"id": "00000000-0000-0000-0000-000000000757",
"viewFieldGroups": {},
"viewFields": {
"happensAt": {
"id": "00000000-0000-0000-0000-000000000746",
},
"linkedRecordCachedName": {
"id": "00000000-0000-0000-0000-000000000749",
},
"name": {
"id": "00000000-0000-0000-0000-000000000745",
},
"properties": {
"id": "00000000-0000-0000-0000-000000000747",
},
"targetCompany": {
"id": "00000000-0000-0000-0000-000000000751",
"id": "00000000-0000-0000-0000-000000000749",
},
"targetDashboard": {
"id": "00000000-0000-0000-0000-000000000758",
},
"targetNote": {
"id": "00000000-0000-0000-0000-000000000754",
},
"targetOpportunity": {
"id": "00000000-0000-0000-0000-000000000752",
},
"targetPerson": {
"id": "00000000-0000-0000-0000-000000000750",
},
"targetTask": {
"id": "00000000-0000-0000-0000-000000000753",
},
"targetWorkflow": {
"id": "00000000-0000-0000-0000-000000000755",
},
"targetWorkflowRun": {
"id": "00000000-0000-0000-0000-000000000757",
},
"targetWorkflowVersion": {
"id": "00000000-0000-0000-0000-000000000756",
},
"workspaceMember": {
"targetNote": {
"id": "00000000-0000-0000-0000-000000000752",
},
"targetOpportunity": {
"id": "00000000-0000-0000-0000-000000000750",
},
"targetPerson": {
"id": "00000000-0000-0000-0000-000000000748",
},
"targetTask": {
"id": "00000000-0000-0000-0000-000000000751",
},
"targetWorkflow": {
"id": "00000000-0000-0000-0000-000000000753",
},
"targetWorkflowRun": {
"id": "00000000-0000-0000-0000-000000000755",
},
"targetWorkflowVersion": {
"id": "00000000-0000-0000-0000-000000000754",
},
"workspaceMember": {
"id": "00000000-0000-0000-0000-000000000747",
},
},
"viewGroups": {},
},
@@ -2549,80 +2543,80 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"workflow": {
"fields": {
"attachments": {
"id": "00000000-0000-0000-0000-000000000777",
},
"automatedTriggers": {
"id": "00000000-0000-0000-0000-000000000775",
},
"coreWorkflowId": {
"id": "00000000-0000-0000-0000-000000000771",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000762",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000765",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000764",
},
"id": {
"id": "00000000-0000-0000-0000-000000000761",
},
"lastPublishedVersionId": {
"id": "00000000-0000-0000-0000-000000000770",
},
"name": {
"id": "00000000-0000-0000-0000-000000000769",
},
"position": {
"id": "00000000-0000-0000-0000-000000000767",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000774",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000768",
},
"statuses": {
"id": "00000000-0000-0000-0000-000000000772",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000776",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000763",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000766",
},
"versions": {
"automatedTriggers": {
"id": "00000000-0000-0000-0000-000000000773",
},
"coreWorkflowId": {
"id": "00000000-0000-0000-0000-000000000769",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000760",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000763",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000762",
},
"id": {
"id": "00000000-0000-0000-0000-000000000759",
},
"lastPublishedVersionId": {
"id": "00000000-0000-0000-0000-000000000768",
},
"name": {
"id": "00000000-0000-0000-0000-000000000767",
},
"position": {
"id": "00000000-0000-0000-0000-000000000765",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000772",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000766",
},
"statuses": {
"id": "00000000-0000-0000-0000-000000000770",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000774",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000761",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000764",
},
"versions": {
"id": "00000000-0000-0000-0000-000000000771",
},
},
"id": "00000000-0000-0000-0000-000000000785",
"id": "00000000-0000-0000-0000-000000000783",
"views": {
"allWorkflows": {
"id": "00000000-0000-0000-0000-000000000784",
"id": "00000000-0000-0000-0000-000000000782",
"viewFieldGroups": {},
"viewFields": {
"createdBy": {
"id": "00000000-0000-0000-0000-000000000781",
},
"name": {
"id": "00000000-0000-0000-0000-000000000778",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000783",
},
"statuses": {
"id": "00000000-0000-0000-0000-000000000779",
},
"name": {
"id": "00000000-0000-0000-0000-000000000776",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000781",
},
"statuses": {
"id": "00000000-0000-0000-0000-000000000777",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000780",
"id": "00000000-0000-0000-0000-000000000778",
},
"versions": {
"id": "00000000-0000-0000-0000-000000000782",
"id": "00000000-0000-0000-0000-000000000780",
},
},
"viewGroups": {},
@@ -2632,80 +2626,80 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"workflowAutomatedTrigger": {
"fields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000787",
"id": "00000000-0000-0000-0000-000000000785",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000790",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000789",
},
"id": {
"id": "00000000-0000-0000-0000-000000000786",
},
"position": {
"id": "00000000-0000-0000-0000-000000000792",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000793",
},
"settings": {
"id": "00000000-0000-0000-0000-000000000795",
},
"type": {
"id": "00000000-0000-0000-0000-000000000794",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000788",
},
"updatedBy": {
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000787",
},
"id": {
"id": "00000000-0000-0000-0000-000000000784",
},
"position": {
"id": "00000000-0000-0000-0000-000000000790",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000791",
},
"settings": {
"id": "00000000-0000-0000-0000-000000000793",
},
"type": {
"id": "00000000-0000-0000-0000-000000000792",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000786",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000789",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000796",
"id": "00000000-0000-0000-0000-000000000794",
},
},
"id": "00000000-0000-0000-0000-000000000808",
"id": "00000000-0000-0000-0000-000000000806",
"views": {
"allWorkflowAutomatedTriggers": {
"id": "00000000-0000-0000-0000-000000000800",
"id": "00000000-0000-0000-0000-000000000798",
"viewFieldGroups": {},
"viewFields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000799",
},
"type": {
"id": "00000000-0000-0000-0000-000000000797",
},
"type": {
"id": "00000000-0000-0000-0000-000000000795",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000798",
"id": "00000000-0000-0000-0000-000000000796",
},
},
"viewGroups": {},
},
"workflowAutomatedTriggerRecordPageFields": {
"id": "00000000-0000-0000-0000-000000000807",
"id": "00000000-0000-0000-0000-000000000805",
"viewFieldGroups": {
"general": {
"id": "00000000-0000-0000-0000-000000000805",
"id": "00000000-0000-0000-0000-000000000803",
},
"system": {
"id": "00000000-0000-0000-0000-000000000806",
"id": "00000000-0000-0000-0000-000000000804",
},
},
"viewFields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000803",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000804",
},
"type": {
"id": "00000000-0000-0000-0000-000000000801",
},
"workflow": {
"createdBy": {
"id": "00000000-0000-0000-0000-000000000802",
},
"type": {
"id": "00000000-0000-0000-0000-000000000799",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000800",
},
},
"viewGroups": {},
},
@@ -2714,134 +2708,125 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"workflowRun": {
"fields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000810",
"id": "00000000-0000-0000-0000-000000000808",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000813",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000812",
},
"endedAt": {
"id": "00000000-0000-0000-0000-000000000822",
},
"enqueuedAt": {
"id": "00000000-0000-0000-0000-000000000820",
},
"id": {
"id": "00000000-0000-0000-0000-000000000809",
},
"name": {
"id": "00000000-0000-0000-0000-000000000817",
},
"position": {
"id": "00000000-0000-0000-0000-000000000815",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000816",
},
"startedAt": {
"id": "00000000-0000-0000-0000-000000000821",
},
"state": {
"id": "00000000-0000-0000-0000-000000000824",
},
"status": {
"id": "00000000-0000-0000-0000-000000000823",
},
"stepLogs": {
"id": "00000000-0000-0000-0000-000000000825",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000826",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000811",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000814",
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000810",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000819",
"endedAt": {
"id": "00000000-0000-0000-0000-000000000820",
},
"workflowVersion": {
"enqueuedAt": {
"id": "00000000-0000-0000-0000-000000000818",
},
"id": {
"id": "00000000-0000-0000-0000-000000000807",
},
"name": {
"id": "00000000-0000-0000-0000-000000000815",
},
"position": {
"id": "00000000-0000-0000-0000-000000000813",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000814",
},
"startedAt": {
"id": "00000000-0000-0000-0000-000000000819",
},
"state": {
"id": "00000000-0000-0000-0000-000000000822",
},
"status": {
"id": "00000000-0000-0000-0000-000000000821",
},
"stepLogs": {
"id": "00000000-0000-0000-0000-000000000823",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000824",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000809",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000812",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000817",
},
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000816",
},
},
"id": "00000000-0000-0000-0000-000000000849",
"id": "00000000-0000-0000-0000-000000000844",
"views": {
"allWorkflowRuns": {
"id": "00000000-0000-0000-0000-000000000833",
"id": "00000000-0000-0000-0000-000000000828",
"viewFieldGroups": {},
"viewFields": {
"createdBy": {
"id": "00000000-0000-0000-0000-000000000831",
},
"name": {
"id": "00000000-0000-0000-0000-000000000827",
},
"startedAt": {
"id": "00000000-0000-0000-0000-000000000830",
"id": "00000000-0000-0000-0000-000000000825",
},
"status": {
"id": "00000000-0000-0000-0000-000000000829",
"id": "00000000-0000-0000-0000-000000000827",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000828",
},
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000832",
"id": "00000000-0000-0000-0000-000000000826",
},
},
"viewGroups": {},
},
"workflowRunRecordPageFields": {
"id": "00000000-0000-0000-0000-000000000848",
"id": "00000000-0000-0000-0000-000000000843",
"viewFieldGroups": {
"general": {
"id": "00000000-0000-0000-0000-000000000846",
"id": "00000000-0000-0000-0000-000000000841",
},
"system": {
"id": "00000000-0000-0000-0000-000000000847",
"id": "00000000-0000-0000-0000-000000000842",
},
},
"viewFields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000839",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000840",
},
"endedAt": {
"id": "00000000-0000-0000-0000-000000000838",
},
"enqueuedAt": {
"id": "00000000-0000-0000-0000-000000000841",
},
"startedAt": {
"id": "00000000-0000-0000-0000-000000000837",
},
"state": {
"id": "00000000-0000-0000-0000-000000000842",
},
"status": {
"id": "00000000-0000-0000-0000-000000000834",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000845",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000843",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000844",
},
"workflow": {
"createdBy": {
"id": "00000000-0000-0000-0000-000000000835",
},
"workflowVersion": {
"endedAt": {
"id": "00000000-0000-0000-0000-000000000833",
},
"enqueuedAt": {
"id": "00000000-0000-0000-0000-000000000836",
},
"startedAt": {
"id": "00000000-0000-0000-0000-000000000832",
},
"state": {
"id": "00000000-0000-0000-0000-000000000837",
},
"status": {
"id": "00000000-0000-0000-0000-000000000829",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000840",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000838",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000839",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000830",
},
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000831",
},
},
"viewGroups": {},
},
@@ -2850,118 +2835,118 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"workflowVersion": {
"fields": {
"coreWorkflowVersionId": {
"id": "00000000-0000-0000-0000-000000000864",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000851",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000854",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000853",
},
"id": {
"id": "00000000-0000-0000-0000-000000000850",
},
"name": {
"id": "00000000-0000-0000-0000-000000000858",
},
"position": {
"id": "00000000-0000-0000-0000-000000000856",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000862",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000857",
},
"status": {
"id": "00000000-0000-0000-0000-000000000861",
},
"steps": {
"id": "00000000-0000-0000-0000-000000000863",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000865",
},
"trigger": {
"id": "00000000-0000-0000-0000-000000000860",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000852",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000855",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000859",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000846",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000849",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000848",
},
"id": {
"id": "00000000-0000-0000-0000-000000000845",
},
"name": {
"id": "00000000-0000-0000-0000-000000000853",
},
"position": {
"id": "00000000-0000-0000-0000-000000000851",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000857",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000852",
},
"status": {
"id": "00000000-0000-0000-0000-000000000856",
},
"steps": {
"id": "00000000-0000-0000-0000-000000000858",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000860",
},
"trigger": {
"id": "00000000-0000-0000-0000-000000000855",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000847",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000850",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000854",
},
},
"id": "00000000-0000-0000-0000-000000000885",
"id": "00000000-0000-0000-0000-000000000880",
"views": {
"allWorkflowVersions": {
"id": "00000000-0000-0000-0000-000000000871",
"id": "00000000-0000-0000-0000-000000000866",
"viewFieldGroups": {},
"viewFields": {
"name": {
"id": "00000000-0000-0000-0000-000000000866",
"id": "00000000-0000-0000-0000-000000000861",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000870",
"id": "00000000-0000-0000-0000-000000000865",
},
"status": {
"id": "00000000-0000-0000-0000-000000000868",
"id": "00000000-0000-0000-0000-000000000863",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000869",
"id": "00000000-0000-0000-0000-000000000864",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000867",
"id": "00000000-0000-0000-0000-000000000862",
},
},
"viewGroups": {},
},
"workflowVersionRecordPageFields": {
"id": "00000000-0000-0000-0000-000000000884",
"id": "00000000-0000-0000-0000-000000000879",
"viewFieldGroups": {
"general": {
"id": "00000000-0000-0000-0000-000000000882",
"id": "00000000-0000-0000-0000-000000000877",
},
"system": {
"id": "00000000-0000-0000-0000-000000000883",
"id": "00000000-0000-0000-0000-000000000878",
},
},
"viewFields": {
"createdAt": {
"id": "00000000-0000-0000-0000-000000000875",
"id": "00000000-0000-0000-0000-000000000870",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000877",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000880",
},
"status": {
"id": "00000000-0000-0000-0000-000000000872",
},
"runs": {
"id": "00000000-0000-0000-0000-000000000875",
},
"status": {
"id": "00000000-0000-0000-0000-000000000867",
},
"steps": {
"id": "00000000-0000-0000-0000-000000000876",
"id": "00000000-0000-0000-0000-000000000871",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000881",
"id": "00000000-0000-0000-0000-000000000876",
},
"trigger": {
"id": "00000000-0000-0000-0000-000000000874",
"id": "00000000-0000-0000-0000-000000000869",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000878",
"id": "00000000-0000-0000-0000-000000000873",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000879",
"id": "00000000-0000-0000-0000-000000000874",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000873",
"id": "00000000-0000-0000-0000-000000000868",
},
},
"viewGroups": {},
@@ -2971,125 +2956,104 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
"workspaceMember": {
"fields": {
"accountOwnerForCompanies": {
"id": "00000000-0000-0000-0000-000000000903",
},
"assignedTasks": {
"id": "00000000-0000-0000-0000-000000000901",
},
"avatarUrl": {
"id": "00000000-0000-0000-0000-000000000897",
},
"blocklist": {
"id": "00000000-0000-0000-0000-000000000905",
},
"calendarEventParticipants": {
"id": "00000000-0000-0000-0000-000000000906",
},
"calendarStartDay": {
"id": "00000000-0000-0000-0000-000000000911",
},
"colorScheme": {
"id": "00000000-0000-0000-0000-000000000895",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000887",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000890",
},
"dateFormat": {
"id": "00000000-0000-0000-0000-000000000909",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000889",
},
"id": {
"id": "00000000-0000-0000-0000-000000000886",
},
"jobTitle": {
"id": "00000000-0000-0000-0000-000000000899",
},
"locale": {
"id": "00000000-0000-0000-0000-000000000896",
},
"messageParticipants": {
"id": "00000000-0000-0000-0000-000000000904",
},
"name": {
"id": "00000000-0000-0000-0000-000000000894",
},
"numberFormat": {
"id": "00000000-0000-0000-0000-000000000912",
},
"ownedOpportunities": {
"id": "00000000-0000-0000-0000-000000000902",
},
"position": {
"id": "00000000-0000-0000-0000-000000000892",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000893",
},
"timeFormat": {
"id": "00000000-0000-0000-0000-000000000910",
},
"timeZone": {
"id": "00000000-0000-0000-0000-000000000908",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000907",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000888",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000891",
},
"userEmail": {
"id": "00000000-0000-0000-0000-000000000898",
},
"userId": {
"assignedTasks": {
"id": "00000000-0000-0000-0000-000000000896",
},
"avatarUrl": {
"id": "00000000-0000-0000-0000-000000000892",
},
"blocklist": {
"id": "00000000-0000-0000-0000-000000000900",
},
"calendarEventParticipants": {
"id": "00000000-0000-0000-0000-000000000901",
},
"calendarStartDay": {
"id": "00000000-0000-0000-0000-000000000906",
},
"colorScheme": {
"id": "00000000-0000-0000-0000-000000000890",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000882",
},
"createdBy": {
"id": "00000000-0000-0000-0000-000000000885",
},
"dateFormat": {
"id": "00000000-0000-0000-0000-000000000904",
},
"deletedAt": {
"id": "00000000-0000-0000-0000-000000000884",
},
"id": {
"id": "00000000-0000-0000-0000-000000000881",
},
"jobTitle": {
"id": "00000000-0000-0000-0000-000000000894",
},
"locale": {
"id": "00000000-0000-0000-0000-000000000891",
},
"messageParticipants": {
"id": "00000000-0000-0000-0000-000000000899",
},
"name": {
"id": "00000000-0000-0000-0000-000000000889",
},
"numberFormat": {
"id": "00000000-0000-0000-0000-000000000907",
},
"ownedOpportunities": {
"id": "00000000-0000-0000-0000-000000000897",
},
"position": {
"id": "00000000-0000-0000-0000-000000000887",
},
"searchVector": {
"id": "00000000-0000-0000-0000-000000000888",
},
"timeFormat": {
"id": "00000000-0000-0000-0000-000000000905",
},
"timeZone": {
"id": "00000000-0000-0000-0000-000000000903",
},
"timelineActivities": {
"id": "00000000-0000-0000-0000-000000000902",
},
"updatedAt": {
"id": "00000000-0000-0000-0000-000000000883",
},
"updatedBy": {
"id": "00000000-0000-0000-0000-000000000886",
},
"userEmail": {
"id": "00000000-0000-0000-0000-000000000893",
},
"userId": {
"id": "00000000-0000-0000-0000-000000000895",
},
},
"id": "00000000-0000-0000-0000-000000000925",
"id": "00000000-0000-0000-0000-000000000913",
"views": {
"allWorkspaceMembers": {
"id": "00000000-0000-0000-0000-000000000924",
"id": "00000000-0000-0000-0000-000000000912",
"viewFieldGroups": {},
"viewFields": {
"assignedTasks": {
"id": "00000000-0000-0000-0000-000000000923",
},
"avatarUrl": {
"id": "00000000-0000-0000-0000-000000000915",
},
"colorScheme": {
"id": "00000000-0000-0000-0000-000000000916",
"id": "00000000-0000-0000-0000-000000000911",
},
"createdAt": {
"id": "00000000-0000-0000-0000-000000000921",
},
"dateFormat": {
"id": "00000000-0000-0000-0000-000000000919",
},
"locale": {
"id": "00000000-0000-0000-0000-000000000917",
"id": "00000000-0000-0000-0000-000000000909",
},
"name": {
"id": "00000000-0000-0000-0000-000000000913",
"id": "00000000-0000-0000-0000-000000000908",
},
"ownedOpportunities": {
"id": "00000000-0000-0000-0000-000000000922",
},
"timeFormat": {
"id": "00000000-0000-0000-0000-000000000920",
},
"timeZone": {
"id": "00000000-0000-0000-0000-000000000918",
},
"userEmail": {
"id": "00000000-0000-0000-0000-000000000914",
"id": "00000000-0000-0000-0000-000000000910",
},
},
"viewGroups": {},

View File

@@ -1,17 +1,21 @@
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
import { v4 } from 'uuid';
let uuidCounter = 0;
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
jest.mock('uuid', () => ({
...jest.requireActual('uuid'),
v4: jest.fn(
() => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
),
v4: jest.fn(),
}));
describe('getStandardObjectMetadataRelatedEntityIds', () => {
let uuidCounter = 0;
beforeEach(() => {
uuidCounter = 0;
(v4 as jest.Mock).mockImplementation(
() =>
`00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
);
});
afterAll(() => {

View File

@@ -79,8 +79,6 @@ export type BuildStandardFlatViewFieldMetadataMapsArgs = Omit<
export const buildStandardFlatViewFieldMetadataMaps = (
args: BuildStandardFlatViewFieldMetadataMapsArgs,
): FlatEntityMaps<FlatViewField> => {
const { flatViewMaps } = args.dependencyFlatEntityMaps;
const allViewFieldMetadatas: FlatViewField[] = (
Object.keys(
STANDARD_FLAT_VIEW_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME,
@@ -100,16 +98,8 @@ export const buildStandardFlatViewFieldMetadataMaps = (
let flatViewFieldMaps = createEmptyFlatEntityMaps();
for (const viewFieldMetadata of allViewFieldMetadatas) {
const parentView =
flatViewMaps.byUniversalIdentifier[
viewFieldMetadata.viewUniversalIdentifier
];
flatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: {
...viewFieldMetadata,
isSystemSideEffect: parentView?.isSystemSideEffect ?? false,
},
flatEntity: viewFieldMetadata,
flatEntityMaps: flatViewFieldMaps,
});
}

View File

@@ -1,7 +1,7 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type AggregateOperations } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type AggregateOperations } from 'twenty-shared/types';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
@@ -51,6 +51,7 @@ export const createStandardViewFieldFlatMetadata = <
viewFieldGroupName = null,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}: CreateStandardViewFieldArgs<O, V>): FlatViewField => {
@@ -79,6 +80,17 @@ export const createStandardViewFieldFlatMetadata = <
);
}
const parentView =
dependencyFlatEntityMaps.flatViewMaps.byUniversalIdentifier[
viewDefinition.universalIdentifier
];
if (!isDefined(parentView)) {
throw new Error(
`Missing parent view ${objectName} ${viewName.toString()} for view field ${viewFieldName}`,
);
}
let viewFieldGroupId: string | null = null;
let viewFieldGroupUniversalIdentifier: string | null = null;
@@ -121,7 +133,7 @@ export const createStandardViewFieldFlatMetadata = <
size,
aggregateOperation,
isActive: true,
isSystemSideEffect: false,
isSystemSideEffect: parentView.isSystemSideEffect,
overrides: null,
universalOverrides: null,
createdAt: now,

View File

@@ -54,7 +54,6 @@ type Assertions = [
| 'deletedAt'
| 'type'
| 'position'
| 'key'
| 'isCompact'
| 'openRecordIn'
| 'kanbanAggregateOperation'

View File

@@ -0,0 +1,186 @@
import { ViewKey, ViewType } from 'twenty-shared/types';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service';
const OBJECT_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000b1';
const VIEW_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000c1';
const OTHER_VIEW_UNIVERSAL_IDENTIFIER = '00000000-0000-4000-8000-0000000000c2';
type TestFlatView = {
universalIdentifier: string;
objectMetadataUniversalIdentifier: string;
key: ViewKey | null;
type: ViewType;
isSystemSideEffect: boolean;
deletedAt: string | null;
};
const buildFlatView = (
overrides: Partial<TestFlatView> = {},
): TestFlatView => ({
universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER,
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
key: null,
type: ViewType.TABLE,
isSystemSideEffect: false,
deletedAt: null,
...overrides,
});
const mapsFrom = (
entities: { universalIdentifier: string; [key: string]: unknown }[],
) => ({
byUniversalIdentifier: Object.fromEntries(
entities.map((entity) => [entity.universalIdentifier, entity]),
),
});
const buildCreationArgs = ({
flatViewToValidate,
existingFlatViews = [],
}: {
flatViewToValidate: TestFlatView;
existingFlatViews?: TestFlatView[];
}) =>
({
flatEntityToValidate: flatViewToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatViewMaps: mapsFrom(existingFlatViews),
flatFieldMetadataMaps: mapsFrom([]),
flatObjectMetadataMaps: mapsFrom([
{
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifiers: existingFlatViews.map(
(existingFlatView) => existingFlatView.universalIdentifier,
),
},
]),
},
additionalCacheDataMaps: { featureFlagsMap: {} },
}) as unknown as Parameters<
FlatViewValidatorService['validateFlatViewCreation']
>[0];
const buildUpdateArgs = ({
flatEntityUpdate,
existingFlatView,
}: {
flatEntityUpdate: Record<string, unknown>;
existingFlatView: TestFlatView;
}) =>
({
universalIdentifier: existingFlatView.universalIdentifier,
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatViewMaps: mapsFrom([existingFlatView]),
flatFieldMetadataMaps: mapsFrom([]),
},
additionalCacheDataMaps: { featureFlagsMap: {} },
}) as unknown as Parameters<
FlatViewValidatorService['validateFlatViewUpdate']
>[0];
describe('FlatViewValidatorService INDEX key reservation', () => {
let service: FlatViewValidatorService;
beforeEach(() => {
service = new FlatViewValidatorService();
});
describe('creation', () => {
it('rejects a caller-provided INDEX view', () => {
const result = service.validateFlatViewCreation(
buildCreationArgs({
flatViewToValidate: buildFlatView({
key: ViewKey.INDEX,
isSystemSideEffect: false,
}),
}),
);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].message).toContain('reserved');
});
it('accepts an engine-emitted INDEX view', () => {
const result = service.validateFlatViewCreation(
buildCreationArgs({
flatViewToValidate: buildFlatView({
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
}),
);
expect(result.errors).toEqual([]);
});
it('rejects a second INDEX view on the same object', () => {
const result = service.validateFlatViewCreation(
buildCreationArgs({
flatViewToValidate: buildFlatView({
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
existingFlatViews: [
buildFlatView({
universalIdentifier: OTHER_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
],
}),
);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].message).toContain('already has an INDEX view');
});
it('ignores a soft-deleted INDEX view when checking the singleton', () => {
const result = service.validateFlatViewCreation(
buildCreationArgs({
flatViewToValidate: buildFlatView({
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
existingFlatViews: [
buildFlatView({
universalIdentifier: OTHER_VIEW_UNIVERSAL_IDENTIFIER,
key: ViewKey.INDEX,
isSystemSideEffect: true,
deletedAt: '2024-01-01T00:00:00.000Z',
}),
],
}),
);
expect(result.errors).toEqual([]);
});
it('accepts a caller-provided view without a key', () => {
const result = service.validateFlatViewCreation(
buildCreationArgs({
flatViewToValidate: buildFlatView({ key: null }),
}),
);
expect(result.errors).toEqual([]);
});
});
describe('update', () => {
it('accepts an update on an INDEX view', () => {
const result = service.validateFlatViewUpdate(
buildUpdateArgs({
flatEntityUpdate: { name: 'Renamed default view' },
existingFlatView: buildFlatView({
key: ViewKey.INDEX,
isSystemSideEffect: true,
}),
}),
);
expect(result.errors).toEqual([]);
});
});
});

View File

@@ -5,6 +5,7 @@ import {
FieldMetadataType,
RelationType,
ViewCalendarLayout,
ViewKey,
ViewType,
} from 'twenty-shared/types';
import { getViewLayoutFromViewType, isDefined } from 'twenty-shared/utils';
@@ -459,6 +460,41 @@ export class FlatViewValidatorService {
});
}
if (flatViewToValidate.key === ViewKey.INDEX) {
if (flatViewToValidate.isSystemSideEffect !== true) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`The INDEX view key is reserved for the engine-owned default view; remove the key from the view definition`,
userFriendlyMessage: msg`The INDEX view key is reserved for the default view`,
});
}
const objectAlreadyHasIndexFlatView =
isDefined(optimisticFlatObjectMetadata) &&
optimisticFlatObjectMetadata.viewUniversalIdentifiers.some(
(viewUniversalIdentifier) => {
const flatView = findFlatEntityByUniversalIdentifier({
universalIdentifier: viewUniversalIdentifier,
flatEntityMaps: optimisticFlatViewMaps,
});
return (
isDefined(flatView) &&
flatView.key === ViewKey.INDEX &&
!isDefined(flatView.deletedAt)
);
},
);
if (objectAlreadyHasIndexFlatView) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Object already has an INDEX view`,
userFriendlyMessage: msg`This object already has a default view`,
});
}
}
if (
isDefined(
flatViewToValidate.kanbanAggregateOperationFieldMetadataUniversalIdentifier,

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Failing manifest update - standalone view fields on existing views rejects a standalone view field whose target view does not exist 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 viewField",
"summary": {
"totalErrors": 1,
"viewField": 1,
},
"userFriendlyMessage": "View not found",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Failing manifest update - standalone view fields on existing views rejects two standalone view fields targeting the same field on the same view 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "View field with same fieldMetadataUniversalIdentifier and viewUniversalIdentifier already exists",
"userFriendlyMessage": "View field already exists",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 viewField",
"summary": {
"totalErrors": 1,
"viewField": 1,
},
"userFriendlyMessage": "View field already exists",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;

View File

@@ -62,13 +62,33 @@ exports[`Sync application should reject hijacking the searchVector system field
"type": "create",
},
],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 fieldMetadata",
"message": "Validation failed for 1 fieldMetadata, 1 viewField",
"summary": {
"fieldMetadata": 1,
"totalErrors": 1,
"totalErrors": 2,
"viewField": 1,
},
"userFriendlyMessage": "Name "searchVector" is not available as it is already used by another field",
"userFriendlyMessage": "Many validation errors",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",

View File

@@ -35,6 +35,27 @@ exports[`syncApplication should create a TEXT field on the standard Company obje
"metadataName": "fieldMetadata",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": false,
"position": 14,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
@@ -738,6 +759,168 @@ exports[`syncApplication should delete old field and create equivalent one when
"metadataName": "index",
"type": "create",
},
{
"flatEntity": {
"anyFieldFilterValue": null,
"applicationUniversalIdentifier": Any<String>,
"calendarEndFieldMetadataUniversalIdentifier": null,
"calendarFieldMetadataUniversalIdentifier": null,
"calendarLayout": null,
"createdAt": Any<String>,
"createdByUserWorkspaceId": null,
"deletedAt": null,
"icon": "IconList",
"id": Any<String>,
"isActive": true,
"isCompact": false,
"isCustom": true,
"isSystemSideEffect": true,
"kanbanAggregateOperation": null,
"kanbanAggregateOperationFieldMetadataUniversalIdentifier": null,
"kanbanColumnWidth": null,
"key": "INDEX",
"mainGroupByFieldMetadataUniversalIdentifier": null,
"name": "All {objectLabelPlural}",
"objectMetadataUniversalIdentifier": Any<String>,
"openRecordIn": "SIDE_PANEL",
"position": 0,
"shouldHideEmptyGroups": false,
"type": "TABLE",
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"visibility": "WORKSPACE",
},
"id": Any<String>,
"metadataName": "view",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 1,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 0,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 2,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 3,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 4,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 5,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
@@ -803,7 +986,9 @@ exports[`syncApplication should delete old field and create equivalent one when
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
"viewFieldUniversalIdentifiers": [],
"viewFieldUniversalIdentifiers": [
"da099a9e-dfb5-5eb3-bc29-e7a789e4b765",
],
"viewFilterUniversalIdentifiers": [],
"viewSortUniversalIdentifiers": [],
},
@@ -842,6 +1027,49 @@ exports[`syncApplication should delete old field and create equivalent one when
"metadataName": "fieldMetadata",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 1,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "delete",
"universalIdentifier": Any<String>,
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": false,
"position": 6,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
@@ -1522,6 +1750,168 @@ exports[`syncApplication should return workspace migration actions on initial sy
"metadataName": "index",
"type": "create",
},
{
"flatEntity": {
"anyFieldFilterValue": null,
"applicationUniversalIdentifier": Any<String>,
"calendarEndFieldMetadataUniversalIdentifier": null,
"calendarFieldMetadataUniversalIdentifier": null,
"calendarLayout": null,
"createdAt": Any<String>,
"createdByUserWorkspaceId": null,
"deletedAt": null,
"icon": "IconList",
"id": Any<String>,
"isActive": true,
"isCompact": false,
"isCustom": true,
"isSystemSideEffect": true,
"kanbanAggregateOperation": null,
"kanbanAggregateOperationFieldMetadataUniversalIdentifier": null,
"kanbanColumnWidth": null,
"key": "INDEX",
"mainGroupByFieldMetadataUniversalIdentifier": null,
"name": "All {objectLabelPlural}",
"objectMetadataUniversalIdentifier": Any<String>,
"openRecordIn": "SIDE_PANEL",
"position": 0,
"shouldHideEmptyGroups": false,
"type": "TABLE",
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"visibility": "WORKSPACE",
},
"id": Any<String>,
"metadataName": "view",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 1,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 0,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 2,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 3,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 4,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"aggregateOperation": null,
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"deletedAt": null,
"fieldMetadataUniversalIdentifier": Any<String>,
"isActive": true,
"isSystemSideEffect": true,
"isVisible": true,
"position": 5,
"size": 180,
"universalIdentifier": Any<String>,
"universalOverrides": null,
"updatedAt": Any<String>,
"viewFieldGroupUniversalIdentifier": null,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"type": "create",
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,

View File

@@ -0,0 +1,114 @@
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { type FieldManifest, type Manifest } from 'twenty-shared/application';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_VIEW_FIELD_ID = uuidv4();
const TEST_SECOND_VIEW_FIELD_ID = uuidv4();
const PERSON_OBJECT_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.universalIdentifier;
const PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.views.personRecordPageFields.universalIdentifier;
const PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.views.personRecordPageFields.viewFieldGroups.general
.universalIdentifier;
const CUSTOM_FIELD_NAME = 'integrationContributedColumn';
const personFieldManifest: FieldManifest = {
universalIdentifier: TEST_FIELD_ID,
type: FieldMetadataType.TEXT,
name: CUSTOM_FIELD_NAME,
label: 'Integration Contributed Column',
description: 'Custom field contributed to the standard Person object',
icon: 'IconStar',
objectUniversalIdentifier: PERSON_OBJECT_UNIVERSAL_IDENTIFIER,
};
const buildManifest = (
overrides?: Partial<Pick<Manifest, 'fields' | 'viewFields'>>,
) =>
buildBaseManifest({
appId: TEST_APP_ID,
roleId: TEST_ROLE_ID,
overrides,
});
describe('Failing manifest update - standalone view fields on existing views', () => {
beforeEach(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'App for testing standalone view field manifest updates',
sourcePath: 'test-manifest-update-view-field',
});
}, 60000);
afterEach(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_ID,
});
});
it('rejects two standalone view fields targeting the same field on the same view', async () => {
const { errors } = await syncApplication({
manifest: buildManifest({
fields: [personFieldManifest],
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier:
PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER,
viewFieldGroupUniversalIdentifier:
PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
},
{
universalIdentifier: TEST_SECOND_VIEW_FIELD_ID,
viewUniversalIdentifier:
PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER,
viewFieldGroupUniversalIdentifier:
PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 11,
isVisible: true,
},
],
}),
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({ errors });
}, 60000);
it('rejects a standalone view field whose target view does not exist', async () => {
const { errors } = await syncApplication({
manifest: buildManifest({
fields: [personFieldManifest],
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier: uuidv4(),
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
},
],
}),
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({ errors });
}, 60000);
});

View File

@@ -1,11 +1,14 @@
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { findViewFields } from 'test/integration/metadata/suites/view-field/utils/find-view-fields.util';
import { findViews } from 'test/integration/metadata/suites/view/utils/find-views.util';
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
@@ -80,6 +83,54 @@ const findTicketObject = async () => {
);
};
// The label identifier view field on the INDEX view must be visible and
// strictly lowest, whatever provisioned it (fieldIndexViewFieldOnCreate on
// creation, objectIndexViewLabelIdentifierOnUpdate on relabel).
const expectLabelIdentifierIndexViewFieldVisibleAndLowest = async ({
objectMetadataId,
labelFieldMetadataId,
}: {
objectMetadataId: string;
labelFieldMetadataId: string;
}) => {
const { data: viewsData } = await findViews({
objectMetadataId,
gqlFields: 'id key',
expectToFail: false,
});
const indexView = viewsData?.getViews.find(
(view) => view.key === ViewKey.INDEX,
);
if (!isDefined(indexView)) {
throw new Error('expected an INDEX view for the ticket object');
}
const { data: viewFieldsData } = await findViewFields({
viewId: indexView.id,
gqlFields: VIEW_FIELD_GQL_FIELDS,
expectToFail: false,
});
const viewFields = viewFieldsData?.getViewFields ?? [];
const labelViewField = viewFields.find(
(viewField) => viewField.fieldMetadataId === labelFieldMetadataId,
);
if (!isDefined(labelViewField)) {
throw new Error('expected an INDEX view field for the label identifier');
}
expect(labelViewField.isVisible).toBe(true);
for (const viewField of viewFields) {
if (viewField.id !== labelViewField.id) {
expect(labelViewField.position).toBeLessThan(viewField.position);
}
}
};
describe('Manifest sync - relabel label identifier onto a newly introduced field', () => {
beforeEach(async () => {
await setupApplicationForSync({
@@ -120,6 +171,17 @@ describe('Manifest sync - relabel label identifier onto a newly introduced field
nameField?.id,
);
// Object created with a custom label identifier backed by a field created
// in the same sync: its INDEX view field is visible and strictly lowest.
if (!isDefined(ticketAfterFirstSync) || !isDefined(nameField)) {
throw new Error('expected the ticket object and its name field');
}
await expectLabelIdentifierIndexViewFieldVisibleAndLowest({
objectMetadataId: ticketAfterFirstSync.id,
labelFieldMetadataId: nameField.id,
});
await syncApplication({
manifest: buildManifest(
buildTicketObject({
@@ -139,6 +201,15 @@ describe('Manifest sync - relabel label identifier onto a newly introduced field
expect(ticketAfterSecondSync?.labelIdentifierFieldMetadataId).toBe(
codeField?.id,
);
if (!isDefined(ticketAfterSecondSync) || !isDefined(codeField)) {
throw new Error('expected the ticket object and its code field');
}
await expectLabelIdentifierIndexViewFieldVisibleAndLowest({
objectMetadataId: ticketAfterSecondSync.id,
labelFieldMetadataId: codeField.id,
});
}, 60000);
it('should relabel onto a newly introduced field when introduction and relabel are split across two syncs, exposing the enriched labelIdentifierFieldMetadataId through the metadata API', async () => {

View File

@@ -1,3 +1,4 @@
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
@@ -6,23 +7,23 @@ import { uninstallApplication } from 'test/integration/metadata/suites/applicati
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
import { findViewFields } from 'test/integration/metadata/suites/view-field/utils/find-view-fields.util';
import { findViews } from 'test/integration/metadata/suites/view/utils/find-views.util';
import { VIEW_FIELD_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import type { FieldManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { type FieldManifest, type Manifest } from 'twenty-shared/application';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType, ViewKey } from 'twenty-shared/types';
import { FieldMetadataType, ViewType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_VIEW_FIELD_ID = uuidv4();
const TEST_SECOND_VIEW_FIELD_ID = uuidv4();
const PERSON_OBJECT_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.universalIdentifier;
const ALL_PEOPLE_VIEW_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.views.allPeople.universalIdentifier;
const PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.views.personRecordPageFields.universalIdentifier;
const PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.person.views.personRecordPageFields.viewFieldGroups.general
.universalIdentifier;
const CUSTOM_FIELD_NAME = 'integrationContributedColumn';
@@ -62,25 +63,25 @@ const findPersonObject = async () => {
return person;
};
const findAllPeopleViewId = async (personObjectId: string) => {
const findRecordPageViewId = async (personObjectId: string) => {
const { data } = await findViews({
objectMetadataId: personObjectId,
gqlFields: 'id key name',
gqlFields: 'id key name type',
expectToFail: false,
});
const allPeopleView = data?.getViews.find(
(view) => view.key === ViewKey.INDEX,
const recordPageView = data?.getViews.find(
(view) => view.type === ViewType.FIELDS_WIDGET,
);
if (!allPeopleView) {
throw new Error('Standard allPeople (INDEX) view not found for Person');
if (!recordPageView) {
throw new Error('Standard record-page view not found for Person');
}
return allPeopleView.id;
return recordPageView.id;
};
const findAllPeopleViewFields = async (viewId: string) => {
const findRecordPageViewFields = async (viewId: string) => {
const { data } = await findViewFields({
viewId,
gqlFields: VIEW_FIELD_GQL_FIELDS,
@@ -106,10 +107,10 @@ describe('Manifest update - standalone view fields on existing views', () => {
});
});
it('attaches a standalone view field to the standard allPeople view without recreating the view', async () => {
it('attaches a standalone view field to the standard record-page view without recreating the view', async () => {
const person = await findPersonObject();
const allPeopleViewId = await findAllPeopleViewId(person.id);
const standardViewFields = await findAllPeopleViewFields(allPeopleViewId);
const recordPageViewId = await findRecordPageViewId(person.id);
const standardViewFields = await findRecordPageViewFields(recordPageViewId);
const standardViewFieldIds = standardViewFields.map(
(viewField) => viewField.id,
);
@@ -122,7 +123,10 @@ describe('Manifest update - standalone view fields on existing views', () => {
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier: ALL_PEOPLE_VIEW_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER,
viewFieldGroupUniversalIdentifier:
PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
@@ -143,7 +147,8 @@ describe('Manifest update - standalone view fields on existing views', () => {
expect(customField).toBeDefined();
const viewFieldsAfterSync = await findAllPeopleViewFields(allPeopleViewId);
const viewFieldsAfterSync =
await findRecordPageViewFields(recordPageViewId);
const contributedViewField = viewFieldsAfterSync.find(
(viewField) => viewField.fieldMetadataId === customField?.id,
@@ -151,7 +156,7 @@ describe('Manifest update - standalone view fields on existing views', () => {
expect(contributedViewField).toBeDefined();
expect(contributedViewField).toMatchObject({
viewId: allPeopleViewId,
viewId: recordPageViewId,
position: 10,
isVisible: true,
size: 150,
@@ -169,8 +174,8 @@ describe('Manifest update - standalone view fields on existing views', () => {
it('removes the contributed view field on uninstall while keeping the standard view intact', async () => {
const person = await findPersonObject();
const allPeopleViewId = await findAllPeopleViewId(person.id);
const standardViewFields = await findAllPeopleViewFields(allPeopleViewId);
const recordPageViewId = await findRecordPageViewId(person.id);
const standardViewFields = await findRecordPageViewFields(recordPageViewId);
const standardViewFieldIds = standardViewFields.map(
(viewField) => viewField.id,
);
@@ -181,7 +186,10 @@ describe('Manifest update - standalone view fields on existing views', () => {
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier: ALL_PEOPLE_VIEW_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier:
PERSON_RECORD_PAGE_VIEW_UNIVERSAL_IDENTIFIER,
viewFieldGroupUniversalIdentifier:
PERSON_RECORD_PAGE_GENERAL_GROUP_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
@@ -199,7 +207,7 @@ describe('Manifest update - standalone view fields on existing views', () => {
expect(customFieldId).toBeDefined();
expect(
(await findAllPeopleViewFields(allPeopleViewId)).some(
(await findRecordPageViewFields(recordPageViewId)).some(
(viewField) => viewField.fieldMetadataId === customFieldId,
),
).toBe(true);
@@ -209,12 +217,14 @@ describe('Manifest update - standalone view fields on existing views', () => {
expectToFail: false,
});
const allPeopleViewIdAfterUninstall = await findAllPeopleViewId(person.id);
const recordPageViewIdAfterUninstall = await findRecordPageViewId(
person.id,
);
expect(allPeopleViewIdAfterUninstall).toBe(allPeopleViewId);
expect(recordPageViewIdAfterUninstall).toBe(recordPageViewId);
const viewFieldsAfterUninstall =
await findAllPeopleViewFields(allPeopleViewId);
await findRecordPageViewFields(recordPageViewId);
expect(
viewFieldsAfterUninstall.some(
@@ -230,55 +240,4 @@ describe('Manifest update - standalone view fields on existing views', () => {
).toBe(true);
}
}, 60000);
it('rejects two standalone view fields targeting the same field on the same view', async () => {
const { errors } = await syncApplication({
manifest: buildManifest({
fields: [personFieldManifest],
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier: ALL_PEOPLE_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
},
{
universalIdentifier: TEST_SECOND_VIEW_FIELD_ID,
viewUniversalIdentifier: ALL_PEOPLE_VIEW_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 11,
isVisible: true,
},
],
}),
expectToFail: true,
});
expect(errors).toBeDefined();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].extensions.code).toBe('METADATA_VALIDATION_FAILED');
}, 60000);
it('rejects a standalone view field whose target view does not exist', async () => {
const { errors } = await syncApplication({
manifest: buildManifest({
fields: [personFieldManifest],
viewFields: [
{
universalIdentifier: TEST_VIEW_FIELD_ID,
viewUniversalIdentifier: uuidv4(),
fieldMetadataUniversalIdentifier: TEST_FIELD_ID,
position: 10,
isVisible: true,
},
],
}),
expectToFail: true,
});
expect(errors).toBeDefined();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].extensions.code).toBe('METADATA_VALIDATION_FAILED');
}, 60000);
});

View File

@@ -20,7 +20,7 @@ import {
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_APP_ID = '0c17e907-f32a-4526-98e4-9addd4302b1a';
const TEST_ROLE_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_SKILL_ID = uuidv4();
@@ -317,11 +317,12 @@ describe('syncApplication', () => {
}, 60000);
it('should delete old field and create equivalent one when field universalIdentifier changes', async () => {
const originalFieldId = uuidv4();
const updatedFieldId = uuidv4();
const originalFieldId = '8abbef24-f8c3-41d5-826b-73a02825e712';
const updatedFieldId = 'a4262080-673d-430a-883b-c5d04610abf1';
const testObject = buildDefaultObjectManifest({
applicationUniversalIdentifier: TEST_APP_ID,
universalIdentifier: '8fc4bfec-01d8-4404-bd1a-dc90fe8e7699',
nameSingular: 'ticket',
namePlural: 'tickets',
labelSingular: 'Ticket',

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updateOne FieldMetadataService morph relation fields v2 - Add one target Should fail adding one object metadata target to pre-existing morph relation where object is already a target 1`] = `
{
@@ -71,12 +71,49 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe
"type": "create",
},
],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 2 fieldMetadata, 1 index",
"message": "Validation failed for 2 fieldMetadata, 2 viewFields, 1 index",
"summary": {
"fieldMetadata": 2,
"index": 1,
"totalErrors": 3,
"totalErrors": 5,
"viewField": 2,
},
"userFriendlyMessage": "Many validation errors",
},
@@ -144,12 +181,49 @@ exports[`updateOne FieldMetadataService morph relation fields v2 - Add one targe
"type": "create",
},
],
"viewField": [
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 2 fieldMetadata, 1 index",
"message": "Validation failed for 2 fieldMetadata, 2 viewFields, 1 index",
"summary": {
"fieldMetadata": 2,
"index": 1,
"totalErrors": 3,
"totalErrors": 5,
"viewField": 2,
},
"userFriendlyMessage": "Many validation errors",
},

View File

@@ -5,6 +5,7 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { findViewFields } from 'test/integration/metadata/suites/view-field/utils/find-view-fields.util';
import { findViews } from 'test/integration/metadata/suites/view/utils/find-views.util';
import { ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
@@ -33,7 +34,18 @@ describe('View side effect on object creation', () => {
createdObjectMetadataId = undefined;
});
it('should create a view and view field for default custom objects standard fields on object metadata creation', async () => {
const objectCreationGqlFields = `
id
labelPlural
description
labelSingular
namePlural
nameSingular
icon
isLabelSyncedWithName
`;
const createDishesObject = async () => {
const {
labelPlural,
description,
@@ -50,16 +62,6 @@ describe('View side effect on object creation', () => {
icon: 'IconBuildingSkyscraper',
isLabelSyncedWithName: false,
};
const objectCreationGqlFields = `
id
labelPlural
description
labelSingular
namePlural
nameSingular
icon
isLabelSyncedWithName
`;
const {
data: { createOneObject },
@@ -69,6 +71,20 @@ describe('View side effect on object creation', () => {
gqlFields: objectCreationGqlFields,
});
return createOneObject;
};
const findIndexView = async (objectMetadataId: string) => {
const {
data: { getViews },
} = await findViews({ objectMetadataId, expectToFail: false });
return getViews.find((view) => view.key === 'INDEX');
};
it('should engine-provision the INDEX view and its view fields on object metadata creation', async () => {
const createOneObject = await createDishesObject();
createdObjectMetadataId = createOneObject.id;
const {
@@ -80,19 +96,93 @@ describe('View side effect on object creation', () => {
expect(createdViews).toBeDefined();
expect(createdViews.length).toBe(2);
const [firstView] = createdViews;
expect(firstView).toMatchObject<Partial<FlatView>>({
const indexView = createdViews.find((view) => view.key === 'INDEX');
if (!isDefined(indexView)) {
throw new Error('expected an INDEX view to be provisioned');
}
expect(indexView).toMatchObject<Partial<FlatView>>({
objectMetadataId: createdObjectMetadataId,
type: ViewType.TABLE,
});
const {
data: { getViewFields: createdViewFields },
data: { getViewFields: indexViewFields },
} = await findViewFields({
viewId: firstView.id,
viewId: indexView.id,
expectToFail: false,
});
expect(createdViewFields.length).toBe(5);
expect(indexViewFields.length).toBe(5);
});
it('should keep the same INDEX view when the object is renamed (lossless, deterministic identifier)', async () => {
const createOneObject = await createDishesObject();
createdObjectMetadataId = createOneObject.id;
const indexViewBeforeRename = await findIndexView(createdObjectMetadataId);
if (!isDefined(indexViewBeforeRename)) {
throw new Error('expected an INDEX view to be provisioned');
}
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: createdObjectMetadataId,
updatePayload: {
labelPlural: 'Renamed dishes',
labelSingular: 'Renamed dish',
},
},
});
const indexViewAfterRename = await findIndexView(createdObjectMetadataId);
if (!isDefined(indexViewAfterRename)) {
throw new Error('expected the INDEX view to survive the rename');
}
expect(indexViewAfterRename.id).toBe(indexViewBeforeRename.id);
const {
data: { getViewFields: indexViewFieldsAfterRename },
} = await findViewFields({
viewId: indexViewAfterRename.id,
expectToFail: false,
});
expect(indexViewFieldsAfterRename.length).toBe(5);
});
it('should cascade-delete the INDEX view and its view fields when the object is deleted', async () => {
const createOneObject = await createDishesObject();
const objectMetadataId = createOneObject.id;
const indexView = await findIndexView(objectMetadataId);
expect(isDefined(indexView)).toBe(true);
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: objectMetadataId,
updatePayload: { isActive: false },
},
});
await deleteOneObjectMetadata({
input: { idToDelete: objectMetadataId },
expectToFail: false,
});
const {
data: { getViews: viewsAfterDelete },
} = await findViews({ objectMetadataId, expectToFail: false });
expect(viewsAfterDelete.length).toBe(0);
});
});

View File

@@ -13,8 +13,6 @@ import { generateRecordName } from 'test/integration/utils/generate-record-name'
import { assertViewStructure } from 'test/integration/utils/view-test.util';
import { ViewOpenRecordIn, ViewType } from 'twenty-shared/types';
import { ViewKey } from 'twenty-shared/types';
describe('View REST API', () => {
let testObjectMetadataId: string;
let testSelectFieldMetadataId: string;
@@ -107,7 +105,7 @@ describe('View REST API', () => {
name: viewName,
icon: 'IconTable',
type: ViewType.TABLE,
key: ViewKey.INDEX,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
@@ -121,7 +119,7 @@ describe('View REST API', () => {
objectMetadataId: testObjectMetadataId,
icon: 'IconTable',
type: ViewType.TABLE,
key: ViewKey.INDEX,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
@@ -161,7 +159,7 @@ describe('View REST API', () => {
name: viewName,
icon: 'IconTable',
type: ViewType.TABLE,
key: ViewKey.INDEX,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
@@ -202,7 +200,7 @@ describe('View REST API', () => {
name: viewName,
icon: 'IconTable',
type: ViewType.TABLE,
key: ViewKey.INDEX,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
@@ -262,7 +260,7 @@ describe('View REST API', () => {
name: viewName,
icon: 'IconTable',
type: ViewType.TABLE,
key: ViewKey.INDEX,
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,

View File

@@ -56,7 +56,7 @@ export const createTestViewWithRestApi = async (
objectMetadataId,
icon: 'IconTable',
type: ViewType.TABLE,
key: 'INDEX',
key: null,
position: 0,
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,

View File

@@ -0,0 +1,15 @@
import { getFieldsWidgetViewUniversalIdentifier } from '@/application/deterministic-identifier/get-fields-widget-view-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const WIDGET = '88888888-8888-4888-8888-888888888888';
describe('getFieldsWidgetViewUniversalIdentifier', () => {
it('derives a deterministic id from its 1:1 FIELDS page-layout widget', () => {
expect(
getFieldsWidgetViewUniversalIdentifier({
applicationUniversalIdentifier: APP,
pageLayoutWidgetUniversalIdentifier: WIDGET,
}),
).toBe('5f45a1bd-97df-56f7-a8ad-7839ef0045b5');
});
});

View File

@@ -0,0 +1,17 @@
import { getSystemViewUniversalIdentifier } from '@/application/deterministic-identifier/get-system-view-universal-identifier.util';
import { ViewKey } from '@/types/ViewKey';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
describe('getSystemViewUniversalIdentifier', () => {
it('derives a deterministic id from the stable INDEX view key within its object', () => {
expect(
getSystemViewUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
viewKey: ViewKey.INDEX,
}),
).toBe('3803a536-0158-554c-bfb4-e5492323e57f');
});
});

View File

@@ -1,12 +1,7 @@
import {
getFieldsWidgetViewUniversalIdentifier,
getIndexViewUniversalIdentifier,
getViewUniversalIdentifier,
} from '@/application/deterministic-identifier/get-view-universal-identifier.util';
import { getViewUniversalIdentifier } from '@/application/deterministic-identifier/get-view-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
const WIDGET = '88888888-8888-4888-8888-888888888888';
describe('getViewUniversalIdentifier', () => {
it('derives a deterministic id from the view name within its object', () => {
@@ -19,25 +14,3 @@ describe('getViewUniversalIdentifier', () => {
).toBe('125140c3-eca9-560d-9020-dad4ac1ebab6');
});
});
describe('getIndexViewUniversalIdentifier', () => {
it('derives a deterministic id from the stable INDEX view key within its object', () => {
expect(
getIndexViewUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
}),
).toBe('3803a536-0158-554c-bfb4-e5492323e57f');
});
});
describe('getFieldsWidgetViewUniversalIdentifier', () => {
it('derives a deterministic id from its 1:1 FIELDS page-layout widget', () => {
expect(
getFieldsWidgetViewUniversalIdentifier({
applicationUniversalIdentifier: APP,
pageLayoutWidgetUniversalIdentifier: WIDGET,
}),
).toBe('5f45a1bd-97df-56f7-a8ad-7839ef0045b5');
});
});

View File

@@ -0,0 +1,14 @@
import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util';
export const getFieldsWidgetViewUniversalIdentifier = ({
applicationUniversalIdentifier,
pageLayoutWidgetUniversalIdentifier,
}: {
applicationUniversalIdentifier: string;
pageLayoutWidgetUniversalIdentifier: string;
}): string =>
computeDeterministicUuid({
entityNamespace: 'view',
value: pageLayoutWidgetUniversalIdentifier,
applicationUniversalIdentifier,
});

View File

@@ -0,0 +1,17 @@
import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util';
import { type ViewKey } from '@/types/ViewKey';
export const getSystemViewUniversalIdentifier = ({
applicationUniversalIdentifier,
objectUniversalIdentifier,
viewKey,
}: {
applicationUniversalIdentifier: string;
objectUniversalIdentifier: string;
viewKey: ViewKey;
}): string =>
computeDeterministicUuid({
entityNamespace: 'view',
value: `${objectUniversalIdentifier}:${viewKey}`,
applicationUniversalIdentifier,
});

View File

@@ -1,7 +1,5 @@
import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util';
import { ViewKey } from '@/types/ViewKey';
// A view is identified by its name within its object.
export const getViewUniversalIdentifier = ({
applicationUniversalIdentifier,
objectUniversalIdentifier,
@@ -16,33 +14,3 @@ export const getViewUniversalIdentifier = ({
value: `${objectUniversalIdentifier}:${name}`,
applicationUniversalIdentifier,
});
// The INDEX view is an object's singleton main table view; its name is
// server-generated, so it is keyed by the stable INDEX view key.
export const getIndexViewUniversalIdentifier = ({
applicationUniversalIdentifier,
objectUniversalIdentifier,
}: {
applicationUniversalIdentifier: string;
objectUniversalIdentifier: string;
}): string =>
computeDeterministicUuid({
entityNamespace: 'view',
value: `${objectUniversalIdentifier}:${ViewKey.INDEX}`,
applicationUniversalIdentifier,
});
// A fields-widget view is 1:1 with its FIELDS page-layout widget, so it is keyed
// by that widget's deterministic universalIdentifier (its name is server-generated).
export const getFieldsWidgetViewUniversalIdentifier = ({
applicationUniversalIdentifier,
pageLayoutWidgetUniversalIdentifier,
}: {
applicationUniversalIdentifier: string;
pageLayoutWidgetUniversalIdentifier: string;
}): string =>
computeDeterministicUuid({
entityNamespace: 'view',
value: pageLayoutWidgetUniversalIdentifier,
applicationUniversalIdentifier,
});

View File

@@ -54,6 +54,7 @@ export {
export { getConnectionProviderUniversalIdentifier } from './deterministic-identifier/get-connection-provider-universal-identifier.util';
export { getFieldPermissionUniversalIdentifier } from './deterministic-identifier/get-field-permission-universal-identifier.util';
export { getFieldUniversalIdentifier } from './deterministic-identifier/get-field-universal-identifier.util';
export { getFieldsWidgetViewUniversalIdentifier } from './deterministic-identifier/get-fields-widget-view-universal-identifier.util';
export { getFrontComponentUniversalIdentifier } from './deterministic-identifier/get-front-component-universal-identifier.util';
export { getIndexUniversalIdentifier } from './deterministic-identifier/get-index-universal-identifier.util';
export { getLogicFunctionUniversalIdentifier } from './deterministic-identifier/get-logic-function-universal-identifier.util';
@@ -79,16 +80,13 @@ export { getSearchFieldUniversalIdentifier } from './deterministic-identifier/ge
export { getSelectOptionUniversalIdentifier } from './deterministic-identifier/get-select-option-universal-identifier.util';
export { getSkillUniversalIdentifier } from './deterministic-identifier/get-skill-universal-identifier.util';
export { getSystemRelationFieldUniversalIdentifier } from './deterministic-identifier/get-system-relation-field-universal-identifier.util';
export { getSystemViewUniversalIdentifier } from './deterministic-identifier/get-system-view-universal-identifier.util';
export { getViewFieldGroupUniversalIdentifier } from './deterministic-identifier/get-view-field-group-universal-identifier.util';
export { getViewFieldUniversalIdentifier } from './deterministic-identifier/get-view-field-universal-identifier.util';
export { getViewFilterUniversalIdentifier } from './deterministic-identifier/get-view-filter-universal-identifier.util';
export { getViewGroupUniversalIdentifier } from './deterministic-identifier/get-view-group-universal-identifier.util';
export { getViewSortUniversalIdentifier } from './deterministic-identifier/get-view-sort-universal-identifier.util';
export {
getViewUniversalIdentifier,
getIndexViewUniversalIdentifier,
getFieldsWidgetViewUniversalIdentifier,
} from './deterministic-identifier/get-view-universal-identifier.util';
export { getViewUniversalIdentifier } from './deterministic-identifier/get-view-universal-identifier.util';
export { SyncableEntity } from './enums/syncable-entities.enum';
export type {
RegularFieldManifest,

View File

@@ -68,6 +68,11 @@ export type ViewManifest = SyncableEntityOptions & {
name: string;
objectUniversalIdentifier: string;
type?: ViewType;
/**
* @deprecated The INDEX key is reserved for the engine-owned default view,
* which the server provisions automatically for every object. This field is
* ignored: manifest views are always created as additional views.
*/
key?: ViewKey;
icon?: string;
position?: number;

View File

@@ -93,40 +93,40 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a",
"views": {
"allAttachments": {
"universalIdentifier": "3f7f3363-7087-44cc-902d-5e8904262316",
"universalIdentifier": "e0c5bfc8-2aa8-5407-9d7e-f374055914b2",
"viewFields": {
"createdAt": {
"universalIdentifier": "6c092c26-b1cb-488f-ae2e-5af4bec1162b",
"universalIdentifier": "cd0ab629-435f-5d59-9b14-1d2ee00954d8",
},
"createdBy": {
"universalIdentifier": "fa363372-0fdf-4bb3-bdf1-0ead354b9225",
"universalIdentifier": "9c0456cf-ab42-596b-b381-048cf5718117",
},
"file": {
"universalIdentifier": "873cf114-5477-4b62-9023-7ea6ad69fbe5",
"universalIdentifier": "2c641732-02a0-5aeb-9262-ff60973b7cf2",
},
"name": {
"universalIdentifier": "be56712f-d7a6-4fbe-b92b-d750f0708a0a",
"universalIdentifier": "d5a4f58e-abf1-52ce-a315-ea7b18e4a946",
},
"targetCompany": {
"universalIdentifier": "b335ad04-059e-4c36-8666-f40431849044",
"universalIdentifier": "7f76f6c5-078d-5064-89b4-e0042ee4e169",
},
"targetDashboard": {
"universalIdentifier": "bcc6d6e1-7c0b-4291-9270-66e42024d8dd",
"universalIdentifier": "4f1ae4ae-c1c9-5b2c-8f6f-9435171fca48",
},
"targetNote": {
"universalIdentifier": "fc8dba49-bcf2-41b8-a435-0c4a3bbf2af6",
"universalIdentifier": "6e24a74a-6d3d-54ce-afd0-7c55daf79d81",
},
"targetOpportunity": {
"universalIdentifier": "15f2d457-dc09-4c52-bf2a-47083d6bf017",
"universalIdentifier": "4e0a4cf7-8e45-5688-90a6-da6d67544f72",
},
"targetPerson": {
"universalIdentifier": "73a4c3a7-c7f9-4ed6-a2b6-117d7efad0f3",
"universalIdentifier": "8cff29b2-23e8-5190-93da-f1b22faea495",
},
"targetTask": {
"universalIdentifier": "c2913c5e-6cc6-438d-9c2f-3212a9b2a82b",
"universalIdentifier": "cc9c44ea-c27e-5f57-9094-85cf14dae3e2",
},
"targetWorkflow": {
"universalIdentifier": "11fcf58b-dbab-42dd-be67-689462111070",
"universalIdentifier": "75d1d092-412f-57b1-9e74-e3f8285a3f0c",
},
},
},
@@ -173,16 +173,16 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-0408-4f38-b8a8-4d5e3e26e24d",
"views": {
"allBlocklists": {
"universalIdentifier": "5a98e88c-67c2-4f61-a5ab-a0d3d6a836bb",
"universalIdentifier": "2ee7be0b-4112-5543-9f29-04c933fd4f98",
"viewFields": {
"createdAt": {
"universalIdentifier": "e7cfcf05-2676-4d43-9eee-4da1016b12ff",
"universalIdentifier": "fc86c1d0-a6f1-58c4-b32c-ddbffdeda2da",
},
"handle": {
"universalIdentifier": "155ae00d-0def-4f62-9473-8a8efa209eee",
"universalIdentifier": "07282d15-ed89-5504-9904-cd242125e9f2",
},
"workspaceMember": {
"universalIdentifier": "05a2f0b9-f2ef-4729-bc42-9e2ad2a34fb2",
"universalIdentifier": "20253618-79e4-57e8-b18f-f506657d5132",
},
},
},
@@ -260,19 +260,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-491b-4aaa-9825-afd1bae6ae00",
"views": {
"allCalendarChannelEventAssociations": {
"universalIdentifier": "001893be-c06c-4ba1-9f18-53bd26f0179f",
"universalIdentifier": "66887c1c-c33a-52b6-8dd1-0714e504ffec",
"viewFields": {
"calendarChannelId": {
"universalIdentifier": "e3adffd2-d820-4c89-912c-34908d90057e",
"universalIdentifier": "6504f7e7-3a66-54b2-adc1-53d90fc7d9e3",
},
"calendarEvent": {
"universalIdentifier": "35656a84-ecb8-4075-a610-8b538d6f8120",
"universalIdentifier": "9be9bb56-f65d-53ee-a6e6-4d01a6e9cb58",
},
"createdAt": {
"universalIdentifier": "8ca74f2f-210b-4afc-81f0-506047400e82",
"universalIdentifier": "2ee9fd11-c7a6-54c8-af41-572ed8cecbd7",
},
"eventExternalId": {
"universalIdentifier": "f779d7e8-f1d8-44a7-b0ef-4409c9b6b466",
"universalIdentifier": "55ea0442-7664-5da2-bc21-d02e101acdcc",
},
},
},
@@ -382,31 +382,31 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-8f1d-4eef-9f85-0d1965e27221",
"views": {
"allCalendarEvents": {
"universalIdentifier": "20202020-c001-4c01-8c01-ca1ebe0ca001",
"universalIdentifier": "d581f9a1-1546-5eef-b5a4-4c7a088dc6fa",
"viewFields": {
"conferenceLink": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf06",
"universalIdentifier": "caee37ae-989a-5d95-ad26-e42530d5a65c",
},
"createdAt": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf08",
"universalIdentifier": "0a502413-f875-5c1d-aa60-93e6f412568e",
},
"endsAt": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf03",
"universalIdentifier": "65fbb3d7-e21c-52f6-a90c-38a424d05284",
},
"isCanceled": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf07",
"universalIdentifier": "ef47c862-92b5-5d72-8e3c-7806244b0a70",
},
"isFullDay": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf04",
"universalIdentifier": "b3f5e6c6-80bf-5ef3-b8d7-1bc521ef8054",
},
"location": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf05",
"universalIdentifier": "ac181cfc-c9cb-50f5-89eb-19c01615b0dc",
},
"startsAt": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf02",
"universalIdentifier": "38902ac0-92dd-5889-bcc2-0e3956a7f18d",
},
"title": {
"universalIdentifier": "20202020-cf01-4c01-8c01-ca1ebe0caf01",
"universalIdentifier": "292e2bd9-118e-56a2-bafc-e182f4f323a7",
},
},
},
@@ -523,31 +523,31 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-a1c3-47a6-9732-27e5b1e8436d",
"views": {
"allCalendarEventParticipants": {
"universalIdentifier": "5228d634-6b69-4a43-be5c-e778fa6fe779",
"universalIdentifier": "23b1277b-8859-54ea-9106-9e6d43e5adc4",
"viewFields": {
"calendarEvent": {
"universalIdentifier": "d9c2f346-b83b-48ae-98d0-e344f97248cd",
"universalIdentifier": "5ef7cac8-d293-559f-88a7-5a4d0234c4c5",
},
"createdAt": {
"universalIdentifier": "63d9d40d-e40c-410c-a14c-2f36c64c3e69",
"universalIdentifier": "41f8cf98-cd93-500d-9b9c-1e1af2e6c3cb",
},
"displayName": {
"universalIdentifier": "3cadc470-9231-4027-9bbe-60e934edb483",
"universalIdentifier": "f96e757e-aa84-5c05-8922-9f962eb3854f",
},
"handle": {
"universalIdentifier": "4140bd68-55e8-475c-8724-7f9f97634a9f",
"universalIdentifier": "e3c1165d-aa09-5636-ac37-a391bacc602e",
},
"isOrganizer": {
"universalIdentifier": "684972f9-c5fe-4fff-bdec-2fc5511c938c",
"universalIdentifier": "949e222c-dbe7-5696-b386-ed3a82d8b452",
},
"person": {
"universalIdentifier": "86546244-9e3d-40e4-87cd-cbc82a353d2e",
"universalIdentifier": "e236d415-822a-5847-a90c-f0699eeb5cc7",
},
"responseStatus": {
"universalIdentifier": "dd0ab0bd-7f33-48fa-9461-fb5d085a2f9f",
"universalIdentifier": "0f3e3c0c-d8f9-57d9-99d5-66407347f210",
},
"workspaceMember": {
"universalIdentifier": "542141b0-ac85-4c43-867b-8d7f559b07ae",
"universalIdentifier": "e54eca2e-7fd9-5743-a514-348067775949",
},
},
},
@@ -667,19 +667,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "ce19efb9-710f-45b2-b141-473abbeea60b",
"views": {
"allCallRecordings": {
"universalIdentifier": "c395b55e-88f0-4d5b-a1fb-0d38b50e0b19",
"universalIdentifier": "6676d746-71e4-5dff-9bf1-418b92c26253",
"viewFields": {
"recordingRequestStatus": {
"universalIdentifier": "3bdedacd-0fd5-4175-8d28-2fe41bb5aa77",
"universalIdentifier": "883d1aca-4113-50ab-b83e-e2e2292bb5aa",
},
"startedAt": {
"universalIdentifier": "3b96351f-66ed-4fa6-acb6-698647573af7",
"universalIdentifier": "23d91b8e-e413-5e10-b818-17c3359fe068",
},
"status": {
"universalIdentifier": "6c4a81a2-d9c1-4f82-984c-f97e083ca710",
"universalIdentifier": "c7e2829f-4eba-5d7f-a1c7-a18ed8bd8025",
},
"title": {
"universalIdentifier": "b1d5051b-071d-4514-93cf-704724cdc8f6",
"universalIdentifier": "fcf0cedc-6d25-53ed-a587-cfb4b0958071",
},
},
},
@@ -799,28 +799,28 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-b374-4779-a561-80086cb2e17f",
"views": {
"allCompanies": {
"universalIdentifier": "20202020-a001-4a01-8a01-c0aba11c0001",
"universalIdentifier": "cb8dc074-a42f-520b-85e5-ab85a1b40b20",
"viewFields": {
"accountOwner": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf004",
"universalIdentifier": "90b29c8d-d6a3-57be-8f3b-879f2807a0a7",
},
"address": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf008",
"universalIdentifier": "68cd0e8b-f730-54f5-90a0-45798bb5bb70",
},
"createdAt": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf005",
"universalIdentifier": "b8b2c693-4f3e-5471-afe2-1bd70dcd9b16",
},
"createdBy": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf003",
"universalIdentifier": "ec94701d-acce-58b5-bd81-75d585a68cf7",
},
"domainName": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf002",
"universalIdentifier": "4b4afe40-aa94-5bca-92a3-b84ccb846811",
},
"linkedinLink": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf007",
"universalIdentifier": "fa5a7f00-9751-5ab1-a410-fe144fedd2dc",
},
"name": {
"universalIdentifier": "20202020-af01-4a01-8a01-c0aba11cf001",
"universalIdentifier": "87bbfcc6-3be8-58a5-852c-f02c9c9bc009",
},
},
},
@@ -937,19 +937,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-3840-4b6d-9425-0c5188b05ca8",
"views": {
"allDashboards": {
"universalIdentifier": "20202020-a012-4a12-8a12-da5ab0b0a001",
"universalIdentifier": "0769edd6-ff42-5c4b-80c4-8aa1d07afba6",
"viewFields": {
"createdAt": {
"universalIdentifier": "20202020-af12-4a12-8a12-da5ab0b0af03",
"universalIdentifier": "f586827d-ed83-51bc-a862-741bd45ead77",
},
"createdBy": {
"universalIdentifier": "20202020-af12-4a12-8a12-da5ab0b0af02",
"universalIdentifier": "79f57be6-3262-517b-b592-4e791be5806e",
},
"title": {
"universalIdentifier": "20202020-af12-4a12-8a12-da5ab0b0af01",
"universalIdentifier": "e0fd54ca-2197-5ba8-ac87-9b7b39f359d0",
},
"updatedAt": {
"universalIdentifier": "20202020-af12-4a12-8a12-da5ab0b0af04",
"universalIdentifier": "d37846b6-01a9-54f9-ab44-43a39375974d",
},
},
},
@@ -1023,28 +1023,28 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-3f6b-4425-80ab-e468899ab4b2",
"views": {
"allMessages": {
"universalIdentifier": "20202020-d001-4d01-8d01-ae55a9e5a001",
"universalIdentifier": "8ac6d466-4071-5ce9-b635-5162034fa3f5",
"viewFields": {
"createdAt": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af07",
"universalIdentifier": "b214ac38-da59-5c1c-ae7e-189dc11f3f37",
},
"headerMessageId": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af05",
"universalIdentifier": "9139b1ad-8555-571c-8433-f038de5a3c82",
},
"messageParticipants": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af03",
"universalIdentifier": "3ad5730a-168c-5d32-be43-71c982ce9505",
},
"messageThread": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af02",
"universalIdentifier": "5072b4d6-d708-5cb5-a97a-7ae2a12b1337",
},
"receivedAt": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af04",
"universalIdentifier": "c8df5f80-0d07-5ecd-ba69-8f975bbdc51a",
},
"subject": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af01",
"universalIdentifier": "dfe53214-95ef-5dae-9c94-bf0c008f5c5d",
},
"text": {
"universalIdentifier": "20202020-df01-4d01-8d01-ae55a9e5af06",
"universalIdentifier": "1aeb24ca-72db-5735-ab13-f9bd99cab092",
},
},
},
@@ -1136,43 +1136,43 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "238acb94-dd4c-4036-bc55-19b99d821efd",
"views": {
"allMessageCampaigns": {
"universalIdentifier": "ffedb368-33f0-43a7-b84e-e622b4e97be9",
"universalIdentifier": "8094a87e-310b-58e7-9c05-a76c59ac6535",
"viewFields": {
"bouncedCount": {
"universalIdentifier": "135d3fb9-bc45-4426-980b-9836ed792e3a",
"universalIdentifier": "3c273c7f-b577-5213-8597-198dac6fdae1",
},
"complainedCount": {
"universalIdentifier": "9957dc6d-05ae-4829-ae7d-7cdf9239cdd0",
"universalIdentifier": "30317d2b-5212-56be-a5fe-0ab15722dde3",
},
"createdAt": {
"universalIdentifier": "6dffa47d-3128-4d54-924a-4f67676116c2",
"universalIdentifier": "88484379-8afd-5f7c-ab3c-7d937bb7ed13",
},
"failedCount": {
"universalIdentifier": "ad346c8e-f462-499e-89c8-da01d5460dc6",
"universalIdentifier": "2f44ac38-194f-56a7-bd94-a292db4bc210",
},
"fromAddress": {
"universalIdentifier": "553a0ea6-1f55-4143-a2d0-8c1ad0cf3dd1",
"universalIdentifier": "2ebb7a5d-e03d-5c6e-955f-4efb99fffb46",
},
"list": {
"universalIdentifier": "c0bf6a48-8695-4520-81e6-7462d35033b5",
"universalIdentifier": "13936b8a-0135-5ac3-81d1-f6a11826451e",
},
"name": {
"universalIdentifier": "4d40646c-853d-48e6-8ddb-81e6dedabc99",
"universalIdentifier": "5a224d5d-9ae7-53c8-ad6d-720bb62a087b",
},
"recipients": {
"universalIdentifier": "2f99f444-4af3-44fd-b08d-46955c4ac2a2",
"universalIdentifier": "40f278c1-2716-5324-8035-c14ea62a4eb7",
},
"sentAt": {
"universalIdentifier": "7f4a9d81-afac-4d27-98de-0a2da65114f7",
"universalIdentifier": "a9389297-3927-5364-8c9d-4bcb8039020e",
},
"sentCount": {
"universalIdentifier": "0be62e57-3955-49bd-a9ef-8f82f7a6e9aa",
"universalIdentifier": "e55b655a-038e-5257-ab2c-f2966900d2de",
},
"status": {
"universalIdentifier": "ef35df97-b1a5-4f16-8b85-5751e5019f63",
"universalIdentifier": "68714967-d906-5082-af8c-2791705ce682",
},
"subject": {
"universalIdentifier": "b017c851-e38d-4a81-ab38-0fb10e9f239e",
"universalIdentifier": "2a4fe259-612f-5d10-a451-a047e605917c",
},
},
},
@@ -1268,22 +1268,22 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-ad1e-4127-bccb-d83ae04d2ccb",
"views": {
"allMessageChannelMessageAssociations": {
"universalIdentifier": "a4f465ac-d5cb-4f24-93ac-7a24bafd398e",
"universalIdentifier": "fda3d57b-b54b-57be-af68-f9857f58577c",
"viewFields": {
"createdAt": {
"universalIdentifier": "af239abd-2c55-4108-a9d8-b5a67f6ca2e2",
"universalIdentifier": "32690396-1e7d-5b95-ad30-94c0c8914957",
},
"direction": {
"universalIdentifier": "ca38195e-985c-4880-85e0-26fa143c1ec7",
"universalIdentifier": "c9603fb3-9cef-5747-8d99-86592bff5f3d",
},
"message": {
"universalIdentifier": "f9f2de0d-3db5-402b-a733-53be6a4667c8",
"universalIdentifier": "9346a2f9-eff0-5909-af3e-12e52e6eef3a",
},
"messageChannelId": {
"universalIdentifier": "b86e652b-04ce-4089-9f71-e190eaf5b798",
"universalIdentifier": "5c6fdb16-a932-53f7-99d6-29e118a06f75",
},
"messageExternalId": {
"universalIdentifier": "7fb9801d-ca3d-4b2d-8d55-c922fcf7fefd",
"universalIdentifier": "ce797f5c-ea26-50fc-8d72-2dac766dc498",
},
},
},
@@ -1367,16 +1367,16 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-a1b0-40b0-8ab0-5b6c7d8e9f0a",
"views": {
"allMessageChannelMessageAssociationMessageFolders": {
"universalIdentifier": "775610fe-f1d1-4959-bdc3-0b437059cfeb",
"universalIdentifier": "982c43ec-d2af-5197-aad9-c61f4365a91b",
"viewFields": {
"createdAt": {
"universalIdentifier": "9da7637e-25c7-4101-8169-b5f6ff159690",
"universalIdentifier": "63b9774c-fd0e-5c17-a1c4-84ca9845f79c",
},
"messageChannelMessageAssociation": {
"universalIdentifier": "1251e67a-e795-4bc2-a468-6cfc838b6a0a",
"universalIdentifier": "7f87dc1b-efea-5286-be19-27b171a21a7e",
},
"messageFolderId": {
"universalIdentifier": "aff2203d-6439-43b8-9cb4-55e8d78bba43",
"universalIdentifier": "e608de61-b9fe-5747-a224-ab1c32290997",
},
},
},
@@ -1454,19 +1454,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "826561ea-4816-411c-baa0-eec5e6ca8866",
"views": {
"allMessageLists": {
"universalIdentifier": "c72bae18-75e9-4cb0-baeb-379d3529b98f",
"universalIdentifier": "75e21a3c-6c76-58ec-9f14-b2e9ecc93204",
"viewFields": {
"campaigns": {
"universalIdentifier": "037030a2-9dad-4bfd-957a-313b362172b4",
"universalIdentifier": "8fd26964-23a7-5913-b431-1b83c03bfdfe",
},
"createdAt": {
"universalIdentifier": "29f3e7de-c40b-4597-8568-7318c146e4da",
"universalIdentifier": "69d178f4-f4b8-5bc2-a01b-bfc21f3a753a",
},
"members": {
"universalIdentifier": "6d314c4b-215a-4094-963a-ff9dd8221aea",
"universalIdentifier": "8e64ce89-0910-58df-a896-ccbe514f3913",
},
"name": {
"universalIdentifier": "c9767580-34e6-420b-923d-b4abd8c13d96",
"universalIdentifier": "9deb8cdc-2734-541f-a6c2-d37e144d4747",
},
},
},
@@ -1580,28 +1580,28 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-a433-4456-aa2d-fd9cb26b774a",
"views": {
"allMessageParticipants": {
"universalIdentifier": "8b7fbe7d-dae0-4285-8bdc-ec078a4de870",
"universalIdentifier": "9ffa51b2-4eef-52a7-9761-10b2d2ca8716",
"viewFields": {
"createdAt": {
"universalIdentifier": "636ff7b6-86b8-49fc-9442-39f4c24ff424",
"universalIdentifier": "9995f355-7956-5f8c-917f-7a3390fd417d",
},
"displayName": {
"universalIdentifier": "8d0c8202-b57f-4450-a090-a7eb26aa2299",
"universalIdentifier": "dd4ef6d0-a1d6-5618-a583-d77222d6a768",
},
"handle": {
"universalIdentifier": "abcbb5d9-b8c2-46bb-b3cc-ea035be8f3be",
"universalIdentifier": "1eb7f5a3-7e3b-5f9b-9362-27b99b1ca88f",
},
"message": {
"universalIdentifier": "ca491a31-8659-4202-9476-f0f72efc80b5",
"universalIdentifier": "a6ae59bd-a5ef-5210-b0bf-ebbcb0386e93",
},
"person": {
"universalIdentifier": "26d0f3f1-43d3-425c-930c-81147451d0f8",
"universalIdentifier": "78872b33-6bba-5cf3-bef0-bfaf528e6877",
},
"role": {
"universalIdentifier": "55b74f7e-7c58-4fce-a44b-a8d9671ec541",
"universalIdentifier": "009f68a7-695c-582b-833e-501af34480ff",
},
"workspaceMember": {
"universalIdentifier": "df62dcbc-c22d-4d34-9fa5-6f70bae02161",
"universalIdentifier": "ca7bd6d7-939d-59eb-bd2d-73777e28c9a5",
},
},
},
@@ -1681,19 +1681,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-849a-4c3e-84f5-a25a7d802271",
"views": {
"allMessageThreads": {
"universalIdentifier": "20202020-d002-4d02-8d02-ae55a9ba2002",
"universalIdentifier": "0f667621-896c-5b8e-8359-3c7c1f8ee1d8",
"viewFields": {
"createdAt": {
"universalIdentifier": "20202020-df02-4d02-8d02-ae55a9ba2f02",
"universalIdentifier": "98c4c4b8-509c-57a4-b1ba-f66de2df38a2",
},
"messages": {
"universalIdentifier": "20202020-df02-4d02-8d02-ae55a9ba2f01",
"universalIdentifier": "0f61a257-5110-5f67-9315-42da5355a6e9",
},
"subject": {
"universalIdentifier": "e5f0d32b-2b6a-47bc-b3bd-f32c96594ec1",
"universalIdentifier": "7074b6e0-abd9-5f2c-bf3b-3c834ef37c1d",
},
"updatedAt": {
"universalIdentifier": "af2c6ac9-7083-4609-8172-d518441f5e9e",
"universalIdentifier": "cd336056-36de-525b-af30-c5c6c38b917b",
},
},
},
@@ -1749,22 +1749,22 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-0b00-45cd-b6f6-6cd806fc6804",
"views": {
"allNotes": {
"universalIdentifier": "20202020-a005-4a05-8a05-a0be5a11a000",
"universalIdentifier": "2bfba20b-7cfa-5d3c-b683-cf9abde2359d",
"viewFields": {
"bodyV2": {
"universalIdentifier": "20202020-af05-4a05-8a05-a0be5a11af02",
"universalIdentifier": "3a923b53-057d-52ee-9b12-43ccbe9b9fc2",
},
"createdAt": {
"universalIdentifier": "20202020-af05-4a05-8a05-a0be5a11af04",
"universalIdentifier": "6c04d549-a0fb-50ea-8132-557c61ad6f58",
},
"createdBy": {
"universalIdentifier": "20202020-af05-4a05-8a05-a0be5a11af03",
"universalIdentifier": "414f18ff-5ce5-589b-8ff3-fe7cad4b0e75",
},
"noteTargets": {
"universalIdentifier": "20202020-af05-4a05-8a05-a0be5a11af01",
"universalIdentifier": "1be85e89-a527-5225-a315-3c3fddfdaf86",
},
"title": {
"universalIdentifier": "20202020-af05-4a05-8a05-a0be5a11af00",
"universalIdentifier": "55d3619b-d72e-5f2b-8f79-a4ad5106dd76",
},
},
},
@@ -1868,22 +1868,22 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-fff0-4b44-be82-bda313884400",
"views": {
"allNoteTargets": {
"universalIdentifier": "d124d587-ef78-402b-9341-7673e6cea033",
"universalIdentifier": "3962cec0-e2f6-5d27-ab3c-40a5dd6279d0",
"viewFields": {
"id": {
"universalIdentifier": "f2d912fe-7c6f-4a9c-b808-b7b5a18d2818",
"universalIdentifier": "e296fb5c-74f5-5924-8506-3b16639564c9",
},
"note": {
"universalIdentifier": "9d4ac173-d32b-4a44-9dbd-8a47ab844f98",
"universalIdentifier": "de9a35c2-dd69-5b62-94ca-006fc7c4c28f",
},
"targetCompany": {
"universalIdentifier": "a9c7f370-4b22-4f29-8e3f-678e91a59576",
"universalIdentifier": "e8175481-58d2-5c1d-a6b4-118448c78073",
},
"targetOpportunity": {
"universalIdentifier": "3efeb162-cd03-458b-9c7b-47032d045204",
"universalIdentifier": "eb132124-1725-5528-affc-e4996415e01f",
},
"targetPerson": {
"universalIdentifier": "b6f67de5-c5cf-4235-b740-a6a007c8eae3",
"universalIdentifier": "48f31dbf-75fc-5780-a58a-960d9ce5c18b",
},
},
},
@@ -1966,25 +1966,25 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-9549-49dd-b2b2-883999db8938",
"views": {
"allOpportunities": {
"universalIdentifier": "20202020-a003-4a03-8a03-0aa0b1ca1ba0",
"universalIdentifier": "d024c8c9-3021-53d0-b612-9da81b02736f",
"viewFields": {
"amount": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1bb0",
"universalIdentifier": "22e16aff-d1b4-59ce-902b-fee1f4adb79c",
},
"closeDate": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1bb2",
"universalIdentifier": "20b2d9f9-8ad9-59db-8a62-2576692c9ad9",
},
"company": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1bb3",
"universalIdentifier": "d0ac287f-801b-54c5-ba49-c842a5e7140c",
},
"createdBy": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1bb1",
"universalIdentifier": "fe527f0f-1690-5077-9b9e-cebaa7c4c5d1",
},
"name": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1baf",
"universalIdentifier": "4a52d5c5-0243-5c57-ab5e-6d1395beb2cf",
},
"pointOfContact": {
"universalIdentifier": "20202020-af03-4a03-8a03-0aa0b1ca1bb4",
"universalIdentifier": "51fbe12a-46c1-50d2-8163-5a50542c600e",
},
},
},
@@ -2177,31 +2177,31 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-e674-48e5-a542-72570eee7213",
"views": {
"allPeople": {
"universalIdentifier": "20202020-a002-4a02-8a02-ae0a1ea11a00",
"universalIdentifier": "0cda0ad4-74da-5ccd-975c-a0e4db43a05a",
"viewFields": {
"company": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af3",
"universalIdentifier": "3530ea58-3f8f-5e5f-a17a-f68d4ad867ba",
},
"createdAt": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af5",
"universalIdentifier": "114a1b7f-71c5-5277-a4e7-b2379d900a99",
},
"createdBy": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af2",
"universalIdentifier": "de1a471e-8ae4-533f-870a-c462561b02d2",
},
"emails": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af1",
"universalIdentifier": "5731e6bb-b5dc-5793-aaa5-386b0da99fd5",
},
"jobTitle": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af7",
"universalIdentifier": "a036c9bc-4e8e-5384-8598-48ed4e516fc6",
},
"linkedinLink": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af8",
"universalIdentifier": "42014c11-5951-59eb-b525-bf14d5570eab",
},
"name": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af0",
"universalIdentifier": "899d55b0-dd24-534e-af40-65d08b9073ce",
},
"phones": {
"universalIdentifier": "20202020-af02-4a02-8a02-ae0a1ea11af4",
"universalIdentifier": "565f0a13-140e-5c59-86b1-7d0ebd18e9dd",
},
},
},
@@ -2342,31 +2342,31 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-1ba1-48ba-bc83-ef7e5990ed10",
"views": {
"allTasks": {
"universalIdentifier": "20202020-a006-4a06-8a06-ba5ca11a1ea0",
"universalIdentifier": "ca2db697-077a-5f45-81e1-513aea4752a1",
"viewFields": {
"assignee": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb4",
"universalIdentifier": "1244827a-ea11-532b-9722-d862948426e5",
},
"bodyV2": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb5",
"universalIdentifier": "a2c8a8f0-2b25-57db-9a25-a8e1beee93a1",
},
"createdAt": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb6",
"universalIdentifier": "98414831-dc54-51a4-a6a7-4144e0d124ae",
},
"createdBy": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb2",
"universalIdentifier": "422888eb-d12d-5096-9b3e-7899cf4722e9",
},
"dueAt": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb3",
"universalIdentifier": "92a126a5-2072-5cfd-b3e1-e0a327358056",
},
"status": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb0",
"universalIdentifier": "6527420a-3345-5581-bb51-f4210f31ef3a",
},
"taskTargets": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eb1",
"universalIdentifier": "2714ec81-03f5-5915-9d5f-1bec691fc1b6",
},
"title": {
"universalIdentifier": "20202020-af06-4a06-8a06-ba5ca11a1eaf",
"universalIdentifier": "1480d069-9159-51ae-b316-5bc1a3f04235",
},
},
},
@@ -2555,22 +2555,22 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1",
"views": {
"allTaskTargets": {
"universalIdentifier": "1dbf1d24-6cca-4f55-ae2f-e3d1b425a495",
"universalIdentifier": "dea38ca0-b564-5ae1-b662-9ce388668438",
"viewFields": {
"id": {
"universalIdentifier": "a49287c9-8aa6-4fca-9ec5-08d643f7239f",
"universalIdentifier": "d1aa56de-7a8a-5670-9cc5-c2c159a4ff4d",
},
"targetCompany": {
"universalIdentifier": "e9fa1305-4ba2-41c5-9198-fdc622b69f90",
"universalIdentifier": "47a3f1f8-e2ce-5a7c-af00-36dda03fbc44",
},
"targetOpportunity": {
"universalIdentifier": "526f3354-34d6-4e7e-a870-5f99c28353c2",
"universalIdentifier": "2c09e527-cf94-5e74-9e23-fb4550c7cf18",
},
"targetPerson": {
"universalIdentifier": "cadc7a33-1527-4ef8-ac00-7ed0b54d1bae",
"universalIdentifier": "611b9723-3be8-5470-8baf-5201053a38f0",
},
"task": {
"universalIdentifier": "1f79839e-42f6-4a69-839a-369e21a7497d",
"universalIdentifier": "634c7721-c9d6-5ada-943f-914065531425",
},
},
},
@@ -2697,49 +2697,43 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5",
"views": {
"allTimelineActivities": {
"universalIdentifier": "20202020-b101-4b01-8b01-ba5cc01aa001",
"universalIdentifier": "28d3e8a9-e840-5034-ade2-b752bc8eb25d",
"viewFields": {
"happensAt": {
"universalIdentifier": "20202020-bf01-4b01-8b01-ba5cc01aa012",
},
"linkedRecordCachedName": {
"universalIdentifier": "20202020-bf01-4b01-8b01-ba5cc01aa017",
"universalIdentifier": "3ccb9611-f9ec-56c5-82df-f12f7e26e694",
},
"name": {
"universalIdentifier": "20202020-bf01-4b01-8b01-ba5cc01aa011",
},
"properties": {
"universalIdentifier": "20202020-bf01-4b01-8b01-ba5cc01aa019",
"universalIdentifier": "821f0d94-72e0-5b04-ba97-50538f5a73c2",
},
"targetCompany": {
"universalIdentifier": "2015825f-0786-4b0d-88a7-dfce1b4b1c1a",
"universalIdentifier": "d271d5b4-86ad-513d-a042-d4f8a9c41665",
},
"targetDashboard": {
"universalIdentifier": "538847e8-ab09-407c-a433-505f6d7be7a1",
"universalIdentifier": "b899313f-2e84-51c2-8322-a02bb695ec91",
},
"targetNote": {
"universalIdentifier": "ab74ed52-0195-4b65-987a-8367c07ee222",
"universalIdentifier": "fd6171eb-8bb7-5d3d-a7f7-bd0ebf972f90",
},
"targetOpportunity": {
"universalIdentifier": "f7b5ced9-eba6-4454-8849-7a92d27c11ca",
"universalIdentifier": "a53f262e-4810-55bc-9e47-f9b7bc9a4da7",
},
"targetPerson": {
"universalIdentifier": "37b38a8b-abd7-4f72-92d2-ad82bbef0296",
"universalIdentifier": "c5bd0e0d-20aa-5112-8517-df71c63e5726",
},
"targetTask": {
"universalIdentifier": "3899138d-e6fa-414c-9432-214c9b797ebb",
"universalIdentifier": "3e21cd2a-77ca-5e49-90be-4532dd804c24",
},
"targetWorkflow": {
"universalIdentifier": "d2c3ddc3-afad-40b9-a2cb-d2765f2f5691",
"universalIdentifier": "8e0e94b7-9c2a-5fda-bbfe-2604a0f9cbb4",
},
"targetWorkflowRun": {
"universalIdentifier": "97910946-04f0-4634-804e-880bc0019225",
"universalIdentifier": "13fff327-8fbe-5a91-8d95-4a9ca455cfe9",
},
"targetWorkflowVersion": {
"universalIdentifier": "4a7e3213-afd5-4691-8bba-0a10e8697afb",
"universalIdentifier": "c4aa8b50-d9bb-5395-a90a-fa0d36312634",
},
"workspaceMember": {
"universalIdentifier": "20202020-bf01-4b01-8b01-ba5cc01aa013",
"universalIdentifier": "09d2d7d9-6809-561b-9dc7-1588b6a18673",
},
},
},
@@ -2807,25 +2801,25 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-62be-406c-b9ca-8caa50d51392",
"views": {
"allWorkflows": {
"universalIdentifier": "20202020-a009-4a09-8a09-a0bcf10aa11a",
"universalIdentifier": "84dddf6b-edfa-55c4-ab1d-886602c1ca38",
"viewFields": {
"createdBy": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11d",
"universalIdentifier": "982cf2c6-a9a2-5557-9733-e3124168ee9f",
},
"name": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11a",
"universalIdentifier": "d0c96ceb-e6f7-562d-b859-35341481cdf2",
},
"runs": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11f",
"universalIdentifier": "869394ba-f9b4-5ce2-8312-b9a9827baea8",
},
"statuses": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11b",
"universalIdentifier": "47e57811-bc0b-5b77-9936-384590672b71",
},
"updatedAt": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11c",
"universalIdentifier": "22bc20fd-c132-50e0-9bff-b0a405e729db",
},
"versions": {
"universalIdentifier": "20202020-af09-4a09-8a09-a0bcf10aa11e",
"universalIdentifier": "4978149a-4ed7-5d59-aafb-2dd69b6cd6fe",
},
},
},
@@ -2875,16 +2869,16 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-3319-4234-a34c-7f3b9d2e4d1f",
"views": {
"allWorkflowAutomatedTriggers": {
"universalIdentifier": "a0a9ef79-3d42-417a-8555-3ee54c18ea51",
"universalIdentifier": "d53c1b59-4f9c-5647-aaf5-ac8197ca1c3f",
"viewFields": {
"createdAt": {
"universalIdentifier": "bb35e66a-2a1e-416b-8105-5749d91ab65f",
"universalIdentifier": "5897f02c-e29d-5085-9bbe-60c48a8360b5",
},
"type": {
"universalIdentifier": "689b4749-aa40-489a-bf0b-475a197ca2e6",
"universalIdentifier": "f4456853-dad8-57e8-81a4-6987477a40f7",
},
"workflow": {
"universalIdentifier": "e5a46195-06fe-4f47-8844-128e35151d37",
"universalIdentifier": "53a781ed-4cd6-58fd-8de9-26e5cc7f01a7",
},
},
},
@@ -2986,25 +2980,16 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-4e28-4e95-a9d7-6c00874f843c",
"views": {
"allWorkflowRuns": {
"universalIdentifier": "20202020-a011-4a11-8a11-a0bcf10abca5",
"universalIdentifier": "7f1e3269-5de1-56e9-b89f-4cccaf4d53d6",
"viewFields": {
"createdBy": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcb3",
},
"name": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcaf",
},
"startedAt": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcb2",
"universalIdentifier": "b1ae1446-834a-54bb-b995-04644ee4f0cf",
},
"status": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcb1",
"universalIdentifier": "f62369bb-a4cc-5d88-ac8e-7740ea318837",
},
"workflow": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcb0",
},
"workflowVersion": {
"universalIdentifier": "20202020-af11-4a11-8a11-a0bcf10abcb4",
"universalIdentifier": "1c323f33-aa9a-5975-8a60-095817bf6412",
},
},
},
@@ -3121,22 +3106,22 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-d65d-4ab9-9344-d77bfb376a3d",
"views": {
"allWorkflowVersions": {
"universalIdentifier": "20202020-a010-4a10-8a10-a0bcf10aae15",
"universalIdentifier": "a305b346-a83c-5bdd-880b-7a8e71310bf3",
"viewFields": {
"name": {
"universalIdentifier": "20202020-af10-4a10-8a10-a0bcf10aaeaf",
"universalIdentifier": "84a34cf6-7fd8-5972-894d-bc21e026f334",
},
"runs": {
"universalIdentifier": "20202020-af10-4a10-8a10-a0bcf10aaeb3",
"universalIdentifier": "255ab204-2ebe-582c-bfbd-4a846490d721",
},
"status": {
"universalIdentifier": "20202020-af10-4a10-8a10-a0bcf10aaeb1",
"universalIdentifier": "6ec4080f-d57e-5837-8f99-b36cebbb2fde",
},
"updatedAt": {
"universalIdentifier": "20202020-af10-4a10-8a10-a0bcf10aaeb2",
"universalIdentifier": "604b9aaa-ed58-5e44-869a-0e6402c00c64",
},
"workflow": {
"universalIdentifier": "20202020-af10-4a10-8a10-a0bcf10aaeb0",
"universalIdentifier": "9c66dd1e-1b6d-5ad9-9e81-25c4c0c20799",
},
},
},
@@ -3280,40 +3265,19 @@ exports[`STANDARD_OBJECTS universal identifiers should never change without an e
"universalIdentifier": "20202020-3319-4234-a34c-82d5c0e881a6",
"views": {
"allWorkspaceMembers": {
"universalIdentifier": "20202020-e001-4e01-8e01-a0bcaeabe100",
"universalIdentifier": "55546e30-498e-51f8-bcf2-14617c1fd8aa",
"viewFields": {
"assignedTasks": {
"universalIdentifier": "af16226e-6375-4676-8bd9-9d1a57076fc4",
},
"avatarUrl": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f2",
},
"colorScheme": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f3",
"universalIdentifier": "d401db50-821d-540d-852f-d5ee06d7b2ed",
},
"createdAt": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f8",
},
"dateFormat": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f6",
},
"locale": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f4",
"universalIdentifier": "03129632-0c81-576f-884f-65d366c2ca3a",
},
"name": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f0",
"universalIdentifier": "7af9ed3a-b187-5dad-b8c6-451b50fed4d8",
},
"ownedOpportunities": {
"universalIdentifier": "8a0503f3-ba61-453e-86dc-6c79f7bc235b",
},
"timeFormat": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f7",
},
"timeZone": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f5",
},
"userEmail": {
"universalIdentifier": "20202020-ef01-4e01-8e01-a0bcaeabe1f1",
"universalIdentifier": "e15281a1-9f03-5b59-87d9-7f6a5b7a8d99",
},
},
},

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,17 @@
// Object universal identifiers referenced from another object's declaration
// (default relation builders need both the host and the source object
// identifiers, and an object literal cannot reference its sibling keys).
// Each STANDARD_OBJECTS entry involved in the default relations reads its own
// universalIdentifier from this record so there is a single source of truth.
// Single source of truth for standard object universal identifiers: an object
// identifier is referenced from its own STANDARD_OBJECTS entry, from its
// STANDARD_OBJECT_FIELDS entry (system fields and INDEX view derivation), and
// sometimes from another object's declaration (default relation builders need
// both the host and the source object identifiers, and an object literal
// cannot reference its sibling keys).
export const STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = {
timelineActivity: '20202020-6736-4337-b5c4-8b39fae325a5',
attachment: '20202020-bd3d-4c60-8dca-571c71d4447a',
blocklist: '20202020-0408-4f38-b8a8-4d5e3e26e24d',
calendarChannelEventAssociation: '20202020-491b-4aaa-9825-afd1bae6ae00',
calendarEvent: '20202020-8f1d-4eef-9f85-0d1965e27221',
calendarEventParticipant: '20202020-a1c3-47a6-9732-27e5b1e8436d',
callRecording: 'ce19efb9-710f-45b2-b141-473abbeea60b',
noteTarget: '20202020-fff0-4b44-be82-bda313884400',
taskTarget: '20202020-5a9a-44e8-95df-771cd06d0fb1',
person: '20202020-e674-48e5-a542-72570eee7213',
@@ -14,11 +20,20 @@ export const STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = {
note: '20202020-0b00-45cd-b6f6-6cd806fc6804',
task: '20202020-1ba1-48ba-bc83-ef7e5990ed10',
workflow: '20202020-62be-406c-b9ca-8caa50d51392',
workflowAutomatedTrigger: '20202020-3319-4234-a34c-7f3b9d2e4d1f',
workflowVersion: '20202020-d65d-4ab9-9344-d77bfb376a3d',
workflowRun: '20202020-4e28-4e95-a9d7-6c00874f843c',
workspaceMember: '20202020-3319-4234-a34c-82d5c0e881a6',
dashboard: '20202020-3840-4b6d-9425-0c5188b05ca8',
message: '20202020-3f6b-4425-80ab-e468899ab4b2',
messageChannelMessageAssociation: '20202020-ad1e-4127-bccb-d83ae04d2ccb',
messageChannelMessageAssociationMessageFolder:
'20202020-a1b0-40b0-8ab0-5b6c7d8e9f0a',
messageList: '826561ea-4816-411c-baa0-eec5e6ca8866',
messageListMember: '27773d24-8ce3-40f8-aa6c-1f590f2c08d2',
messageCampaign: '238acb94-dd4c-4036-bc55-19b99d821efd',
messageParticipant: '20202020-a433-4456-aa2d-fd9cb26b774a',
messageThread: '20202020-849a-4c3e-84f5-a25a7d802271',
} as const;
export type StandardObjectWithUniversalIdentifierName =

View File

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ export { ALL_METADATA_NAME } from './constants/all-metadata-name.constant';
export { DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS } from './constants/default-relations-object-standard-ids.constant';
export { IDENTIFIER_MAX_CHAR_LENGTH } from './constants/identifier-max-char-length.constant';
export { RESERVED_METADATA_NAME_KEYWORDS } from './constants/reserved-metadata-name-keywords.constant';
export { STANDARD_OBJECT_FIELDS } from './constants/standard-object-fields.constant';
export type { StandardObjectWithUniversalIdentifierName } from './constants/standard-object-universal-identifiers.constant';
export { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from './constants/standard-object-universal-identifiers.constant';
export { STANDARD_OBJECTS } from './constants/standard-object.constant';

View File

@@ -0,0 +1,57 @@
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from '@/application/constants/TwentyStandardApplicationUniversalIdentifier';
import { getSystemViewUniversalIdentifier } from '@/application/deterministic-identifier/get-system-view-universal-identifier.util';
import { getViewFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-view-field-universal-identifier.util';
import { ViewKey } from '@/types/ViewKey';
type StandardViewFieldUniversalIdentifier = { universalIdentifier: string };
export const buildStandardObjectIndexView = <
const TViewFieldName extends string,
>({
objectUniversalIdentifier,
fields,
viewFieldNames,
}: {
objectUniversalIdentifier: string;
fields: Record<string, StandardViewFieldUniversalIdentifier>;
viewFieldNames: readonly TViewFieldName[];
}): {
universalIdentifier: string;
viewFields: Record<TViewFieldName, StandardViewFieldUniversalIdentifier>;
} => {
const viewUniversalIdentifier = getSystemViewUniversalIdentifier({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier,
viewKey: ViewKey.INDEX,
});
const viewFields = Object.fromEntries(
viewFieldNames.map((viewFieldName) => {
const field = fields[viewFieldName];
if (field === undefined) {
throw new Error(
`Missing field "${viewFieldName}" for the INDEX view of object ${objectUniversalIdentifier}`,
);
}
return [
viewFieldName,
{
universalIdentifier: getViewFieldUniversalIdentifier({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier: field.universalIdentifier,
}),
},
];
}),
) as Record<TViewFieldName, StandardViewFieldUniversalIdentifier>;
return {
universalIdentifier: viewUniversalIdentifier,
viewFields,
};
};