diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts index 72ec0961e5..429abd1738 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-1775500013000-refactor-navigation-commands.command.ts @@ -202,7 +202,6 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac workspaceId, position: nextPosition++, now, - universalIdentifier, }), ); } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001500-backfill-record-page-layouts.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001500-backfill-record-page-layouts.command.ts index 1367d0a1b7..2e5f185049 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001500-backfill-record-page-layouts.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-23/1-23-workspace-command-1780000001500-backfill-record-page-layouts.command.ts @@ -13,6 +13,8 @@ import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-la import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util'; +import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; +import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @@ -506,32 +508,39 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace const allViewFields: UniversalFlatViewField[] = []; for (const customObject of customObjectsWithoutPageLayout) { + const fieldsView = computeFlatRecordPageFieldsViewToCreate({ + objectMetadata: customObject, + flatApplication: twentyStandardFlatApplication, + }); + const objectFieldMetadatas = Object.values( flatFieldMetadataMaps.byUniversalIdentifier, ) .filter(isDefined) .filter((field) => field.objectMetadataId === customObject.id); - const { - pageLayouts, - pageLayoutTabs, - pageLayoutWidgets, - recordPageFieldsView, - recordPageFieldsViewFields, - } = computeFlatDefaultRecordPageLayoutToCreate({ - objectMetadata: customObject, - flatApplication: twentyStandardFlatApplication, + const viewFields = computeFlatViewFieldsToCreate({ objectFlatFieldMetadatas: objectFieldMetadatas, + viewUniversalIdentifier: fieldsView.universalIdentifier, + flatApplication: twentyStandardFlatApplication, labelIdentifierFieldMetadataUniversalIdentifier: customObject.labelIdentifierFieldMetadataUniversalIdentifier, - workspaceId, + excludeLabelIdentifier: true, }); + const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } = + computeFlatDefaultRecordPageLayoutToCreate({ + objectMetadata: customObject, + flatApplication: twentyStandardFlatApplication, + recordPageFieldsView: fieldsView, + workspaceId, + }); + allPageLayouts.push(...pageLayouts); allTabs.push(...pageLayoutTabs); allWidgets.push(...pageLayoutWidgets); - allViews.push(recordPageFieldsView); - allViewFields.push(...recordPageFieldsViewFields); + allViews.push(fieldsView); + allViewFields.push(...viewFields); } const result = diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/__tests__/build-navigation-command-menu-item-operations-or-throw.util.spec.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/__tests__/build-navigation-command-menu-item-operations-or-throw.util.spec.ts index fff9d6d8df..35cae2d219 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/__tests__/build-navigation-command-menu-item-operations-or-throw.util.spec.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/__tests__/build-navigation-command-menu-item-operations-or-throw.util.spec.ts @@ -57,10 +57,6 @@ const buildExistingNavigationItem = ({ workspaceId: WORKSPACE_ID, position, now: NOW, - universalIdentifier: v5( - objectUniversalIdentifier, - NAVIGATION_COMMAND_UUID_NAMESPACE, - ), }); describe('buildNavigationCommandMenuItemOperationsOrThrow', () => { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util.ts index 0d3838c399..b0bb1458ba 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util.ts @@ -69,7 +69,6 @@ export const buildNavigationCommandMenuItemOperationsOrThrow = ({ workspaceId, position: nextPosition++, now, - universalIdentifier: commandMenuItemUniversalIdentifier, }), ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts index f76412f34b..7f0279d217 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts @@ -1,4 +1,5 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +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'; @@ -10,6 +11,9 @@ import { NAVIGATION_INTERPOLATED_SHORT_LABEL, } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; +const NAVIGATION_COMMAND_UUID_NAMESPACE = + 'b31830da-2ae0-48eb-a915-12fa4ab96dd3'; + const baseObjectMetadata = { id: 'obj-id-1', universalIdentifier: 'obj-universal-1', @@ -25,14 +29,18 @@ const baseArgs = { workspaceId: 'ws-id-1', position: 5, now: '2026-01-01T00:00:00.000Z', - universalIdentifier: 'nav-universal-1', }; describe('buildNavigationFlatCommandMenuItem', () => { - it('should use the provided universalIdentifier', () => { + it('should produce a deterministic universalIdentifier via UUID v5', () => { const result = buildNavigationFlatCommandMenuItem(baseArgs); - expect(result.universalIdentifier).toBe('nav-universal-1'); + const expectedUniversalIdentifier = v5( + baseObjectMetadata.universalIdentifier, + NAVIGATION_COMMAND_UUID_NAMESPACE, + ); + + expect(result.universalIdentifier).toBe(expectedUniversalIdentifier); }); it('should set label and shortLabel as interpolation templates', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts index e872b8a479..8120c89dfe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts @@ -1,6 +1,7 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { FeatureFlagKey } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +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'; @@ -53,7 +54,6 @@ export const buildNavigationFlatCommandMenuItem = ({ workspaceId, position, now, - universalIdentifier, }: { objectMetadata: { id: string; @@ -67,8 +67,12 @@ export const buildNavigationFlatCommandMenuItem = ({ workspaceId: string; position: number; now: string; - universalIdentifier: string; }): FlatCommandMenuItem => { + const universalIdentifier = v5( + objectMetadata.universalIdentifier, + NAVIGATION_COMMAND_UUID_NAMESPACE, + ); + const conditionalAvailabilityExpression = buildNavigationConditionalAvailabilityExpression({ universalIdentifier: objectMetadata.universalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util.ts deleted file mode 100644 index 797bb71f9f..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { compositeTypeDefinitions, RelationType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { - FlatEntityMapsException, - FlatEntityMapsExceptionCode, -} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; -import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; -import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; -import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; - -type FlatIndexFieldForName = { - fieldMetadataUniversalIdentifier: string; - order: number; - subFieldName: string | null; -}; - -export const computeFlatIndexNameOrThrow = ({ - flatObjectMetadata, - objectFlatFieldMetadatas, - universalFlatIndexFieldMetadatas, - isUnique, - indexWhereClause, -}: { - flatObjectMetadata: UniversalFlatObjectMetadata; - objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; - universalFlatIndexFieldMetadatas: FlatIndexFieldForName[]; - isUnique: boolean; - indexWhereClause: string | null; -}): string => { - const orderedIndexColumnNames = [...universalFlatIndexFieldMetadatas] - .sort((a, b) => a.order - b.order) - .map((flatIndexField) => { - const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find( - (flatFieldMetadata) => - flatFieldMetadata.universalIdentifier === - flatIndexField.fieldMetadataUniversalIdentifier, - ); - - if (!isDefined(relatedFlatFieldMetadata)) { - throw new FlatEntityMapsException( - 'Could not find flat index field related field in cache', - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); - } - - // Composite parent with an explicit sub-field → single sub-column. - // Composite parent without sub-field falls through to the legacy - // scalar branch below, which produces a deterministic name based on - // the parent name (the runner handles the multi-column SQL expansion - // via isIncludedInUniqueConstraint). - if ( - isCompositeFieldMetadataType(relatedFlatFieldMetadata.type) && - isDefined(flatIndexField.subFieldName) - ) { - const property = compositeTypeDefinitions - .get(relatedFlatFieldMetadata.type) - ?.properties.find( - (compositeProperty) => - compositeProperty.name === flatIndexField.subFieldName, - ); - - if (!isDefined(property)) { - throw new FlatEntityMapsException( - `Composite sub-field "${flatIndexField.subFieldName}" not found on ${relatedFlatFieldMetadata.name}`, - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); - } - - return computeCompositeColumnName( - { - name: relatedFlatFieldMetadata.name, - type: relatedFlatFieldMetadata.type, - }, - property, - ); - } - - const isManyToOneRelation = - isMorphOrRelationUniversalFlatFieldMetadata(relatedFlatFieldMetadata) && - relatedFlatFieldMetadata.universalSettings?.relationType === - RelationType.MANY_TO_ONE; - - return isManyToOneRelation - ? computeMorphOrRelationFieldJoinColumnName({ - name: relatedFlatFieldMetadata.name, - }) - : relatedFlatFieldMetadata.name; - }); - - return generateDeterministicIndexNameV2({ - flatObjectMetadata, - orderedIndexColumnNames, - isUnique, - indexWhereClause, - }); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util.ts deleted file mode 100644 index 0a693f61c1..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getIndexUniversalIdentifier } from 'twenty-shared/application'; - -import { computeFlatIndexNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; -import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type'; -import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; - -type FlatIndexWithoutDeterministicIdentifiers = Omit< - UniversalFlatIndexMetadata, - | 'name' - | 'universalIdentifier' - | 'objectMetadataUniversalIdentifier' - | 'applicationUniversalIdentifier' - | 'universalFlatIndexFieldMetadatas' -> & { - universalFlatIndexFieldMetadatas: Array< - Omit< - UniversalFlatIndexMetadata['universalFlatIndexFieldMetadatas'][number], - 'indexMetadataUniversalIdentifier' - > - >; -}; - -export const generateFlatIndexMetadataWithDeterministicUniversalIdentifierOrThrow = - ({ - flatObjectMetadata, - objectFlatFieldMetadatas, - flatIndex, - }: { - flatObjectMetadata: UniversalFlatObjectMetadata; - objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; - flatIndex: FlatIndexWithoutDeterministicIdentifiers; - }): UniversalFlatIndexMetadata => { - const name = computeFlatIndexNameOrThrow({ - flatObjectMetadata, - objectFlatFieldMetadatas, - universalFlatIndexFieldMetadatas: - flatIndex.universalFlatIndexFieldMetadatas, - isUnique: flatIndex.isUnique, - indexWhereClause: flatIndex.indexWhereClause, - }); - - const universalIdentifier = getIndexUniversalIdentifier({ - applicationUniversalIdentifier: - flatObjectMetadata.applicationUniversalIdentifier, - objectUniversalIdentifier: flatObjectMetadata.universalIdentifier, - name, - }); - - return { - ...flatIndex, - name, - universalIdentifier, - objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier, - applicationUniversalIdentifier: - flatObjectMetadata.applicationUniversalIdentifier, - universalFlatIndexFieldMetadatas: - flatIndex.universalFlatIndexFieldMetadatas.map((indexField) => ({ - ...indexField, - indexMetadataUniversalIdentifier: universalIdentifier, - })), - }; - }; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts index c1ca5bfd4d..d6b78aef58 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts @@ -1,4 +1,15 @@ -import { computeFlatIndexNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/compute-flat-index-name-or-throw.util'; +import { compositeTypeDefinitions, RelationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; @@ -14,11 +25,69 @@ export const generateFlatIndexMetadataWithNameOrThrow = ({ objectFlatFieldMetadatas, flatIndex, }: GenerateFlatIndexArgs): UniversalFlatIndexMetadata => { - const name = computeFlatIndexNameOrThrow({ + const orderedIndexColumnNames = flatIndex.universalFlatIndexFieldMetadatas + .sort((a, b) => a.order - b.order) + .map((flatIndexField) => { + const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find( + (flatFieldMetadata) => + flatFieldMetadata.universalIdentifier === + flatIndexField.fieldMetadataUniversalIdentifier, + ); + + if (!isDefined(relatedFlatFieldMetadata)) { + throw new FlatEntityMapsException( + 'Could not find flat index field related field in cache', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + // Composite parent with an explicit sub-field → single sub-column. + // Composite parent without sub-field falls through to the legacy + // scalar branch below, which produces a deterministic name based on + // the parent name (the runner handles the multi-column SQL expansion + // via isIncludedInUniqueConstraint). + if ( + isCompositeFieldMetadataType(relatedFlatFieldMetadata.type) && + isDefined(flatIndexField.subFieldName) + ) { + const property = compositeTypeDefinitions + .get(relatedFlatFieldMetadata.type) + ?.properties.find( + (compositeProperty) => + compositeProperty.name === flatIndexField.subFieldName, + ); + + if (!isDefined(property)) { + throw new FlatEntityMapsException( + `Composite sub-field "${flatIndexField.subFieldName}" not found on ${relatedFlatFieldMetadata.name}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return computeCompositeColumnName( + { + name: relatedFlatFieldMetadata.name, + type: relatedFlatFieldMetadata.type, + }, + property, + ); + } + + const isManyToOneRelation = + isMorphOrRelationUniversalFlatFieldMetadata(relatedFlatFieldMetadata) && + relatedFlatFieldMetadata.universalSettings?.relationType === + RelationType.MANY_TO_ONE; + + return isManyToOneRelation + ? computeMorphOrRelationFieldJoinColumnName({ + name: relatedFlatFieldMetadata.name, + }) + : relatedFlatFieldMetadata.name; + }); + + const name = generateDeterministicIndexNameV2({ flatObjectMetadata, - objectFlatFieldMetadatas, - universalFlatIndexFieldMetadatas: - flatIndex.universalFlatIndexFieldMetadatas, + orderedIndexColumnNames, isUnique: flatIndex.isUnique, indexWhereClause: flatIndex.indexWhereClause, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 7df78ee864..74f76c5201 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -2,10 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { - getIndexViewUniversalIdentifier, - getNavigationCommandUniversalIdentifier, -} from 'twenty-shared/application'; import { ViewKey, ViewOpenRecordIn, @@ -14,12 +10,15 @@ import { } from 'twenty-shared/types'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; -import { v4 } from 'uuid'; +import { v4, v5 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; -import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; +import { + buildNavigationFlatCommandMenuItem, + NAVIGATION_COMMAND_UUID_NAMESPACE, +} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; @@ -31,6 +30,9 @@ import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-meta import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; +import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; +import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; @@ -41,6 +43,7 @@ import { ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util'; +import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; @@ -417,8 +420,6 @@ export class ObjectMetadataService extends TypeOrmQueryService; }; }): FlatCommandMenuItem | undefined { - const commandMenuItemUniversalIdentifier = - getNavigationCommandUniversalIdentifier({ - applicationUniversalIdentifier, - objectUniversalIdentifier, - }); + const commandMenuItemUniversalIdentifier = v5( + objectUniversalIdentifier, + NAVIGATION_COMMAND_UUID_NAMESPACE, + ); return findFlatEntityByUniversalIdentifier({ flatEntityMaps: flatCommandMenuItemMaps, @@ -877,8 +919,6 @@ export class ObjectMetadataService extends TypeOrmQueryService - getFieldUniversalIdentifier({ - applicationUniversalIdentifier, - objectUniversalIdentifier: objectMetadataUniversalIdentifier, - name: fieldName, - }); - return { id: { ...id, - universalIdentifier: getSystemFieldUniversalIdentifier(id.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -58,7 +51,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, createdAt: { ...createdAt, - universalIdentifier: getSystemFieldUniversalIdentifier(createdAt.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -66,7 +59,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, createdBy: { ...createdBy, - universalIdentifier: getSystemFieldUniversalIdentifier(createdBy.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -74,7 +67,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, deletedAt: { ...deletedAt, - universalIdentifier: getSystemFieldUniversalIdentifier(deletedAt.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -82,7 +75,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, position: { ...position, - universalIdentifier: getSystemFieldUniversalIdentifier(position.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -90,7 +83,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, searchVector: { ...searchVector, - universalIdentifier: getSystemFieldUniversalIdentifier(searchVector.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -99,7 +92,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, updatedAt: { ...updatedAt, - universalIdentifier: getSystemFieldUniversalIdentifier(updatedAt.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -107,7 +100,7 @@ const buildObjectSystemFlatFieldMetadatas = ({ }, updatedBy: { ...updatedBy, - universalIdentifier: getSystemFieldUniversalIdentifier(updatedBy.name), + universalIdentifier: v4(), applicationUniversalIdentifier, objectMetadataUniversalIdentifier, createdAt: now, @@ -133,11 +126,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.TEXT, isLabelSyncedWithName: false, isUnique: false, - universalIdentifier: getFieldUniversalIdentifier({ - applicationUniversalIdentifier, - objectUniversalIdentifier: objectMetadataUniversalIdentifier, - name: 'name', - }), + universalIdentifier: v4(), name: 'name', label: 'Name', icon: 'IconAbc', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts index ab7977bf28..bae4cf6def 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts @@ -1,5 +1,7 @@ +import { v4 } from 'uuid'; + import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; -import { generateFlatIndexMetadataWithDeterministicUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index-metadata-with-deterministic-universal-identifier.util'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; import { type DefaultFlatFieldForCustomObjectMaps } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type'; @@ -14,33 +16,39 @@ export const buildDefaultIndexesForCustomObject = ({ objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; defaultFlatFieldForCustomObjectMaps: DefaultFlatFieldForCustomObjectMaps; }) => { - const createdAt = new Date().toISOString(); + const tsFlatVectorIndexUniversalIdentifier = v4(); + const createdAt = new Date(); + const tsVectorFlatIndex = generateFlatIndexMetadataWithNameOrThrow({ + objectFlatFieldMetadatas, + flatIndex: { + createdAt: createdAt.toISOString(), + universalFlatIndexFieldMetadatas: [ + { + createdAt: createdAt.toISOString(), + fieldMetadataUniversalIdentifier: + defaultFlatFieldForCustomObjectMaps.fields.searchVector + .universalIdentifier, + indexMetadataUniversalIdentifier: + tsFlatVectorIndexUniversalIdentifier, + order: 0, + subFieldName: null, + updatedAt: createdAt.toISOString(), + }, + ], - const tsVectorFlatIndex = - generateFlatIndexMetadataWithDeterministicUniversalIdentifierOrThrow({ - flatObjectMetadata, - objectFlatFieldMetadatas, - flatIndex: { - createdAt, - updatedAt: createdAt, - universalFlatIndexFieldMetadatas: [ - { - createdAt, - updatedAt: createdAt, - fieldMetadataUniversalIdentifier: - defaultFlatFieldForCustomObjectMaps.fields.searchVector - .universalIdentifier, - order: 0, - subFieldName: null, - }, - ], - indexType: IndexType.GIN, - indexWhereClause: null, - isCustom: false, - isUnique: false, - isSystemSideEffect: true, - }, - }); + indexType: IndexType.GIN, + indexWhereClause: null, + isCustom: false, + isUnique: false, + isSystemSideEffect: true, + objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier, + universalIdentifier: tsFlatVectorIndexUniversalIdentifier, + updatedAt: createdAt.toISOString(), + applicationUniversalIdentifier: + flatObjectMetadata.applicationUniversalIdentifier, + }, + flatObjectMetadata, + }); return { indexes: { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts index 0e7ece4ede..5dd6b4a758 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts @@ -1,52 +1,36 @@ -import { - getPageLayoutTabUniversalIdentifier, - getPageLayoutWidgetUniversalIdentifier, - getRecordPageLayoutUniversalIdentifier, -} from 'twenty-shared/application'; import { v4 } from 'uuid'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; -import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; -import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; import { TAB_PROPS, WIDGET_PROPS, } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template'; -import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; 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'; -import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; export const computeFlatDefaultRecordPageLayoutToCreate = ({ objectMetadata, flatApplication, - objectFlatFieldMetadatas, - labelIdentifierFieldMetadataUniversalIdentifier, + recordPageFieldsView, workspaceId, }: { flatApplication: FlatApplication; objectMetadata: UniversalFlatObjectMetadata & { id: string }; - objectFlatFieldMetadatas: UniversalFlatFieldMetadata[]; - labelIdentifierFieldMetadataUniversalIdentifier: string | null; + recordPageFieldsView: UniversalFlatView & { id: string }; workspaceId: string; }): { pageLayouts: FlatPageLayout[]; pageLayoutTabs: FlatPageLayoutTab[]; pageLayoutWidgets: FlatPageLayoutWidget[]; - recordPageFieldsView: UniversalFlatView & { id: string }; - recordPageFieldsViewFields: UniversalFlatViewField[]; } => { const now = new Date().toISOString(); const pageLayoutId = v4(); - const pageLayoutUniversalIdentifier = getRecordPageLayoutUniversalIdentifier({ - applicationUniversalIdentifier: flatApplication.universalIdentifier, - objectUniversalIdentifier: objectMetadata.universalIdentifier, - }); + const pageLayoutUniversalIdentifier = v4(); const tabDefinitions = [ { key: 'home' as const, widgetKey: 'fields' as const }, @@ -58,24 +42,14 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ const pageLayoutTabs: FlatPageLayoutTab[] = []; const pageLayoutWidgets: FlatPageLayoutWidget[] = []; - let recordPageFieldsView: (UniversalFlatView & { id: string }) | undefined; - let recordPageFieldsViewFields: UniversalFlatViewField[] = []; for (const { key, widgetKey } of tabDefinitions) { const tabProps = TAB_PROPS[key]; const widgetProps = WIDGET_PROPS[widgetKey]; const tabId = v4(); - const tabUniversalIdentifier = getPageLayoutTabUniversalIdentifier({ - applicationUniversalIdentifier: flatApplication.universalIdentifier, - pageLayoutUniversalIdentifier, - title: tabProps.title, - }); + const tabUniversalIdentifier = v4(); const widgetId = v4(); - const widgetUniversalIdentifier = getPageLayoutWidgetUniversalIdentifier({ - applicationUniversalIdentifier: flatApplication.universalIdentifier, - pageLayoutTabUniversalIdentifier: tabUniversalIdentifier, - title: widgetProps.title, - }); + const widgetUniversalIdentifier = v4(); pageLayoutTabs.push({ id: tabId, @@ -101,48 +75,31 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ const isFieldsWidget = widgetKey === 'fields'; - if (isFieldsWidget) { - recordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({ - objectMetadata, - flatApplication, - pageLayoutWidgetUniversalIdentifier: widgetUniversalIdentifier, - }); - recordPageFieldsViewFields = computeFlatViewFieldsToCreate({ - flatApplication, - objectFlatFieldMetadatas, - labelIdentifierFieldMetadataUniversalIdentifier, - viewUniversalIdentifier: recordPageFieldsView.universalIdentifier, - excludeLabelIdentifier: true, - }); - } + const configuration = isFieldsWidget + ? { + configurationType: WidgetConfigurationType.FIELDS, + viewId: recordPageFieldsView.id, + newFieldDefaultVisibility: true, + } + : { + configurationType: + WidgetConfigurationType[ + widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType + ], + }; - const configuration = - isFieldsWidget && recordPageFieldsView - ? { - configurationType: WidgetConfigurationType.FIELDS, - viewId: recordPageFieldsView.id, - newFieldDefaultVisibility: true, - } - : { - configurationType: - WidgetConfigurationType[ - widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType - ], - }; - - const universalConfiguration = - isFieldsWidget && recordPageFieldsView - ? { - configurationType: WidgetConfigurationType.FIELDS, - viewUniversalIdentifier: recordPageFieldsView.universalIdentifier, - newFieldDefaultVisibility: true, - } - : { - configurationType: - WidgetConfigurationType[ - widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType - ], - }; + const universalConfiguration = isFieldsWidget + ? { + configurationType: WidgetConfigurationType.FIELDS, + viewUniversalIdentifier: recordPageFieldsView.universalIdentifier, + newFieldDefaultVisibility: true, + } + : { + configurationType: + WidgetConfigurationType[ + widgetKey.toUpperCase() as keyof typeof WidgetConfigurationType + ], + }; pageLayoutWidgets.push({ id: widgetId, @@ -172,12 +129,6 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ }); } - if (!recordPageFieldsView) { - throw new Error( - 'Record page fields widget is missing from the default record page layout', - ); - } - const pageLayout: FlatPageLayout = { id: pageLayoutId, universalIdentifier: pageLayoutUniversalIdentifier, @@ -200,11 +151,5 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null, }; - return { - pageLayouts: [pageLayout], - pageLayoutTabs, - pageLayoutWidgets, - recordPageFieldsView, - recordPageFieldsViewFields, - }; + return { pageLayouts: [pageLayout], pageLayoutTabs, pageLayoutWidgets }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts index 721ae66d0a..4ea594b8e6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util.ts @@ -1,4 +1,3 @@ -import { getFieldsWidgetViewUniversalIdentifier } from 'twenty-shared/application'; import { ViewOpenRecordIn, ViewType, @@ -13,11 +12,9 @@ import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-m export const computeFlatRecordPageFieldsViewToCreate = ({ objectMetadata, flatApplication, - pageLayoutWidgetUniversalIdentifier, }: { flatApplication: FlatApplication; objectMetadata: UniversalFlatObjectMetadata & { id: string }; - pageLayoutWidgetUniversalIdentifier: string; }): UniversalFlatView & { id: string } => { const createdAt = new Date().toISOString(); @@ -43,10 +40,7 @@ export const computeFlatRecordPageFieldsViewToCreate = ({ mainGroupByFieldMetadataUniversalIdentifier: null, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, position: 0, - universalIdentifier: getFieldsWidgetViewUniversalIdentifier({ - applicationUniversalIdentifier: flatApplication.universalIdentifier, - pageLayoutWidgetUniversalIdentifier, - }), + universalIdentifier: v4(), visibility: ViewVisibility.WORKSPACE, createdByUserWorkspaceId: null, isActive: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts index e238e71434..c30049ace1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts @@ -1,4 +1,4 @@ -import { getViewFieldUniversalIdentifier } from 'twenty-shared/application'; +import { v4 } from 'uuid'; 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'; @@ -59,11 +59,7 @@ export const computeFlatViewFieldsToCreate = ({ createdAt, updatedAt: createdAt, deletedAt: null, - universalIdentifier: getViewFieldUniversalIdentifier({ - applicationUniversalIdentifier: flatApplication.universalIdentifier, - viewUniversalIdentifier, - fieldMetadataUniversalIdentifier: field.universalIdentifier, - }), + universalIdentifier: v4(), isVisible: true, size: DEFAULT_VIEW_FIELD_SIZE, position: index, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/command-menu-item/build-standard-flat-command-menu-item-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/command-menu-item/build-standard-flat-command-menu-item-maps.util.ts index 65e990aba2..fcc92c67d7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/command-menu-item/build-standard-flat-command-menu-item-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/command-menu-item/build-standard-flat-command-menu-item-maps.util.ts @@ -1,7 +1,4 @@ -import { - getNavigationCommandUniversalIdentifier, - TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, -} from 'twenty-shared/application'; +import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; @@ -81,11 +78,6 @@ export const buildStandardFlatCommandMenuItemMaps = ({ workspaceId, position, now, - universalIdentifier: getNavigationCommandUniversalIdentifier({ - applicationUniversalIdentifier: - TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: flatObject.universalIdentifier, - }), }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts index aed5165eaa..b7260aab95 100644 --- a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-command-menu-item-universal-identifier.util.spec.ts @@ -1,19 +1,53 @@ import { - getCommandMenuItemUniversalIdentifier, + getGlobalCommandMenuItemUniversalIdentifier, + getGlobalObjectContextCommandMenuItemUniversalIdentifier, getNavigationCommandUniversalIdentifier, + getRecordSelectionCommandMenuItemUniversalIdentifier, } from '@/application/deterministic-identifier/get-command-menu-item-universal-identifier.util'; const APP = '11111111-1111-4111-8111-111111111111'; const OBJECT = '22222222-2222-4222-8222-222222222222'; -describe('getCommandMenuItemUniversalIdentifier', () => { - it('derives a deterministic id from the command label within its application', () => { +describe('getGlobalCommandMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the engineComponentKey within GLOBAL', () => { expect( - getCommandMenuItemUniversalIdentifier({ + getGlobalCommandMenuItemUniversalIdentifier({ applicationUniversalIdentifier: APP, - label: 'Create Record', + engineComponentKey: 'EXPORT_RECORDS', }), - ).toBe('e284adf7-5332-5253-9c94-9e6b3c57237f'); + ).toBe('bba78a2e-c117-52af-b25d-126702e8cf57'); + }); +}); + +describe('getGlobalObjectContextCommandMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the engineComponentKey within GLOBAL_OBJECT_CONTEXT', () => { + expect( + getGlobalObjectContextCommandMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + engineComponentKey: 'CREATE_NEW_RECORD', + }), + ).toBe('77fb3fcd-ee35-50ea-9c3c-ab95cef5e0e2'); + }); +}); + +describe('getRecordSelectionCommandMenuItemUniversalIdentifier', () => { + it('includes the target object when scoped to one', () => { + expect( + getRecordSelectionCommandMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + engineComponentKey: 'COMPOSE_EMAIL', + objectUniversalIdentifier: OBJECT, + }), + ).toBe('dd10a2b1-229b-572a-a887-ea20e487cfd3'); + }); + + it('omits the object for object-agnostic record-selection commands', () => { + expect( + getRecordSelectionCommandMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + engineComponentKey: 'DELETE_RECORDS', + }), + ).toBe('36b20ed1-6172-5da5-9a21-58de9c8acb22'); }); }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts index 2f87020c8a..5ed80e0b4a 100644 --- a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-navigation-menu-item-universal-identifier.util.spec.ts @@ -1,14 +1,54 @@ -import { getNavigationMenuItemUniversalIdentifier } from '@/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util'; +import { + getFolderNavigationMenuItemUniversalIdentifier, + getLinkNavigationMenuItemUniversalIdentifier, + getObjectNavigationMenuItemUniversalIdentifier, + getViewNavigationMenuItemUniversalIdentifier, +} from '@/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util'; const APP = '11111111-1111-4111-8111-111111111111'; +const OBJECT = '22222222-2222-4222-8222-222222222222'; +const VIEW = '44444444-4444-4444-8444-444444444444'; -describe('getNavigationMenuItemUniversalIdentifier', () => { - it('derives a deterministic id from the item name within its application', () => { +describe('getFolderNavigationMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the folder name within its application', () => { expect( - getNavigationMenuItemUniversalIdentifier({ + getFolderNavigationMenuItemUniversalIdentifier({ applicationUniversalIdentifier: APP, - name: 'Home', + name: 'Workflows', }), - ).toBe('dc0ca522-7f09-567d-a7f7-d33824495fc4'); + ).toBe('ca043c51-ac75-515d-902b-dd836626bba4'); + }); +}); + +describe('getObjectNavigationMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the object it targets', () => { + expect( + getObjectNavigationMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + objectUniversalIdentifier: OBJECT, + }), + ).toBe('071361bd-47c8-5513-be33-998dd287f8c7'); + }); +}); + +describe('getViewNavigationMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from the view it targets', () => { + expect( + getViewNavigationMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + }), + ).toBe('6504fd6d-ed38-5f34-87fc-26025130260f'); + }); +}); + +describe('getLinkNavigationMenuItemUniversalIdentifier', () => { + it('derives a deterministic id from its target URL', () => { + expect( + getLinkNavigationMenuItemUniversalIdentifier({ + applicationUniversalIdentifier: APP, + link: 'https://example.com', + }), + ).toBe('635cef97-cc61-53a3-873e-38fecf3ecbd7'); }); }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-search-field-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-search-field-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..6c80a5cf24 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-search-field-universal-identifier.util.spec.ts @@ -0,0 +1,15 @@ +import { getSearchFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-search-field-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const FIELD = '33333333-3333-4333-8333-333333333333'; + +describe('getSearchFieldUniversalIdentifier', () => { + it('derives a deterministic id from the field it makes searchable', () => { + expect( + getSearchFieldUniversalIdentifier({ + applicationUniversalIdentifier: APP, + fieldMetadataUniversalIdentifier: FIELD, + }), + ).toBe('db4e5b93-15b6-5c83-a0b3-6c031e0ca072'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-group-universal-identifier.util.spec.ts b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-group-universal-identifier.util.spec.ts new file mode 100644 index 0000000000..9bff7c3fd5 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/__tests__/get-view-field-group-universal-identifier.util.spec.ts @@ -0,0 +1,16 @@ +import { getViewFieldGroupUniversalIdentifier } from '@/application/deterministic-identifier/get-view-field-group-universal-identifier.util'; + +const APP = '11111111-1111-4111-8111-111111111111'; +const VIEW = '44444444-4444-4444-8444-444444444444'; + +describe('getViewFieldGroupUniversalIdentifier', () => { + it('derives a deterministic id from the group name within its view', () => { + expect( + getViewFieldGroupUniversalIdentifier({ + applicationUniversalIdentifier: APP, + viewUniversalIdentifier: VIEW, + name: 'General', + }), + ).toBe('26350cf1-bec3-5617-b525-c0115811963e'); + }); +}); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts index 5794689ee7..b646603068 100644 --- a/packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-command-menu-item-universal-identifier.util.ts @@ -2,18 +2,55 @@ import { computeDeterministicUuid } from '@/application/deterministic-identifier const NAVIGATION_COMMAND_DISCRIMINATOR = 'navigation'; -// A command menu item is identified by its label within its application -// (not every command is backed by a front component). -export const getCommandMenuItemUniversalIdentifier = ({ +// A command menu item is identified by its availabilityType + engineComponentKey +// (the same engineComponentKey is reused across availability types and, for +// RECORD_SELECTION, across objects). One util per availabilityType, prefixed by +// the type, with the target object added only for RECORD_SELECTION. + +// GLOBAL: a command available everywhere, identified by its engineComponentKey. +export const getGlobalCommandMenuItemUniversalIdentifier = ({ applicationUniversalIdentifier, - label, + engineComponentKey, }: { applicationUniversalIdentifier: string; - label: string; + engineComponentKey: string; }): string => computeDeterministicUuid({ entityNamespace: 'commandMenuItem', - value: label, + value: `GLOBAL:${engineComponentKey}`, + applicationUniversalIdentifier, + }); + +// GLOBAL_OBJECT_CONTEXT: a command whose object is resolved at runtime (not stored), +// identified by its engineComponentKey. +export const getGlobalObjectContextCommandMenuItemUniversalIdentifier = ({ + applicationUniversalIdentifier, + engineComponentKey, +}: { + applicationUniversalIdentifier: string; + engineComponentKey: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'commandMenuItem', + value: `GLOBAL_OBJECT_CONTEXT:${engineComponentKey}`, + applicationUniversalIdentifier, + }); + +// RECORD_SELECTION: a command acting on selected records; the same engineComponentKey +// is replicated per object, so the target object is part of the identity +// (null/absent for the object-agnostic record-selection commands). +export const getRecordSelectionCommandMenuItemUniversalIdentifier = ({ + applicationUniversalIdentifier, + engineComponentKey, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + engineComponentKey: string; + objectUniversalIdentifier?: string | null; +}): string => + computeDeterministicUuid({ + entityNamespace: 'commandMenuItem', + value: `RECORD_SELECTION:${engineComponentKey}:${objectUniversalIdentifier ?? ''}`, applicationUniversalIdentifier, }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts index 5f9fe4d2b3..1fd8189121 100644 --- a/packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util.ts @@ -1,7 +1,12 @@ import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; -// A navigation menu item is identified by its name within its application. -export const getNavigationMenuItemUniversalIdentifier = ({ +// NavigationMenuItem is a single polymorphic entity: every variant shares +// name/icon/color/position/folder, but each type is identified by a different +// target, so each variant has its own discriminator (prefixed by the type to +// keep the shared `navigationMenuItem` namespace collision-free). + +// A FOLDER navigation item is identified by its name within its application. +export const getFolderNavigationMenuItemUniversalIdentifier = ({ applicationUniversalIdentifier, name, }: { @@ -10,6 +15,48 @@ export const getNavigationMenuItemUniversalIdentifier = ({ }): string => computeDeterministicUuid({ entityNamespace: 'navigationMenuItem', - value: name, + value: `FOLDER:${name}`, + applicationUniversalIdentifier, + }); + +// An OBJECT navigation item is identified by the object it targets. +export const getObjectNavigationMenuItemUniversalIdentifier = ({ + applicationUniversalIdentifier, + objectUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + objectUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'navigationMenuItem', + value: `OBJECT:${objectUniversalIdentifier}`, + applicationUniversalIdentifier, + }); + +// A VIEW navigation item is identified by the view it targets. +export const getViewNavigationMenuItemUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'navigationMenuItem', + value: `VIEW:${viewUniversalIdentifier}`, + applicationUniversalIdentifier, + }); + +// A LINK navigation item is identified by its target URL. +export const getLinkNavigationMenuItemUniversalIdentifier = ({ + applicationUniversalIdentifier, + link, +}: { + applicationUniversalIdentifier: string; + link: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'navigationMenuItem', + value: `LINK:${link}`, applicationUniversalIdentifier, }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-search-field-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-search-field-universal-identifier.util.ts new file mode 100644 index 0000000000..0e618fea0a --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-search-field-universal-identifier.util.ts @@ -0,0 +1,15 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A search field is 1:1 with the field it makes searchable, so it is keyed by that field. +export const getSearchFieldUniversalIdentifier = ({ + applicationUniversalIdentifier, + fieldMetadataUniversalIdentifier, +}: { + applicationUniversalIdentifier: string; + fieldMetadataUniversalIdentifier: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'searchFieldMetadata', + value: fieldMetadataUniversalIdentifier, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-group-universal-identifier.util.ts b/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-group-universal-identifier.util.ts new file mode 100644 index 0000000000..818f1e1437 --- /dev/null +++ b/packages/twenty-shared/src/application/deterministic-identifier/get-view-field-group-universal-identifier.util.ts @@ -0,0 +1,17 @@ +import { computeDeterministicUuid } from '@/application/deterministic-identifier/compute-deterministic-uuid.util'; + +// A view field group is identified by its name within its view. +export const getViewFieldGroupUniversalIdentifier = ({ + applicationUniversalIdentifier, + viewUniversalIdentifier, + name, +}: { + applicationUniversalIdentifier: string; + viewUniversalIdentifier: string; + name: string; +}): string => + computeDeterministicUuid({ + entityNamespace: 'viewFieldGroup', + value: `${viewUniversalIdentifier}:${name}`, + applicationUniversalIdentifier, + }); diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index c6c723a475..af044e7949 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -31,7 +31,9 @@ export type { DeterministicEntityNamespace } from './deterministic-identifier/de export { getAgentUniversalIdentifier } from './deterministic-identifier/get-agent-universal-identifier.util'; export { getApplicationVariableUniversalIdentifier } from './deterministic-identifier/get-application-variable-universal-identifier.util'; export { - getCommandMenuItemUniversalIdentifier, + getGlobalCommandMenuItemUniversalIdentifier, + getGlobalObjectContextCommandMenuItemUniversalIdentifier, + getRecordSelectionCommandMenuItemUniversalIdentifier, getNavigationCommandUniversalIdentifier, } from './deterministic-identifier/get-command-menu-item-universal-identifier.util'; export { getConnectionProviderUniversalIdentifier } from './deterministic-identifier/get-connection-provider-universal-identifier.util'; @@ -40,7 +42,12 @@ export { getFieldUniversalIdentifier } from './deterministic-identifier/get-fiel 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'; -export { getNavigationMenuItemUniversalIdentifier } from './deterministic-identifier/get-navigation-menu-item-universal-identifier.util'; +export { + getFolderNavigationMenuItemUniversalIdentifier, + getObjectNavigationMenuItemUniversalIdentifier, + getViewNavigationMenuItemUniversalIdentifier, + getLinkNavigationMenuItemUniversalIdentifier, +} from './deterministic-identifier/get-navigation-menu-item-universal-identifier.util'; export { getObjectPermissionUniversalIdentifier } from './deterministic-identifier/get-object-permission-universal-identifier.util'; export { getObjectUniversalIdentifier } from './deterministic-identifier/get-object-universal-identifier.util'; export { getPageLayoutTabUniversalIdentifier } from './deterministic-identifier/get-page-layout-tab-universal-identifier.util'; @@ -52,8 +59,10 @@ export { getPageLayoutWidgetUniversalIdentifier } from './deterministic-identifi export { getPermissionFlagUniversalIdentifier } from './deterministic-identifier/get-permission-flag-universal-identifier.util'; export { getRolePermissionFlagUniversalIdentifier } from './deterministic-identifier/get-role-permission-flag-universal-identifier.util'; export { getRoleUniversalIdentifier } from './deterministic-identifier/get-role-universal-identifier.util'; +export { getSearchFieldUniversalIdentifier } from './deterministic-identifier/get-search-field-universal-identifier.util'; export { getSelectOptionUniversalIdentifier } from './deterministic-identifier/get-select-option-universal-identifier.util'; export { getSkillUniversalIdentifier } from './deterministic-identifier/get-skill-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';