feat(server): add isSystemSideEffect & merge createOneObject/createOneField side-effect migrations (#21673)

## Context

When an object is created via the metadata API, `createOneObject`
creates its side-effect entities (INDEX view + viewFields, indexes,
navigation menu item, "go to" command menu item, record-page fields
view, page layout/tabs/widgets) across **three separate
`validateBuildAndRunWorkspaceMigration` calls**, purely because the
protection behavior (mutations → overrides, delete → deactivate, reset →
reactivate) was keyed on *"owned by the standard app"*, forcing the side
effects into batches with different application owners. This
misrepresents ownership and breaks atomicity.

This PR separates two orthogonal concepts:

- **Ownership** (`applicationId`), the true owner: the caller's
application (the workspace custom app today, 3rd-party apps later).
- **Protection** (`isSystemSideEffect`), the row was generated by the
system, so user mutations route to overrides, deletion becomes
deactivation, and reset restores defaults.

Once side effects are re-owned to the caller, the old `applicationId ===
standardApp` check can no longer tell an original side-effect row from a
user-added one so a dedicated `isSystemSideEffect` flag carries the
protection instead.

This is **PR 1 of 2** (forward-only). It makes newly created objects and
fields correct; existing workspaces are handled by a follow-up backfill
(see *Out of scope*).

## What this PR does

- **`isSystemSideEffect` column** on the 8 affected entities (`view`,
`viewField`, `indexMetadata`, `commandMenuItem`, `pageLayout`,
`pageLayoutTab`, `pageLayoutWidget`, `fieldMetadata`), with
`@WasIntroducedInUpgrade` + an entry in the flat-entity property
configuration (`toCompare: true`, read-only).
- **Single atomic migration in `createOneObject`**: the three
`validateBuildAndRunWorkspaceMigration` calls are merged into one, owned
by the caller (`resolvedOwnerFlatApplication`) and the record-page
view/fields, page layout, and navigation command item are re-owned to
the caller and flagged `isSystemSideEffect: true`.
`buildNavigationFlatCommandMenuItem` is parameterized with
`applicationUniversalIdentifier` (no longer hardcoded to the standard
app).
- **Field-creation side effects** (`createManyFields`/`createOneField`
already run as a single caller-owned migration, so no re-ownership/merge
was needed): the auto-created viewField is flagged `isSystemSideEffect:
true`, and a new field now also propagates to the object's **INDEX/table
view** (added there as a **hidden** column, `isVisible: false`) in
addition to
the record-page FIELDS widget. The INDEX view is targeted directly by
`key = INDEX` (it is not a page-layout widget), de-duplicated per
`(viewId, fieldMetadataUniversalIdentifier)` to respect the per-view
unique index.
The unique-field index is likewise flagged the inverse relation field
stays unflagged (`isSystem: false`).
- **Protection predicate** extended: `isCallerOverridingEntity` and the
removal/reset split strategies now treat `isSystemSideEffect` rows as
protected even when caller-owned (route to
overrides / deactivate / reset) and the page-layout-reset guards allow
resetting flagged entities.
- **Standard compute maps** set the flag consistently so a re-sync
produces no diff (standard-object side effects stay `false`; per-object
nav command items and custom-object base fields are `true`).
- **Read-only GraphQL exposure** of `isSystemSideEffect` on the view /
view-field / page-layout / tab / widget / command-menu-item DTOs (not
exposed on create/update inputs). => Todo: needs to take this new flag
into account. This is fine for now because isSystem remains on
object/field.
- **Fast instance command** (`2-14`) adding the 8 columns (`NOT NULL
DEFAULT false`).

## Scope decisions

- **`pageLayout` is not an `OverridableEntity`**, its own row has
nothing user-overridable (all customization lives on tabs/widgets). It's
dual-purpose (`RECORD_PAGE` side-effect vs. user `DASHBOARD`), so it
gets `isSystemSideEffect` for protection only, no `overrides` jsonb.
- **`navigationMenuItem` is out of scope.**: Those are side effects only
for the metadata API and not marked as "system" (they can be
deleted/updated etc...)
- **`viewFieldGroup` is not a side effect**, it's only created via the
explicit view-field-group API, never by object/field creation, so it
gets no flag.

## Out of scope (follow-ups)

**PR 2** — slow per-workspace backfill (re-own + flag existing side
effects, recreate missing ones) and deterministic v5 identifiers for
base fields / pageLayout / tab.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21673?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Weiko
2026-06-17 15:56:27 +02:00
committed by GitHub
parent 57d15fa73a
commit 3ee93b5ec9
102 changed files with 4572 additions and 530 deletions
@@ -356,6 +356,7 @@ export class CommandMenuItemService {
flatCommandMenuItemToDelete.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
isSystemSideEffect: flatCommandMenuItemToDelete.isSystemSideEffect,
});
const deactivatedFlatCommandMenuItem = {
@@ -14,6 +14,7 @@ import { type SerializedRelation } from 'twenty-shared/types';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { type CommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item-payload.union';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
@@ -129,6 +130,12 @@ export class CommandMenuItemEntity
@JoinColumn({ name: 'pageLayoutId' })
pageLayout: Relation<PageLayoutEntity> | null;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -35,6 +35,7 @@ const baseCommandMenuItem = {
payload: { objectMetadataItemId: 'obj-id-1' },
workspaceId: 'ws-id-1',
isActive: true,
isSystemSideEffect: false,
createdAt: new Date(),
updatedAt: new Date(),
};
@@ -22,6 +22,7 @@ import {
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { WasRemovedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { RENAME_IS_UI_READ_ONLY_TO_IS_UI_EDITABLE_UPGRADE_COMMAND_NAME } from 'src/engine/metadata-modules/object-metadata/constants/rename-is-ui-read-only-to-is-ui-editable-upgrade-command-name.constant';
import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
import { AssignIfIsGivenFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-if-is-given-field-metadata-type.type';
@@ -122,6 +123,12 @@ export class FieldMetadataEntity<
@Column({ default: false })
isSystem: boolean;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@WasIntroducedInUpgrade({
upgradeCommandName:
RENAME_IS_UI_READ_ONLY_TO_IS_UI_EDITABLE_UPGRADE_COMMAND_NAME,
@@ -10,7 +10,6 @@ import {
NAVIGATION_INTERPOLATED_LABEL,
NAVIGATION_INTERPOLATED_SHORT_LABEL,
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
const NAVIGATION_COMMAND_UUID_NAMESPACE =
'b31830da-2ae0-48eb-a915-12fa4ab96dd3';
@@ -26,6 +25,7 @@ const baseArgs = {
objectMetadata: baseObjectMetadata,
commandMenuItemId: 'cmd-id-1',
applicationId: 'app-id-1',
applicationUniversalIdentifier: 'app-universal-1',
workspaceId: 'ws-id-1',
position: 5,
now: '2026-01-01T00:00:00.000Z',
@@ -86,12 +86,10 @@ describe('buildNavigationFlatCommandMenuItem', () => {
expect(result.position).toBe(5);
});
it('should set applicationUniversalIdentifier from TWENTY_STANDARD_APPLICATION', () => {
it('should set applicationUniversalIdentifier from the provided argument', () => {
const result = buildNavigationFlatCommandMenuItem(baseArgs);
expect(result.applicationUniversalIdentifier).toBe(
TWENTY_STANDARD_APPLICATION.universalIdentifier,
);
expect(result.applicationUniversalIdentifier).toBe('app-universal-1');
});
it('should set engineComponentKey to NAVIGATION', () => {
@@ -6,7 +6,6 @@ import { v5 } from 'uuid';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const NAVIGATION_COMMAND_UUID_NAMESPACE =
'b31830da-2ae0-48eb-a915-12fa4ab96dd3';
@@ -51,6 +50,7 @@ export const buildNavigationFlatCommandMenuItem = ({
objectMetadata,
commandMenuItemId,
applicationId,
applicationUniversalIdentifier,
workspaceId,
position,
now,
@@ -63,6 +63,7 @@ export const buildNavigationFlatCommandMenuItem = ({
};
commandMenuItemId: string;
applicationId: string;
applicationUniversalIdentifier: string;
workspaceId: string;
position: number;
now: string;
@@ -82,8 +83,7 @@ export const buildNavigationFlatCommandMenuItem = ({
id: commandMenuItemId,
universalIdentifier,
applicationId,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
applicationUniversalIdentifier,
workspaceId,
label: NAVIGATION_INTERPOLATED_LABEL,
shortLabel: NAVIGATION_INTERPOLATED_SHORT_LABEL,
@@ -105,6 +105,7 @@ export const buildNavigationFlatCommandMenuItem = ({
pageLayoutId: null,
pageLayoutUniversalIdentifier: null,
isActive: true,
isSystemSideEffect: true,
overrides: null,
universalOverrides: null,
createdAt: now,
@@ -116,6 +116,7 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
pageLayoutId: commandMenuItemEntity.pageLayoutId,
pageLayoutUniversalIdentifier,
isActive: commandMenuItemEntity.isActive,
isSystemSideEffect: commandMenuItemEntity.isSystemSideEffect,
overrides: commandMenuItemEntity.overrides,
universalOverrides,
};
@@ -77,6 +77,7 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
isActive: true,
isSystemSideEffect: false,
overrides: null,
universalOverrides: null,
createdAt: now,
@@ -53,6 +53,7 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
entityApplicationUniversalIdentifier:
existingFlatCommandMenuItem.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingFlatCommandMenuItem.isSystemSideEffect,
});
const { overrides, updatedEditableProperties } =
@@ -40,6 +40,11 @@ type MetadataEntityPropertyConfiguration<
export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
fieldMetadata: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
defaultValue: {
toCompare: true,
toStringify: true,
@@ -265,6 +270,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
view: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
key: { toCompare: true, toStringify: false, universalProperty: undefined },
deletedAt: {
toCompare: true,
@@ -445,6 +455,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
viewField: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
isVisible: {
toCompare: true,
toStringify: false,
@@ -550,6 +565,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
index: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
indexType: {
toCompare: true,
toStringify: false,
@@ -893,6 +913,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
pageLayout: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
name: { toCompare: true, toStringify: false, universalProperty: undefined },
type: { toCompare: true, toStringify: false, universalProperty: undefined },
objectMetadataId: {
@@ -923,6 +948,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
pageLayoutWidget: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
title: {
toCompare: true,
toStringify: false,
@@ -997,6 +1027,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
pageLayoutTab: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
title: {
toCompare: true,
toStringify: false,
@@ -1092,6 +1127,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
},
commandMenuItem: {
isSystemSideEffect: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
label: {
toCompare: true,
toStringify: false,
@@ -1,5 +1,6 @@
type EntityWithApplicationIdentifier = {
applicationUniversalIdentifier: string;
isSystemSideEffect?: boolean;
};
export const splitEntitiesByRemovalStrategy = <
@@ -22,7 +23,8 @@ export const splitEntitiesByRemovalStrategy = <
for (const entity of entitiesToRemove) {
if (
entity.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
workspaceCustomApplicationUniversalIdentifier &&
!entity.isSystemSideEffect
) {
toHardDelete.push(entity);
} else {
@@ -2,6 +2,7 @@ type EntityWithApplicationIdentifierAndOverrides = {
applicationUniversalIdentifier: string;
isActive: boolean;
overrides: unknown;
isSystemSideEffect?: boolean;
};
export const splitEntitiesByResetStrategy = <
@@ -28,7 +29,8 @@ export const splitEntitiesByResetStrategy = <
for (const entity of entities) {
if (
entity.applicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier
workspaceCustomApplicationUniversalIdentifier &&
!entity.isSystemSideEffect
) {
toHardDelete.push(entity);
} else {
@@ -32,6 +32,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
icon: 'icon',
id: faker.string.uuid(),
isActive: true,
isSystemSideEffect: false,
name: 'flatFieldMetadataName',
label: 'flat field metadata label',
isNullable: true,
@@ -32,6 +32,7 @@ export const getRelationTargetFlatFieldMetadataMock = ({
const createdAt = '2024-01-01T00:00:00.000Z';
return {
isSystemSideEffect: false,
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
viewFilterIds: [],
@@ -127,6 +127,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isSystemSideEffect": false,
"isUIEditable": true,
"isUnique": false,
"kanbanAggregateOperationViewUniversalIdentifiers": [],
@@ -161,6 +162,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isSystemSideEffect": false,
"isUIEditable": true,
"isUnique": false,
"kanbanAggregateOperationViewUniversalIdentifiers": [],
@@ -197,6 +199,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isSystemSideEffect": false,
"isUIEditable": true,
"isUnique": false,
"kanbanAggregateOperationViewUniversalIdentifiers": [],
@@ -231,6 +234,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isSystemSideEffect": false,
"isUIEditable": true,
"isUnique": false,
"kanbanAggregateOperationViewUniversalIdentifiers": [],
@@ -263,6 +267,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"indexType": "BTREE",
"indexWhereClause": null,
"isCustom": true,
"isSystemSideEffect": true,
"isUnique": false,
"name": "IDX_f687e4e4252800dddd8e5518362",
"objectMetadataUniversalIdentifier": Any<String>,
@@ -285,6 +290,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"indexType": "BTREE",
"indexWhereClause": null,
"isCustom": true,
"isSystemSideEffect": true,
"isUnique": false,
"name": "IDX_cb15d901e889e25d0a9acecb595",
"objectMetadataUniversalIdentifier": Any<String>,
@@ -38,6 +38,7 @@ export const generateIndexForFlatFieldMetadata = ({
indexWhereClause: null,
isCustom: true,
isUnique: flatFieldMetadata.isUnique ?? false,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
flatObjectMetadata.universalIdentifier,
universalIdentifier: indexMetadataUniversalIdentifier,
@@ -41,6 +41,7 @@ export const getDefaultFlatFieldMetadata = ({
createFieldInput.isRemoteCreation,
),
isSystem: createFieldInput.isSystem ?? false,
isSystemSideEffect: false,
isUnique: createFieldInput.isUnique ?? false,
label: createFieldInput.label,
name: createFieldInput.name,
@@ -27,6 +27,7 @@ export const getFlatIndexMetadataMock = (
indexWhereClause: null,
isCustom: false,
isUnique: false,
isSystemSideEffect: false,
name: 'defaultFlatIndexMetadataName',
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
@@ -71,6 +71,7 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({
position: lowestViewFieldPosition - 1,
isVisible: true,
isActive: true,
isSystemSideEffect: flatView.isSystemSideEffect,
size: DEFAULT_VIEW_FIELD_SIZE,
viewId: flatView.id,
viewUniversalIdentifier: flatView.universalIdentifier,
@@ -92,11 +93,16 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({
accumulator.flatViewFieldsToCreate.push(flatViewFieldToCreate);
} else if (
labelMetadataIdentifierViewField.position > lowestViewFieldPosition
labelMetadataIdentifierViewField.position > lowestViewFieldPosition ||
labelMetadataIdentifierViewField.isVisible === false
) {
const updatedFlatViewField = {
...labelMetadataIdentifierViewField,
position: lowestViewFieldPosition - 1,
position:
labelMetadataIdentifierViewField.position > lowestViewFieldPosition
? lowestViewFieldPosition - 1
: labelMetadataIdentifierViewField.position,
isVisible: true,
};
accumulator.flatViewFieldsToUpdate.push(updatedFlatViewField);
@@ -45,6 +45,7 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({
title: createPageLayoutTabInput.title,
position: createPageLayoutTabInput.position ?? 0,
isActive: true,
isSystemSideEffect: false,
pageLayoutId: createPageLayoutTabInput.pageLayoutId,
pageLayoutUniversalIdentifier,
workspaceId,
@@ -60,6 +60,7 @@ export const fromUpdatePageLayoutTabInputToFlatPageLayoutTabToUpdateOrThrow = ({
entityApplicationUniversalIdentifier:
existingFlatPageLayoutTabToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingFlatPageLayoutTabToUpdate.isSystemSideEffect,
});
const { overrides, updatedEditableProperties } =
@@ -42,6 +42,7 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({
title: pageLayoutTabEntity.title,
position: pageLayoutTabEntity.position,
isActive: pageLayoutTabEntity.isActive,
isSystemSideEffect: pageLayoutTabEntity.isSystemSideEffect,
pageLayoutId: pageLayoutTabEntity.pageLayoutId,
workspaceId: pageLayoutTabEntity.workspaceId,
universalIdentifier: pageLayoutTabEntity.universalIdentifier,
@@ -65,6 +65,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
id: pageLayoutWidgetId,
...commonProperties,
isActive: true,
isSystemSideEffect: false,
workspaceId,
createdAt,
updatedAt: createdAt,
@@ -89,6 +89,8 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro
entityApplicationUniversalIdentifier:
existingFlatPageLayoutWidgetToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect:
existingFlatPageLayoutWidgetToUpdate.isSystemSideEffect,
});
const { overrides, updatedEditableProperties } =
@@ -44,6 +44,7 @@ export const fromCreatePageLayoutInputToFlatPageLayoutToCreate = ({
type: createPageLayoutInput.type ?? PageLayoutType.RECORD_PAGE,
objectMetadataId: createPageLayoutInput.objectMetadataId ?? null,
objectMetadataUniversalIdentifier,
isSystemSideEffect: false,
workspaceId,
createdAt,
updatedAt: createdAt,
@@ -64,6 +64,7 @@ export const transformPageLayoutEntityToFlatPageLayout = ({
name: pageLayoutEntity.name,
type: pageLayoutEntity.type,
objectMetadataId: pageLayoutEntity.objectMetadataId,
isSystemSideEffect: pageLayoutEntity.isSystemSideEffect,
workspaceId: pageLayoutEntity.workspaceId,
universalIdentifier: pageLayoutEntity.universalIdentifier,
applicationId: pageLayoutEntity.applicationId,
@@ -59,6 +59,7 @@ export const fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow =
entityApplicationUniversalIdentifier:
existingFlatViewFieldGroupToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: false,
});
const { overrides, updatedEditableProperties } =
@@ -1,3 +1,5 @@
import { ViewKey } from 'twenty-shared/types';
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
@@ -21,13 +23,30 @@ const buildEmptyFlatEntityMaps = () => ({
});
const buildFlatViewMaps = (
entries: { id: string; universalIdentifier: string }[] = [],
entries: {
id: string;
universalIdentifier: string;
key?: ViewKey | null;
isActive?: boolean;
isSystemSideEffect?: boolean;
objectMetadataUniversalIdentifier?: string | null;
deletedAt?: string | null;
}[] = [],
): FlatViewMaps =>
({
byUniversalIdentifier: Object.fromEntries(
entries.map((entry) => [
entry.universalIdentifier,
{ universalIdentifier: entry.universalIdentifier, id: entry.id },
{
universalIdentifier: entry.universalIdentifier,
id: entry.id,
key: entry.key ?? null,
isActive: entry.isActive ?? true,
isSystemSideEffect: entry.isSystemSideEffect ?? false,
objectMetadataUniversalIdentifier:
entry.objectMetadataUniversalIdentifier ?? null,
deletedAt: entry.deletedAt ?? null,
},
]),
),
universalIdentifierById: Object.fromEntries(
@@ -713,6 +732,196 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
});
});
describe('isSystemSideEffect inheritance from parent view', () => {
it('should flag the created view field when the parent view is a system side effect', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: 'field-uid-1',
},
],
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
buildFieldsWidget(),
]),
flatViewFieldMaps: buildFlatViewFieldMaps(),
flatViewMaps: buildFlatViewMaps([
{
id: VIEW_ID,
universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: true,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toHaveLength(1);
expect(result[0].isSystemSideEffect).toBe(true);
});
it('should not flag the created view field when the parent view is not a system side effect', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: 'field-uid-1',
},
],
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
buildFieldsWidget(),
]),
flatViewFieldMaps: buildFlatViewFieldMaps(),
flatViewMaps: buildFlatViewMaps([
{
id: VIEW_ID,
universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER,
isSystemSideEffect: false,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toHaveLength(1);
expect(result[0].isSystemSideEffect).toBe(false);
});
});
describe('INDEX view propagation', () => {
it('should add a hidden, flagged view field to the object INDEX view even without a fields 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: 'index-view-db-id',
universalIdentifier: 'index-view-uid',
key: ViewKey.INDEX,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
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', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: [
{
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: 'field-uid-1',
},
],
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
buildFieldsWidget({ viewId: 'index-view-db-id', isVisible: true }),
]),
flatViewFieldMaps: buildFlatViewFieldMaps(),
flatViewMaps: buildFlatViewMaps([
{
id: 'index-view-db-id',
universalIdentifier: 'index-view-uid',
key: ViewKey.INDEX,
objectMetadataUniversalIdentifier:
OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
},
]),
flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(),
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
});
expect(result).toHaveLength(1);
expect(result[0].isVisible).toBe(true);
});
});
describe('unique universal identifiers', () => {
it('should generate unique universalIdentifier for each created view field', () => {
const result = computeFlatViewFieldsFromFieldsWidgets({
@@ -1,8 +1,8 @@
import { ViewKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
@@ -19,6 +19,11 @@ type FieldToCreateInfo = {
fieldMetadataUniversalIdentifier: string;
};
type FieldViewTarget = {
viewId: string;
isVisible: boolean;
};
const isFieldsWidgetConfiguration = (
configuration: AllPageLayoutWidgetConfiguration,
): configuration is FieldsConfigurationDTO => {
@@ -28,25 +33,65 @@ const isFieldsWidgetConfiguration = (
);
};
const getMatchingFieldsWidgets = ({
const getFieldViewTargets = ({
objectMetadataUniversalIdentifier,
flatPageLayoutWidgetMaps,
flatViewMaps,
}: {
objectMetadataUniversalIdentifier: string;
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
}): FlatPageLayoutWidget[] =>
Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(widget) =>
widget.isActive &&
widget.type === WidgetType.FIELDS &&
widget.objectMetadataUniversalIdentifier ===
objectMetadataUniversalIdentifier &&
isFieldsWidgetConfiguration(widget.configuration) &&
isDefined(widget.configuration.viewId) &&
isDefined(widget.configuration.newFieldDefaultVisibility),
);
flatViewMaps: FlatViewMaps;
}): FieldViewTarget[] => {
const targets: FieldViewTarget[] = [];
const seenViewIds = new Set<string>();
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
).filter(isDefined)) {
if (
!widget.isActive ||
widget.type !== WidgetType.FIELDS ||
widget.objectMetadataUniversalIdentifier !==
objectMetadataUniversalIdentifier ||
!isFieldsWidgetConfiguration(widget.configuration)
) {
continue;
}
const { viewId, newFieldDefaultVisibility } = widget.configuration;
if (
!isDefined(viewId) ||
!isDefined(newFieldDefaultVisibility) ||
seenViewIds.has(viewId)
) {
continue;
}
seenViewIds.add(viewId);
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;
};
const findLastViewFieldGroupId = ({
viewId,
@@ -130,11 +175,13 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
];
const nextPositionByKey = new Map<string, number>();
const queuedViewFieldKeys = new Set<string>();
for (const objectMetadataUniversalIdentifier of objectMetadataUniversalIdentifiers) {
const matchingWidgets = getMatchingFieldsWidgets({
const targets = getFieldViewTargets({
objectMetadataUniversalIdentifier,
flatPageLayoutWidgetMaps,
flatViewMaps,
});
const fieldsForObject = fieldsToCreate.filter(
@@ -143,16 +190,7 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
objectMetadataUniversalIdentifier,
);
for (const widget of matchingWidgets) {
if (!isFieldsWidgetConfiguration(widget.configuration)) {
continue;
}
const configuration = widget.configuration;
const viewId = configuration.viewId!;
const isVisible = configuration.newFieldDefaultVisibility!;
for (const { viewId, isVisible } of targets) {
const viewUniversalIdentifier =
flatViewMaps.universalIdentifierById[viewId] ?? null;
@@ -160,6 +198,10 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
continue;
}
const isSystemSideEffect =
flatViewMaps.byUniversalIdentifier[viewUniversalIdentifier]
?.isSystemSideEffect ?? false;
const viewFieldGroupId = findLastViewFieldGroupId({
viewId,
flatViewFieldGroupMaps,
@@ -184,6 +226,14 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
}
for (const field of fieldsForObject) {
const dedupKey = `${viewId}:${field.fieldMetadataUniversalIdentifier}`;
if (queuedViewFieldKeys.has(dedupKey)) {
continue;
}
queuedViewFieldKeys.add(dedupKey);
const position = nextPositionByKey.get(positionKey)!;
nextPositionByKey.set(positionKey, position + 1);
@@ -200,6 +250,7 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
position,
aggregateOperation: null,
isActive: true,
isSystemSideEffect,
universalOverrides: null,
createdAt: now,
updatedAt: now,
@@ -67,6 +67,7 @@ export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({
position: createViewFieldInput.position ?? 0,
aggregateOperation: createViewFieldInput.aggregateOperation ?? null,
isActive: true,
isSystemSideEffect: false,
universalOverrides: null,
viewFieldGroupUniversalIdentifier,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
@@ -64,6 +64,7 @@ export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({
entityApplicationUniversalIdentifier:
existingFlatViewFieldToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingFlatViewFieldToUpdate.isSystemSideEffect,
});
const { overrides, updatedEditableProperties } =
@@ -87,6 +87,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
visibility: createViewInput.visibility ?? ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: createdByUserWorkspaceId ?? null,
isActive: true,
isSystemSideEffect: false,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
@@ -49,6 +49,7 @@ export const fromDeleteViewInputToFlatViewOrThrow = ({
entityApplicationUniversalIdentifier:
existingFlatViewToDelete.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingFlatViewToDelete.isSystemSideEffect,
});
if (shouldDeactivate) {
@@ -75,6 +75,7 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
entityApplicationUniversalIdentifier:
existingFlatViewToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingFlatViewToUpdate.isSystemSideEffect,
});
const { overrides, updatedEditableProperties } =
@@ -12,6 +12,8 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -78,4 +80,10 @@ export class IndexMetadataEntity
nullable: false,
})
indexType: IndexType;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
}
@@ -243,6 +243,7 @@ export class IndexMetadataService {
indexWhereClause: null,
isCustom: true,
isUnique: false,
isSystemSideEffect: false,
objectMetadataUniversalIdentifier:
flatObjectMetadata.universalIdentifier,
universalIdentifier: indexMetadataUniversalIdentifier,
@@ -65,6 +65,7 @@ describe('generateFlatIndexMetadataWithNameOrThrow', () => {
indexWhereClause: overrides.indexWhereClause ?? null,
isUnique: overrides.isUnique,
isCustom: false,
isSystemSideEffect: false,
universalFlatIndexFieldMetadatas: overrides.fieldIds.map((id, order) => ({
createdAt: now,
updatedAt: now,
@@ -23,6 +23,7 @@ const PARTIAL_ID_FIELD = {
isUnique: true,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: 'uuid',
@@ -51,6 +52,7 @@ const PARTIAL_CREATED_AT_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: 'now',
@@ -79,6 +81,7 @@ const PARTIAL_UPDATED_AT_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: 'now',
@@ -107,6 +110,7 @@ const PARTIAL_DELETED_AT_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: null,
@@ -135,6 +139,7 @@ const PARTIAL_CREATED_BY_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: { name: "''", source: "'MANUAL'" },
@@ -163,6 +168,7 @@ const PARTIAL_UPDATED_BY_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: { name: "''", source: "'MANUAL'" },
@@ -191,6 +197,7 @@ const PARTIAL_POSITION_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: 0,
@@ -219,6 +226,7 @@ const PARTIAL_SEARCH_VECTOR_FIELD = {
isUnique: false,
isActive: true,
isSystem: true,
isSystemSideEffect: true,
isUIEditable: false,
isLabelSyncedWithName: false,
defaultValue: null,
@@ -145,7 +145,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
existingFlatObjectMetadata,
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
applicationId: resolvedOwnerFlatApplication.id,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
});
const validateAndBuildResult =
@@ -465,7 +467,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: string;
ownerFlatApplication?: FlatApplication;
}): Promise<FlatObjectMetadata> {
const { workspaceCustomFlatApplication, twentyStandardFlatApplication } =
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
@@ -497,7 +499,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
});
const flatDefaultViewFieldsToCreate = computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
flatApplication: resolvedOwnerFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
@@ -525,11 +527,38 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{
objectMetadata: flatObjectMetadataToCreate,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
applicationId: resolvedOwnerFlatApplication.id,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
flatCommandMenuItemMaps,
},
);
const flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: resolvedOwnerFlatApplication,
});
const flatRecordPageFieldsViewFieldsToCreate =
computeFlatViewFieldsToCreate({
flatApplication: resolvedOwnerFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
const flatDefaultRecordPageLayoutsToCreate =
this.computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: resolvedOwnerFlatApplication,
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
workspaceId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -540,12 +569,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: [flatDefaultViewToCreate],
flatEntityToCreate: [
flatDefaultViewToCreate,
flatRecordPageFieldsViewToCreate,
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: flatDefaultViewFieldsToCreate,
flatEntityToCreate: [
...flatDefaultViewFieldsToCreate,
...flatRecordPageFieldsViewFieldsToCreate,
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
@@ -562,6 +597,29 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
commandMenuItem: {
flatEntityToCreate: [flatCommandMenuItemToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayout: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayouts,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
...(isDefined(flatNavigationMenuItemToCreate)
? {
navigationMenuItem: {
@@ -586,100 +644,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
const commandMenuItemMigrationResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [flatCommandMenuItemToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (commandMenuItemMigrationResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
commandMenuItemMigrationResult,
'Multiple validation errors occurred while creating command menu item',
);
}
const flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
});
const flatRecordPageFieldsViewFieldsToCreate =
computeFlatViewFieldsToCreate({
flatApplication: twentyStandardFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
const flatDefaultRecordPageLayoutsToCreate =
this.computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
workspaceId,
});
const pageLayoutMigrationResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: flatRecordPageFieldsViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayout: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayouts,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (pageLayoutMigrationResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
pageLayoutMigrationResult,
'Multiple validation errors occurred while creating page layouts for object',
);
}
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -737,6 +701,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
@@ -847,6 +812,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadata,
workspaceId,
applicationId,
applicationUniversalIdentifier,
flatCommandMenuItemMaps,
}: {
objectMetadata: {
@@ -859,6 +825,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
};
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
flatCommandMenuItemMaps: {
byUniversalIdentifier: Record<string, FlatCommandMenuItem | undefined>;
};
@@ -876,6 +843,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadata,
commandMenuItemId: v4(),
applicationId,
applicationUniversalIdentifier,
workspaceId,
position: nextPosition,
now: new Date().toISOString(),
@@ -909,6 +877,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatCommandMenuItemMaps,
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
isBeingEnabled: boolean;
isBeingDisabled: boolean;
@@ -918,6 +887,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
};
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): {
commandMenuItemsToCreate: FlatCommandMenuItem[];
commandMenuItemsToUpdate: FlatCommandMenuItem[];
@@ -943,6 +913,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadata: existingFlatObjectMetadata,
workspaceId,
applicationId,
applicationUniversalIdentifier,
flatCommandMenuItemMaps,
}),
],
@@ -134,6 +134,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isNullable: true,
isActive: true,
isSystem: false,
isSystemSideEffect: true,
isUIEditable: true,
defaultValue: null,
createdAt: now,
@@ -40,6 +40,7 @@ export const buildDefaultIndexesForCustomObject = ({
indexWhereClause: null,
isCustom: false,
isUnique: false,
isSystemSideEffect: true,
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
universalIdentifier: tsFlatVectorIndexUniversalIdentifier,
updatedAt: createdAt.toISOString(),
@@ -64,6 +64,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
widgetIds: [widgetId],
widgetUniversalIdentifiers: [widgetUniversalIdentifier],
isActive: true,
isSystemSideEffect: true,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -119,6 +120,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
objectMetadataId: objectMetadata.id,
objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier,
isActive: true,
isSystemSideEffect: true,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -141,6 +143,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({
tabUniversalIdentifiers: pageLayoutTabs.map(
(tab) => tab.universalIdentifier,
),
isSystemSideEffect: true,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -43,6 +43,7 @@ export const computeFlatRecordPageFieldsViewToCreate = ({
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
@@ -65,6 +65,7 @@ export const computeFlatViewFieldsToCreate = ({
position: index,
aggregateOperation: null,
isActive: true,
isSystemSideEffect: true,
universalOverrides: null,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
}));
@@ -15,6 +15,8 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
@@ -70,6 +72,12 @@ export class PageLayoutTabEntity
})
layoutMode: PageLayoutTabLayoutMode;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -19,6 +19,8 @@ import {
type Relation,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
@@ -100,6 +102,12 @@ export class PageLayoutWidgetEntity<
PageLayoutWidgetConfigurationTypeSettings<TWidgetConfigurationType>
>;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -14,6 +14,8 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
@@ -69,6 +71,12 @@ export class PageLayoutEntity
@JoinColumn({ name: 'defaultTabToFocusOnMobileAndSidePanelId' })
defaultTabToFocusOnMobileAndSidePanel: Relation<PageLayoutTabEntity> | null;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -112,7 +112,8 @@ export class PageLayoutResetService {
if (
widget.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
workspaceCustomFlatApplication.universalIdentifier &&
!widget.isSystemSideEffect
) {
throw new PageLayoutWidgetException(
`Custom widget "${id}" cannot be reset to default`,
@@ -255,7 +256,8 @@ export class PageLayoutResetService {
if (
tab.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
workspaceCustomFlatApplication.universalIdentifier &&
!tab.isSystemSideEffect
) {
throw new PageLayoutTabException(
`Custom tab "${id}" cannot be reset to default`,
@@ -404,7 +406,8 @@ export class PageLayoutResetService {
if (
layout.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
workspaceCustomFlatApplication.universalIdentifier &&
!layout.isSystemSideEffect
) {
throw new PageLayoutException(
`Custom page layout "${id}" cannot be reset to default`,
@@ -328,6 +328,7 @@ export class PageLayoutUpdateService {
layoutMode: tabInput.layoutMode ?? PageLayoutTabLayoutMode.GRID,
overrides: null,
isActive: true,
isSystemSideEffect: false,
};
},
);
@@ -345,6 +346,7 @@ export class PageLayoutUpdateService {
entityApplicationUniversalIdentifier:
existingTab.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingTab.isSystemSideEffect,
});
const editableProperties = {
@@ -384,6 +386,7 @@ export class PageLayoutUpdateService {
entityApplicationUniversalIdentifier:
existingTab.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingTab.isSystemSideEffect,
});
const editableProperties = {
@@ -605,6 +608,7 @@ export class PageLayoutUpdateService {
overrides: null,
universalOverrides: null,
isActive: true,
isSystemSideEffect: false,
universalConfiguration:
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
configuration: widgetInput.configuration,
@@ -721,6 +725,7 @@ export class PageLayoutUpdateService {
entityApplicationUniversalIdentifier:
existingWidget.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect: existingWidget.isSystemSideEffect,
});
const configuration = widgetInput.configuration ?? null;
@@ -11,6 +11,7 @@ describe('isCallerOverridingEntity', () => {
callerApplicationUniversalIdentifier: CUSTOM_APP_ID,
entityApplicationUniversalIdentifier: STANDARD_APP_ID,
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
isSystemSideEffect: false,
}),
).toBe(true);
});
@@ -21,6 +22,7 @@ describe('isCallerOverridingEntity', () => {
callerApplicationUniversalIdentifier: CUSTOM_APP_ID,
entityApplicationUniversalIdentifier: CUSTOM_APP_ID,
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
isSystemSideEffect: false,
}),
).toBe(false);
});
@@ -31,6 +33,7 @@ describe('isCallerOverridingEntity', () => {
callerApplicationUniversalIdentifier: OTHER_APP_ID,
entityApplicationUniversalIdentifier: STANDARD_APP_ID,
workspaceCustomApplicationUniversalIdentifier: CUSTOM_APP_ID,
isSystemSideEffect: false,
}),
).toBe(false);
});
@@ -2,15 +2,18 @@ export const isCallerOverridingEntity = ({
callerApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
isSystemSideEffect,
}: {
callerApplicationUniversalIdentifier: string;
entityApplicationUniversalIdentifier: string;
workspaceCustomApplicationUniversalIdentifier: string;
isSystemSideEffect: boolean;
}): boolean => {
return (
callerApplicationUniversalIdentifier ===
workspaceCustomApplicationUniversalIdentifier &&
entityApplicationUniversalIdentifier !==
workspaceCustomApplicationUniversalIdentifier
(entityApplicationUniversalIdentifier !==
workspaceCustomApplicationUniversalIdentifier ||
isSystemSideEffect)
);
};
@@ -250,6 +250,7 @@ export class FieldsWidgetUpsertService {
existingGroup.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: false,
});
const { overrides, updatedEditableProperties: sanitizedGroupProps } =
@@ -349,6 +350,7 @@ export class FieldsWidgetUpsertService {
existingField.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: existingField.isSystemSideEffect,
});
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
@@ -435,6 +437,7 @@ export class FieldsWidgetUpsertService {
existingField.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: existingField.isSystemSideEffect,
});
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
@@ -530,6 +533,7 @@ export class FieldsWidgetUpsertService {
overrides: null,
universalOverrides: null,
isActive: true,
isSystemSideEffect: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -641,6 +645,7 @@ export class FieldsWidgetUpsertService {
existingField.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: existingField.isSystemSideEffect,
});
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
@@ -700,6 +705,7 @@ export class FieldsWidgetUpsertService {
existingField.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: existingField.isSystemSideEffect,
});
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
@@ -788,6 +794,7 @@ export class FieldsWidgetUpsertService {
overrides: null,
universalOverrides: null,
isActive: true,
isSystemSideEffect: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -15,7 +15,9 @@ import {
type SerializedRelation,
} from 'twenty-shared/types';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
@@ -80,6 +82,12 @@ export class ViewFieldEntity
@Column({ nullable: true, type: 'uuid' })
viewFieldGroupId: string | null;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -24,6 +24,7 @@ import {
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
@@ -204,6 +205,12 @@ export class ViewEntity
})
visibility: ViewVisibility;
@WasIntroducedInUpgrade({
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
})
@Column({ nullable: false, default: false, type: 'boolean' })
isSystemSideEffect: boolean;
@Column({ nullable: true, type: 'uuid' })
createdByUserWorkspaceId: string | null;
@@ -443,6 +443,7 @@ export class ViewWidgetUpsertService {
existingField.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
applicationUniversalIdentifier,
isSystemSideEffect: existingField.isSystemSideEffect,
});
const { overrides, updatedEditableProperties: sanitizedFieldProps } =
@@ -523,6 +524,7 @@ export class ViewWidgetUpsertService {
overrides: null,
universalOverrides: null,
isActive: true,
isSystemSideEffect: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
@@ -329,6 +329,7 @@ export class ViewService {
existingFlatView.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
isSystemSideEffect: existingFlatView.isSystemSideEffect,
});
const now = new Date().toISOString();